본문 바로가기

Wargame/pwnable.kr

[Grotesque] aeg

 

추석에 할 거 없어서 pwnable.kr에 접속해서 문제를 이것저것 보다가,

점수가 높기도 하고.. 재미도 있어보여서 aeg 문제를 열어봤는데, 이게 왠걸? angr을 써볼법하게 만들어져서 너무 좋았다.

예전부터 angr 봐야지봐야지 했는데 사정으로 인해(쿠키런도 해야되고 테라리아도 해야되고 운동도 해야되고 피아노도..롤도..) 하지 못했으니, 지금 기회될 때 공부해야지 싶었다.

 

이 문제는 angr을 쓰면 귀찮은 작업이 다수 해결된다. (안쓰면.. ㅎㅎ 안쓰고 푸는 것도 하나의 방법이지만, 권장하진 않는다)

 

 - 400명 이상 푼 문제는 PoC 코드를 공개하겠다.

 - 이 글에는 문제 풀이, 힌트 등이 있으니 실력 증진을 원하는 분들은 충분히 삽질한 후 읽어주시길 바란다.


1. 코드흐름

====================================================

* 여친이랑 '테라리아'라는 게임을 하려고 맥북에 설치된 윈도우 vm을 지웠다. 용량이 부족해서 ....-0-. 그래서 이 문제를 풀 땐 IDA 툴 안쓰고 분석했으니, 눈이 어지러워도 이해해주시길 바란다 ㅠ 최대한 그림 편집을 잘 해봐야지 '-'

 

문제 서버에서 바이너리를 받아오고, gdb 디버거로 분석한다. (이 문제는 소스코드가 공개되지 않았다.)

 

아참 그전에, 이 문제는 main함수가 안보이니 readelf -S aeg_binary를 이용해 .text영역 주소를 잘 확인해준다.

 

libc_start_main에 break 걸고 쭉 따라가다보면 call rax가 있는데, 여기가 main이 시작되는 곳이다.

이렇게 시작점을 찾아도 되고 아니면 그냥 .text영역을 이잡듯이 뒤져가면서 main인 것 같은 곳을 탐색하면 된다.

 

0x6975a5c 주소가 시작주소이다. 참고로, 바이너리를 받아올 때마다 주소가 달라진다. 0x6975a5c는 방금전에 받아온 바이너리의 주소이다.

 

argv[1]의 길이가 2000byte를 넘지 않았다면, (위에서는 1000byte(0x3e8)랬지만 사실은 2000byte이다. sscanf 부분에서 2000byte짜리를 1000byte로 변환한다.) for문을 이용해 argv[1] 값을 %02x형태로 0x6b789c0에 넣는다.

 

 

xor하기 전에 imul, shr 등 별 이상한 일을 많이 하는데 신경 안써도 된다. 그 밑으로 내리다보면 xor이 두개 있는데, 

0x6b789c0에 있는 값들을 하나하나 xor해서 encoding한 후 "payload encoded. let's go!" 를 출력한다.

 

encoding된 문자를 3개씩 edi, esi, edx에 넣고 특정 연산을 수행한후, 특정 문자와 값이 일치하는지 비교(cmp)하는데

이 과정을 총 16번 거치게 된다. 총 48byte의 문자를 비교하게 된다.

48byte의 문자가 모두 일치한다면,

 

rbp - 0x10인 위치에 len(argv[1]) - 0x30 길이만큼 0x6b789f0에 존재하는 인코딩된 값을 밀어넣는다.

 

 

2. 고려사항

====================================================

1. 바이너리를 받아올 때마다 바이너리의 총 크기, 주소값, operand 값 등이 무작위로 변경된다.

 - 하지만 바이너리의 큰 틀은 변하지 않으니 안심해도 좋다.

2. 바이너리는 b64encoding하여 사용자들에게 배포된다.

3. 사용자가 argv[1]에 48byte의 문자를 적절하게 입력하면 memcpy 함수를 이용해 우리가 원하는 값을 rbp-0x10에 입력할 수 있다.

 - rbp-0x10에 넣을 수 있다는 건.. rbp control이 가능하다는 얘기다.

 

 

