본문 바로가기

Games/CTF

PHDays 2014 miXer

문제로 매우 단순한 Linux x86 ELF 바이너리가 주어진다. strip 조차 되어있지 않다. 실행시키면 바로 Segmentation Fault 가 나면서 죽는다. IDA 로 확인해보니 main 함수가 아래처럼 되어있었다. 문제의 설명에 따르면 "eventually the binary is blended, can you restore it?" 라고 한 것으로 보아 바이너리에 뭔가 조작이 가해졌는데, 이를 원상복귀 시켜야되는 문제 같았다.



괴상한 쓰레기코드들이 가득한 메인함수를 보던중 처음에는 XOR 같은 인코딩이 되어있나 싶기도하고 일단 HEX 로 살펴보았다.  그러던중 처음 어셈블리 바이트시퀀스들중 55 57 89 ... 등등 뭔가 눈에 익은 바이트들이 보였다. 55 라면 push ebp 의 opcode 일터... push ebp 는 함수의 시작에서 스택프레임을 잡기위해 대부분 제일먼서 수행되는 명령중 하나이다. 이것을 보는순간 금방 감을 잡았다. 아무래도 원래의 main 함수의 기계어 배열을 가지고 바이트 순서를 부분적으로 뒤섞은 결과가 문제인것 같다.  


어떤 규칙으로 이 기계어 바이트들의 순서가 뒤섞인것인지를 어셈블리 명령들에 대한 사전지식으로 퍼즐을 풀듯이 풀어나가야 했다. 예를 들면 맨처음의 3 바이트 E4 83 F0 의 경우 사실 알고보면 83 e4 f0 의 순서가 바뀐 것이라는것을 유추 할 수 있다.  이는 and $0xfffffff0, %esp 가 기계어로 나타난 것인데, 이또한 push ebp 처럼 함수의 시작부분에서 스택주소를 정렬하는 용도로 자주 쓰이는 명령중 하나이다.  objdump 를 통해서 여러 바이너리들의 함수 프롤로그 부분들을 조사하다보니 컴파일러가 생각보다 어셈블리 코드에 대한 기계어 생성을 "규칙적으로" 하는 패턴들을 발견 할 수 있었다.



함수의 프롤로그 부분에서 몇몇 레지스터셋을 스택에 저장할때 그 순서가 레지스터들의 집합에 따라 거의 항상 일정하다는 사실을 발견 할 수 있었다.  예를들어 아래처럼 스택상에 ebp, edi, esi, ebx 4개를 저장할때의 순서는 대부분 언제나 EBP -> EDI -> ESI -> EBX 라는 것을 발견 할 수 있었다.  문제 바이너리의 초반부를 보면 55, 57, 53, 56 을 볼 수 있는데, 이는 곧 4개의 레지스터를 푸시하려 했음을 추정할 수 있다. 만약 원래의 정상적인 원본 바이너리에서 이 4개의 레지스터를 스택에 push 하는 기계어 코드를 컴파일러가 생성했다면 필시 55, 57, 56, 53 의 순서였을 것이다. 따라서 문제의 "55 57 53 56" 에서 53 과 56 의 순서가 뒤바뀌었다 라는것을 알 수 있고. 이것을 기반으로 함수의 에필로그 부분에서 마찬가지로 4개의 레지스터가 올바른 순서로 pop 되어야 한다는 것을 생각 할 수 있다.



이런식으로 온갖 어셈블리 명령들의 모양새와 컴파일러가 이들을 배치하는 방식을 조사해보면서 문제의 바이너리 바이트 순서를 조사했다.  그런데 아무리 조사해도 서로 순서를 Swapping 하는 알고리즘으로는 복원이 되지 않는것 같았다.  그래서 전략을 바꿔서 상대 오프셋을 조사하는 방식으로 적용해봤다.  그러다보니 어떠한 부분적 패턴들이 발견되었고 최종적으로 전체 패턴을 찾을 수 있었다.  분석방식은 아래와 같다.

만약 반드시 AA, BB, CC, DD 라는 순서로 하나의 명령어가 존재한다면, 문제의 바이너리중에 AA 를 찾았을때 그 주변에 BB 가 존재하는 상대 오프셋을 기록해보고, 다시 그 근처에 CC 를 찾기위한 오프셋을 찾아보고.. 이런식으로 접근 했을때 원본 바이너리에 대해서 +, - 방향으로 점프를 해가는 오프셋이 어떤 규칙에 의해서 반복될 것이라고 예상했다.  그리고 그 예상이 옳았음을 여러가지 어셈블리 명령들의 패턴을 분석해가며 입증 할 수 있었다.  최종적으로 찾은 패턴은 55 (push ebp, 첫번째 명령이라 가정) 로부터 [+6, -4, -1, +4, -2, -5, -1, +2, +5, +6] 의 패턴으로 바이트들을 짚어가게되면 계속해서 유효한 형태의 어셈블리 명령들이 이어지게 됨을 알아낼 수 있었다.

이 패턴을 찾고나서는 파이썬으로 간단히 바이트들을 원래대로 뒤섞는 스크립트를 만들고 main 함수의 바이너리를 패치해서 원래의 바이너리를 복원 할 수 있었다.


import sys, os


fd = open('raw')

r = fd.read().decode('hex')

#print 'length : ' + str(len(r))


pattern = [+6, -4, -1, +4, -2, -5, -1, +2, +5, +6]

res = ''

i=3

k=0

while i<len(r):

res += r[i]

i += pattern[ k % 10 ]

k += 1


print res



복원된 바이너리를 실행시키니 정상적으로 실행되었고 flag 를 던져주었다.

어셈블리와 컴파일러의 특성을 통해 푸는 퍼즐이라고 할수 있는 문제였다.  상당히 머리도 아프지만 단서 하나하나를 찾아가는 재미가 쏠쏠한 문제였다.  풀이시간은 5~6시간가량 걸린듯 하다. 라온에서는 2시간정도만에 푼것같은데 정말 대단한것 같다...





'Games > CTF' 카테고리의 다른 글

Codegate 2014 Angry Doraemon Writeup  (0) 2014.03.03
Olympic CTF 2014 Echof writeup  (1) 2014.02.10
PHDays 2014 FreeBDSM  (0) 2014.01.27
Whitehat RSA Signing  (0) 2014.01.07
HDCON2013 final cft2  (10) 2013.10.31