#include <stdio.h> #include <string.h> static int counter = 0; extern char Here, Start, End; int main() { asm volatile( "Here:" ); printf("/* Program invoked.\n"); printf("Hello World!\n"); memcpy(&Here, &Start, (int) &End - (int) &Start); printf(" #%d */\n", ++counter); return 0; } void dummyCodeContext() { int (*callPrintf)( const char *format, ... ); asm volatile( "Start:" ); (*(callPrintf = &printf))( "/* Dummy code context invoked.\n"); asm volatile( "End:" ); }這裡用到 Inline assembly,主要是標示程式碼位址的 label,在 GCC 的編譯環境下可通用,並且冠以 "volatile" 修飾是確保依據指令順序、避免因為編譯器最佳化而調整,因為 C Programming Language 在設計上即有「限制 self-modifying code」的考量,所以我們得稍微迂迴,才得以在執行時期找到其 Image 中之絕對位址 (注意:這裡針對 Linux process / memory model)。在 main() 中,我們試圖呼叫 memcpy,將原本放在 dummyCodeContext() 中部份程式碼複製並蓋掉 [Here:] 開頭的程式碼,概念上就如同前面提到的圖例。
$ gcc -W -Wall -ggdb -O0 -o pre-hello pre-hello.c $ ./pre-hello /* Program invoked. Hello World! 程式記憶體區段錯誤奇怪,竟然發生 SegFault,用 gdb 看看:
$ gdb ./pre-hello GNU gdb 6.4.90-debian Copyright (C) 2006 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i486-linux-gnu"... Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". (gdb) run Starting program: /tmp/hello-C++/pre-hello /* Program invoked. Hello World! Program received signal SIGSEGV, Segmentation fault. 0x080483cc in Here () at pre-hello.c:12 12 memcpy(&Here, &Start, (int) &End - (int) &Start);要說明這個現象,又得將 Linux memory model 拿出來複習,尤其是 memory page 的部份,「深入淺出 Hello World」系列的演講有提過重點,並且一般的 POSIX/UNIX System Programming 書籍也有解釋,這裡就忽略細節。簡單來說,預設 memory page 的保護限制我們對 code context 作寫入的動作 (data 與 code 是獨立的 section),要改變預設的行為,可透過 mprotect(2),以下節錄 man page;
NAME mprotect - control allowable accesses to a region of memory SYNOPSIS #include <sys/mman.h> int mprotect(const void *addr, size_t len, int prot); DESCRIPTION The function mprotect() specifies the desired protection for the memory page(s) containing part or all of the interval [addr,addr+len-1]. If an access is disallowed by the protection given it, the program receives a SIGSEGV.又因為 SM 版的 "Hello World" 規模不大,基本上我們可以假設全部 code 都會在同一 page 中,以下是解除 memory write protection 的程式碼片段:
unsigned page = (unsigned) &Here & ~( getpagesize() - 1 ); /* chmod u=rwx page */ if (mprotect((char*) page, getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC ) ) { perror( "mprotect failed" ); }看來突破「枷鎖」後,我們終於大行 "SM" 之實,不過呢,為了比較清楚地整合剛剛的程式碼片段,這裡玩個 C++ 小技巧。依據 C++ 語言規範,一個 object (class instance) 之 constructor 會優先於 main 前執行完畢,而其 destructor 則會於 main 執行完畢再進行善後動作,於是,利用這個概念,我們「升級」剛剛的 C 語言程式為 C++ 程式,以下是程式碼列表:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> using namespace std; static int counter = 0; extern char Here, Start, End; int main() { asm volatile( "Here:" ); printf("/* Program invoked.\n"); printf("Hello World!\n"); memcpy(&Here, &Start, (int) &End - (int) &Start); printf(" #%d */\n", ++counter); return 0; } void dummyCodeContext() { int (*callPrintf)( const char *format, ... ); asm volatile( "Start:" ); (*(callPrintf = &printf))( "/* Dummy code context invoked.\n"); asm volatile( "End:" ); } static char shellcode[] = "\x31\xc0" /* xor %eax, %eax */ "\x40" /* inc %eax */ "\xcd\x80"; /* int $0x80 */ class Foo { public: Foo() { unsigned page = (unsigned) &Here & ~( getpagesize() - 1 ); /* chmod u=rwx page */ if (mprotect((char*) page, getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC ) ) { perror( "mprotect failed" ); } main(); } virtual ~Foo() { memcpy(&Here, &shellcode, sizeof(shellcode)); main(); } } foo_instance;程式碼看起來變多,但反而有種 [快速堆積式程式設計?] 的感覺,跟稍早的程式列表相比,main() 與 dummyCodeContext() 的實做一行都沒有改變,而我們在 class Foo 中,透過 constructor 處理 memory page protection 的 WRITE 處理,然後... 呼叫 main (?!),是的,這樣會讓 main() 的實做多跑一次 (原本就會被 C Runtime 所呼叫),當然這跟其他 C++ 的「奇計淫巧」相比,實在沒什麼。另外值得一提的是,我們在 class Foo destructor 中,將一段 shellcode 複製塞入 [Here:] 開頭的程式碼,這個 shellcode 就以字串形式存在,好像很單純。為了統計 "Hello World" 到底被印了幾次,我們弄個 counter 的變數來儲存,先來猜猜看,counter 應該會是多少?可以確定的是,main() 會被呼叫三次,分別是 constructor 與 destructor 貢獻與原本的行為,真好,完全不需要多加迴圈,結果 main() 沒有修改一行程式碼,就被執行三次,C++ 真是神奇的語言啊,「快速堆積」必備。
$ g++ -W -Wall -ggdb -O0 -o hello hello.cpp $ ./hello /* Program invoked. Hello World! #1 */ /* Dummy code context invoked. /* Dummy code context invoked. #2 */看來 memcpy 的動作是成功的,再回頭看看被改寫的部份:
int main() { asm volatile( "Here:" ); printf("/* Program invoked.\n"); printf("Hello World!\n"); memcpy(&Here, &Start, (int) &End - (int) &Start); printf(" #%d */\n", ++counter); return 0; }memcpy 寫入的位址自 [Here:] 開始,這是在第一次執行 main() 時 (也就是 class Foo constructor 之際),所以呢,原本該印出 "/* Program invoked." 與 "Hello World!" 的輸出,在第二次執行 main() 時 (C Runtime 的呼叫行為),程式碼被更換為 dummyCodeContext() 中印出 "* Dummy code context invoked." 的部份,不過,問題沒那麼簡單,反而疑惑變多了:
static char shellcode[] = "\x31\xc0" /* xor %eax, %eax */ "\x40" /* inc %eax */ "\xcd\x80"; /* int $0x80 */這個看似單純 char array 的 shellcode,原本的功能是擺放資料,不過卻被塞入特定的機械碼,是由三個組合語言命令所組譯得到的,其功能就是執行 exit 系統呼叫,在執行第三次 main() 時 (也就是於 class Foo destructor),我們已經先 memcpy shellcode 到 自 [Here:] 開始的位址去,換言之,原本位於 data section 的資料,頓時植入 code section,接著,第三次呼叫 main() 時,exit 系統呼叫被觸發,這也導致程式流程終止,原本該遞增 counter 數值並印出的動作,也就沒機會執行到。
$ gdb ./hello GNU gdb 6.4.90-debian Copyright (C) 2006 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i486-linux-gnu"... Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". (gdb) b main Breakpoint 1 at 0x8048623: file hello.cpp, line 15. (gdb) run Starting program: /tmp/hello-C++/hello Breakpoint 1, Here () at hello.cpp:15 15 printf("/* Program invoked.\n"); (gdb) next /* Program invoked. 16 printf("Hello World!\n"); (gdb) next Hello World! 17 memcpy(&Here, &Start, (int) &End - (int) &Start); (gdb) next 18 printf(" #%d */\n", ++counter); (gdb) next #1 */ 19 return 0; (gdb) next 20 } (gdb) next Foo (this=0x8049be8) at hello.cpp:47 47 } (gdb) next 0x080486b3 in __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at hello.cpp:52 52 } foo_instance; (gdb) next 0x080486e7 in global constructors keyed to main () at hello.cpp:53 Line number 53 out of range; hello.cpp has 52 lines. (gdb) next 0x0804885d in __do_global_ctors_aux () (gdb) next Single stepping until exit from function __do_global_ctors_aux, which has no line number information. 0x080484a1 in _init () (gdb) next Single stepping until exit from function _init, which has no line number information. 0x080487fe in __libc_csu_init () (gdb) next Single stepping until exit from function __libc_csu_init, which has no line number information. 0xb7d6a85d in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6 (gdb) next Single stepping until exit from function __libc_start_main, which has no line number information. 程式記憶體區段錯誤在解說前,我們直接看到最後一行,是的,gdb 自己就「程式記憶體區段錯誤」,話說 Debugger 設計不就是要協助開發者處理 SegFault,而 gdb 遇到 "Orz Programming 2.0" 之 SM 版 "Hello World" 竟然沒轍,舉白旗宣佈投降,丟了 SegFault 出來,那該怎麼辦呢?難道要我們 "Debugging the Debugger" 嗎?
breakpoint 1, Here () at hello.cpp:15 15 printf("/* Program invoked.\n"); (gdb) next /* Program invoked. 16 printf("Hello World!\n"); (gdb) next Hello World! 17 memcpy(&Here, &Start, (int) &End - (int) &Start); (gdb) next 18 printf(" #%d */\n", ++counter); (gdb) next #1 */ 19 return 0; (gdb) next 20 } (gdb) next Foo (this=0x8049be8) at hello.cpp:47 47 }很好,就如我們預期,並且也執行了 memcpy 的動作。接下來就有趣了:
(gdb) next 0x080486b3 in __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at hello.cpp:52 52 } foo_instance; (gdb) next 0x080486e7 in global constructors keyed to main () at hello.cpp:53 Line number 53 out of range; hello.cpp has 52 lines. (gdb) next因為第一次 main() 的執行圓滿落幕,constructor 即將結束,將控制權移轉給原本就該被執行的 main(),可是這時候發現一個奇怪的事情:"Line number 53 out of range; hello.cpp has 52 lines.",是的,就是因為 self-modifying code,然而,我們硬是逼迫 gdb 作 source-level debugging,但是現在 Runtime code section 已非 gdb 所預期,於是乎,前面種下的「惡果」讓錯誤一路到底,最後,流程 __do_global_ctors_aux () ==> _init () ==> __libc_csu_init () ==> __libc_start_main (),stack 中資訊錯亂,就爆炸了。
神秘禮物是啥?
由 guest 發表於 July 31, 2006 08:58 AM1. Above program assumes the return address of the self-modifying "memcpy()" will be a valid IA32 instruction, which is not necessarily true due to variable-length nature of IA32 instructions.
2. The "/* Dummy code context invoked." was printed twice _possibly_ because you've overwritten the epilog of main() after memcpy() so the first was printed from the copied code while the second one was printed from the original code site.
3. Most use of indirect function invocation in tricky code are to confuse the compiler to get rid of optimizations such as function inlining. Your code seems being in this category.
IIRC, C 中允許呼叫 main 可是標準 C++ 中不允許喔 :p
由 scw 發表於 July 31, 2006 01:05 PM今晚半夜快一點, 大同的學弟給我這網站, 果真奇怪... ==_==|||
後來讓我手癢...
不過, 很悲哀的是,
上面範例,
你都可以成功, 連續兩次Dummy code context invoked.
我跟我學弟... 很悲哀...
顯示一次Dummy code context invoked.後...
就立刻exception.程式掛點!!
這樣就掛掉了, 接下來, 別說研究...
實在無法著手, 畢竟手邊沒有成功的程式.
若有成功範例binary, 只要我逆向看一下組合語言, 追一下, 那超簡單, 不用多久, 就會徹底答案出來.
好吧,
既然這樣,
就讓我花一個深夜,
逆向工程,
完整弄出一個這樣的範例!!!
很簡單,
後來當然我知道, 為何我們一模一樣程式, 我卻會失敗!?
因為, image base address!!
我Linux上的ld script如下:
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS;
我後來修正為:
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = 0x10ec7b00); . = 0x10ec7b00 + SIZEOF_HEADERS;
就正常.... :)
原因很簡單, 如下:
10ec8100 :
10ec8100: 83 ec 0c sub $0xc,%esp
10ec8103: 68 b8 83 ec 10 push $0x10ec83b8
10ec8108: e8 ef fe ff ff call 10ec7ffc
10ec810d: 83 c4 10 add $0x10,%esp
10ec8110: 83 ec 0c sub $0xc,%esp
10ec8113: 68 cd 83 ec 10 push $0x10ec83cd
^^^^^^^^^^
只要想辦法, 讓83 ec 10這三個bytes產生出來就可以啦!!!
因為sub $0x10,%esp 就是 0x10ec83
因此只要產生push $0x10ec83??(最後一個byte隨便), 所以必須改ld script, 才能編出這樣組合語言.
然後memcpy會剛好毀掉10ec8100到10ec8115 ==> 印出第一個Dummy code context invoked
緊接著, 我只是設法把esp-0x10, 立刻復援之前esp的值, 當然啦, 會再立刻印出第二個Dummy code context invoked
10ec8118: e8 df fe ff ff call 10ec7ffc
這一題,
以後,
拿來考其他搞hack的人,
就知道,
對方有沒有基本的sense!!
--
CIH
Software Magician
是指"體悟"吧?
由 aguai 發表於 August 4, 2006 10:33 PMjserv,這樣的 code 太 unpredictible 了。前面 CIH 還 work around 才能動。還要考量到 cache 和 CPU 的 code fetching/data path 等問題。基本上,每台機器 run 的結果會不太一樣,會因 CPU 的不同,而有不同的結果。也會因為系統執行的 process 的不同,影嚮到 cache 和 code fetching 的結果。以前在 DOS 下,就有一些防 debugger 的技術,就是透過 cache 和 code fetching 的 timming,產生 debugger 和實際執行結果不同,而使的 debugger 沒法正確的 trace。
當然,這是個有趣的問題,現在已經很少人知道了,這篇文章還是很有參考價值。
在 C++ 中
不是 object 會在 main 之前會被 construct 吧?
應該是 global 的物件才會於 main 之前建構
範例中
foo_instance 位於 main 的 scope 之外
所以它是一個 global object