2021.01.15


1. Reversing.Kr - Easy Unpack (Reversing, 100pts)

이번 문제파일은 압축파일 형태로 제공된다. 압축을 풀면, Easy_UnpackMe.exe와 ReadMe.txt을 얻을 수 있다. ReadMe.txt 파일을 열면 아래 [그림 1]과 같은 화면을 볼 수 있다.

[그림 1] ReadMe.txt


이 문제에서 요구하는 것은 오로지 OEP(Original Entry Point) 뿐이다. 문제 제목으로도 알 수 있듯, 이번 문제는 패킹된 바이너리 파일이 주어지며 패킹이 풀리고 실제 프로그램이 동작을 시작하는 지점(OEP)을 찾는 것이 목표이다. 따라서 굳이 프로그램을 실행해서 볼 필요는 없다. 실행하더라도 빈 화면만 나와서 딱히 의미가 없다.

패킹을 푸는 방법은 여러가지가 있지만, 이번 문제는 그렇게 어려운 문제는 아닐듯 하다. (일단은 Easy니까..) 대표적인 바이너리 패킹 방식으로는 UPX가 있는데, PEview로 파일을 보았을 때 이것은 UPX가 아닌 별도로 커스텀한 패킹 방식을 사용한 듯 하다. 아래 [그림 2]는 PEview로 Easy_UnpackMe.exe를 확인한 모습이다. 일반적인 PE 구조와 달리 .Gogi 섹션과 .GWan 섹션이 들어있음을 볼 수 있다.

[그림 2] PEview


그러면 이제 디버거를 통해 언패킹 루틴을 간단하게 살펴보고, OEP를 찾아보도록 하자. 나는 OllyDbg 1.1을 사용하여 열었는데, 두 가지의 메시지가 출력된다. 첫 번째 메시지는 EP를 찾을 수 없다는 것, 두 번째 메시지는 해당 코드가 압축, 암호화 등의 조치가 되어있어 분석이 어렵다는 것. 두 메시지 모두 패킹된 바이너리라면 오히려 당연한 내용이니 그냥 넘어가면 되겠다.

아무튼 디버거로 프로그램을 열면 0x40A04B부터 시작한다. LoadLibraryA() 함수로 kernel32.dll을 불러오고, GetProcAddress() 함수로 GetModuleHandleA(), FreeLibrary() 함수의 주소를 가져온다. 그 이후 0x40A099 ~ 0x40A0C1 구간을 반복해서 돌며 0x409000 ~ 0x4094EE를 1바이트씩 가져와 0x10, 0x20, 0x30, 0x40, 0x50 값들과 XOR 연산을 한다. 참고로 0x409000 ~ 0x4094EE는 .Gogi 섹션에 해당한다. 실제 어셈블리어로 된 루틴은 아래 [그림 3]을 통해 확인할 수 있다.

[그림 3] 반복문을 통한 언패킹


이후 반복문을 지나면 다시 GetProcAddres() 함수로 VirtualProtect() 함수의 주소를 가져온 뒤, VirtualProtect()를 실행한다. 위의 PUSH한 인자들을 보면 해당 함수를 통해 0x405000에서부터 0x1000 바이트만큼 0x4(PAGE_READWRITE) 권한을 부여하였다. 그리고 0x40A678에 변경 전 상태를 저장하는 포인터를 두었다.

[그림 4] VirtualProtect


그렇다면 그 이후에는 당연히 0x405000에서부터 무언가를 쓰기 시작할 것이다. 다만 예상과는 달리 [그림 3]과 같이 XOR 연산이 아닌 User32.dll을 불러오고 GetModuleHandleA()를 통해 상당한 수의 함수 주소들을 가져오는 부분이 있었다.

그 이후에는 다시 한번 VirtualProtect(0x401000, 0x4000, 0x4, 0x40A678)이 실행된 이후에 드디어 XOR 연산을 하는 반복문이 나왔다. 0x401000 ~ 0x405000 부분을 [그림 3]과 같은 방식으로 XOR 연산한다.

그 이후 마지막으로 VirtualProtect(0x406000, 0x3000, 0x4, 0x40A678)을 실행한 후 마찬가지로 같은 방식으로 XOR 연산한다. 그리고.. 그 모든 연산이 끝나고 맨 마지막에는 어떤 주소로 점프하는 구문이 있는데, 이는 일반적으로 언패킹이 끝나고 OEP로 넘어가기 위한 부분이다.

[그림 5] Jump to OEP


