赞
踩
// main extern int g_nShared; extern void func(int *a, int *b) ; void main(void) { int n = 2; func(&n, &g_nShared); } // func.c #include<stdio.h> int g_nShared = 1; int g_nTmp = 0; void func(int *a, int *b) { if(!a || !b) return; g_nTmp = *a; *a = *b; *b = g_nTmp; }
按时间分类:
多个模块合成一个可执行文件,方法有两种:
编译:
gcc -static -fno-stack-protector main.c func.c -save-temps --verbose -o func.out
-save-temps可以保留中间的编译文件。
看下main.o的section header,VMA(虚拟内存地址)和LMA(加载内存地址)都是0,因为还没有链接。
$ objdump -h main.o main.o: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000025 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 00000065 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000065 2**0 ALLOC 3 .comment 0000002a 0000000000000000 0000000000000000 00000065 2**0 CONTENTS, READONLY 4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000008f 2**0 CONTENTS, READONLY 5 .eh_frame 00000038 0000000000000000 0000000000000000 00000090 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
而链接后的func.out,相似节已经被合并,VMA和LMA都被填充:
$ objdump -h func.out func.out: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn ... 5 .text 0008f490 00000000004004d0 00000000004004d0 000004d0 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE ... 18 .got 000000f8 00000000006b8ef8 00000000006b8ef8 000b8ef8 2**3 CONTENTS, ALLOC, LOAD, DATA 19 .got.plt 000000d0 00000000006b9000 00000000006b9000 000b9000 2**3 CONTENTS, ALLOC, LOAD, DATA 20 .data 00001af0 00000000006b90e0 00000000006b90e0 000b90e0 2**5 CONTENTS, ALLOC, LOAD, DATA ... 25 .bss 000016f8 00000000006bb2e0 00000000006bb2e0 000bb2d8 2**5 ALLOC
反汇编看下main.o, func地址偏移是0:
$ objdump -d -j .text main.o main.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp) f: 48 8d 45 fc lea -0x4(%rbp),%rax 13: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 1a <main+0x1a> 1a: 48 89 c7 mov %rax,%rdi 1d: e8 00 00 00 00 callq 22 <main+0x22> 22: 90 nop 23: c9 leaveq 24: c3 retq
相比,func.out已经获取了func函数的偏移:
$ objdump -d -j .text func.out | grep -A 20 "<main>" 0000000000400b6d <main>: 400b6d: 55 push %rbp 400b6e: 48 89 e5 mov %rsp,%rbp 400b71: 48 83 ec 10 sub $0x10,%rsp 400b75: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp) 400b7c: 48 8d 45 fc lea -0x4(%rbp),%rax 400b80: 48 8d 35 69 85 2b 00 lea 0x2b8569(%rip),%rsi # 6b90f0 <g_nShared> 400b87: 48 89 c7 mov %rax,%rdi 400b8a: e8 03 00 00 00 callq 400b92 <func> # 400b8f+3==func 400b8f: 90 nop 400b90: c9 leaveq 400b91: c3 retq 0000000000400b92 <func>: 400b92: 55 push %rbp 400b93: 48 89 e5 mov %rsp,%rbp 400b96: 48 89 7d f8 mov %rdi,-0x8(%rbp)
另外对于main.o重定位文件,最重要的是重定位表:
$ objdump -r main.o
main.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000016 R_X86_64_PC32 g_nShared-0x0000000000000004
000000000000001e R_X86_64_PLT32 func-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
0x16,是指objdump -d -j .text main.o
那里,偏移0x16的地方需要重定位。
R_X86_64_PC32,是相对偏移类型。
-0x4是Elf32_Rela.r_addend的值。
后缀.a就是静态链接库,根据上面静态编译的func.out也能想到,.a是一组目标文件经过了压缩打包,比如printf.o, scanf.o, malloc.o。 压缩打包的工具是ar,可以看下libc.a的内容:
$ ar -t /usr/lib32/libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
...
目的:多个程序使用内存中的一个库,节省内存。
编译示例代码func.c为动态库:
gcc -shared -fpic func.c -m32 -o func.so
pic放到后面解释。
主程序修改一下,调用两次动态库的func函数, 并按注释的gcc命令编译:
// gcc main.c ./func.so -no-pie -m32 -o main.out
// -no-pie: Don't produce a position independent executable.
// 注意要带上这个参数,不然没有用.got.plt section
extern int g_nShared;
extern void func(int *a, int *b) ;
void main(void) {
int n = 2;
func(&n, &g_nShared);
func(&n, &g_nShared);
}
PIC: Position Independent Code, 位置无关代码,代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。它是共享库必有的属性,这样才能被多个进程使用。
PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。
$ objdump -h func.so func.so: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .note.gnu.build-id 00000024 00000000000001c8 00000000000001c8 000001c8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA ... 15 .got 00000028 0000000000200fd8 0000000000200fd8 00000fd8 2**3 CONTENTS, ALLOC, LOAD, DATA 16 .got.plt 00000018 0000000000201000 0000000000201000 00001000 2**3 CONTENTS, ALLOC, LOAD, DATA ... $ readelf -r func.so Relocation section '.rela.dyn' at offset 0x400 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000200e78 000000000008 R_X86_64_RELATIVE 5d0 ... 000000200ff0 000600000006 R_X86_64_GLOB_DAT 0000000000201028 g_nTmp + 0 ...
可以看到这个全局变量g_nTmp位于.got section内。
.got可以保存全局变量引用,那么反汇编看下func():
$ objdump -M intel -d func.so | grep -A 20 "<func>" 00000000000005da <func>: 5da: 55 push rbp 5db: 48 89 e5 mov rbp,rsp 5de: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi 5e2: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi 5e6: 48 83 7d f8 00 cmp QWORD PTR [rbp-0x8],0x0 5eb: 74 33 je 620 <func+0x46> 5ed: 48 83 7d f0 00 cmp QWORD PTR [rbp-0x10],0x0 5f2: 74 2c je 620 <func+0x46> 5f4: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 5f8: 8b 10 mov edx,DWORD PTR [rax] 5fa: 48 8b 05 ef 09 20 00 mov rax,QWORD PTR [rip+0x2009ef] # 200ff0 <g_nTmp-0x38> 601: 89 10 mov DWORD PTR [rax],edx 603: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 607: 8b 10 mov edx,DWORD PTR [rax] 609: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 60d: 89 10 mov DWORD PTR [rax],edx 60f: 48 8b 05 da 09 20 00 mov rax,QWORD PTR [rip+0x2009da] # 200ff0 <g_nTmp-0x38> 616: 8b 10 mov edx,DWORD PTR [rax] 618: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 61c: 89 10 mov DWORD PTR [rax],edx
rip+0x2009ef == 0x601+0x2009ef == 0x200ff0, 这就是重定位表里的offset。
一个程序的数据段和代码段的相对距离是不变的常量,GOT全局偏移表可以保存全局变量和库函数的引用。
每个条目占8字节,加载时会进行重定位并填入符号的绝对地址。
为了引入RELRO保护机制,GOT被拆分为两部分:
还有一个.plt.got,存放 __cxa_finalize 函数对应的 PLT 条目,可先忽略。
.got.plt section包含一个GOT数组:
__stack_chk_fail
, 用来检查canary安全防护;__libc_start_main
, 它会调用main();可以用010editor看一下main.out section header,里面有内存偏移,条目大小(0x4,数量0x18/0x46):
另外是.plt section,Procedure Linkage Table,虽说翻译过来是表,但它是可执行的(不是函数指针):
__libc_start_main
;静态链接要比动态链接的程序稍微快点,两个原因:
延迟绑定(Lazy Binding)的基本思想就是,当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。
和windows的延迟加载模块不一样
ELF通过PLT和GOT的配合来实现延迟加载。每个库函数都有一组PLT和GOT。
看下编译好的main.out:
$ readelf -S main.out
...
[ 9] .rel.dyn REL 08048390 000390 000010 08 A 5 0 4
[10] .rel.plt REL 080483a0 0003a0 000018 08 AI 5 23 4
[11] .init PROGBITS 080483b8 0003b8 000023 00 AX 0 0 4
[12] .plt PROGBITS 080483e0 0003e0 000040 04 AX 0 0 16
[13] .plt.got PROGBITS 08048420 000420 000008 08 AX 0 0 8
...
[21] .dynamic DYNAMIC 08049f08 000f08 0000f0 08 WA 6 0 4
[22] .got PROGBITS 08049ff8 000ff8 000008 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4
...
.rel.dyn
记录了加载时需要重定位的变量,.rel.plt
记录的是需要重定位的函数。
gdb调试,第一次调用func:
→ 0x8048583 <main+61> call 0x8048410 <func@plt> ↳ 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014 0x8048416 <func@plt+6> push 0x10 0x804841b <func@plt+11> jmp 0x80483e0 0x8048420 <__gmon_start__@plt+0> jmp DWORD PTR ds:0x8049ff8 0x8048426 <__gmon_start__@plt+6> xchg ax, ax 0x8048428 add BYTE PTR [eax], al gef➤ x/4x 0x804a014 0x804a014: 0x08048416 0x00000000 0x00000000 0x00000000 gef➤ si ... 0x8048400 <__libc_start_main@plt+0> jmp DWORD PTR ds:0x804a010 0x8048406 <__libc_start_main@plt+6> push 0x8 0x804840b <__libc_start_main@plt+11> jmp 0x80483e0 → 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014 0x8048416 <func@plt+6> push 0x10 0x804841b <func@plt+11> jmp 0x80483e0 gef➤ si ... 0x8048406 <__libc_start_main@plt+6> push 0x8 0x804840b <__libc_start_main@plt+11> jmp 0x80483e0 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014 → 0x8048416 <func@plt+6> push 0x10 0x804841b <func@plt+11> jmp 0x80483e0 0x8048420 <__gmon_start__@plt+0> jmp DWORD PTR ds:0x8049ff8 0x8048426 <__gmon_start__@plt+6> xchg ax, ax
0x804a014, 正位于.got.plt。也就是说func@plt
要进入.got.plt去寻找func的地址。现在func还没有加载,jmp里保存的就是jmp前的下一条指令(0x8048416),
→ 0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
这个0x10,是指func函数在.rel.plt中的偏移,在010editor里也可以看一下,下图中有3个函数数据项,依次要填入GOT[3:6]
,本次延迟加载实验关注点是,执行一次func后0x804a014要填入func的地址:
继续si跟进,进入了.plt:
→ 0x80483e0 push DWORD PTR ds:0x804a004
0x80483e6 jmp DWORD PTR ds:0x804a008
...
gef➤ x /10wx 0x804a000
0x804a000: 0x08049f08 0xf7ffd940 0xf7fead40 0x080483f6
0x804a010: 0xf7df6eb0 0x08048416 0x00000000 0x00000000
...
gef➤ x /10x 0x804a008
0x804a008: 0xf7fead40 ...
上面输出了.got.plt的内容:
_dl_runtime_resolve
__stack_chk_fail
__libc_start_main
也就是说,PLT[0]这里先压栈func函数在.rel.plt中的偏移0x10,再压栈GOT[1]。
跟进这个jmp _dl_runtime_resolve
,就调用了链接器:
→ 0xf7fead40 push eax
0xf7fead41 push ecx
0xf7fead42 push edx
...
0xf7fead40 in ?? () from /lib/ld-linux.so.2
多单步几次,就进入了func:
→ 0xf7fcd45d <func+0> push ebp
0xf7fcd45e <func+1> mov ebp, esp
0xf7fcd460 <func+3> call 0xf7fcd49f <__x86.get_pc_thunk.ax>
0xf7fcd465 <func+8> add eax, 0x1b9b
回到main,第二次调用func:
→ 0x8048599 <main+83> call 0x8048410 <func@plt>
↳ 0x8048410 <func@plt+0> jmp DWORD PTR ds:0x804a014
0x8048416 <func@plt+6> push 0x10
0x804841b <func@plt+11> jmp 0x80483e0
gef➤ x /10x 0x804a014
0x804a014: 0xf7fcd45d
可以看到,0x804a014,也就是GOT[5]里已经填充了func的函数地址。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。