今回やること
最近、リバースエンジニアリングについて勉強している。
しかし、最近取り上げる題材は分かりやすいものばかり。
その原因は以下の2つ!
- 最適化処理をしていない
- シンボル情報が残っている
そりゃ、簡単なわけだ。
というわけで、これからは難しいものにも挑戦したいので、今回は「最適化処理で行われること」と「シンボル情報に含まれるもの」について記事を書いていこうと思う。
最適化処理とは?
コンパイラがソースコードをコンパイルする際に、プログラムの動きは変えずに、より効率的で高速な機械語へ作り変える処理のこと。
同じソースコードをコンパイルしても、最適化次第ではアセンブリコードに変化が起きる。
実際に確認してみる
0-ソースコード
使用するコードと動きは以下
動きは次の通り
main()実行
↓
引数に足したい数字を入れてadd()を呼び出す
↓
引数で受け取った数字を足して、戻り値として返す
↓
printfで出力
↓
終了
#include <stdio.h>
long add(long a, long b, long c, long d) {
return a + b + c + d;
}
int main() {
long x = add(1, 2, 3, 4);
printf("%ld\n", x);
return 0;
}
1-コンパイル
このソースコードを最適化のレベル別にコンパイルしてみる。
今回は最適化なし(オプション無し)と最適化有り(-O2 オプリョン)でコンパイルする
【最適化無し】
$ gcc main.c -o level0
【最適化有り】
$ gcc main.c -o level2 -O2
2-アセンブリとデコンパイル
それぞれ、gdbでmain関数のアセンブリを見てみると同時にghidraでデコンパイルを見てみる
【最適化無し】
- gdb
$gdb ./level0
>>> disas main
0x0000555555555168 <+0>: push rbp
0x0000555555555169 <+1>: mov rbp,rsp
0x000055555555516c <+4>: sub rsp,0x10
0x0000555555555170 <+8>: mov ecx,0x4
0x0000555555555175 <+13>: mov edx,0x3
0x000055555555517a <+18>: mov esi,0x2
0x000055555555517f <+23>: mov edi,0x1
0x0000555555555184 <+28>: call 0x555555555139 <add>
0x0000555555555189 <+33>: mov QWORD PTR [rbp-0x8],rax
0x000055555555518d <+37>: mov rax,QWORD PTR [rbp-0x8]
0x0000555555555191 <+41>: mov rsi,rax
0x0000555555555194 <+44>: lea rax,[rip+0xe69]
0x000055555555519b <+51>: mov rdi,rax
0x000055555555519e <+54>: mov eax,0x0
0x00005555555551a3 <+59>: call 0x555555555030 <printf@plt>
0x00005555555551a8 <+64>: mov eax,0x0
0x00005555555551ad <+69>: leave
0x00005555555551ae <+70>: ret
【最適化有り】
- gdb
$gdb ./level2
>>> disas main
0x0000000000001050 <+0>: sub rsp,0x8
0x0000000000001054 <+4>: mov esi,0xa
0x0000000000001059 <+9>: lea rdi,[rip+0xfa4] # 0x2004
0x0000000000001060 <+16>: xor eax,eax
0x0000000000001062 <+18>: call 0x1030 <printf@plt>
0x0000000000001067 <+23>: xor eax,eax
0x0000000000001069 <+25>: add rsp,0x8
0x000000000000106d <+29>: ret
比べてみると、、、
最適化をしていないlevel0はソースコードに忠実なアセンブリになっている。
最適化を入れたlevel2は、計算も何もせずに最初から合計値をprintf関数で出力するだけになっている。また、add()関数も呼び出されなくなっている。
最適化をしたlevel2ではadd()関数は呼び出されなくなったが、実行ファイルの中には残っている。
- gdbで見てみる
>>> info func
~略~
0x0000555555555160 add
~略~
>>> disas add
Dump of assembler code for function add:
0x0000555555555160 <+0>: add rdi,rsi
0x0000555555555163 <+3>: add rdi,rdx
0x0000555555555166 <+6>: lea rax,[rdi+rcx*1]
0x000055555555516a <+10>: ret
End of assembler dump.
不要な関数まで削除するには、-ffunction-sectionsと-Wl,--gc-sectionsなどのリンカ最適化が必要になる。
最適化時の実行速度について
実行速度についてはclaud君を最大限利用して検証して、改めたまとめた記事が以下になります。
シンボル情報とは?
シンボル情報とは、「プログラム中の名前」と「実際のアドレス」を対応付ける情報のこと。
通常のgccでコンパイルした実行ファイルには、関数名やグローバル変数名などのシンボル情報が含まれている。シンボル情報は「名前」と「実際のアドレス」を対応付けるための情報であり、デバッガや解析ツールが関数名を表示できる理由となっている。
確認方法
先程作成したlevel0についてnmコマンドを使用して確認してみる
【シンボル情報あり】
アドレスと関数名が結びついていることがわかる
$nm level0
0000000000003de0 d _DYNAMIC
0000000000003fe8 d _GLOBAL_OFFSET_TABLE_
0000000000002000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000002108 r __FRAME_END__
000000000000200c r __GNU_EH_FRAME_HDR
0000000000004018 D __TMC_END__
000000000000210c r __abi_tag
0000000000004018 B __bss_start
w __cxa_finalize@GLIBC_2.2.5
0000000000004008 D __data_start
00000000000010f0 t __do_global_dtors_aux
0000000000003dd8 d __do_global_dtors_aux_fini_array_entry
0000000000004010 D __dso_handle
0000000000003dd0 d __frame_dummy_init_array_entry
w __gmon_start__
U __libc_start_main@GLIBC_2.34
0000000000004018 D _edata
0000000000004020 B _end
00000000000011b0 T _fini
0000000000001000 T _init
0000000000001050 T _start
0000000000001139 T add
0000000000004018 b completed.0
0000000000004008 W data_start
0000000000001080 t deregister_tm_clones
0000000000001130 t frame_dummy
0000000000001168 T main
U printf@GLIBC_2.2.5
00000000000010b0 t register_tm_clones
【シンボル情報無し】
シンボル情報の削除にはstripコマンドを使用する
$strip level0
削除後に改めてnmコマンドで確認してみる
$nm level0
nm: level0: シンボルがありません
他の確認方法として、readelf -Sでセクション一覧を見る方法もある。
シンボル情報が残っている場合には以下のようなセクションが存在する。
#strip未実行
$readelf -S level0
~略~
[28] .symtab SYMTAB 0000000000000000 00003038
0000000000000378 0000000000000018 29 18 8
[29] .strtab STRTAB 0000000000000000 000033b0
00000000000001df 0000000000000000 0 0 1
~略~
最後に
今回まとめたことによって、CTFでプレイしている解析対象とは違い、実際のプログラムの解析は難易度が上がることがわかった。
そのうち、チャレンジしていきたい。


