LD_PRELOAD提供了平民化的注入方式固然友善,同時也有不便:注入庫出錯後調試比較困難。我琢磨了幾天找到了可行的調試方法,當然未必是最有效的辦法。抛出陋文,希望引來美玉~
首先,寫一段代碼作為普通的動态庫,公開接口,供人調用,如下:
//true.c
int fake(const char* s1,const char* s2)
{
return 0;
}
gcc -g3 -O0 -o libtrue.so true.c -fPIC -shared
echo "/root/Desktop">>/etc/ld.so.conf
ldconfig
這差不多是個空函數。
下面是LD_PRELOAD将要注入的代碼:
//fake.c
#include <string.h>
#include <stdio.h>
int fake(const char* s1,const char* s2)
{
printf("s1:%s-s2:%s\n",s1,s2);
while(1)
sleep(1);
return 0;
}
Makefile
all:
gcc -g3 -O0 -fPIC -shared -Wa,-adlhn -c fake.c -fno-builtin-strcmp > fake.cod
gcc -g3 -O0 -fPIC -shared -o fake.so fake.o -Wl,-Map,Sym.map
fake.c除了生成調試資訊以外,同時生成符号映射檔案,why?
先不解釋為什麼,先來看下Sym.map中有什麼:
.init 0x0000000000000498 0x18
*(.init)
.init 0x0000000000000498 0x9 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
0x0000000000000498 _init
.init 0x00000000000004a1 0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbeginS.o
.init 0x00000000000004a6 0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtendS.o
.init 0x00000000000004ab 0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
.plt 0x00000000000004b0 0x40
*(.plt)
.plt 0x00000000000004b0 0x40 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
*(.iplt)
.text 0x00000000000004f0 0x148
*(.text.unlikely .text.*_unlikely)
*(.text .stub .text.* .gnu.linkonce.t.*)
.text 0x00000000000004f0 0x17 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
*fill* 0x0000000000000507 0x9 90909090
.text 0x0000000000000510 0xaa /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbeginS.o
*fill* 0x00000000000005ba 0x2 90909090
.text 0x00000000000005bc 0x40 fake.o
0x00000000000005bc fake
*fill* 0x00000000000005fc 0x4 90909090
.text 0x0000000000000600 0x36 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtendS.o
*fill* 0x0000000000000636 0x2 90909090
.text 0x0000000000000638 0x0 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
*(.gnu.warning)
.fini 0x0000000000000638 0xe
*(.fini)
.fini 0x0000000000000638 0x4 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
0x0000000000000638 _fini
.fini 0x000000000000063c 0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbeginS.o
.fini 0x0000000000000641 0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
0x0000000000000646 PROVIDE (__etext, .)
0x0000000000000646 PROVIDE (_etext, .)
0x0000000000000646 PROVIDE (etext, .)
這裡僅截取了Sym.map代碼段。代碼段中有.init節.text節.fini節。每節中又由諾幹.o檔案組成,如crt運作時庫相關的crti.o以及fake.c編譯後生成的fake.o。編譯器将源碼編譯為.o中間檔案後,還需要把所有的中間檔案按相同的頁面屬性連接配接到一起,并配置設定連結位址(關于編譯連接配接的詳細介紹可以參看<程式員的自我修養>)。sym.map檔案顯示了連結時,各個.o檔案在整個fake.so檔案中的偏移:如fake.o在檔案中的偏移是0x5bc
現在說明一下需要sym.map的原因:因為fake.so是動态庫,程式運作時,加載到記憶體中的位置不固定。因為他的不固定性,是以很難下斷點或者反彙編。但是,程式運作起來後,可以通過cat /proc/pidnum/maps檢視程序記憶體加載的情況,并獲得fake.so加載的基址。有了基址,加上偏移,就可以确定fake.c提供的代碼在程序空間中的具體位址:
圖中顯示fake.so被加載到位址7ffff7deb000-7ffff7dee000,其中7ffff7deb000是基址。加上前面sym.map顯示的偏移,現在可以預知fake提供的函數在位址7ffff7deb5bc處。
繼續往下,有了動态庫,還要有測試檔案調用動态庫接口:
//test.c
#include <string.h>
#include <unistd.h>
extern int fake(const char* s1,const char* s2);
int main()
{
if(fake("1","2") == 0)
{
printf("nothing\n");
}
return 0;
}
gcc -o test test.c
strip --strip-all test
為了比較真實的模拟由他人釋出的程式的環境,對于test.c不僅不生成調試資訊,同時還剝離符号表。
下面開始注入并啟動調試。
export LD_PRELOAD=/root/Desktop/fake.so
gdb test
由于沒有調試資訊,gdb找不到main函數,是以無法在main函數下斷點,直接導緻start後程式跑飛了:
(gdb) start
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n
Starting program: /root/Desktop/test
s1:1-s2:2
好吧,目前隻能重新調試它,并嘗試找到程式從哪開始的:
[[email protected] Desktop]# gdb test
(gdb) info files
Symbols from "/root/Desktop/test".
Local exec file:
`/root/Desktop/test', file type elf64-x86-64.
Entry point: 0x400520
這樣就得到了程式的入口,并于此下斷點然後start運作,程式在0x400520處停下:
(gdb) b *0x400520
Breakpoint 1 at 0x400520
(gdb) start
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n
Starting program: /root/Desktop/test
Breakpoint 1, 0x0000000000400520 in ?? ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6_5.3.x86_64
(gdb)
這時,程序的依賴的各個動态庫也業已完成加載,可以檢視記憶體加載情況:
[[email protected] ~]# ps x|grep test
4352 pts/8 S+ 0:00 gdb test
4381 pts/8 T 0:00 /root/Desktop/test
[[email protected] ~]# cat /proc/4381/maps
00400000-00401000 r-xp 00000000 fd:00 131451 /root/Desktop/test
00600000-00601000 rw-p 00000000 fd:00 131451 /root/Desktop/test
3326400000-3326420000 r-xp 00000000 fd:00 75830 /lib64/ld-2.12.so
332661f000-3326620000 r--p 0001f000 fd:00 75830 /lib64/ld-2.12.so
3326620000-3326621000 rw-p 00020000 fd:00 75830 /lib64/ld-2.12.so
3326621000-3326622000 rw-p 00000000 00:00 0
3326c00000-3326d8b000 r-xp 00000000 fd:00 75831 /lib64/libc-2.12.so
3326d8b000-3326f8a000 ---p 0018b000 fd:00 75831 /lib64/libc-2.12.so
3326f8a000-3326f8e000 r--p 0018a000 fd:00 75831 /lib64/libc-2.12.so
3326f8e000-3326f8f000 rw-p 0018e000 fd:00 75831 /lib64/libc-2.12.so
3326f8f000-3326f94000 rw-p 00000000 00:00 0
7ffff7bea000-7ffff7bed000 rw-p 00000000 00:00 0
7ffff7bed000-7ffff7bee000 r-xp 00000000 fd:00 136044 /root/Desktop/libtrue.so
7ffff7bee000-7ffff7ded000 ---p 00001000 fd:00 136044 /root/Desktop/libtrue.so
7ffff7ded000-7ffff7dee000 rw-p 00000000 fd:00 136044 /root/Desktop/libtrue.so
7ffff7dfc000-7ffff7dfd000 r-xp 00000000 fd:00 136014 /root/Desktop/fake.so
7ffff7dfd000-7ffff7ffc000 ---p 00001000 fd:00 136014 /root/Desktop/fake.so
7ffff7ffc000-7ffff7ffd000 rw-p 00000000 fd:00 136014 /root/Desktop/fake.so
7ffff7ffd000-7ffff7ffe000 rw-p 00000000 00:00 0
7ffff7ffe000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso]
7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
嗯,fake.so在7ffff7dfc000,fake.o應該位于7ffff7dfc5bc處了。另外也可以看到,被LD_PRELOAD覆寫的libtrue.so被加載到7ffff7bed000處
可以直接在這個位置下斷,當然不放心的話,可以用objdump檢視test的反彙編:
[[email protected] Desktop]# objdump -d test
Disassembly of section .plt:
00000000004004d8 <[email protected]>:
4004d8: ff 35 d2 04 20 00 pushq 0x2004d2(%rip) # 6009b0 <_fini+0x2002a8>
4004de: ff 25 d4 04 20 00 jmpq *0x2004d4(%rip) # 6009b8 <_fini+0x2002b0>
4004e4: 0f 1f 40 00 nopl 0x0(%rax)
00000000004004e8 <[email protected]>:
4004e8: ff 25 d2 04 20 00 jmpq *0x2004d2(%rip) # 6009c0 <_fini+0x2002b8>
4004ee: 68 00 00 00 00 pushq $0x0
4004f3: e9 e0 ff ff ff jmpq 4004d8 <_init+0x18>
...
0000000000400520 <.text>:
400520: 31 ed xor %ebp,%ebp
400522: 49 89 d1 mov %rdx,%r9
400525: 5e pop %rsi
400526: 48 89 e2 mov %rsp,%rdx
400529: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40052d: 50 push %rax
40052e: 54 push %rsp
40052f: 49 c7 c0 30 06 40 00 mov $0x400630,%r8
400536: 48 c7 c1 40 06 40 00 mov $0x400640,%rcx
40053d: 48 c7 c7 04 06 40 00 mov $0x400604,%rdi
400544: e8 bf ff ff ff callq 400508 <[email protected]>
其中
00000000004004e8 <[email protected]>:
4004e8: ff 25 d2 04 20 00 jmpq *0x2004d2(%rip) # 6009c0 <_fini+0x2002b8>
4004ee: 68 00 00 00 00 pushq $0x0
4004f3: e9 e0 ff ff ff jmpq 4004d8 <_init+0x18>
是test向fake.so中導出的函數跳轉的位址,可以在此處也下個斷點。
======================================================================
附注,測試這段代碼時,我已經關閉了随機位址加載是以objdump -d輸出的連接配接位址和test加載位址相同,都是0x400520。關閉随機位址加載的方法如下:
[ro[email protected] ~]# echo 0>/proc/sys/kernel/randomize_va_space
======================================================================
(gdb) b *0x04004e8
Breakpoint 2 at 0x4004e8
(gdb) b *0x7ffff7dfc5bc
Breakpoint 3 at 0x7ffff7dfc5bc: file fake.c, line 5.
(gdb)
繼續運作,程式在0x4004e8出停下後反彙編看看,然後繼續運作到fake.so中
(gdb) c
Continuing.
Breakpoint 2, 0x00000000004004e8 in [email protected] ()
(gdb) x /32i $pc
=> 0x4004e8 <[email protected]>: jmpq *0x2004d2(%rip) # 0x6009c0 <[email protected]>
0x4004ee <[email protected]+6>: pushq $0x0
0x4004f3 <[email protected]+11>: jmpq 0x4004d8
(gdb) c
Continuing.
Breakpoint 3, fake (s1=0x3326621188 "",
s2=0x332640e9f0 "UH\211\345AWAVAUATE1\344S1\333H\203\354HH\307E\250")
at fake.c:5
5 {
(gdb) x /32i $pc
(gdb) x /32i $pc
=> 0x7ffff7dfc5bc <fake>: push %rbp
0x7ffff7dfc5bd <fake+1>: mov %rsp,%rbp
0x7ffff7dfc5c0 <fake+4>: sub $0x10,%rsp
0x7ffff7dfc5c4 <fake+8>: mov %rdi,-0x8(%rbp)
0x7ffff7dfc5c8 <fake+12>: mov %rsi,-0x10(%rbp)
0x7ffff7dfc5cc <fake+16>: lea 0x73(%rip),%rax # 0x7ffff7dfc646
0x7ffff7dfc5d3 <fake+23>: mov -0x10(%rbp),%rdx
0x7ffff7dfc5d7 <fake+27>: mov -0x8(%rbp),%rcx
0x7ffff7dfc5db <fake+31>: mov %rcx,%rsi
0x7ffff7dfc5de <fake+34>: mov %rax,%rdi
0x7ffff7dfc5e1 <fake+37>: mov $0x0,%eax
0x7ffff7dfc5e6 <fake+42>: callq 0x7ffff7dfc4c0 <[email protected]>
0x7ffff7dfc5eb <fake+47>: mov $0x1,%edi
0x7ffff7dfc5f0 <fake+52>: mov $0x0,%eax
0x7ffff7dfc5f5 <fake+57>: callq 0x7ffff7dfc4e0 <[email protected]>
0x7ffff7dfc5fa <fake+62>: jmp 0x7ffff7dfc5eb <fake+47>
在這段反彙編代碼中,我們看到了2個函數:printf/sleep,有點像fake.c中的代碼了。
由于,fake.so是源碼編譯的,可以在此看到源碼,并下斷點:
0x7ffff7dfc604 <__do_global_ctors_aux+4>: push %rbx
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) list
1 #include <string.h>
2 #include <stdio.h>
3
4 int fake(const char* s1,const char* s2)
5 {
6 printf("s1:%s-s2:%s\n",s1,s2);
7
8 while(1)
9 sleep(1);
10 return 0;
(gdb) b 8
Breakpoint 4 at 0x7ffff7dfc5eb: file fake.c, line 8.
(gdb)
當然,還支援檢視變量:
$2 = 0x40072a "1"
(gdb) p s2
$3 = 0x400728 "2"
(gdb)
至此,調試LD_PRELOAD注入的so檔案結束,3q
qq:[email protected]