3. 필요 지식

====================================================

1. 어셈을 간단하게라도 읽을 줄 알아야한다.

 

2. 문제를 쉽게 풀기 위해서는 angr 모듈을 잘 이용해야 한다. angr은 symbolic execution을 이용해 도달해야 할 목적지 주소(target_addr)까지 필요한 input을 계산해준다. 

관련 주소: nextline.tistory.com/157 (symbolic execution 관련 그림)

관련 주소: docs.angr.io/ (angr docs)

관련 주소: github.com/angr/angr-doc/blob/master/CHEATSHEET.md (리버싱에 필요한 기술을 담고 있다. 수동탐색(manually exploring), 자동탐색(explore) 등등 간단하게 알아야 할 정보들이 포함되어 있다.)

 

3. mprotect 함수의 사용법을 알아야 한다.

관련 주소: www.lazenca.net/display/TEC/03.ROP%28Return+Oriented+Programming%29+-+mmap%2C+mprotect 

 

4. rop(return oriented programming) 기법을 사용할 줄 알아야 한다. 

관련 주소: shayete.tistory.com/entry/6-Return-Oriented-Programming?category=857069

참고로, 32bit 환경의 gadget과 64bit 환경의 gadget 적용방식이 다르니 이를 잘 인지하여 공격하셔야 합니다.

 

5. objdump, grep, head, tail 등의 도구를 이용해 바이너리 내의 필요한 값을 잘 확인할 수 있어야 한다.

관련 주소: m.blog.naver.com/PostView.nhn?blogId=yyb7436&logNo=80119340568&proxyReferer=https:%2F%2Fwww.google.com%2F (grep, head, tail 이용하는 법)

관련 주소: m.blog.naver.com/PostView.nhn?blogId=s2kiess&logNo=220066239893&proxyReferer=https:%2F%2Fwww.google.com%2F (objdump 간략 설명)


4. 문제 풀이

====================================================

바이너리의 취약점을 찾는 것 자체는 쉽다. 그러나 10초안에 자동으로 exploit코드를 생성하는 게 번거로울 뿐이다.

문제풀이를 위해 먼저 문제서버에서 바이너리를 받아오자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python2
from pwn import *
import base64
import os
r= remote('pwnable.kr'9005)
print (r.recvuntil("wait...\n"))
string_base64 = r.recvuntil("\n")
= open("aeg_binary.z""wb")
f.write(base64.b64decode(string_base64))
f.close()
 
# unzip aeg_binary.z
os.system("gunzip aeg_binary.z")
os.system("chmod +777 aeg_binary")
print (r.recvuntil("up!\n"))
cs

위 코드를 쓰면 aeg_binary라는 바이너리가 받아진다.

 

이후 코드 분석을 해야하는데.. 분석된 결과는 위의 1. 코드흐름에 잘 나와있다.

나는 angr을 이용해 문제를 풀었으므로 이와 관련한 풀이를 알려주겠다.

 

1. 48byte key 얻어내기

angr는 symbolic execution을 이용해 바이너리 분석을 지원해주는 파이썬 프레임워크이다. 정말 핫한 녀석이다.(핫했던?? 아직도 핫한..?)

aeg 바이너리의 48byte key를 추출하기 위해 angr를 이용할 수 있는데, 바이너리 내에 도달할 타겟 주소(find)와 회피해야할 주소(avoid)를 먼저 찾아야 한다.

 

 - 도달해야 할 주소(find)라면.. 우리 목표는 aeg바이너리 exploit 코드를 짜는 것이니, 당연 48byte key를 입력하고 난  뒤에 실행될 주소이다. 그러니 이 주소는 memcpy가 실행되는 주소 혹은 memcpy가 존재하는 주소로 분기하는 곳을 찍어주면 되겠다.

 - 회피해야 할 주소(avoid)는?? 48byte key를 입력하는 과정에서 자꾸 프로그램을 종료시키려는 분기문들을 적어주면 되겠다. aeg 바이너리는 48byte key를 3글자씩 총 16번 검사한다. 16번 검사하는 과정에서 총 16번 분기하므로, 16개의 회피주소를 얻으면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import angr
