はじめに
この記事ではUbuntu上で生成した実行ファイルを逆アセンブルします。
ついでに実行ファイルを直接修正して挙動を変えます。
環境: Ubuntu 23.04
サンプルコード1
Hello world!
を5回出力するプログラムです。
#include <stdio.h>
int main(void) {
for(int i = 0; i < 5; i++) {
printf("Hello world!\n");
}
return 0;
}
$ gcc sample1.c -o sample1
$ ./sample1
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
この実行ファイルをobjdump
コマンドで逆アセンブルします。
main関数の部分は以下の通りです。
$ objdump -d -M intel sample1
...
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
1151: 48 83 ec 10 sub rsp,0x10
1155: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
115c: eb 13 jmp 1171 <main+0x28>
115e: 48 8d 05 9f 0e 00 00 lea rax,[rip+0xe9f] # 2004 <_IO_stdin_used+0x4>
1165: 48 89 c7 mov rdi,rax
1168: e8 e3 fe ff ff call 1050 <puts@plt>
116d: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1
1171: 83 7d fc 04 cmp DWORD PTR [rbp-0x4],0x4
1175: 7e e7 jle 115e <main+0x15>
1177: b8 00 00 00 00 mov eax,0x0
117c: c9 leave
117d: c3 ret
...
出力する回数を変える
まず出力する回数を変えます。
ループカウンタに1加算して条件式を評価しているは以下の部分です。
116d: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1
1171: 83 7d fc 04 cmp DWORD PTR [rbp-0x4],0x4
1175: 7e e7 jle 115e <main+0x15>
rbp-0x4
番地の値に1加算してその値が4以下なら文字列出力処理へジャンプしています。
0x1171
番地の比較命令83 7d fc 04
の04
が終了条件のリテラル値の様です。
この値を変更してループ回数が変化するかを確認します。
dd
コマンドを使って実行ファイルを書き換えます。
$ echo -en '\x09' | dd of=sample1 bs=1 seek=$((0x1174)) count=1 conv=notrunc
1+0 records in
1+0 records out
1 byte copied, 0.000391533 s, 2.6 kB/s
$ objdump -d -M intel sample1 | grep 1171:
1171: 83 7d fc 09 cmp DWORD PTR [rbp-0x4],0x9
リテラル値が0x4
から0x9
に変わっています。
修正した実行ファイルを動かします。
$ ./sample1
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
文字列が10回出力されるようになりました。
出力する文字列を変える
次に文字列出力をしている部分を読みます。
115e: 48 8d 05 9f 0e 00 00 lea rax,[rip+0xe9f] # 2004 <_IO_stdin_used+0x4>
1165: 48 89 c7 mov rdi,rax
1168: e8 e3 fe ff ff call 1050 <puts@plt>
rdiレジスタにrip + 0xe9f
のアドレスを代入してputs関数を呼び出しています。
ripレジスタには次の命令のアドレスが格納されているのでrip + 0xe9f
は
0x1165 + 0xe9f = 0x2004
となります。
実行ファイルの0x2004
番地のデータをhexdump
コマンドで確認します。
$ hexdump -C sample1 | grep 00002000
00002000 01 00 02 00 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 |....Hello world!|
ここには画面に出力される文字列がありました。
world!
の部分をbinary
に書き換えて実行します。
$ echo -en '\x62\x69\x6e\x61\x72\x79' | dd of=sample1 bs=1 seek=$((0x200a)) count=6 conv=notrunc
6+0 records in
6+0 records out
6 bytes copied, 0.00053478 s, 11.2 kB/s
$ hexdump -C sample1 | grep 00002000
00002000 01 00 02 00 48 65 6c 6c 6f 20 62 69 6e 61 72 79 |....Hello binary|
$ ./sample1
Hello binary
Hello binary
Hello binary
Hello binary
Hello binary
Hello binary
Hello binary
Hello binary
Hello binary
Hello binary
下記のオフセット値0xe9f
も修正します。
文字列のアドレスを6バイトずらしてみます。
115e: 48 8d 05 9f 0e 00 00 lea rax,[rip+0xe9f] # 2004
$ echo -en '\xa5' | dd of=sample1 bs=1 seek=$((0x1161)) count=1 conv=notrunc
1+0 records in
1+0 records out
1 byte copied, 0.000426931 s, 2.3 kB/s$ objdump -d -M intel sample1 | grep 115e:
115e: 48 8d 05 a5 0e 00 00 lea rax,[rip+0xea5] # 200a <_IO_stdin_used+0xa>
$ ./sample1
binary
binary
binary
binary
binary
binary
binary
binary
binary
binary
文字列が6文字ずれて出力されました。
サンプルコード2
8192分の1の確率でSucess
が、それ以外の場合はFailure
が出力されるプログラムです。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
srand((unsigned int)time(NULL));
if (rand() % 8192 != 0) {
printf("Failure\n");
return 1;
}
printf("Success\n");
return 0;
}
強運がないとSuccess
を出力させるのは難しそうです。。
$ gcc sample2.c -o sample2
$ ./sample2
Failure
$ ./sample2
Failure
$ ./sample2
Failure
$ ./sample2
Failure
$ ./sample2
Failure
常にSuccessを出力させる
このプログラムを逆アセンブルすると以下の様に出力されます。
$ objdump -d -M intel sample2
...
00000000000011a9 <main>:
11a9: f3 0f 1e fa endbr64
11ad: 55 push rbp
11ae: 48 89 e5 mov rbp,rsp
11b1: bf 00 00 00 00 mov edi,0x0
11b6: e8 e5 fe ff ff call 10a0 <time@plt>
11bb: 89 c7 mov edi,eax
11bd: e8 ce fe ff ff call 1090 <srand@plt>
11c2: e8 e9 fe ff ff call 10b0 <rand@plt>
11c7: 25 ff 1f 00 00 and eax,0x1fff
11cc: 85 c0 test eax,eax
11ce: 74 16 je 11e6 <main+0x3d>
11d0: 48 8d 05 2d 0e 00 00 lea rax,[rip+0xe2d] # 2004 <_IO_stdin_used+0x4>
11d7: 48 89 c7 mov rdi,rax
11da: e8 a1 fe ff ff call 1080 <puts@plt>
11df: b8 01 00 00 00 mov eax,0x1
11e4: eb 14 jmp 11fa <main+0x51>
11e6: 48 8d 05 1f 0e 00 00 lea rax,[rip+0xe1f] # 200c <_IO_stdin_used+0xc>
11ed: 48 89 c7 mov rdi,rax
11f0: e8 8b fe ff ff call 1080 <puts@plt>
11f5: b8 00 00 00 00 mov eax,0x0
11fa: 5d pop rbp
11fb: c3 ret
...
乱数が0かどうかを確認しているのは以下の部分です。
11c2: e8 e9 fe ff ff call 10b0 <rand@plt>
11c7: 25 ff 1f 00 00 and eax,0x1fff
11cc: 85 c0 test eax,eax
11ce: 74 16 je 11e6 <main+0x3d>
0x11ce
番地のje
命令をjmp
命令に書き換えます。
これで乱数に関わらず常にSuccess
の出力処理を行うようになります。
$ echo -en '\xeb' | dd of=sample2 bs=1 seek=$((0x11ce)) count=1 conv=notrunc
1+0 records in
1+0 records out
1 byte copied, 0.000151262 s, 6.6 kB/s
$ objdump -d -M intel sample2 | grep 11ce:
11ce: eb 16 jmp 11e6 <main+0x3d>
$ ./sample2
Success
$ ./sample2
Success
$ ./sample2
Success
$ ./sample2
Success
$ ./sample2
Success
おわりに
実行ファイルを解析して直接修正するのは面白かったです。
アセンブリ言語を読むのは得意ではないので、徐々に慣れていきたいです。
参考文献