# 2020 程安 Reverse筆記1 (逆向的各種trick) ###### tags: `程式安全` ## Compiler Walk Through 1. hello.c ```c= #include <stdio.h> #define A 'hello' int main(){ puts(A); return 0; } ``` 2. Preprocecssor把`define`展開 `gcc -E -o hello.i hello.c` preprocessor會 ```c= //cat hello.i ... extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); # 858 "/usr/include/stdio.h" 3 4 extern int __uflow (FILE *); extern int __overflow (FILE *, int); # 873 "/usr/include/stdio.h" 3 4 # 2 "hello.c" 2 # 4 "hello.c" int main(){ puts("hello"); return 0; } ``` 3. Compiler將code編譯成assembly `gcc -S hello.s hello.i` ```asm= .file "hello.c" .text .section .rodata .LC0: .string "hello" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi movl $0, %eax call put@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .file "hello.c" .text .section .rodata .LC0: .string "hello" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi call puts@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4: ``` 4. Assembler組譯器將assembly轉成machine code `gcc -c -o hello.o hello.s` 此時可以用`objdump`dump出來,但還無法執行,因為reloc的部分還沒有處理 ex.第11行應該是`call puts@plt`,但因為library還沒link進來,所以會直接跳到下一行 ```asm= hello.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: f3 0f 1e fa endbr64 4: 55 push rbp 5: 48 89 e5 mov rbp,rsp 8: 48 8d 3d 00 00 00 00 lea rdi,[rip+0x0] # f <main+0xf> f: e8 00 00 00 00 call 14 <main+0x14> 14: b8 00 00 00 00 mov eax,0x0 19: 5d pop rbp 1a: c3 ``` 5. Linker負責link library function `gcc -o hello hello.o` 這時才會變成真正可執行的binary ## Calling Convention ### cdecl (C預設的呼叫慣例,Linux全部都是這種) : caller清空stack - 由<font color='red'>caller</font>清空stack - ex. `main`執行`myadd(1,2)` ```asm main: ... push 2 push 1 call myadd add esp, 0x8 ; 由main(caller)清空stack ... ; 0x8 = 2個參數 ``` - 優: 實作不定長度參數的function很方便(ex `printf`等format系列的) - 缺: 浪費code空間,因為不用每次呼叫function後都要加上`add rsp,...`,這個指令佔3byte ### stdcall (Win32 API) : callee清空stack - 聲明語法`int __stdcall function(arg,...)` - 如何用gcc編譯stdcall: `int __attribute__((stdcall))function(arg,...)` - 由<font color='red'>callee</font>清空stack - ex. `main`執行`myadd(1,2)` ```asm main: ... push 2 push 1 call myadd ... myadd: mov ..., [ebp+0x8] mov ..., [ebp+0xc] ... leave ret 0x8 ; 由myadd(callee)清空stack ; 0x8 = 2個參數 ``` - 缺: 不定長度參數的function無法處理 - 優: 節省code空間 ### fastcall - 聲明語法`int __fastcall function(arg,...)` - 前兩個<font color='red'>DWORD以下size</font>的參數分別用`rcx`及`rdx`傳遞,後面的參數一樣用stack傳,由<font color='red'>callee</font>清空stack - 優: 較快 ### thiscall - `thiscall`無法被明確聲明,因為`thiscall`是c++中class中function的預設呼叫慣例 - 如果參數個數確定,`this`指標通過<font color='red'>`ecx`</font>傳遞 如果參數不定長度,`this`指標由<font color='red'>stack</font>傳遞(在參數都push上去之後) - ex myadd1 ```clike= #include <iostream> #include <stdarg.h> using namespace std; //va_list class test{ public: int myadd1(int a, int b){ //參數固定長度 return a+b; } int myadd2(int num,...){ //參數不定長度 va_list arg; va_start(arg, num); int res=0; for (int i = 0; i < num; i++) { res += i; } return res; } }; int main(){ test A; int res1 = A.myadd1(1,2); int res2 = A.myadd2(3, 1, 2, 3); cout << res1 << endl; cout << res2 << endl; return 0; } ``` - 轉成assembly後 ```asm 00401410 <_main>: 401410: 8d 4c 24 04 lea ecx,[esp+0x4] 401414: 83 e4 f0 and esp,0xfffffff0 401417: ff 71 fc push DWORD PTR [ecx-0x4] 40141a: 55 push ebp 40141b: 89 e5 mov ebp,esp 40141d: 51 push ecx 40141e: 83 ec 34 sub esp,0x34 401421: e8 3a 06 00 00 call 401a60 <___main> 401426: 8d 45 ef lea eax,[ebp-0x11] 401429: c7 44 24 04 02 00 00 mov DWORD PTR [esp+0x4],0x2 401430: 00 401431: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 401438: 89 c1 mov ecx,eax ; 參數固定,用ecx傳遞this指標 40143a: e8 89 28 00 00 call 403cc8 <__ZN4test6myadd1Eii> 40143f: 83 ec 08 sub esp,0x8 401442: 89 45 f4 mov DWORD PTR [ebp-0xc],eax 401445: c7 44 24 10 03 00 00 mov DWORD PTR [esp+0x10],0x3 40144c: 00 40144d: c7 44 24 0c 02 00 00 mov DWORD PTR [esp+0xc],0x2 401454: 00 401455: c7 44 24 08 01 00 00 mov DWORD PTR [esp+0x8],0x1 40145c: 00 40145d: c7 44 24 04 03 00 00 mov DWORD PTR [esp+0x4],0x3 401464: 00 401465: 8d 45 ef lea eax,[ebp-0x11] 401468: 89 04 24 mov DWORD PTR [esp],eax ; 參數不固定,用stack傳遞指標 40146b: e8 70 28 00 00 call 403ce0 <__ZN4test6myadd2Eiz> 401470: 89 45 f0 mov DWORD PTR [ebp-0x10],eax 401473: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 401476: 89 04 24 mov DWORD PTR [esp],eax 401479: b9 .byte 0xb9 ``` ## Optimization最佳化 - `gcc --help=optimizers`看有哪些最佳化選項 `gcc -Q -O1 --help=optimizers`可以看在`O1`哪些optimizer是開啟或關閉,有`O0-O3`(`O0`表沒有最佳化) - 常用 - 常數摺疊(constant folding): 把結果直接算出來 - ex. `a = 1*2*3*4;` $\Rightarrow$ `a = 24;` - 常數傳播(constant propogate) - ex. ```c int x = 14; int y = x - 5; return x+y; ``` $\Rightarrow$ propogate ```c int x = 14; int y = 14 - 5; return 14 + y ; ``` $\Rightarrow$ folding ```c int x = 14; int y = 9; return 14 + y ; ``` $\Rightarrow$ propogate ```c int x = 14; int y = 9; return 14 + 9 ; ``` $\Rightarrow$ folding ```c int x = 14; //被拔掉 int y = 9; //被拔掉 return 23 ; //不會用到x跟y,所以optimization會拔掉x跟y的宣告 ``` - Tail call: 返回值是call某function後的結果 Tail Recursion: 返回值為遞迴呼叫的結果 - ex. ```c int search(int data){ return advance_search(data); } ``` - 為了減少stack的使用 - ex. ```c= #include <stdio.h> int sum(int n) { if (n == 0) return 0; else return sum(n - 1) + n; } int main(){ printf('%d',sum(10)); return 0; } //gcc -o add add.c //gcc -o add2 add.c -O2 //optimize ``` - add ```asm= sum: push rbp mov rbp,rsp sub rsp,0x10 mov DWORD PTR [rbp-0x4],edi cmp DWORD PTR [rbp-0x4],0x0 jne 1165 <sum+0x1c> mov eax,0x0 jmp 1177 <sum+0x2e> mov eax,DWORD PTR [rbp-0x4] sub eax,0x1 mov edi,eax call 1149 <sum> mov edx,DWORD PTR [rbp-0x4] add eax,edx leave ret ``` - add2 ```asm= sum: xor eax,eax test edi,edi ;檢查edi是否=0 je 1197 <sum+0x17> ; =0跳到第9行 nop WORD PTR [rax+rax*1+0x0] add eax,edi sub edi,0x1 ; 若edi - 1 = 0會設定ZF jne 1190 <sum+0x10> ; if ZF=0,跳第2行 ret ``` - magic trick : 減少除法(div)的使用 - 想法 1. 用乘法處理 2. 如果是2的冪次方更好,用shift的即可 - ex. 計算`x / 9` $\frac{x}{9} = \frac{x}{2^n} \cdot \frac{2^n}{9}$ `n`可由編譯器決定,因此compile時就可以事先將$\frac{2^n}{9}$算好 (ex. `n`取`33`, `hex(ceil(2^33/9)) = 0x38e38e39`) $\Rightarrow \frac{x}{9} = \frac{x \cdot 0x38e38e39}{2^{33}}$ $\rightarrow$ 即將$x\cdot0x38e38e39$向右移33bit = 右移1bit - 如果是`idiv`還要多考慮sign的問題 - 因為c的除法是<font color='red'>向下取整</font>, i.e $-3/2 = -2$, $3/2 = 1$ - 除數為負 $\lfloor\frac{-3}{2}\rfloor + 1 = \lceil\frac{-3}{2}\rceil$ - 除數為正 $\lfloor\frac{3}{2}\rfloor + 0 = \lceil\frac{3}{2}\rceil$ - `0x38e38e39`必為整數,因此判斷乘數的正負即可 - 第31bit即sign bit - sign bit = 0: 算數右移31bit得`0` - `結果 = (x*0x38e38e39 >> 1) - 0` - sign bit = 1: 算術右移31bit得`11...1 = -1` - `結果 = (x*0x38e38e39 >> 1) - (-1)` ## Class and Struct ### struct內對齊: `min(datatype_size, pack)` - ex 計算struct佔的記憶體空間 ```c= #include <stdio.h> typedef struct { unsigned char v1; //1byte unsigned int v2; //4byte unsigned short v3; //2byte unsigned char v4[3]; //3byte unsigned long long v5; //8byte } myStrA; void main() { // 輸出struct大小 printf("Size of myStrA = %d\n", sizeof(myStrA)); // 建立方便辨識的測試資料 myStrA a; a.v1 = 0x11; a.v2 = 0x22334455; a.v3 = 0x6677; a.v4[0] = 0x12; a.v4[1] = 0x34; a.v4[2] = 0x56; a.v5 = 0x8899AABBCCDDEEFF; // 輸出 myStrA 內部實際的資料 unsigned char *ptr = (unsigned char *) &a; int i, s = sizeof(myStrA); printf("a = "); for (i = 0; i < s; ++i) { printf("%02X ", ptr[i]); } printf("\n"); } /* 結果: Size of myStrA = 24 a = 11 00 00 00 55 44 33 22 77 66 12 34 56 00 00 00 FF EE DD CC BB AA 99 88 |v1| | v2 | v3 | v4 | | v5 | ``` :::info 可以發現雖然v1只有1byte,但在記憶體上還是保留了4byte, 而short佔2byte卻保留了8byte 原因是因為preprocessor在處理時預設會以<font color='red'>該datatype長度的倍數</font>對其進行對齊 - v1為char,char以1byte的倍數對齊,此時offset = 0x0,放上v1後offset = 0x1 - v2為int,int以4byte的倍數對齊,因此compiler跳過3byte,讓offset = 0x4再放上v2,offset = 0x8 - v3為short,short以2byte得倍數對齊,8為2的倍數,因此直接放上v3,offset = 0xa - v4為一個char array,char以1byte的倍數對齊,因此直接把三個element放上去,offset 0xd - v5為long int,以8byte的倍數對齊,因此跳過3byte,讓offset為0x10再放上v5 ::: - 此外,可以用`#pragma pack()`指定對齊的長度(1,2,4,16,沒指定預設為8),compiler會<font color='blue'>拿指定的長度跟datatype預設的長度</font>做比較,並<font color='red'>取小的那個</font> - ex. code前面加上`#pragma pack(2)`之後結果變為 ``` Size of myStrA = 20 a = 11 00 55 44 33 22 77 66 12 34 56 00 FF EE DD CC BB AA 99 88 |v1| | v2 | v3 | v4 | | v5 | ``` :::danger - v1放完後offset = 0x1, v2為int,因為指定2byte對齊 故取`min(2,4) = 2`對齊,所以v1放完只跳過1byte - v4放完後offset = 0xb v5取`min(2,8) = 2`對齊,因此只跳過1byte ::: ### struct本身也要對齊: `min(max_size_member, pack)` - ex ```c= #include <stdio.h> typedef struct { unsigned char v1; //1byte unsigned int v2; //4byte unsigned short v3; //2byte } myStrA void main() { // 輸出struct大小 printf("Size of myStrA = %d\n", sizeof(myStrA)); // 建立方便辨識的測試資料 myStrA a; a.v1 = 0x11; a.v2 = 0x22334455; a.v3 = 0x6677; // 輸出 myStrA 內部實際的資料 unsigned char *ptr = (unsigned char *) &a; int i, s = sizeof(myStrA); printf("a = "); for (i = 0; i < s; ++i) { printf("%02X ", ptr[i]); } printf("\n"); } /* 結果 Size of myStrA = 12 a = 11 00 00 00 55 44 33 22 77 66 00 00 |v1| | v2 | v3 | ``` :::info - struct內應該佔 1 + 3 + 4 + 2 = 10byte,卻佔了12byte - 因為struct本身也要對齊, 取`min(max_size_member, pack) = min(sizeof(int), pack=8) = min(4,8) = 4`對齊, 因此把struct補到12byte ::: ### C++ name mangle - 為了解決不能使用跟c library一樣函數名稱的問題,C++加入了namespace來解決 - C++的繼承後Override或Overloading(多載)可能造成多個函數共用一個函數名稱(差別在<font color='blue'>參數個數或型態不同</font>) `Note: override要父class在該函數前面宣告為virtual(虛函式),繼承的class才可以改寫` - 為了讓compiler可以區分多個不同的同名函式,必須要對產生的符號動一點手腳,這就是<font color='red'>Name Mangling</font> ```c= #include <iostream> using namespace std; int func(int a){ return a; } float func(float b){ return b; } class C { public: int func(int c){ return c; }; class C2 { public: int func(int d){ return d; } }; C2* do_something(){ C2 *c2 = new C2; return c2; } }; namespace N { int func(int e){ return e; }; class C { public: int func(int f){ return f; }; }; } class C1:public C{ //C1繼承C public: float func(float g){ //override return g; } }; int main(){ int a = func(1); //func(int) float b = func(2.5f); //func(float) //class C C *c = new C; int c_ = c->func(3); //C::func(int) //class C::C2 C::C2 *d = c->do_something(); int d_ = d->func(4); //C::C2::func(int) //namespace N int e = N::func(5); //N::func(int) N::C *f = new N::C; //class N::C int f_ = f->func(6); //N::C::func(int) C1 *g = new C1; float g_ = g->func(7.5f); //C1::func(float) cout << a << endl; cout << b << endl; cout << c_ << endl; cout << d_ << endl; cout << e << endl; cout << f_ << endl; cout << g_ << endl; return 0; } ``` :::info objdump出來之後,每個function的name變成 a. `func(int)` $\Rightarrow$ `_Z4funci` b. `func(float)` $\Rightarrow$ `_Z4funcf` c. `C::func(int)` $\Rightarrow$ `_ZN1C4funcEi` d. `C::C2::func(int)` $\Rightarrow$ `_ZN1C2C24funcEi` e. `N::func(int)` $\Rightarrow$ `_ZN1N4funcEi` f. `N::C::func(int)` $\Rightarrow$ `_ZN1N1C4funcEi` g. `C1::func(float)` $\Rightarrow$ `_ZN2C14funcEf` <br> - 可以用`c++filt <mangling_name>`來還原 ![](https://i.imgur.com/fceF9av.png) ::: ### C++ virtual table - 當new一個class,會去找該class的constructor(或其父class的constructor) - 會把this(struct的第一個member)設成一個function(該類別的第一個virtual function) - ex ```clike= #include <string> #include <algorithm> //for std::reverse #include <iostream> using namespace std; class human{ protected: string nation; public: virtual string getNation(){ return this->nation; } virtual string ReverseNation(){ reverse(nation.begin(), nation.end()); } }; class Taiwanese:public human{ public: Taiwanese(){ nation = "Taiwan"; } string getNation(){ return "You should be a Chinese!!"; } }; int main(){ Taiwanese* MinYeon = new Taiwanese(); cout << *(string *)(((char*)MinYeon)+8) << endl; // origin nation //MinYeon->nation is not work, since nation is protected cout << MinYeon->getNation() << endl; } /* output: Taiwan You should be a Chinese!! ``` 開ida看之後 - `human`的constructor ![](https://i.imgur.com/EEMaAcn.png =400x) ```= 03CB0 ; `vtable for'Taiwanese 03CB0 _ZTV9Taiwanese dq 0 ; offset to this 03CB8 dq offset _ZTI9Taiwanese ; `typeinfo for'Taiwanese 03CC0 off_3CC0 dq offset _ZN9Taiwanese9getNationB5cxx11Ev 03CC0 ; DATA XREF: Taiwanese::Taiwanese(void)+1D↑o 03CC8 dq offset _ZN5human13ReverseNationB5cxx11Ev 03CD0 public _ZTV5human ; weak 03CD0 ; `vtable for'human 03CD0 _ZTV5human dq 0 ; offset to this 03CD8 dq offset _ZTI5human ; `typeinfo for'human 03CE0 off_3CE0 dq offset _ZN5human9getNationB5cxx11Ev 03CE0 ; DATA XREF: human::human(void)+10↑o 03CE0 ; human::~human()+10↑o 03CE8 dq offset _ZN5human13ReverseNationB5cxx11Ev ``` - 每個類別的struct最前面(this)都指向自己的vtable,vtable的每個entry分別指向該類別中的virtual function - 一個繼承父類別的子類別,也會繼承父類別的vtable,如果有override某個函式,則對應的vtable entry會指向override的function(ex.第4行),否則指向父類別的function(ex.第6行) - ex. `myDerived`繼承`myBase`,並override`func2` ![](https://i.imgur.com/zZpUGlO.png =400x) :::warning IDA tricks `shift + F1`可以開啟Local types,可以右鍵edit修改struct member ::: ### C++ Polymorphism(多態/多型)