株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎
今回はLinux上でptraceシステムコールを用いて、ハードウェアブレークポイントを実装してみます。
ブレークポイントを設置したメモリ領域が読み書きされたり、実行された場合プログラムが停止し解析が可能になります。
デバッガの原理を知ることにより、より低レイヤーに興味を持っていただければ幸いです。
今回やること
ハードウェアブレークポイントを実装してメモリのアクセス元を調べたり、プログラムにブレークポイントを設置します。
簡単のため対象プロセスはシングルスレッドを想定します。
※今回の内容はある程度ptraceについてやデバッガの基本的な原理が分かっている方向けです。
ハードウェアブレークポイントについて
プログラムをINT 3命令(x86の場合)等に書き換えるソフトウェアブレークポイントと違い、CPUのデバッグレジスタを用いてブレークポイントを設置します。
以下の方の記事が参考になるのでご一読ください。(※Windows用ですが実装の参考にさせて頂きました)
qiita.com
リバースエンジニアリング的観点からの利点
・プログラム領域を書き換える訳ではないため、チェックサム等がある場合も検知されない。
・特定メモリ領域にアクセスする処理を知ることが出来る。(通称WatchPoint)
特にWatchPointが重要であり、特定の文字列や特定の重要な値を参照してる箇所を特定できればプログラムの解析が容易になります。
逆に言えば、解析から守る上ではこういったデバッガの原理を知ることは非常に重要なことであると思います。
※WatchPointは該当メモリのアクセス属性を変更して、強制的に例外を発生させる方法も存在します。
ptraceシステムコールについて
当ブログでも幾つかの記事を書かせていただきました。
ptraceシステムコールによって対象プロセスのメモリの値を読み書きしたり、レジスタの値を取得・改変したり可能です。
自作ゲーム:チートチャレンジ - 株式会社Ninjastars 技術研究部
Linux/Android マルチスレッド対応のデバッガの実装 - 株式会社Ninjastars 技術研究部
今回はデバッグレジスタにアクセスするのに、PTRACE_PEEKUSER/PTRACE_POKEUSERというオプションを利用します。
詳しくはManのページを参照ください。
surf.ml.seikei.ac.jp
導入
まず64bitのOS上で32bitをコンパイルするため32bit用のライブラリをインストール必要があります。
sudo apt-get install libc6-dev-i386
gcc -no-pie counter.c -o counter gcc -no-pie hwbp.c -o hwbp gcc -m32 -no-pie counter.c -o counter32 gcc -m32 -no-pie hwbp.c -o hwbp32
上記ファイルについての内容は以下です。
- counter:変数の値とアドレスを表示するプログラム。
- hwbp:指定したアドレスにハードウェアブレークポイントを設置するプログラム。
実験
./counter
sudo ./hwbp pid x.address 3 #pidとx.addressはcounterに応じて変更。
counterを実行し、hwbpでxのアドレスにハードウェアブレークポイント(Read/Write)を設置します。
counter自身がxにアクセスする毎に、デバッガであるhwbpがアクセス時のRIPの値を取得します。
hwbpの引数の3はブレークする条件です。
0:Execute(実行時のみ)
1:Write Only(メモリ書き込み時のみ)
2:未定義
3:Read/Write(メモリ読み込み/書き込み時)
引数を変えたりして、色々と試してみましょう。
実装
各レジスタの役割は最初に引用させていただいたQiitaの方の記事に詳しく書いてあります。
DR7レジスタの表は下記サイトが理解しやすかったので引用させて頂きます。
x86asm.net
DR7レジスタについて
bit(s) | mnemonic | description |
---|---|---|
31-30 | LEN3 | Length of Breakpoint #3 |
29-28 | R/W3 | Type of Transaction to Trap for Breakpoint #3 |
27-26 | LEN2 | Length of Breakpoint #2 |
25-24 | R/W2 | Type of Transaction to Trap for Breakpoint #2 |
23-22 | LEN1 | Length of Breakpoint #1 |
21-20 | R/W1 | Type of Transaction to Trap for Breakpoint #1 |
19-18 | LEN0 | Length of Breakpoint #0 |
17-16 | R/W0 | Type of Transaction to Trap for Breakpoint #0 |
6 | L3 | Local Exact Breakpoint #3 Enabled |
4 | L2 | Local Exact Breakpoint #2 Enabled |
2 | L1 | Local Exact Breakpoint #1 Enabled |
0 | L0 | Local Exact Breakpoint #0 Enabled |
ブレークポイントのサイズと値の対応
00b | 1byte |
01b | 2 byte |
10b | 未定義(プロセッサによっては8 byte) |
11b | 4 byte |
ブレークポイントのタイプと値の対応
00b | 実行時にブレークする。この時サイズは1byte(値:00b)でなければいけない。 |
01b | メモリ書き込み時ブレークする。 |
10b | 未定義(※CR4のDEフラグによって異なる。) |
11b | メモリ読み込み時または書き込み時にブレークする。実行時はブレークしない。 |
まずDR0,DR1,DR2,DR3のレジスタにはブレークポイントを設置するアドレスを代入します。
次にDR7レジスタに上記表に応じて各bitに値を代入します。
//DR7レジスタの現在の状態を取得 int Dr7 = ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[7]), 0); //slotに応じて0,2,4,6bit目にフラグを立てる Dr7 |= 1<<(available*2); //slotに応じて16,20,24,28bit目にブレークポイントのタイプを代入 Dr7 |= condition<<(available*4+16); //slotに応じて18,22,26,30bit目にブレークポイントの長さ(上記表の対応で)を代入 Dr7 |= length<<(available*4+18); //DR7レジスタの値を更新する。 ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[7]), Dr7);
このような形でハードウェアブレークポイントを設置します。
後はデバッガの原理で発生した例外のSIGTRAPを補足し、制御します。
まとめ
今回はハードウェアブレークポイントについて簡潔に説明させていただきました。
特にWatchPointを有効に利用すると、プロセスメモリ解析からその裏で動く処理を特定することが可能です。
「自分で実際に実装してみる」ことを通して解析の楽しさを感じたり、セキュリティ対策について学ぶきっかけとなれば幸いです。
ソースコード
counter.c
#include<stdio.h> #include<unistd.h> int main() { int x = 0; printf("pid:%d\n",getpid()); while(1) { #if defined(__i386__) printf("x.value = %d x.addrss = 0x%x\n",x,(unsigned int)&x); #else printf("x.value = %d x.addrss = 0x%llx\n",x,(unsigned long long)&x); #endif x++; sleep(5); } return 0; }
hwbp.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.h> #include <stddef.h> #define CONDITION_EXECUTE 0x0 #define CONDITION_WRITE_ONLY 0x1 #define CONDITION_IO_READ_WRITE 0x2//x86・x64では未実装 #define CONDITION_READ_WRITE 0x3 #define LENGTH_BYTE 0x0 #define LENGTH_WORD 0x1 #define LENGTH_QWORD 0x2 #define LENGTH_DWORD 0x3 char hardware_breakpoints[4] = {0,0,0,0}; int get_slot(int pid) { int Dr6 = ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[6]), 0); int slot = 0; if (Dr6&0x1 && hardware_breakpoints[0] == 1) slot = 0; else if(Dr6&0x2 && hardware_breakpoints[1] == 1) slot = 1; else if(Dr6&0x4 && hardware_breakpoints[2] == 1) slot = 2; else if(Dr6&0x8 && hardware_breakpoints[3] == 1) slot = 3; else slot = -1; return slot; } int bp_del_hw(int pid,int slot) { //入力値の例外処理 if(!(0<=slot && 3>=slot)) return -1; if(hardware_breakpoints[slot]==0) return 0; hardware_breakpoints[slot] = 0; int Dr7 = ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[7]), 0); Dr7 &= ~(3<<slot*2); Dr7 &= ~(3<<slot*4+16); Dr7 &= ~(3<<slot*4+18); ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[slot]), 0); ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[7]), Dr7); return 1; } int bp_set_hw(int pid ,void * address, int length, int condition) { //入力値の例外処理 if(!(0<=length && 3>=length)) return -1; if(!(0<=condition && 3>=condition)) return -1; int available = 0; if(0 == hardware_breakpoints[0]) available = 0; else if(0 == hardware_breakpoints[1]) available = 1; else if(0 == hardware_breakpoints[2]) available = 2; else if(0 == hardware_breakpoints[3]) available = 3; else{ return -1; } hardware_breakpoints[available] = 1; int Dr7 = ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[7]), 0); Dr7 |= 1<<(available*2); Dr7 |= condition<<(available*4+16); Dr7 |= length<<(available*4+18); int r1 = ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[available]), address); int r2 = ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[7]), Dr7); if(r1==0 && r2==0) { return 1; } else { printf("set breakpoint error\n"); return -1; } } int main(int argc, char *argv[]) { pid_t pid=atoi(argv[1]); unsigned long long address=strtoll(argv[2],NULL,0); int condition = atoi(argv[3]); //対象プロセスにアタッチ ptrace(PTRACE_ATTACH, pid, 0, 0); waitpid(pid, NULL, 0); //ハードウェアブレークポイントを設置 bp_set_hw(pid,address,LENGTH_BYTE,condition); //プロセスを再開させる int status; ptrace(PTRACE_CONT, pid, 0, 0); printf("Continuing.\n"); while(1) { //ブレイクするまで待機する waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("Program exited normally.\n"); exit(0); } if (WIFSTOPPED(status)) printf("Breakpoint.\n"); else exit(1); int signum = WSTOPSIG(status); if(signum==SIGTRAP) { //Dr0-3のどのレジスタ番号であるかを取得 //int slot = get_slot(pid); //printf("slot:%d\n",slot); //レジスタの値を取得 struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, 0, ®s); #if defined(__i386__) printf("EIP:0x%x\n",(unsigned int)regs.eip); #else printf("RIP:0x%llx\n",(unsigned long long)regs.rip); #endif ptrace(PTRACE_CONT,pid,0,0); } else if(signum==19 ||signum==21) { //シグナル番号19と21は無視する。 ptrace(PTRACE_CONT, pid, 0, 0); } else { ptrace(PTRACE_CONT,pid,0,signum); } } //bp_del_hw(pid,0); //プロセスからデタッチする。処理が動き出す。 //ptrace(PTRACE_DETACH, pid, 0, 0); return 0; }
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com
株式会社Ninjastarsエンジニア
一瀬健二郎