
2026/03/05 2:05
**macOS でのコード注入 ― 楽しむためだけ、収益は一切ない(2024)**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
記事では、Mach API を用いて macOS の実行中プロセスにコードを注入する方法を示し、関数の置き換えとグローバルデータの変更を行う最小限で機能的なプロトタイプを提示しています。まず、
data というグローバル変数を出力し foo() を呼び出す単純な C++ テストプログラムから始まります。インジェクタはテストプログラムが書き込んだ data.txt ファイルを読み取り、対象プロセスの PID と foo() および data のアドレスを取得し、task_for_pid でアタッチします。
プロセスは
task_suspend で停止し、後で task_resume で再開されます。メモリは vm_read_overwrite で読み取り、vm_write で書き込みを行います。例では data を 123 から 456 に変更し、パッチ適用後にプログラムが 857 を出力することで、注入された関数 bar() が foo() の代わりに実行されていることを確認しています。
コードを注入するには、インジェクタ自身の
bar() をコンパイルし、ダミーの barEnd() マーカーでサイズを計算します。次に vm_allocate でターゲットプロセスに可実行メモリを確保し、writeBytes で機械語を書き込み、別途保護呼び出しで読み取り/実行権限へ変更します。トランプル(ジャンプ)コードは foo() の先頭に書かれます(x86_64 では 14 バイト、arm64 では 16 バイト)。記事ではトランプルを書き込む際に VM_PROT_COPY を使用しています。
テストプログラムの
foo() は、ARM トランプル用に十分なスペースを確保するために -fpatchable-function-entry=4,0 でコンパイルし、最初に 4 バイト分の NOP を挿入する必要があります。著者はこれは最小限のプロトタイプであり、本番環境向けではないことを指摘し、トランプル上書き中のスレッド停止やデバッガ統合の欠如など潜在的な問題点も挙げています。
コードは GitHub(
https://github.com/badlogic/macinject)でホストされており、macOS 10.14+ で CMake を使ってビルドできます。この手法はライブパッチングやデバッグツールの基盤として利用できるほか、同様の仕組みがマルウェアに悪用される可能性も示しています。本文
2024‑07‑20
概要
本ガイドでは、Mach API(
task_for_pid、vm_write など)を使って macOS 上で実行中のプロセスにコードを注入する方法 を解説します。内容は以下の通りです。
- CMake の設定とエンタイトルメント
- 実行中プロセスへのアタッチ
- スレッドの停止/再開
- リモートメモリの読み書き
- コード注入とトランスペイリング(trampoline)
1. CMake の設定とエンタイトルメント
cmake_minimum_required(VERSION 3.10) project(macinject) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14") # ---------- テストプログラム ---------- file(GLOB_RECURSE TEST_SOURCES "test/*.cpp") add_executable(test ${TEST_SOURCES}) set_target_properties( test PROPERTIES COMPILE_FLAGS "-O0 -g -fpatchable-function-entry=4,0" ) # ---------- 注入プログラム ---------- file(GLOB_RECURSE SOURCES "src/*.cpp") add_executable(macinject ${SOURCES}) # デバッガ権限でインジェクタを署名 add_custom_command( TARGET macinject POST_BUILD COMMAND codesign --entitlements "${CMAKE_SOURCE_DIR}/entitlements.plist" \ -s "Apple Development" $<TARGET_FILE:macinject> )
entitlements.plist:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.debugger</key> <true/> </dict> </plist>
ビルド手順:
git clone https://github.com/badlogic/macinject cd macinject mkdir build && cd build cmake .. make # ソースを変更したら再ビルド
2. 実行中プロセスへのアタッチ
テストプログラムは
data.txt に自分の PID、foo のアドレス、data のアドレスを書き込みます。
#include <cstdio> #include <cstdlib> #include <mach-o/arch.h> #include <mach/mach.h> #include <unistd.h> struct RemoteProcess { pid_t pid; void* fooAddr; void* dataAddr; mach_port_t task; }; bool attach(RemoteProcess &proc) { FILE *file = fopen("data.txt", "r"); if (!file) { perror("open data.txt"); return false; } fscanf(file, "%d", &proc.pid); fscanf(file, "%p", &proc.fooAddr); fscanf(file, "%p", &proc.dataAddr); fclose(file); kern_return_t kr = task_for_pid(mach_task_self(), proc.pid, &proc.task); if (kr != KERN_SUCCESS) { fprintf(stderr, "task_for_pid failed: %s\n", mach_error_string(kr)); return false; } printf("Attached to pid %d\n", proc.pid); return true; }
3. スレッドの停止/再開
bool suspend(RemoteProcess &proc) { if (task_suspend(proc.task) != KERN_SUCCESS) { perror("task_suspend"); return false; } printf("Task suspended\n"); return true; } bool resume(RemoteProcess &proc) { if (task_resume(proc.task) != KERN_SUCCESS) { perror("task_resume"); return false; } printf("Task resumed\n"); return true; }
4. リモートメモリの読み書き
bool writeBytes(RemoteProcess &proc, void *remoteAddr, const void *src, size_t n) { if (vm_write(proc.task, reinterpret_cast<vm_address_t>(remoteAddr), reinterpret_cast<vm_offset_t>(const_cast<void*>(src)), n) != KERN_SUCCESS) { perror("vm_write"); return false; } printf("Wrote %zu bytes to %p\n", n, remoteAddr); return true; } bool readBytes(RemoteProcess &proc, void *remoteAddr, void *dst, size_t n) { vm_size_t outSize; if (vm_read_overwrite(proc.task, reinterpret_cast<vm_address_t>(remoteAddr), n, reinterpret_cast<vm_address_t>(dst), &outSize) != KERN_SUCCESS || outSize != n) { perror("vm_read_overwrite"); return false; } printf("Read %zu bytes from %p\n", n, remoteAddr); return true; }
5. コード注入とトランスペイリング
5.1 メモリ確保と実行権限付与
bool allocate(RemoteProcess &proc, size_t sz, void **addr) { vm_address_t a = 0; if (vm_allocate(proc.task, &a, sz, VM_FLAGS_ANYWHERE) != KERN_SUCCESS) { perror("vm_allocate"); return false; } *addr = reinterpret_cast<void*>(a); printf("Allocated %zu bytes at %p\n", sz, *addr); return true; } bool changeProtection(RemoteProcess &proc, void *addr, size_t sz, vm_prot_t prot) { if (vm_protect(proc.task, reinterpret_cast<vm_address_t>(addr), sz, false, prot) != KERN_SUCCESS) { perror("vm_protect"); return false; } printf("Changed protection of %p to %x\n", addr, static_cast<unsigned>(prot)); return true; }
5.2 注入する関数のビルド
int bar() { return 857; } void barEnd() {} // サイズ測定用マーカー int main(int argc,char**argv) { RemoteProcess proc; attach(proc); suspend(proc); /* --- Part 1: data を変更する --- */ int old, newv = 456; readBytes(proc, proc.dataAddr, &old, sizeof(old)); printf("Old value: %d\n", old); writeBytes(proc, proc.dataAddr, &newv, sizeof(newv)); resume(proc); /* --- Part 2: bar() を注入する --- */ sleep(4); // テストが新しいデータを出力できるように待つ suspend(proc); int barSize = reinterpret_cast<char*>(&barEnd) - reinterpret_cast<char*>(&bar); void *remoteBar; allocate(proc, barSize, &remoteBar); writeBytes(proc, remoteBar, (void*)&bar, barSize); changeProtection(proc, remoteBar, barSize, VM_PROT_READ | VM_PROT_EXECUTE); /* --- Part 3: foo() を bar() に差し替えるトランスペイリング --- */ trampoline(proc, proc.fooAddr, remoteBar); resume(proc); }
5.3 トランスペイリング実装
bool trampoline(RemoteProcess &proc, void *oldFunc, void *newFunc) { const NXArchInfo *arch = NXGetLocalArchInfo(); if (!arch) { perror("NXGetLocalArchInfo"); return false; } unsigned char instr[16]; size_t sz; if (strcmp(arch->name,"x86_64")==0) { // 14 バイトの絶対間接ジャンプ instr[0] = 0xFF; instr[1] = 0x25; *(uint32_t*)(instr+2)=0; // 未使用領域 *(uint64_t*)(instr+6)=reinterpret_cast<uint64_t>(newFunc); sz = sizeof(instr); } else if (strcmp(arch->name,"arm64")==0 || strcmp(arch->name,"arm64e")==0) { // LDR X16, #8 ; BR X16 instr[0] = 0x50; instr[1] = 0x00; instr[2] = 0x00; instr[3] = 0x58; instr[4] = 0x00; instr[5] = 0x02; instr[6] = 0x1F; instr[7] = 0xD6; *(uint64_t*)(instr+8)=reinterpret_cast<uint64_t>(newFunc); sz = sizeof(instr); } else { fprintf(stderr,"Unsupported arch %s\n",arch->name); return false; } // 書き込み可能にする changeProtection(proc, oldFunc, sz, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); writeBytes(proc, oldFunc, instr, sz); // もう一度実行権限を付与 changeProtection(proc, oldFunc, sz, VM_PROT_READ | VM_PROT_EXECUTE); printf("Patched %p → %p\n", oldFunc, newFunc); return true; }
6. デモの実行
# ターミナル A ./build/test # ターミナル B(テストが起動した後) ./build/macinject
テストプログラムは最初に
123 を表示し、注入後は 456、最後にトランスペイリングで foo() が bar() に差し替わったため 857 を出力します。
注意点
- 本コードは 教育目的 であり、実運用を想定したものではありません。
- ターゲットプロセスに他のスレッドが存在する場合、全スレッド停止が妨げになる可能性があります。
- macOS では適切なエンタイトルメント(上記
)を持ち、必要に応じて root 権限または「デバッグ」を許可しておく必要があります。entitlements.plist
安全かつ責任あるハッキング・学習を楽しんでください!