【バイナリ解析】実行ファイルの挙動を変えてみよう

## はじめに

この記事ではUbuntu上で生成した実行ファイルを逆アセンブルします。
ついでに実行ファイルを直接修正して挙動を変えます。

環境: Ubuntu 23.04

## サンプルコード1

`Hello world!`を5回出力するプログラムです。

```c:sample1.c
#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`が出力されるプログラムです。

```c:sample2.c
#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
```

## おわりに

実行ファイルを解析して直接修正するのは面白かったです。
アセンブリ言語を読むのは得意ではないので、徐々に慣れていきたいです。

## 参考文献

https://kazmax.zpp.jp/cmd/o/objdump.1.html

https://ja.wikipedia.org/wiki/Dd_(UNIX)

http://softwaretechnique.web.fc2.com/OS_Development/Tips/IA32_Instructions/J.html