import claripy
 
########################## angr Project ! ###########################
# automatic exploit system
# try exploit
= angr.Project('aeg_binary', auto_load_libs=False)  # link aeg_binary
key = claripy.BVS('key'48*8)
state = p.factory.entry_state(addr=entry_addr)
state.memory.store(key_addr, key)
#s.use_technique(angr.exploration_techniques.DFS())
 
= p.factory.simulation_manager(state)
print ("[*] angr explore start..")
s.explore(find=find_addr, avoid=avoid_list)
print ("[*] angr find 48byte key )
cs

이런 느낌으로 코드를 작성하면 된다.

위 코드에서 entry_addr, find_addr, avoid_list는 바이너리에서 추출한 주소들을 따로 저장해놓은 변수, 리스트로, objdump, grep, head, tail을 이용해 잘 받아오면 되겠다. 여기서 entry_addr가 제일 중요하다. 그냥 main 시작부분을 entry로 잡아놓으면 48byte key 획득시간이 너무 오래걸린다. symbolic execution 원리를 잘 모르겠지만, 아마 중간에 symbolic 분석 과정에서 뭔가 비교해야할 경우의 수가 많아서 오래걸리는 것 같다.

 

objdump, grep, head, tail의 사용 예를 간단하게 보여주면 아래와 같다.

1
2
3
4
5
6
7
8
9
import subprocess
 
print ("[*] extract parameters" )
######################## collect xxxx addr #######################
cmd = 'objdump -d aeg_binary | grep %eax| head -3 | tail -1'
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.PIPE)
addr_Idx = out.index(b':')
xxxx_addr = int(out[1:addr_Idx], 16)
print (" @xxxx_addr: " + hex(xxxx_addr))
cs

objdump -d aeg_binary |grep %eax |head -3 |tail -1 을 입력하게 될 경우,

 1. aeg_binary를 대상으로 objdump -d를 실행한다.

 2. 덤프결과 중 %eax가 포함된 결과만 따로 뽑아서(grep) 출력해준다.

 3. 출력된 결과중 위에서 3번째 줄까지만(head -3) 출력한다.

 4. 출력된 3개의 줄 중 마지막 한줄 (tail -1)만 출력한다.

결과적으로 저 명령어를 실행하면 objdump 출력 결과 중, 위에서 3번째 줄만 출력된다.

 

2. exploit 코드 힌트

angr를 잘 이용했다면 48byte 키가 3초 안에 나올텐데, 그 뒤에 exploit 코드를 짜는건 어렵지 않다. rop gadget 찾는게 좀 어려울 뿐이지..

힌트를 주자면,

 - main의 맨 위 부분 중 코드흐름에 그렇게 큰 영향을 안주는 부분?에서 뭘 하는지 유심히 보자. rdi, rsi, rdx, rax 등 rbp주소를 참고하여 여러 레지스터를 쓰고 있을 것이다.

  * 이는 rdi, rsi, rdx 값을 우리 임의로 조작할 수 있다는 뜻이다. 저 부분을 이용하기 위해 pr (pop rbp, ret) 가젯을 찾아보자.

 - aeg 바이너리는 실행권한이 걸린 곳이 없다. mprotect를 이용해 원하는 곳에 rwx 권한을 주자.

 - 64bit 환경의 바이너리는 함수 실행 전에 rdi, rsi, rdx 등 레지스터에 값이 들어있어야 해당 레지스터를 참조하여 함수를 실행한다.

 

 

이제 코드를 잘 짜서 문제를 풀면 끝!

 

'Wargame > pwnable.kr' 카테고리의 다른 글

[Grotesque] lokihardt  (2) 2021.04.01
[Grotesque] maze  (2) 2020.10.12
[Rookies] crypto1  (0) 2020.09.14
[Rookies] note  (0) 2020.09.10
[Toddler] fd  (0) 2020.09.06