[그림 5]를 보면 알 수 있듯 0x40A1FB에는 어느 주소로 점프하는 구문이 있다. 해당 주소는 현재 있는 곳에서 꽤 멀리 떨어진 곳이고, 0x40A1FB 이후에는 아무런 명령어가 존재하지 않는다. 전형적인 언패킹 이후 OEP로 넘어가는 루틴이다. 앞서 언급했던 UPX 또한 이러한 루틴을 갖는다.

따라서, [그림 5]에서 검은 색으로 칠해진 부분이 바로 OEP이며, 실제로 해당 주소로 넘어가서 Ctrl+A를 누르면, 익숙한 스택 프레임 생성 명령어를 확인할 수 있다.

[그림 6] 55 8B EC


이렇게 OEP 주소를 알게 되었으면, 그것이 곧 플래그이니 reversing.kr에 가서 입력하면 100점을 얻을 수 있다.

이번 문제는 푸는 것 자체는 상당히 빨리 풀 수 있을 문제였으나, 단순히 푸는 것 자체에 의의를 두기 보다는 언패킹 루틴을 천천히 분석해나가며 푸는 것이 문제의 취지에 더 맞는 풀이방식이 아니었을까 생각해본다. (뭔가 허무해서 이런 말 남기는거 아님)

2. Reversing.Kr - Easy ELF (Reversing, 100pts)

이번 문제는 ELF 파일을 분석하는 문제이다. 그래서 칼리를 켜서 실행해보았다.

[그림 1] Wrong


해당 파일을 실행하면 먼저 ‘Reversing.Kr Easy ELF’가 출력되며, 입력값을 넣을 수 있게 된다. 나는 [그림 1]과 같이 ‘password’를 입력했는데, 당연하게도 Wrong이 출력되었다. 아마 이전에 풀었던 Easy Crack과 유사하게 특정 입력값을 넣어야하는 문제인듯 하다.

기드라로도 이 파일을 볼 수 있지만, 우선 gdb를 통해 살펴보기로 했다.

[그림 2] scanf()


[그림 3] 입력값 검증 루틴


우선 [그림 2]와 같이 scanf() 함수로 입력값을 받아 0x804a020에 저장한다. 그 후, [그림 3]과 같이 입력값에서 임의의 1바이트를 가져와 특정 값과 비교하는 식으로 입력값을 검증한다. 이 부분 외에는 별도의 검증루틴은 보이지 않았다.

우선 검증하는 데이터는 0x804a020 ~ 0x804a025까지이므로 입력값의 길이는 5바이트여야 함을 알 수 있다. (5바이트 + NULL(‘\0’)) 따라서, 지금부터 편의상 입력값을 input이라고 하고, 입력값 중 i번째 글자를 input[i]라고 하겠다. (0번부터 4번까지) 이제부터 천천히 [그림 3]의 명령어들을 훑어보면서 살펴보자.

  • 우선 0x804a021(이하 input[1])은 0x31(‘1’)과 비교하는데, 이를 통해 input[1]은 ‘1’이어야 함을 알 수 있다.
  • 이어서 input[0]은 우선 0x34와 XOR 연산 후 0x78과 비교하는데, 따라서 input[0] == 0x78 ^ 0x34 == 0x4C(‘L’) 임을 알 수 있다.
  • input[2]는 0x32와 XOR 연산 후 0x7C와 비교한다. 따라서 input[2] == 0x7C ^ 0x32 == 0x4E(‘N’)이다.
  • input[3]은 0xFFFFFF88과 XOR 연산 후 0xDD와 비교한다. 따라서 input[3] == 0xDD ^ 0xFFFFFF88이 되는데, 이 때 input[3]은 1바이트이므로 0xFFFFFF88은 0x88로 바꿀 수 있다. 따라서 input[3] == 0xDD ^ 0x88 == 0x55(‘U’)이다.
  • 마지막 input[4]는 곧바로 0x58과 비교하는 것으로 보아 input[4] == 0x58(‘X’)이다.
  • 그리고, 0x804a025는 test 구문으로 값을 검증하는 것으로 보아 예상대로 문자열의 끝을 의미하는 NULL(0x00)이어야 한다.

이제 input[0]부터 input[4]까지 순서대로 연결하면 ‘L1NUX’가 됨을 알 수 있다. 이것을 입력하면 아래 [그림 4]와 같이 ‘Correct!’ 메시지를 출력하는 것을 볼 수 있다.

[그림 4] Correct!


‘L1NUX’가 곧 플래그이므로 이것을 reversing.kr에 가서 입력하면 100점을 얻을 수 있다. 예상대로 이 문제는 Easy Crack과 비슷한 유형이었으나, 그것보다 훨씬 간단한(?) 문제였던 것 같다.