December 08, 2009

透過 gdb 動態改變程式執行示例

昨日在修平技術學院講課時,想到一個可展示透過 gdb 動態改變程式執行的範例,適用於 GNU/Linux IA32 (x86) 硬體架構。考慮以下程式碼 (func.c):
#include <stdlib.h>

void hello() {
  puts("Hello World!");
  exit(1);
}

void func() {
  void *buffer[1];
}

int main() {
  func();
  return 0;
}
從這個簡單的小程式,我們可以發現:
  • 函式 hello() 無法被呼叫執行
  • 函式 func() 宣告了一個可存放 pointer 的空間,被 main() 呼叫後,即行返回
但我們可透過 gdb,在不重新編譯的情況下,動態改變程式執行的流程,讓函式 hello() 得以被呼叫。首先將 debug info 加進前述的小程式,以利分析追蹤:
$ gcc -g -o func func.c
接著就祭出 gdb:
$ gdb func
GNU gdb (GDB) 7.0-ubuntu
...
Reading symbols from /home/jserv/testbed/func...done.
先對函式 main() 作反組譯:
(gdb) disassemble main
Dump of assembler code for function main:
0x0804843a <main+0>:	push   %ebp
0x0804843b <main+1>:	mov    %esp,%ebp
0x0804843d <main+3>:	call   0x8048432 <func>
0x08048442 <main+8>:	mov    $0x0,%eax
0x08048447 <main+13>:	pop    %ebp
0x08048448 <main+14>:	ret    
End of assembler dump.
我們可留意到兩個細節:
  • 反組譯輸出包含 "call 0x8048432" 的指令,該位址即函式 func() 的進入點
  • "call" 指令的下個指令的位址為 "0x08048442",如果我們能在這個位址動手腳,就可達到預期的目標,比方說額外呼叫函式 hello()
方向確立後,咱們來發揮 gdb 的威力。將中斷點設定於函式 func():
(gdb) break func
Breakpoint 1 at 0x8048438: file func.c, line 10.
儘管函式 func() 的進入點為 "0x08048432",不過 gdb 會停留在函式內首個有效的指令,我們可見該位址是 "0x8048438",對照反組譯輸出:
(gdb) disassemble func
Dump of assembler code for function func:
0x08048432 <func+0>:	push   %ebp
0x08048433 <func+1>:	mov    %esp,%ebp
0x08048435 <func+3>:	sub    $0x10,%esp
0x08048438 <func+6>:	leave  
0x08048439 <func+7>:	ret    
End of assembler dump.
設定好中斷點後,執行這個小程式,動態觀察程式的狀態,預期會停在剛剛設定的函式 func():
(gdb) run
Starting program: /home/jserv/testbed/func 

Breakpoint 1, func () at func.c:10
10	}
回顧稍早的程式碼列表,預留的 void *buffer[1] 給予我們一個機會去探索 x86 stack 的變化。因為 buffer[0] 實際位於函式 func() 的宣告中,而將索引值往後指呢?來看看:
(gdb) print *(buffer + 1)
$1 = (void *) 0xbffff808
(gdb) print *(buffer + 2)
$2 = (void *) 0x8048442
gdb 告訴我們,*(buffer + 2) 的內含值就是位址 "0x8048442",有沒有覺得很熟悉呢?不就是函式 main() 裡頭,指令 call 的下一個位址嗎?回顧一下反組譯輸出:
(gdb) disassemble main
Dump of assembler code for function main:
0x0804843a <main+0>:	push   %ebp
0x0804843b <main+1>:	mov    %esp,%ebp
0x0804843d <main+3>:	call   0x8048432 <func>
0x08048442 <main+8>:	mov    $0x0,%eax
0x08048447 <main+13>:	pop    %ebp
0x08048448 <main+14>:	ret    
End of assembler dump.
至此,透過 gdb 動態改變程式執行的範例就剩下臨門一腳,在 gdb 的提示符號打入以下指令,要求將位址對齊到函式 hello():
(gdb) print *(buffer + 2) = (void(*)()) hello
$3 = (void *) 0x8048414
繼續程式執行,來檢驗我們的想法:
(gdb) c
Continuing.
Hello World!

Program exited with code 01.
由上可見:
  • 函式 hello() 的確被執行到,所以輸出 "Hello World!" 字樣
  • 函式 hello() 的 puts() 呼叫後又呼叫 exit(1),將控制權交給 gdb,並顯示 "Program exited with code 01",符合預期
這個技巧也是許多 shellcode 的基礎原理之一,實務上在低階處理的 runtime code patching 也是不可或缺的。
由 jserv 發表於 December 8, 2009 02:42 PM
迴響

