제공되는 파일
게임 실행 모습
총을 쏘는 게임이다. 처음에 너무 느려서 안 움직이는 줄 알았는데, 아주 천천히 움직이긴 한다.
조금 나가보면, 이런 고구마 군단이 있는 걸 볼 수 있다.
고구마에 찰싹 붙으면 HP가 떨어진다.
또, 총으로 고구마를 가까이서 계속 쏘면 사라진다.
(참고로 게임 실행하면 마우스 꺼내기가 곤란한데, Alt를 누르면 커서가 게임 밖으로 나온다.)
너무 귀여운 게임이다.
Flag는 어디있을까?
/data 폴더를 살펴보면 딱히 flag로 의심되는 파일은 없다.
디버깅을 통해 살펴보자.
문제 분석
# 참고로 아래 캡처 사진들에서 프로그램은 0xE90000에 Image가 로딩되었다.
# ImageBase: 0xE90000
먼저, 프로그램에서 사용되는 문자열 목록을 보자.
"Game Over! You are dead"와 "Game Clear!" 문자열이 보인다.
먼저 Game Over 문자열이 사용된 코드를 살펴보자.
다른 문제들에서는 조건 분기문을 통해서 성공과 실패가 나뉘는데,
지금 이 코드 부분만 봐서는 성공을 결정하는 조건과 실패를 결정하는 조건이 분리되어 있는 것 같다.
Game Clear 문자열이 사용된 코드도 살펴보자.
Game Over과 Game Clear 메시지 박스를 띄우는 두 개의 코드를 살펴봤다.
여기서 한 가지 특이한 점을 발견할 수 있다.
Game Over 함수의 경우 인자가 다음과 같다.
MessageBoxA(ecx, "Game Over! You are dead", "ReversingKr - FPS Game", 40)
위 함수처럼 MessageBox의 두 번째와 세 번째 인자에는 문자열이 들어간다.
그런데, Game Clear 메시지 박스는 두 번째 인자가 어떤 문자열인지 나오지 않는다.
아마 이 두 번째 문자열이 바로 이 문제의 Flag일 것이다.
저 메모리 주소로 가보자.
의미를 알기 힘든 문자열이 보인다.
혹시 모르니까 Game Clear 메세지 박스를 띄워보자.
반복문 안에서 분기가 일어나지 않도록 하면, Clear 메시지박스를 띄울 수 있다.
반복문에서는 fpx.E99194에서 시작해서 0x210 만큼씩 떨어진 위치들의 값들을 확인하는데,
현재는 다 1로 되어있어서 간단히 패치할 수 있다. (나는 je를 jne로 바꿔서 실행했다)
Game Clear! 메세지 박스가 나타났다.
역시 알 수 없는 이상한 값이 쓰여있다.
아무래도 Flag 값이 암호화되어 있는 것 같다.
저 Flag 값의 주소는 fps.E97028이다. (주소는 로딩된 ImageBase에 따라 달라진다. 상대주소는 0x7028이다.)
이 주소를 사용하는 코드를 살펴보자. (메모리 덤프에서 해당 주소로 이동 후 Ctrl + R)
두 개의 코드에서 해당 주소를 참조한다.
두 번째 코드는 우리가 방금 살펴본 메세지 박스 부분이다.
그럼 첫 번째 코드를 살펴보자.
한 바이트 씩 XOR 연산을 한다.
다른 주소에서 한 바이트 값을 읽어와 cl에 저장한 뒤, Flag가 저장돼 있는 주소의 한 바이트와 XOR 한다.
이 부분은 복호화하는 부분이다.
( ∵ 파일(raw data)에서 암호화 된 문자열로 존재하기 때문 )
문제를 풀기 위해 필요한 모든 코드 부분을 찾았다.
이제는 더 집중적으로 분석해, Flag를 복호화해 보자.
집중 분석
Flag를 복호화하기 위해서 집중적으로 봐야 할 중요한 코드는 두 가지이다.
위의 문제 분석에 있는 코드 사진들 중에서 코드 설명에 '중요 코드'라고 적어놨다.
이 코드들을 집중 분석할 것인데, 여기부터는 하나하나 캡처해서 설명하지 않고
분석한 내용을 정리해서 적겠다.
< '중요 코드 2' 분석 >
push ecx
call fps.E93440
cmp eax, FFFFFFFF
je fps.E9343E
fps.E93440 함수를 호출하고 나면 EAX 레지스터에 값이 리턴된다.
이 값은 게임에서 조준점이 무엇을 가리키고 있느냐에 따라 값이 달라진다.
허공을 가리키고 있으면 FFFFFFFF 값이 리턴되는 것 같고,
고구마를 가리키고 있으면 1 BYTE의 값이 리턴된다.
이것은 고구마의 Serial number로 추정된다. (고구마마다 특정한 값이 정해져 있는 것 같음)
허공을 가리키고 있으면 jump 하고, 고구마를 가리키고 있으면 분기가 일어나지 않는다.
분기가 일어나지 않았을 때의 코드가 중요하다.
mov ecx, eax
imul ecx, ecx, 210
mov edx, dword ptr ds:[ecx + E99190]
test edx, edx
jg fps.E93435
고구마의 Serial 번호에 0x210을 곱한다.
해당 값을 인덱스로 하여 특정 위치의 값을 가져온다.
0x210은 고구마 구조체의 크기로 추정한다.
고구마 구조체는 아래 그림처럼 메모리에서 연속적으로 적재되어 있다.
이 구조체 안의 특정 값이 test 명령을 통해서 0인지 확인한다.
만약 0이 아니라면 분기가 일어나고 값을 감소시키고 함수가 리턴된다.
이 값은 고구마의 체력으로 추정된다.
고구마의 체력이 0이 되어, 분기가 일어나지 않으면 아래의 코드가 실행된다.
mov dword ptr ds:[ecx + E99194], 0
mov cl, byte ptr ds:[ecx + E99184]
xor byte ptr ds:[eax + E97028], cl
pop ecx
ret
[ecx + E99194] 값을 0으로 바꾼다.
이 값은 고구마의 생존 여부로 추정된다.
[ecx + E99184]의 한 바이트와 [eax + E97028]의 한 바이트를 XOR 한다.
[eax + E97028]은 Flag의 한 바이트를 가리킨다.
즉, 고구마 구조체 안에 Flag를 복호화시킬 수 있는 값이 저장되어 있고,
고구마가 하나 죽을 때마다, Flag의 특정 값 한 바이트를 복호화시키는 것이다.
< '중요 코드 1' 분석 >
mov eax, fps.E99194
cmp dword ptr ds:[eax], 1
je fps.E93A01
add eax, 210
cmp eax, fps.E9F8B4
jl fps.E939C5
// MessageBoxA( , "Game Clear!", Flag, )
Game Clear! 메시지 박스를 띄우는 코드의 윗부분이다.
고구마 구조체의 생존 여부 값이 모두 0이어야 Game Clear! 메시지 박스가 띄워진다.
즉, 모든 고구마를 죽였을 때 Flag 값이 복호화되어 Game Clear 메시지 박스가 띄어지게 된다.
(고구마의 수가 Flag 길이와 같을 때)
위 분석들을 통해서 고구마 구조체에서 우리가 주목해야 할 필드는 다음 그림과 같다는 것을 알 수 있다.
(위 주소값은 체력 필드의 주소가 0x10이라고 했을 때의 각 필드의 상대적인 위치를 나타낸 것이다.)
문제를 풀기 위한 모든 분석이 끝났다.
이제 획득한 정보들을 바탕으로 Flag를 복호화하는 코드를 작성한다.
문제 풀이
이 문제는 다음의 과정으로 풀 것이다.
1. 고구마 구조체 전체의 바이트 데이터를 파일로 만든다.
2. 구조체 안의 복호화 값들을 추출한다.
3. 복호화 값으로 플래그를 복호화한다.
그럼 가장 먼저, 고구마 구조체 데이터를 추출해야 한다.
가장 첫 번째 구조체의 복호화 값의 주소는 0xE99184이다.
현재 로딩된 Image Base 값이 0xE90000이므로, 상대주소는 0x9184이다.
RVA와 Virtual Size 값을 봤을 때, 이 값은 .data 섹션에 있어야 한다.
그런데 Size of Raw Data의 크기가 0xA00밖에 안 된다.
Virtual Size와 비교해 봤을 때 큰 차이가 나고 있다.
이는 고구마 구조체는 프로그램이 메모리에 올라온 후에, 메모리에 기록된다는 이야기이다.
고구마 구조체를 얻기 위해서는 프로그램을 메모리에 올린 후에 그 메모리를 덤프해야한다.
이를 위해 x64dbg의 플러그인을 사용할 수 있다.
플러그인 탭에 Scylla 플러그인을 선택한다.
위와 같은 창이 뜰 텐데,
OEP는 프로그램의 EP를 넣어주고,
'IAT Autosearch' 버튼 클릭 -> 'Get Imports' 버튼 클릭 -> 'Dump' 버튼 클릭
해서 덤프를 뜬다.
(IAT 목록이 다르게 뜰 수도 있을 것 같다. 이는 크게 신경 쓰지 않아도 될 것 같다.)
덤프 한 파일을 PEView로 보았을 때 위와 같다.
Size of Raw Data가 커진 것을 볼 수 있다.
이제 고구마 구조체의 RVA를 RAW로 변환해서 데이터를 추출하자.
고구마 구조체의 RVA는 0x9184 ~ 0xF8A4 이다.
(시작 offset은 '중요 코드 2'에서, 끝 offset은 '중요 코드 1'에서 구할 수 있다.)
이를 RAW로 변환하면 0x6F84 ~ 0xD6A4 가 된다.
HxD를 통해 해당 offset을 추출해 파일로 저장하자.
플래그도 찾자.
플래그의 RVA: 0x7028
플래그의 RAW: 0x4E28
플래그가 몇 글자인지 아직 모르는 상태이다.
일단 시작주소로부터 0x00이 나오기 바로 전 A9까지를 복사하자.
이제 남은 건 고구마 구조체들로부터 복호화 키를 추출하고 플래그를 복호화하는 일뿐이다.
필요한 모든 데이터는 준비됐다.
파이썬으로 이제 복호화하는 코드를 작성하자.
filePath = "C:\\Users\\경로\\Direct3D_FPS\\decoding_keys.txt"
flagStringList = "43 6B 66 6B 62 75 6C 69 4C 45 5C 45 5F 5A 46 1C 07 25 25 29 70 17 34 39 01 16 49 4C 20 15 0B 0F F7 EB FA E8 B0 FD EB BC F4 CC DA 9F F5 F0 E8 CE F0 A9".split()
with open(filePath, "rb") as f:
data = f.read()
data = data.hex()
unit = 0x210
decodingKeys = []
for i in range(0, len(data)-(unit*2), unit * 2): # 마지막 하나는 해당되지 않으므로 제외시킴.
decodingKey = data[i:i+2]
decodingKeys.append(decodingKey)
# print(i, ":", decodingKey)
resultList = []
for i in range(len(decodingKeys)):
obfuscatedFlag = int(flagStringList[i], base=16)
decodingKey = int(decodingKeys[i], base=16)
resultList.append(obfuscatedFlag ^ decodingKey)
print(''.join([chr(i) for i in resultList]))
결과는 다음과 같이 나오게 된다.
결론
호박감자가 쓴, 호박고구마 문제 풀이
'리버싱' 카테고리의 다른 글
[CodeEngn] Basic RCE L09 (0) | 2023.08.04 |
---|---|
Position 파이썬 코드 해설 (0) | 2023.07.03 |
Reversing.kr Position 문제 풀이 (0) | 2023.07.03 |
Reversing.kr ImagePrc 문제 풀이 (0) | 2023.06.18 |
Reversing.kr Replace 문제 풀이 (0) | 2023.06.16 |