###### tags: `CTF Write Up` # 2020 nullcon HackIM ## Reverse1: returminator ### 題目說明 這題給了一隻 ELF、python 腳本與一個二進制檔案,腳本會開啟二進位檔,依據給定的長度讀進來後利用popen執行ELF並將其餵入ELF中,最後將return code 回傳回腳本,當所有 return code 都跟原本給定的一樣時就正確了 --- 附個原始碼 ```python o = [296, 272, 272, 272, 296, 360, 272, 424, 272, 208, 120, 120, 120, 96, 120, 120, 120, 120, 120, 120, 120, 208, 120, 120, 208, 208, 208, 208, 208, 272, 120, 208, 208] r = [208, 225, 237, 20, 214, 183, 79, 105, 207, 217, 125, 66, 123, 104, 97, 99, 107 , 105, 109, 50, 48, 202, 111, 111, 29, 63, 223, 36, 0, 124, 100, 219, 32] cmd = ['./main'] rets = [] with open('blob', 'rb') as f: for offset in o: data = f.read(offset) p = subprocess.Popen(cmd, stdin=subprocess.PIPE) print(data) p.stdin.write(data) p.communicate() rets.append(p.returncode) print(rets) if all([rets[i] == r[i] for i in range(len(r))]): print('Yes!') else: print('No!') ``` ### 分析 再來就是分析 ELF,利用ida 來分析可以發現幾件事 - 會讀取一個名為 flag 的檔案 - 有個 bufer overflow - 正常 return code 必定為 0 ``` __int64 __fastcall main(__int64 a1, char **a2, char **a3) { char s; // [rsp+0h] [rbp-30h] FILE *stream; // [rsp+28h] [rbp-8h] puts("Hello world!"); stream = fopen("flag", "r"); if ( !stream ) exit(1); fgets(&s, 35, stream); fclose(stream); memset(dest, 0, 0x23uLL); strcpy(dest, &s); read(0, &s, 0x400uLL); return 0LL; } ``` 到這邊應該就會有想法了,傳進來的輸入會蓋上return address,利用 ROP 的概念去將 return code 改掉。 接下來要追蹤二進制檔裡的資料是不是就是 ROP gadget,下面這張可以看到前面的'a' 是在做 padding,接在後面的address `0x40112a` main return 後的第一個位址。 ``` 6161 6161 6161 6161 ..@.....aaaaaaaa 000018c0: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa 000018d0: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa 000018e0: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa 000018f0: a211 4000 0000 0000 a040 4000 0000 0000 ..@......@@..... 00001900: 9a11 4000 0000 0000 1500 0000 0000 0000 ..@............. 00001910: a411 4000 0000 0000 ea11 4000 0000 0000 ..@.......@..... 00001920: d611 4000 0000 0000 a211 4000 0000 0000 ..@.......@..... 00001930: a040 4000 0000 0000 9c11 4000 0000 0000 .@@.......@..... 00001940: 1400 0000 0000 0000 a811 4000 0000 0000 ..........@..... 00001950: ee11 4000 0000 0000 db11 4000 0000 0000 ..@.......@..... 00001960: bd11 4000 0000 0000 a411 4000 0000 0000 ..@.......@..... 00001970: c511 4000 0000 0000 ea11 4000 0000 0000 ..@.......@..... 00001980: ff11 4000 0000 0000 ..@..... ``` ### 自動化 讓我們來順一下整體脈絡 1. 腳本每次傳入的ROP chain 都不一樣,經過計算共有 32 次 2. ROP chain 和 從 flag 讀進去的值都會影響 return code 再來需要知道的是每個 ROP chain 執行了甚麼,但是 trace 32 次,真不是人做的,因此這邊要使用新學的工具,(動態插針 PIN)[https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool] 來幫我們輸出所執行的 rop chain #### PIN 這邊我就不花時間介紹這個工具了,網路上已經有很多介紹文了,我就用我的code來解釋就好。 為了分離出 ROP chain 的指令我們必須先知道 main ret 的 address、和結束的位置,抓出後就可以來寫 code 了 程式如下,簡單來說就是在每個 instruction 前都檢查是不是 ret 假如是的話就輸出跳過去的那個 address 的 assembly。 ``` ofstream OutFile; int static isretAddr = 0; int const static mainRetAddr = 0x00000000004012a6; int const static callExitAddr =0x00000000004011ff; int static traceFlag = 0; // This function is called before every instruction is executed VOID dumpAssembly(UINT64 insaddr,std::string insDis) { // fprintf(,"%lx\t%s\n",insaddr, insDis.c_str()); OutFile.setf(ios::showbase); OutFile << (int *)insaddr << ':'<< insDis.c_str()<< endl; std::cout << (int *)insaddr << ':'<< insDis.c_str()<< endl; } /* * Instrumentation routines */ VOID Instruction(INS ins, VOID *v) { if (mainRetAddr == (int)INS_Address(ins)){ traceFlag = 1; } else if(callExitAddr == (int)INS_Address(ins) ){ traceFlag = 0; } if(traceFlag == 1){ if( INS_IsRet(ins) ) { isretAddr = 1; INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)dumpAssembly, IARG_ADDRINT, INS_Address(ins), IARG_PTR, new string(INS_Disassemble(ins)),IARG_END); } else if(isretAddr){ INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)dumpAssembly, IARG_ADDRINT, INS_Address(ins), IARG_PTR, new string(INS_Disassemble(ins)),IARG_END); isretAddr = 0; } } } /* ===================================================================== */ /* Main */ /* ===================================================================== */ int main(int argc, char * argv[]) { // Initialize pin if (PIN_Init(argc, argv)) return Usage(); OutFile.open(KnobOutputFile.Value().c_str()); // Register Instruction to be called to instrument instructions INS_AddInstrumentFunction(Instruction, 0); // Register Fini to be called when the application exits PIN_AddFiniFunction(Fini, 0); // Start the program, never returns PIN_StartProgram(); return 0; } ``` 最後就可以得到完整rop chain,像是下面這樣 ``` 0x00000000004012a6:ret 0x00000000004011a2:pop rax 0x00000000004011a3:ret 0x000000000040119a:pop rdi 0x000000000040119b:ret 0x00000000004011a4:add rax, rdi 0x00000000004011a7:ret 0x00000000004011ea:mov rdi, rax 0x00000000004011ed:ret 0x00000000004011d6:movzx rdi, byte ptr [rdi] 0x00000000004011da:ret 0x00000000004011a2:pop rax 0x00000000004011a3:ret 0x000000000040119c:pop rsi 0x000000000040119d:ret 0x00000000004011a8:add rax, rsi 0x00000000004011ab:ret 0x00000000004011ee:mov rsi, rax 0x00000000004011f1:ret 0x00000000004011db:movzx rsi, byte ptr [rsi] 0x00000000004011df:ret 0x00000000004011a2:pop rax 0x00000000004011a3:ret 0x000000000040119e:pop rdx 0x000000000040119f:ret 0x00000000004011ac:add rax, rdx 0x00000000004011af:ret 0x00000000004011f2:mov rdx, rax 0x00000000004011f5:ret 0x00000000004011e0:movzx rdx, byte ptr [rdx] 0x00000000004011e4:ret 0x00000000004011bd:xor rax, rax 0x00000000004011c0:ret 0x00000000004011a4:add rax, rdi 0x00000000004011a7:ret 0x00000000004011a8:add rax, rsi 0x00000000004011ab:ret 0x00000000004011ac:add rax, rdx 0x00000000004011af:ret 0x00000000004011ea:mov rdi, rax 0x00000000004011ed:ret ``` 這樣trace起來就方便很多了,讀懂組語後其實就是很無聊的要你做一元一次方程式去一一解出每個 flag 單字是甚麼 flag `hackim20{B4byR0pDo0dOodXXduDoo} `