你好,请问一下,怎么我执行你的代码发现有些不同呢?
NU gdb 6.3.50-20050815 (Apple version gdb-966) (Tue Mar 10 02:43:13 UTC 2009)
Copyright 2004 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 "i386-apple-darwin"...Reading symbols for shared libraries ... done

(gdb) disassemble main
Dump of assembler code for function main:
0x00001fe1 : push %ebp
0x00001fe2 : mov %esp,%ebp
0x00001fe4 : sub $0x8,%esp
0x00001fe7 : call 0x1fd9
0x00001fec : mov $0x0,%eax
0x00001ff1 : leave
0x00001ff2 : ret
End of assembler dump.
(gdb) disassemble func
Dump of assembler code for function func:
0x00001fd9 : push %ebp
0x00001fda : mov %esp,%ebp
0x00001fdc : sub $0x18,%esp
0x00001fdf : leave
0x00001fe0 : ret
End of assembler dump.
(gdb) break func
Breakpoint 1 at 0x1fdf: file func.c, line 10.
(gdb) run
Starting program: /Users/karpar/func
Reading symbols for shared libraries ++. done

Breakpoint 1, func () at func.c:10
10 }
(gdb) print *(buffer+1)
$1 = (void *) 0x0
(gdb) print *(buffer+2)
$2 = (void *) 0x0
(gdb)

karpar 發表於 December 8, 2009 09:49 PM

karpar,

感謝分享 Apple Mac 上執行的狀況,理由很簡單,因為是 i386-apple-darwin :)

軟體的組態與 GNU/Linux 不同,是符合預期的。小弟這邊沒有實際的系統可以分析 Apple Mac 的 Mach-O 格式執行檔。

jserv 發表於 December 8, 2009 11:08 PM

大概明白了你的意思了,謝謝jserv

karpar 發表於 December 9, 2009 11:21 AM

不了解您要表達的是什麼?是在探討Stack內容對程式的影響嗎?
如果要以gdb動態改變程式執行,是x86沒有PC暫存器可以直接修改嗎?
還是您有其它想表達的意義?

Danny Chang 發表於 December 14, 2009 11:33 PM

To Danny,

延伸的意思就是,若在 func() 多加一行:

*(buffer + 2) = (void(*)()) hello

那麼,在 x86 架構上,會導致額外去呼叫 hello(),當然整個 call stack 也會被破壞

jserv 發表於 December 15, 2009 12:36 PM

博主并没有讲明原理。在IA32的GDB环境下,调用栈从高地址向低地址“生长”,函数内第一个变量的内存地址向回(也就是向栈底位置,即向高地址)偏移该变量的内存长度,再偏移函数参数所占的内存长度后得到的内存地址中存放着该函数的返回地址,将该地址存储的地址值改成你要调用的函数FUNC的指针地址,就可以在该函数结束后自动跳转到FUNC函数执行。

adreaman 發表於 December 30, 2009 11:39 PM

To adreaman,

感謝補充,受教了

jserv 發表於 December 30, 2009 11:49 PM

在OpenSolaris 2009.06中也正常。
GNU gdb 6.3.50_2004-11-23-cvs
Copyright 2004 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 "i386-pc-solaris2.11"...
(gdb) disassemble main
Dump of assembler code for function main:
0x08050d08 : push %ebp
0x08050d09 : mov %esp,%ebp
0x08050d0b : sub $0x8,%esp
0x08050d0e : and $0xfffffff0,%esp
0x08050d11 : mov $0x0,%eax
0x08050d16 : add $0xf,%eax
0x08050d19 : add $0xf,%eax
0x08050d1c : shr $0x4,%eax
0x08050d1f : shl $0x4,%eax
0x08050d22 : sub %eax,%esp
0x08050d24 : call 0x8050d00
0x08050d29 : mov $0x0,%eax
0x08050d2e : leave
0x08050d2f : ret
End of assembler dump.
(gdb) break func
Breakpoint 1 at 0x8050d06: file func.c, line 10.
(gdb) disassemble func
Dump of assembler code for function func:
0x08050d00 : push %ebp
0x08050d01 : mov %esp,%ebp
0x08050d03 : sub $0x4,%esp
0x08050d06 : leave
0x08050d07 : ret
End of assembler dump.
(gdb) run
Starting program: /export/home/roy/func

Breakpoint 1, func () at func.c:10
10 }
(gdb) print *(buffer + 1)
$1 = (void *) 0x8047a2c
(gdb) print *(buffer + 2)
$2 = (void *) 0x8050d29
(gdb) print *(buffer + 2) = (void(*)()) hello
$3 = (void *) 0x8050ce0
(gdb) c
Continuing.
Hello World!

Program exited with code 01.

Roy 發表於 January 2, 2010 03:59 PM
發表迴響









記住我的資訊?