Reversing.kr의 5번째 문제인 Replace다. 이전 문제인 Music Player보다는 조금 더 푼 사람이 많은데 아마 비주얼 베이직 프로그램이 아니어서 그런지도 모른다. 이번 문제에서는 단순히 문제 파일만 주어지고 어떻게 뭘 하라는 지시가 없어서 좀 헷갈릴 수 있다. 일단 프로그램을 실행시켜보자.
사용자 입력을 받는 텍스트 박스 하나와 Check 버튼, 그리고 Wrong이라는 문자열로 구성된 화면이 나타났다. Wrong 문자열이 있는 부분은 사용자 입력에 아무 반응이 없으며 텍스트 박스에는 숫자만 입력할 수 있다. 일단 아무 값이나 입력한 후 Check 버튼을 눌러보면 어떤 반응도 없이 그대로 프로그램이 종료되는 것을 볼 수 있다. 그런데 뭔가 프로그램이 종료되는 모습이 부드럽지가 않은데, 정확히 말하면 잠깐동안 로딩 아이콘이 마우스 커서에 나타났다가 프로그램이 종료된다. 이를 알아보기 위해 Git Bash같은 터미널로 프로그램을 실행시켜보면 정상적인 종료가 아니란 것을 알 수 있다.
프로그램에 어떤 변화를 가하지 않았는데도 이렇게 비정상 종료(Segmentation Fault)가 발생한다면 프로그램 내부 코드를 살펴보는 것 말고는 방법이 없을 것이다. 일단 다른 정보는 얻을 수 없으니 디버거로 실행시켜봐야 할 것이다. 먼저 기본적인 문자열 탐색을 해보면 다음과 같은 결과를 얻을 수 있다.
함수 이름이나 기타 문자열과 더불어 맨 위에 "Correct!" 라는 문자열을 찾을 수 있다. 아까 프로그램을 실행시켰을 때 나타나던 "Wrong!" 문자열과 완전 반대의 의미인데 그렇다면 저 문자열을 사용하는 곳이 프로그램의 어떤 문제를 해결하고 정상적으로 진행할 경우 도달하는 곳이 아닐까? 일단 저곳으로 이동해보기로 했다.
운이 좋게도 근처에서 관련 함수를 찾을 수 있었다. GetDlgItemInt 함수는 MSDN 문서에 따르면 다이얼로그 박스의 특정 컨트롤이 가지는 값을 정수값으로 가져오는 함수로 프로그램에서 사용자가 입력할 수 있는 공간은 숫자밖에 입력되지 않았던 텍스트 박스 하나뿐이기 때문에 이 컨트롤의 값을 가져오는 것으로 추측할 수 있다. SetDlgItemTextA는 버튼을 눌렀을 때 로직이 수행된 후 결과가 맞다면 Correct 문자열을 세팅하는 역할을 수행할 것이다. 그런데 해당 함수의 호출 부분을 보면 스택에 매개변수를 삽입한 후 바로 함수 호출이기 때문에, 그리고 바로 위의 코드가 jmp 명령어기 때문에 이 함수를 호출하려면 0x00401073으로 바로 점프해야 한다. 그렇다면 'jmp 0x00401073'을 검색해보면 이곳으로 오는 단서를 찾을 수 있지 않을까?
이상하게도 아무런 결과를 얻을 수 없었다. 즉 이 명령어로 점프하도록 하는 코드가 없다는 것이며 그렇다면 이 Correct 코드는 영원히 불리지 않는 것인데 이 프로그램은 어떻게든 풀릴 것이고 그러면 저 코드를 수행해야 "Correct!"라는 문자열을 표시하는 게 아닌가? 좀 더 자세히 알아보기 위해 모듈 간 함수 호출을 살펴보기로 했다.
kernel32.dll, ntdll.dll 등 여타 모듈의 함수 호출은 제외하고 user32.dll의 함수 호출 목록을 살펴보면 아까 봤던 GetDlgItemInt와 SetDlgItemTextA 함수가 있다. DialogBoxParamA는 MSDN 문서에 따르면 모달 다이얼로그 박스를 만드는 함수로 우리가 보는 이 윈도우를 만드는 함수일 것이다. EndDialog는 모달 다이얼로그 박스를 종료하는 함수로 프로그램 종료 시 호출될 것이다. 이것말고는 별다른 함수 호출을 찾을 수 없었는데 그렇다면 대체 프로그램 코드가 어떻게 구성되어 있는 것일까? 일단 Check 버튼을 눌렀을 때 호출될만한 GetDlgItemInt 함수에 중단점을 걸고 따라가보길 했다.
일단 추측한대로 이 함수는 Check 버튼을 눌렀을 때 동작하며 함수의 호출 결과는 사용자의 입력값을 16진수로 나타낸 값이며 eax 레지스터에 저장된다. 이 값은 다시 0x004084D0에 저장된다. 이후 0x0040466F를 호출한 후 eax 레지스터를 초기화하고 0x00404690으로 점프한다. 0x0040466F를 분석하기 앞서 일단 코드가 어떻게 진행되는지 보기 위해 점프 명령어를 따라가보도록 하자.
0x00404690으로 점프하면 0x004084D0, 즉 사용자로부터 받은 입력값을 다시 eax 레지스터에 저장한다. 그리고 0x0040469F를 스택에 삽입하고 0x00404689를 호출한다. 이후 0x0040466F에 0xC39000C6을 복사하고 0x0040466F 호출, eax 증가, 0x0040466F 호출 후 0x0040466F에 0x6E8을 복사한다. 이후 eax 레지스터에 대해 pop하고 eax 레지스터에 0xFFFFFFFF를 복사, 0x00401071로 점프하는데 이는 윗부분에 있는 아까 초반부의 코드로 다시 0x00401084로 점프하여 성공 문자열을 세팅하는 코드를 건너뛰고 진행하게 된다.
그런데 하나하나 따라가보면 알겠지만 0x004046A9의 함수 호출에서 예외가 발생한다. 자세히 알아보기 위해 함수 내부로 들어가보면 다음과 같은 명령어를 볼 수 있다.
0x0040466F은 mov byte ptr ds:[eax], 90 명령어부터 시작하고 있다. 그리고 바로 다음 명령어에서 ret을 통해 반환하고 있는데 왜 이곳에서 예외가 발생하는 것일까? 그것은 바로 eax 레지스터가 가리키는 곳의 메모리 영역에 권한이 없기 때문이다. 현재 eax 레지스터는 0x60160A9D를 가리키고 있는데 이는 이 프로그램의 메모리 맵에 포함되어 있지 않다. 그러므로 이곳에 mov 명령어를 통해 값을 쓸 수 없기 때문에 예외가 발생하여 프로그램이 종료되는 것이다. 그렇다면 이 eax 레지스터에 들어가 있는 값이 어떻게 생성되는지를 파악해 볼 필요가 있다. 확인을 위해 일단 프로그램을 다시 실행시키고 이번엔 GetDlgItemInt부터 레지스터를 관찰하면서 진행해보자.
일단 아래로 점프하기 전 윗부분부터 살펴보면 다음과 같이 여러번 점프하는 것을 볼 수 있다. 이때 유의할 것은 디스어셈블러에 따라 0x0040466F는 엔터키를 눌러서 직접 들어가거나 F7로 Step Into(x96dbg 기준)해야 위의 그림처럼 어떻게 진행되는지 자세히 볼 수 있다. 그냥 0x0040466F로 이동하면 다른 명령어 코드들과 겹쳐서 아래처럼 다르게 해석될 수 있다.
어쨌든 순서대로 살펴보면 다음과 같이 분석할 수 있다(그림에 나와있는 순서와 무관).
-
0x004084D0에 eax 레지스터의 값(사용자입력값) 복사
-
0x0040466F 호출
-
0x0040467A 호출
-
0x00406016에 0x619060EB 저장
-
0x00404689 호출
-
0x004084D0에 저장된 값 inc(+1)
-
-
0x004084D0에 저장된 값 inc(+1)
-
-
0x004084D0에 0x601605C7 덧셈
-
eax 레지스터 값 증가, pushad / popad 등 수행
-
0x00404689 호출
-
0x004084D0에 저장된 값 inc(+1)
-
-
0x004084D0에 저장된 값 inc(+1)
-
함수 반환
절차적으로 수행되는 어셈블리 코드 특성상 함수 호출로 한번 수행한 명령어도 바로 다음에 있다면 다시 한번 수행되기 때문에 같은 작업(inc [0x004084D0])이 여러번 반복되는 걸 볼 수 있는데 이를 좀 더 간소화시켜보면 다음과 같다.
-
0x004084D0 = 사용자 입력값
-
0x00406016 = 0x619060EB
-
사용자 입력값 증가 * 4번 수행
-
사용자 입력값에 0x601605C7 덧셈
즉 사용자가 입력한 값에 0x601605C7 + 1 + 1 + 1 + 1 하는 것과 동일하다. 그 외에는 별로 신경 쓸 필요가 없는데 왜냐하면 점프로 이동한 아래쪽의 코드에서 eax 레지스터로 복사되는게 0x004084D0의 값, 즉 위의 코드에서 이런저런 값들이 더해진 사용자 입력값이기 때문이다.
eax 레지스터는 0x00404690에서 0x004084D0이 저장하고 있는 값을 복사함으로써 사용자 입력값 + 0x601605C7 + 4가 저장된다. 그리고 0x00404689를 호출하여 0x004084D0을 증가시키고 0x0040466F가 가리키는 곳에 0xC39000C6을 복사한다. 이 부분을 눈여겨 봐야 할 것이 0x0040466F는 바로 근처로 조금만 이동해도 볼 수 있는 영역으로 즉 프로그램 코드 영역이다. 이곳에 0xC39000C6을 복사한다는 것은 프로그램 코드를 런타임에 변경한다는 것이 된다. 위의 그림을 보면 0x0040466F에 C600 90 / C3 명령어가 어셈블되어 있는데 이는 인디언 차이로 0xC39000C6이 거꾸로 적용된 것이다.
변경된 0x0040466F 코드를 호출하는 0x004046A9에서는 이미 봤다시피 예외가 발생하는데, mov byte ptr ds:[eax], 90은 eax 레지스터가 가리키고 있는 0x60160A9D에 값을 쓸 권한이 없기 때문이다. 그렇다면 이 eax를 변경해야 할 것인데 위의 코드에서 계산된 사용자 입력값 + 0x601605C7 + 4가 eax에 저장된다는 것을 파악했다. 그렇다면 사용자 입력값을 맞게 입력해야 이 eax가 올바른 위치를 가리키게 될 것이다. 그런데 "올바른" 위치란 어디가 될 것인가? 이는 코드의 좀 더 아래쪽을 살펴볼 필요가 있다.
만약 위의 호출에서 예외가 발생하지 않고 그냥 넘어간다면 마지막에 0x00401071로 점프하게 되며, 이는 다시 0x00401084로 점프하면서 성공 문자열을 세팅하는 로직을 그냥 넘어가버린다. 그렇다면 0x00401071에서 점프하지 않도록 nop으로 어셈블하면 되지 않을까? 라고 생각할 수 있지만 그것만으로는 이 문제에 대한 어떤 해답이 되지 않는다. 나머지 코드도 더 분석해보면 알겠지만 저렇게 성공 문자열을 세팅하는 것 말고는 프로그램에 더이상 아무런 변화가 없다. 플래그를 메시지 박스로 띄운다거나 윈도우 타이틀에 나타내는 것도 아니다. 그냥 문자열이 "Wrong!"에서 "Correct!"로 바뀌는 것이기 때문에 이는 어떤 플래그를 찾았다고 할 수가 없는데, 그렇다면 위에서 본 예외가 발생하는 함수 호출에서 사용하는 eax 레지스터에 "올바른", 즉 함수 호출 시 예외가 발생하지 않도록 하는 값을 넣어줄 수 있는 사용자 입력이 이 프로그램의 플래그가 되리라 추측할 수 있다.
그런데 이 올바른 위치에 0x90을 복사한다는 것은 무엇일까? 얼핏 보면 그냥 16진수 값이라 생각할 수 있지만 지금 저 명령어가 값을 복사하는 곳은 프로그램 코드 영역이다. 즉 0x90은 어떤 데이터가 아닌 어셈블리 명령어라고 생각해야 하며 이는 문서를 참고해 볼 때 NOP 명령을 의미한다. 즉 프로그램 코드의 특정 명령어(1바이트)를 NOP으로 없애버리는 것이다.
그렇다면 지금 상황에서 없애야 할 명령어는 무엇이 있을까? 바로 성공 문자열을 세팅시키는 로직을 건너뛰는 0x00401071에 위치한 jmp 명령어다. 하지만 해당 명령어는 EB 11로 2바이트인데 mov byte ptr ds:[eax], 90으로 어떻게 둘 다 지울 수 있을까? 다행히 프로그램에서 inc eax 후 다시 호출함으로써 [eax], [eax+1]의 바이트를 NOP으로 지워버릴 수 있게 하고 있다. 즉 eax가 가리키는 명령어 1바이트를 지우는 함수 0x0040466F를 이용하여 jmp 0x00401084를 지우기 위해 해당 명령어가 위치한 0x00401071을 eax 레지스터가 가리키게 만드는 사용자 입력값을 계산해야 하며, 이것이 플래그가 된다고 추측할 수 있다.
사용자 입력값 + 0x601605C7 + 4 = 0x00401071이기 때문에 사용자 입력값에서 오버플로우를 일으켜서 다시 0x00부터 시작하도록 큰 값을 넣어줘야 한다. 간단히 0xFFFFFFFF - 0x601605C7 + 1(오버플로우를 일으키기 위함) + 0x00401071 - 4를 해주면 계산할 수 있으며 윈도우10 계산기를 활용하면 간편하게 구할 수 있다. 이렇게 구한 값을 프로그램에 넣고 Check 버튼을 눌러보면 예외가 발생하지 않고 정상적으로 동작하는 것을 확인할 수 있다.
그럼 예측한 대로 정말 코드가 nop으로 바뀌었을까? 이 역시 직접 확인해볼 수 있다.
1234를 넣었을 때와는 달리 0x00401071이라는 메모리 영역을 가리키고 있으며 해당 영역의 명령어가 nop으로 어셈블 된 것을 확인할 수 있다.
이번 문제는 특이하게 프로그램 코드를 일체 수정하지 않고 분석만을 통해 올바른 값을 입력하여 푸는 문제였다. 프로그램만 떡하니 주고 아무런 설명도 없어서 올바른 값을 입력해서 메시지 박스를 띄워야 하는 건가.. 라고 생각해서 시행착오가 많았던 것 같다. mov 명령어로 실행 코드를 실시간으로 변경한다는 생각은 못했기 때문에 0xC39000C6 값을 어떻게 바꿔야 할지 감이 잡히지 않았다. 아예 저 코드를 nop으로 처리하고 넘어가도 그냥 성공 문자열만 뜰 뿐 플래그를 알 수 없었기 때문에 막막했지만 카카오톡 오픈채팅방에 힌트를 구한 결과 어떻게든 풀어낼 수 있었다. x86 명령어 중에서도 0xE8, 0xE9, 0x55, 0x8B, 0xEC, 0x90 정도는 외우고 있는게 좋다고 하던데 0x90이 NOP 명령어란 힌트를 얻지 못했다면 런타임 실행 코드 변경이란 생각도 하지 못했을 것 같다.
아직도 갈 길이 멀다고 생각하지만 이렇게 풀 수 있어서 앞으로의 문제풀이도 새로운 방향으로 생각해볼 수 있는 귀중한 경험이 되었다. 혼자서 끙끙댔다면 아마 지금까지도 붙잡고 있으면서 집중력도 흐트러졌겠지만 남들한테 물어보고 분석했던 내용을 정리해서 질문했기 때문에 더 효율적으로 문제를 풀 수 있었다고 생각한다.
'챌린지 > Reversing.kr' 카테고리의 다른 글
Challenge - Music Player (0) | 2020.12.03 |
---|---|
Challenge - Easy Unpack (0) | 2020.11.23 |
Challenge - Easy Keygen (0) | 2020.11.11 |
Challenge - Easy Crack (0) | 2020.11.02 |