pwndbgとは、gdbを便利にしてくれる拡張スクリプトです。
gdbとは、主にgccでコンパイルしたプログラムをデバッグするツールです(ざっくり)。
pwndbgはCTFerがよく使っているイメージがある。
この記事では、これ以上ない程単純なreversingの問題を、勉強のためにあえてpwndbg縛りで解きます
リバースエンジニアリングは、自作やCTFなど許可されたファイル以外にはしないようにしましょう(そんなレベルの記事ではないですが、念のため)。
環境
WSL2 (win10)
Ubuntu 20.04.4 LTS
pwndbgはこれ。
https://github.com/pwndbg/pwndbg
検索する時dbgとgdbを間違えやすいので注意(1敗)
gdb
とコマンドをうつことで、pwndbgを開ける。
(pwndbgの文字が出ていないならインストールに失敗してます)
動作が確認出来たら、一旦q
で閉じます。
ターゲット
ターゲットはこちら。
#include <stdio.h>
void main(){
int a,b,c;
a=2;
b=3;
c = a * b;
if (c == 20){
printf("C is %d\n",c);
printf("Awesome!\n");
}
else{
printf("C is %d\n",c);
}
}
user@WSL$ gcc tar1.c -o tar1
user@WSL$ ./tar1
C is 6
当然cは6になり、Awesome!とは表示されない。ので、Awesomeを出させます。
なおCTFなどではこんなソースコードは与えられません。pwndbgを使う前にGhidraなどの解析ソフトでデコンパイルし、到達したい場所のある程度の目星を付ける必要があります。
pwndbgで開いてみる。
user@WSL$ gdb -q tar1
pwndbg> start
pwndbgが色々ログを出してくれます。
startコマンドでは、mainやinitなどの丁度いい所で処理を一旦ストップしてくれます (便利!)
ちゃんと説明すると、丁度いい所にブレークポイントを貼っています。
pwndbg> start
Temporary breakpoint 1 at 0x1169
Temporary breakpoint 1, 0x0000555555555169 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
*RAX 0x555555555169 (main) ◂— endbr64
*RBX 0x5555555551d0 (__libc_csu_init) ◂— endbr64
*RCX 0x5555555551d0 (__libc_csu_init) ◂— endbr64
*RDX 0x7fffffffdd18 —▸ 0x7fffffffdf5c ◂— 'SHELL=/bin/bash'
*RDI 0x1
*RSI 0x7fffffffdd08 —▸ 0x7fffffffdf3f ◂— '/{path}/tar1'
R8 0x0
*R9 0x7ffff7fe0d60 (_dl_fini) ◂— endbr64
*R10 0x7
*R11 0x2
*R12 0x555555555080 (_start) ◂— endbr64
*R13 0x7fffffffdd00 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x0
*RSP 0x7fffffffdc18 —▸ 0x7ffff7deb083 (__libc_start_main+243) ◂— mov edi, eax
*RIP 0x555555555169 (main) ◂— endbr64
──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
► 0x555555555169 <main> endbr64
0x55555555516d <main+4> push rbp
0x55555555516e <main+5> mov rbp, rsp
0x555555555171 <main+8> sub rsp, 0x10
0x555555555175 <main+12> mov dword ptr [rbp - 0xc], 2
0x55555555517c <main+19> mov dword ptr [rbp - 8], 3
0x555555555183 <main+26> mov eax, dword ptr [rbp - 0xc]
0x555555555186 <main+29> imul eax, dword ptr [rbp - 8]
0x55555555518a <main+33> mov dword ptr [rbp - 4], eax
0x55555555518d <main+36> cmp dword ptr [rbp - 4], 0x14
0x555555555191 <main+40> jne main+78 <main+78>
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdc18 —▸ 0x7ffff7deb083 (__libc_start_main+243) ◂— mov edi, eax
01:0008│ 0x7fffffffdc20 —▸ 0x7ffff7ffc620 (_rtld_global_ro) ◂— 0x50a6600000000
02:0010│ 0x7fffffffdc28 —▸ 0x7fffffffdd08 —▸ 0x7fffffffdf3f ◂— '/{path}/tar1'
03:0018│ 0x7fffffffdc30 ◂— 0x100000000
04:0020│ 0x7fffffffdc38 —▸ 0x555555555169 (main) ◂— endbr64
05:0028│ 0x7fffffffdc40 —▸ 0x5555555551d0 (__libc_csu_init) ◂— endbr64
06:0030│ 0x7fffffffdc48 ◂— 0xaf2eec1db698b8c3
07:0038│ 0x7fffffffdc50 —▸ 0x555555555080 (_start) ◂— endbr64
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
► f 0 0x555555555169 main
f 1 0x7ffff7deb083 __libc_start_main+243
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>
出ているのはスタックの中身やアセンブリ言語など。今見るのはここ!
目的のif文はアセンブリ言語でcmp
なので、直前まで移動する。
pwndbg> n
をするごとに1行ずつ処理されるので、n
を連打。
cmp
の直前まで来た。
ここで、
pwndbg> i r
を入力すると
現在のレジスタの情報が見れます。便利!
先ほどのアセンブリ言語からeax
と0x14
を比較して分岐していることが分かるので、レジスタrax
が0x14
であれば望み通りの分岐に行ってくれる。
pwndbg> set $rax=0x14
を入力し、もう一度i r
で確認する。
レジスタの中身の書き換えに成功!
pwndbg> c
でそのまま最後まで一気に実行。
無事(?)、6となるはずのcが20に書き換えられ、出力されるはずのないコードが出力されました!
......
ところで、レジスタの中身を書き換えられるのなら、比較した結果のTrueかFalseを直接書き換えればいいと思いませんか?
最初から新しい状態でstart
し、cmpの後まで来ました。この状態で、レジスタの中身は次の通りです。
直前のcmpの結果はeflagsに格納されています。eflagsは各ビットがフラグとして扱われているレジスタです。
計算機の勉強をしたことがある人なら、桁上がりでCF(キャリーフラグ)がセットされる、などと聞いたことがあるのでは。
重要なのはZF(ゼロフラグ)です。これは6番目のフラグです。
直前の操作の結果が0になったときだけセットされます。
cmp命令の中身は実質引き算で、差が0であれば等しいとされるのです。
というわけで、eflagsの6番目を書き換えてZFを1にしてしまいましょう。
pwndbg> set $eflags |= (1<<6)
無事ZFが1になりました。
c
で最後まで行きます。
cが20ではなく6のままなのに、if(c==20)を通過してしまいました。
おわり
とても便利……
普通にデバッガとして使う場合でも超便利。
このツール、まだまだ色んなことができます。
(ブレークポイントを使わないgdb記事があっていいのか)
プログラムの任意の処理に対応するアドレスを特定すれば、そこにブレークポイントを貼って処理を開始することでブレークポイントで停止し、レジスタの中身などを確認することができます(効率的!)
おわり