ORW(Open-Read-Write)小记

ORW

[toc]

若程序使用了沙箱机制,例如seccomp,可能会禁用execve的系统调用,此时我们便只能使用ORW(open read write)的系统调用来读取flag文件。

查看沙箱:seccomp-tools

1
seccomp-tools dump ./file

ropper工具的使用

类似于ROPgadgetsropper也可查找gadgets,但ROPgadgtes有时候无法查找到某些gadgets,而且ropper的速度相对来说会快一些。

可以直接在搜索gadgets

1
ropper --file ./libc.so.6 --search pop rax

或者是进入ropper,然后使用ropper内置的终端加载要查找的文件,连续查找gadgets,就不需要多次读取文件内的gadgets了:

1
2
3
4
ropper # 进入内置的ropper终端
file ./libc.so.6 # 加载要搜索gadgets的文件
search rdi
search rsi

open函数

若我们要发起一个open函数的系统调用,自然首先需要明白它的各个参数。详细信息如下:

1
int open(const char *filename, int flags, mode_t mode);

其中,第一个参数filename,也就是rdi对应的内容,我们填写为字符串类型的要打开的文件名即可。

第二个参数为flags,即读写模式,若读文件则设置为0,若写文件则设置为1,读写文件设置为2

第三个参数在ORW中我们一般可以不填。

此外,open函数的系统调用号rax2

open系统调用被禁用

尝试使用openat()函数,它的系统调用号是257,可以syscall调用也可以直接libc调用

1
int openat(int dirfd, const char *pathname, int flags, ...);

只需要写openat(0, '/flag\x00')的形式即可

不知道文件名

可以使用getdents系统调用。先使用open(path, 0x10000)打开目录,然后使用如下方式将其读入到buf

1
getdents(int fd, char* buf, int length);

write函数/read函数

这两个函数比较常用,此处不再赘述。

writev系统调用

可以代替write。其中:

1
2
3
4
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
fd:要写入数据的文件描述符。
iov:指向一个iovec结构数组的指针,每个结构包含一个指向数据缓冲区的指针和该缓冲区的长度。
iovcnt:iovec结构数组中元素的数量。

由此,例如我需要输出0x80000处的长度为0x100的数据,我可以先在堆上构造这个iovec结构体:

1
payload = p64(0x80000) + p64(0x100)

假设构造的这个payload位于地址heap_base + 0x360,那么使用writev如下即可:

1
writev(1, heap_base + 0x360, 1)

setcontext函数

低版本(glibc 2.27及以下)

在低版本的glibc中,setcontext中有一段gadgets如下:

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

可以看到,该版本的setcontext函数在<setcontext+53>处时可以通过rdi控制几乎所有的寄存器。但是rdi又如何控制呢?

我们可以想到,rdi是函数的第一个参数,因此若我们控制__free_hook<setcontext+53>,就可以在free的时候将程序劫持到<setcontext+53>。那么应该如何布置地址空间,才能让我们彻底控制程序执行流程呢?

观察这一段gadgets,发现在<setcontext+53>处控制了rsp[rdi+0xa0],随后在<setcontext+87>push[rdi+0xa8]。相当于说是把栈迁移到了[rdi+0xa0]处,然后再往栈里面push了一个值[rdi+0xa8]。若我们控制[rdi+0xa8]ret,那么即可使得程序执行[rdi+0xa0]了,也就是控制了程序执行的流程。那么,如何才能劫持[rdi+0xa0]的值呢?

现在来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 首先假设已经劫持__free_hook为setcontext+53
// 我们执行:
free(chunk_a);
// 假设chunk_a的地址为0x5630f8b000
// 那么由于本来会执行:
mov rsp, [rdi + 0xa0];
// 实际上即:
mov rsp, [0x5630f8b000 + 0xa0];
// 即:
mov rsp, [0x5630f8b0a0];
// 即意味着我们只需要控制chunk_a偏移0xa0的地方的chunk,就是控制了上面提到的`[rdi+0xa0]`。以这里为例子继续看:
// 假设chunk_b的地址为0x5630f8b0a0,我们已经对其进行了控制
// chunk_b存放的内容为:p64(addr_of_orw_gadgets) p64(retn)
// 程序继续执行到<setcontext+87>,即:
mov rcx, [rdi+0xa8];
// 即
mov rcx, [0x5630f8b0a8];
// 我们知道0x5630f8b0a8处的内容为retn,那么会pop rip,程序开始执行orw_gadgets。

glibc 2.29

该版本开始,setcontext中使用的就不是rdi来对寄存器进行设置了,而是使用的rdx。因此从这个版本开始,需要使用额外的gadget来通过rdi控制rdx,后面的步骤不变。这个版本的setcontext为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000055E35                 mov     rsp, [rdx+0A0h]
.text:0000000000055E3C mov rbx, [rdx+80h]
.text:0000000000055E43 mov rbp, [rdx+78h]
.text:0000000000055E47 mov r12, [rdx+48h]
.text:0000000000055E4B mov r13, [rdx+50h]
.text:0000000000055E4F mov r14, [rdx+58h]
.text:0000000000055E53 mov r15, [rdx+60h]
.text:0000000000055E57 mov rcx, [rdx+0A8h]
.text:0000000000055E5E push rcx
.text:0000000000055E5F mov rsi, [rdx+70h]
.text:0000000000055E63 mov rdi, [rdx+68h]
.text:0000000000055E67 mov rcx, [rdx+98h]
.text:0000000000055E6E mov r8, [rdx+28h]
.text:0000000000055E72 mov r9, [rdx+30h]
.text:0000000000055E76 mov rdx, [rdx+88h]
.text:0000000000055E7D xor eax, eax
.text:0000000000055E7F retn

glibc2.29下一般使用这个gadget

1
mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;

glibc 2.30

同样,但这个版本使用的gadget一般为:

1
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];

此外也可能是setcontext+61setcontext+61为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:000000000005803D                 mov     rsp, [rdx+0A0h] *
.text:0000000000058044 mov rbx, [rdx+80h]
.text:000000000005804B mov rbp, [rdx+78h]
.text:000000000005804F mov r12, [rdx+48h]
.text:0000000000058053 mov r13, [rdx+50h]
.text:0000000000058057 mov r14, [rdx+58h]
.text:000000000005805B mov r15, [rdx+60h]
.text:0000000000058126 mov rcx, [rdx+0A8h] *
.text:000000000005812D push rcx *
.text:000000000005812E mov rsi, [rdx+70h]
.text:0000000000058132 mov rdi, [rdx+68h]
.text:0000000000058136 mov rcx, [rdx+98h]
.text:000000000005813D mov r8, [rdx+28h]
.text:0000000000058141 mov r9, [rdx+30h]
.text:0000000000058145 mov rdx, [rdx+88h]
.text:000000000005814C xor eax, eax
.text:000000000005814E retn

glibc2.31

这个版本的gadget和上面一样,但是变为了setcontext+61

通过_environ获取栈地址

orw时,除了可以通过setcontext以外,还可以通过_environ来获得栈地址。libc中的_environ中存放了一个栈地址,该栈地址中存放的是环境变量的地址,因此知道libc地址的情况可以通过_environ来获取栈的地址,从而控制程序执行流。如下所示:

1
2
3
libc.address = 0x7fxxxxxx
_environ = libc.sym['_environ']
# _environ中存放了栈地址

ORW(Open-Read-Write)小记
http://example.com/2023/09/21/system/unsorted/orw/
作者
Ltfall
发布于
2023年9月21日
许可协议