11, 12월에 잠시 딴 짓을 하다가 블로그를 너무 신경을 못 써서 이번엔 5번째 문제 풀자마자

바로 6번째 문제를 잡아봤다.


자~ 그럼 6번째 문제 풀이 시작..


결과부터 말하자면 C++의 API 함수 중에서 그래픽컬 한 부분은 전혀 모르기 때문에 -_-; 꼼수를 조금 도입했다. 쩝!~~






[그림. 1] ImagePrc 실행 화면


기존 것들처럼 또 뭔가 Check 하는 녀석인 것 같은데...

뭘 말하는지 설명 파일도 없고, 그냥 무작정 Check 를 눌러봤더니...






[그림. 2] Wrong 메시지 출력


또 뭔가 틀렸다는 메시지를 출력해준다. 이전에는 뭔가 값을 입력하고 비교 체크를 했는데 이번엔 뭔가 적는 것도 없고...

이게 뭔가 싶기도 했다. 뭐! 어쩌라고???

바보같이 샘플 파일의 이름은 완전히 배제한 체로 그냥 무조건 까보기로 했다. (뭐든 성급하면 손해 보는 듯.. ㅠㅠ)






[그림. 3] 사용되는 API 함수들...


EP에 접근한 뒤에 무슨 API 함수들이 사용되나 대충 보려고 열어봤더니만...

그래픽컬한 작업 등에 사용되는 API 함수들이 보이지 않던가!?


GetStockObject, GetDC, LoadCursor, BitBit, ReleaseDC 등등.. 솔직히 대~~~충 의미만 좀 알뿐, 정확히 사용해보라고 하면 할 줄 모르는.. 뭐 눈치밥으로는 대충 알만한 API 함수들의 목록~


혹시나 해서 ImagePrc.exe 파일을 실행하고 흰 공백에 마우스로 그려보니, 그림이 그려지더라. -_-;


는 뻥이고~~ [그림. 3]처럼 확인하기도 전에 EP에서 바로 트레이싱 좀 해보다가 초면에 맞딱들인 처음 보는 함수인 GetStockObject가  보여서 검색을 해보니 원색을 제외한, 흰색, 검은색, 회색의 선 모양을 그릴 때에 사용되는 오브젝트 함수라는 어느 분의 설명을 보고 냄새를 맡고 그 뒤에 [그림. 3]처럼 창을 열어 보니 그래픽 관련 API 함수들이 있었다.


예제 파일의 빈 화면에는 마우스로 펜 처럼 그림이 그려진다는 사실을 그 후에 알게 됨 ㅡㅡ;






[그림. 4] 여전히 Wrong~


아무튼 뭔가 써지기는 써지지만 여전히 Wrong... 기존에 풀었던 문제를 대조해보자면 뭔가 또 비교를 하긴 하겠지!

시작은 Wrong의 메시지를 출력해주는 부분에서 해보도록 하자.

EP에 접근했다면 "All referenced text strings" 기능을 이용해서 Wrong이라는 문자열을 사용하는 위치를 찾으면 쉽게 찾아진다.






[그림. 5] MessageBox API 함수


Wrong 을 출력해주는 API 함수를 찾았다.

저 함수로 분기를 태우는 곳은 4013AA 주소로 확인이 됐고, 그럼 분기문 타기 전에 비교하는 부분이 존재하겠지!

그 부분을 체크해보자.






[그림. 6] 메시지 분기점


Wrong 메시지로의 분기점 위로 레지스터에 뭔가 작업을 하고 CMP로 비교하는 부분이 보인다.

무슨 작업을 하는 걸까?


그 작업하는 영역 더 그 위로도 FindResourceA, LoadResource, LockResource 함수들이 보이는데 이 부분은 파일 내의

리소스를 찾고 그것이 존재하면 로드하는 행위이다.

물론 제작자는 리소스를 넣어놨으니 예외처리 따윈 없었고 바로바로 사용하도록 코딩한 듯...

검색을 해보니 이 리소스에는 이미지 말고도 dll이 들어갈 수도 있다고 나온 듯 한데.. 맞나? 기억도 가물거리고...

이미지 외에는 본 적이 없으므로.. 음~ pass


암튼 MSDN을 찾아보니 파일의 리소스 영역을 갖다 쓰려면 저 API 함수들을 순차적으로 모두 써야되는 것 같다.






[그림. 7] 리소스 호출


FindResourceA 함수 사용 결과 리소스 자원의 참조 위치인 0x0047E048를 알 수 있다.

(LoadResource 시킬 인자 값으로 EAX가 PUSH 되고 있으니 FindResourceA의 결과는 EAX에 저장된 해당 주소인 것...)






[그림. 8] Resource Section


PEView로 확인해보니 리소스 섹션이 존재하며 실제로 리소스의 위치를 저장하는 포인터가 0x7E048 임을 알 수 있다.

(ImageBase가 0x400000이기 때문에 메모리 로드 시 최종적으로 0x47E048이 되겠지..)


저 포인터의 값을 참조하면 그 값이 바로 리소스가 있는 주소 되시겠다.

Size도 딱 보니 0x15F90 인데, [그림. 6]의 4013AE 주소의 코드인 CMP EDI, 15F90 이라는 것과 뭔가 좀 같은 부분도 보인다.

자~ 그렇다면 리소스를 로드한 뒤에 분기문이 뛰기 전 사이의 코드들은 무엇을 하는 걸까?

다시 한 번 훑어보자.






[그림. 9] 데이터 비교


[그림. 6]과 같은 곳의 코드 영역이다. 화면에 선택된 부분이 사용자가 그린 그림과 리소스 영역의 그림의 데이터를 비교하는 영역 되시겠다. 


40139D 주소 부분까지 트레이싱 하면 ESI 레지스터에는 사용자가 그린 그림의 데이터 영역 주소가 저장되고,

EAX에는 리소스 영역의 주소가 셋팅된다.


저 코드를 간단히 설명하자면 사용자가 입력한 그림과 리소스 영역의 그림을 한 개의 데이터(즉 하나의 pixel에 해당되는 16진수 값)를 모두 일일히 비교하면서 다른 게 있을 경우 JNZ 분기문을 타서 Wrong 메시지 함수로 보내버리고 다르지 않다면 0x15F90 번 만큼 비교 작업을 뺑뺑이 돌리겠다는 코드이다.


모든 데이터가 맞아 떨어져서 뺑뺑이가 끝나면 특정 영역을 CALL 하고 아무 일도 일어나지 않는다.

Wrong이 출력되지 않았으니 성공이라고 볼 수 있겠지만...


"이러면 정답을 뭐라고 사이트에 기재해야 하나?" 라는 생각을 잠깐 했다


비트맵 화면이니깐 그 화면을 봐야 정답을 알 수 있을 것 같은데, 어떻게 출력시키지?

리소스에 저장된 데이터가 정답일텐데, 그게 어떤 이미지 일지를 API 함수를 이용해서 보는 방법을 나는 모른다. ㅠㅠ

그것만 알아도 코드 패치 좀 하는 건데.. 뭐 암튼 gg


C++에서 그래픽컬한 함수를 전혀 사용할 줄 모르므로... 정답은 그냥 꼼수로 확인하기로 맘 먹음.


해당 예제 파일은 CreateWindowExA 함수를 이용하여 X:200 Y:150의 창을 생성하는데, 이 창 자체가 페인팅이 가능한 창이었다.

즉, 비트맵 이미지 영역은 X: 200, Y:150이라는 소리


포토샵을 켜서 가로 200과 세로 150의 비트맵 화면을 만들고 그 영역을 전부 빨간색으로 Fill 시킨 후에 BMP 파일로 저장.

BMP를 사용한 이유는 무압축 방식이니깐 포맷 방식도 아주 심플할 것이라는 단순 사고에서 나옴 -_-;


hxd로 bmp 파일을 까보니깐 내가 채워넣은 색수에 해당되는 16진수 값이 채워진 영역은 그냥 봐도 딱 알정도로 나오더라.

(내가 채운 색의 hex 값은 포토샵에서도 나오니깐 확인하기 참으로 편하다. ㅋ)


자~~ 사이즈도 같게 했고, 그 영역에 예제 파일의 리소스 영역의 hex 값을 몽땅 over write 시키고 저장하니

그 BMP를 출력한 결과 정답이 짜잔 하고 나옴.


정답은 사람들이 뭔가 딱 그 거시기 했을 때 환호하는 문장에 포함되는 단어였다. 게임에서도 꽤 나오는... -_-;


이번 문제는 그림을 어떻게 API 함수를 이용해서 출력시켜야 하냐를 무쟈게 고민한 것 빼고는 빠르게 풀렸다.

블로그 이미지

slays

생각나는 것, 알게된 것을 끄적여보자.

댓글을 달아 주세요

5번째 문제 풀이 시작... (블로그를 너무 간만에 하게 됐다. 흠!~~)






[그림. 1] Replace 실행 모습



예제 파일의 실행 모습은 뭐 기존의 것과 별 차이는 보이지 않는다.







[그림. 2] Access violation error


빈 칸에 숫자 밖에 입력이 안 되는 형식이므로 난 숫자 "12345"를 입력하고 Check를 눌렀는데, 누르는 순간에

Access violation error 가 똿~~ 


windbg를 기본 디버거로 잡아놨었기 때문에 애러 화면에 대한 정보를 볼 수 있었다.


0x40466F 주소의 명령문에서 애러가 났는데 확인해 보면 eax의 주소에 byte 단위의 0x90 값을 MOV 시키려고 했다가 뻗어버린 것을 알 수 있다.

해당 명령이 실행될 때 EAX에는 0x60163604라는 값이 지정되어 있는데, 존재하지도 않는 주소에 특정 값을 넣으려 하니

Access violation error가 발생했다고 볼 수 있겠다.


참고로 해당 예제는 PE파일의 Relocation이 일어나지 않기 때문에 windbg에서 나온 주소를 다른 디버거로 열어본다 해도 주소는 변하지 않고 고정이니 저 주소를 그대로 찾아가면 된다.






[그림. 3] 0x40466F 주소의 명령 코드 - 01


파일을 실행하지 않고 바로 해당 주소로 가보면 windbg에서 보여주었던 명령어 따위는 안 보이고 call 40467A 명령이 보이게 된다. 그 밑에는 "81" 이라는 값만이 덩그러니 보이는데, 흠! 0x81 값은 

뭐, 아무튼 애러가 난 뒤의 해당 주소의 명령과 실행 전의 해당 명령 코드는 서로 다른 것을 알 수 있는데, 그렇다면 어떤 단계를 거쳐서 해당 주소의 해석이 달라진다고 볼 수 있지 않을까?

그 것을 염두하고 분석을 진행하자.






[그림. 4] GetDigitemInt 함수


본 예제는 DialogBoxParamA 함수를 이용하여 다이얼로그 박스를 생성하고 거기에다가 특정 키 값(숫자만 가능)을 요구하는 구조였다. 사용자가 입력한 키 값을 어떤 식으로든 활용한다고 볼 수 있는데, 그 값을 이용하기 위해서는 GetDigitemInt 함수를 이용하게 될 것이며 역시나 함수가 존재하고 있다. 예제 파일을 실행한 후에 저 함수 주소에 BP를 걸고 진행을 해보자.


본 함수는 다이얼로그 박스에 입력된 사용자 값을 EAX에 16진수 값으로 리턴해준다.

나는 12345를 입력했으므로 EAX에는 0x3039 값이 리턴이 되었다.


함수 호출이 끝나면 EAX값을 4084D0 주소에 저장하는데, 저장하는 이유가 있을테니 저 주소를 주시하도록 하자.

그 다음에 CALL 40466F 라는 명령이 나오는데, 저 주소는 [그림. 2]에서 언급한 Access violation error 가 발생한 주소가 아닌가?

그러나 아직은 해당 주소로 진입을 하면 [그림. 3]에 보이는 명령어 코드들이 기다리고 있다.

[그림. 3]을 보면 CALL 40467A명령어가 보이는데, 그 함수를 실행한 뒤 빠져나오면 나면 다음 실행될 코드는?

바로 위에서 언급된 "81" 코드인데, 이건 뭔가 좀 이상하지 않나? 뭐! 분명 뭔가 일어나겠지? 아무튼 40467A로 우선 들어가보자.






[그림. 5] 0x40467A 주소 코드


16진수 0x619060EB 값을 406016 주소에 넣고, 그 후에 CALL 404689 라는 주소를 호출하는데 그 주소에는 4084D0 주소의 DWORD 값을 1만큼 증가시키는 명령이 보인다.

(참고로 4084D0은 아까 사용자가 입력한 12345(16진수로 0x3039) 값이 저장되어 있었다.)


아무튼 1 증가 시켰으므로 4084D0 주소의 현재 값은 0x303A 되시겠다.


그 후, 0x40468F 주소의 RETN 명령이 기다리는데, 아까 404684에서 CALL이 한 번 일어났으므로 리턴될 곳은

0X404689 주소이다. (어라? 또 INC ~~~~ 명령이 수행되겠군 -_-)


한 번 더 수행되었으니 4084D0 주소의 현재 값은 0x303B 되시겠고, 또 다시 RETN을 수행하게 된다. (뺑뺑이 돌리넵...)


자 두 번째로 리턴될 주소는 0x404674이다.

0x404674의 주소는 아까 [그림. 3]에서 봤던 코드 "81"이 존재하는 영역이었다.

F7을 눌러서 트레이싱을 한 번 해보자.







[그림. 6] 0x40467E 주소


F7을 눌러서 트레이싱을 해보니 바로 0x40467E 주소로 넘어와 버렸다.

그렇다면 실행된 코드는 404674에서 40467D 의 값들이 하나의 코드로서 실행 되었다는 것인데, 그 사이의 Hex Dump를

코드로 확인해보면 다음과 같이 나온다.


"ADD DWORD PTR DS:[4084D0], 601605C7"


디스어셈블리로 화면에 해석이 제대로 나오지 못했을 뿐, 정작 실행된 코드는 위와 같다.

그러므로 사용자 값이 저장된 위치의 값은 현재 0x60163602 가 되는 샘이다.


근데 여기서 [그림. 6]에 또 다시 수행될 코드를 보면 404684 ~ 40468F 코드가 기다리고 있다.

이 코드 부분은 [그림. 5]에서 봤었던 코드가 아닌가?


즉, 4084D0 주소의 값을 두 번이나 다시 INC 시킬 것이기 때문에 4084D0의 값은 0x60163604가 되겠다.

하지만 아까와 달리 이번에 RETN이 되는 위치는 CALL STACK 상 0x40106A 되시겠다.

(직접 해보시면 앎)


리턴된 주소 40106A에서는 EAX 값을 초기화 시킨 후 404690으로 점프를 시킨다.







[그림. 7] 0x404690 주소


jmp 타고 넘어온 주소의 코드이다. 이 화면은 [그림. 5]에서도 볼 수 있는 화면이다.

0484D0 주소(이 주소는 위에서 계속 언급했던 사용자 값이 저장된 주소)의 값을 DWORD 크기 만큼 EAX에 넣고,

다음 코드에서는 40469F 주소를 PUSH 시킨다.

(EAX에 값을 왜 넣을까? 그 부분은 뒤에 나오게 된다.)


그 후에 CALL 404689 가 나오는데 이렇게 되면 STACK 에는 RETN 시 돌아갈 주소 2개가 연속으로 쌓이게 되는 것을 알아두자.


위 그림에서 트레이싱을 계속 하면 40469A 주소의 CALL 명령에 따라서 또 다시 0x404689의 INC 코드가 실행된 후에

다시 RETN 명령을 만나게 된다.

(이 단계로 인해 4084D0의 값은 0x60163605가 된다.)


그리고 현재 스택 상에는 RETN으로 돌아갈 주소 2개가 연속으로 쌓여있다. (Return to 0x40469F가 2개 연속 쌓여있음)


40469F 주소로 리턴하면  0xC39000C6 값을 40466F 주소에 넣는 명령을 실행하고 그 뒤에 CALL 40466F가 수행이된다.







[그림. 8] 0x40466F 주소의 명령 코드 - 02


40466F는 windbg에서 access violation error가 발생된 위치였는데, 첫 번째 디버깅에서는 분명 문제가 없을 코드였지만

지금은 windbg에서 봤던 그 애러를 발생시키는 코드가 존재하고 있다.

[그림. 7] 에서의 40469F 주소의 코드가 40466F의 코드를 변경시킨 것이고 EAX에 4084D0 주소의 값을 넣는 코드도 있었는데,

그 코드를 주소로 삼아서 0x90 값을 넣으려고 하나 접근할 수 없는 메모리 주소이기에 애러가 발생하게 되는 것이다.


자~ 그렇다면 이 부분을 해결해야 하지 않을까? 제대로 된 주소 참조여야 애러가 터지지 않겠고....

그리고 0x90 값을 왜 넣으려 했을까? 해당 값은 어셈 명령어로는 NOP 명령을 의미한다.

즉 어느 주소의 코드를 NOP로 만들어서 우회시키기 위한 행위일 것 같은데....


만약 EAX에 제대로 된 주소가 지정되어 있다고 가정을 하고 계속 트레이싱을 해보면??

그 주소에는 NOP가 하나 생성될 것이다.


그 후에 40467의 RETN 명령어가 실행이 될텐데, 그림에서 스텍 상태를 보면 리턴할 주소는 4046AE이다.

4046AE의 주소에는 INC EAX 명령이 기다리는데, EAX에 제대로 된 주소가 지정이 되었다고 가정을 한 상태니 1 증가한 다음 주소가 EAX에 지정이 될 것이고, 그 다음 명령어인 CALL 40466F 로 인해서 또 다시 NOP를 입력하는 코드로 넘어간 후에

NOP를 입력시킨 후에 RETN을 또 하게 된다.


CALL 40466F가 방금 4046AF 주소에서 수행되었으니 다음의 RETN 주소는 4046B4가 될 것이다.

(Access violation error가 날 때 마다 그 명령어를 수행하지 않기 위해 New Origin here 기능으로 EIP를 RETN 쪽으로 변경해보자)


아무튼 4046B4 까지 도달하면 40466F의 주소에 0x6E8 값을 넣게 되는데, 그 명령이 수행되면 40466F의 주소는

[그림. 7]에서 봤던 그 주소의 명령어 코드로 다시 바뀌게 된다.


4046BE 코드에서 POP EAX가 실행되어 남아있던 RETURN 주소인 40469F 주소가 스택에서 정리되고

JMP 401071코드로서 해당 영역을 빠져나온다.






[그림. 10] SetDigitemTextA 함수 구간


JMP로 빠져나온 401071 주소를 보면 401084 주소로 점프를 시킨다.

그런데, 점프되는 영역 사이를 보면 SetDigitemTextA 함수가 사용이 되는데, 이 함수는 다이얼로그 박스의 문자열을 세팅해줄 때

사용되는 함수이다.

401073 주소의 명령을 보면 이상하게 적혀있지만 저 주소의 hex dump를 해석하면 PUSH 00406034가 되며, 406034 주소에는

Correct 라는 문자열이 존재한다.

그렇다면 현재 다이얼로그 박스에 표시된 Wrong을 Correct로 변경시켜주는 역할의 함수인 것인데...

저 함수가 실행되기 위해서는 401071 주소의 코드가 실행이 되면 안 되는 것 아닌가?


저 코드를 우회시키면 게임을 끝날 것 같은데, 마침 401071의 코드는 "EB 11" 이다.

2btye 크기의 코드인데, 아까 0x90 값을 두 번 입력하는 행위를 하지 않았던가?


그 두 번의 NOP 값이 401071과 401072 주소에 각각 입력이 되었다면 JMP 코드는 없어지며 SetDigitemTextA가 실행이 될 수 있다는 결론에 도달하게 된다.


정리해보면 access violation error 가 발생했던 40466F 주소의 코드인 "MOV BYTE PTR DS: [EAX], 90" 에서 EAX 레지스터에

401071 주소가 세팅이 되었다면 그 주소의 2byte 코드는 모두 NOP가 되는 것인데, EAX는 사용자가 입력했던 값이 저장되었던

4084D0 주소의 값에서 몇몇 연산을 거친 후에 EAX에 복사되는 것을 확인 했었다.


즉, 사용자 입력 값이 저장된 4084D0 주소에서 처음으로 값을 늘리고 지지고 볶고 건드렸던 몇 개의 연산 과정과

NOP가 실제로 셋팅되어야 할 401071 주소와의 관계를 역으로 계산해보면 사용자가 처음에 입력했어야 할 값이 계산이 될 것이다.


계산은 16진수로 하되 최종 정답은 다시 10진수로 변환해야 한다는 것을 잊지 말자.






[그림. 11] Correct!


성공~~

블로그 이미지

slays

생각나는 것, 알게된 것을 끄적여보자.

댓글을 달아 주세요

네 번째 문제 풀이 시작.


이번 예제 파일에도 ReadMe.txt 파일이 존재한다. 읽어보자.


This MP3 Player is limited to 1 minutes.

You have to play more than one minute.


There are exist several 1-minute-check-routine.

After bypassing every check routine, you will see the perfect flag.


MP3 Player가 1분 밖에 재생을 못한다고 하는데, 그 1분을 체크하는 몇 몇 루틴을 모두 재껴버리면

Flag 값을 볼 수 있다고 한다.


나머지 첨부된 파일들을 보면 msvbvm60.dll, Music_Player.exe 가 존재하는데, msvbvm60.dll은 Visual Basic 엔진이다.

즉, 이번 문제는 Visual Basic으로 만들어진 녀석이란 건데.... 하아~ 난 VB에 정이 붙질 않는다. 쩝! ㅠㅠ






[그림. 1] 재생 화면


3분 넘는 곡을 재생해보니 Time Bar 부분이 01:00 으로 제한이 되어 있고, 실제로 1분만 재생된다.

저 1분 제한을 풀어주면 정답을 알 수 있단 말이지...






[그림. 2] 1분 제한 메시지


1분의 플레이가 모두 완료되면 [그림. 2]와 같은 메시지 박스가 출력된다.

자!~ 이 부분이 분석 시작에 도움을 주는 힌트가 될 터...






[그림. 3] ThunRTMain 함수 호출


VB 6.0으로 만들어진 파일은 Entry Point에 들어오자마자 VB 엔진 영역을 호출시킨다. 위 그림의 CALL 함수는 401472의 점프문을 호출하고, 그 점프문이 실행되면 VB 엔진 영역으로 이동하게 된다.

난 "개취" 상 디버거 옵션에서  show symbolic address 기능을 켜놓고 사용하기 때문에 CALL 뒤에 주소가 아닌 심볼명이 나타난 것이다. (착오 없으시길....)


해당 함수를 호출하기 전에 PUSH 된 것은 ThunRTMain 함수에 대한 인자값인데, 그 주소 영역에 RT_MainStruct 구조체가 셋팅이 되어지고, 그 구조체는 또 다른 구조체의 주소들로 구성되어 있다고 알려져 있다.

[그림. 3]에서 HEX 영역에 표기한 4017CC 영역 부분이 그 구조체가 세트되어 있는 영역이지만 그 구조체의 구조를 알지도 못하고, MS에서는 공개도 안 했다고 하니... 뭐가 뭔지 알 수가 없다. 

컴파일 옵션(P mode, N mode 라는 게 있다고 하네... 본 적은 없음.)에 따라서 VB의 저러한 구성이 달라진다고는 하지만 여태 본 VB 파일들은 죄다 저런 유형이었으며 그 점 때문에 암튼 VB로 만든 파일에 정이 가지 않는다.

(VB Decompiler 가 있다면 속은 좀 편해질지도.. 유료로 질러? -_-;)


자.. 복잡한 과정을 거치지 말고 역으로 접근을 해보자.

1분의 재생이 끝나고 발생한 이벤트인 "1분 미리듣기만 가능합니다." 메시지 박스....

Visual Basic 에서 메시지 박스를 발생시키는 함수 코드를 찾아서 그 기점으로 한 번 훑어보는 건 어떨까?






[그림. 4] rtcMsgBox 메시지 함수 호출 목록



Visual Basic 에서 메시지 함수를 호출하는 함수는 rtcMsgBox 이다.

그렇다면 1분 메시지를 발생시키는 함수는 어디일까?

모두 BP를 걸고 1분 재생을 시켜보면 알 수 있지 않을까 싶다.

BP를 다 걸고 파일 자체를 완전히 실행시켜서 플레이어가 구동되도록 하자.


참고로 내 Windows 7 x64 OS에서는 문제가 하나 생겼다. (다른 이들도 그랬을지 모르겠지만...)


F9를 눌러서 플레이어를 우선 실행 시키고, OPEN 버튼을 눌러서 내가 재생시킬 MP3 파일을 선택을 해봐야 할텐데....

OPEN 버튼을 눌러서 파일창이 실행이 되려다가 난데없이 디버거에서 EXCEPTION이 걸린다.

Exception 0000006BA 라는 코드가 뜨는데, 검색해보니 윈도우 7에서의 다이얼로그 관련 버그라고 하는 글귀도 있다.

예외가 걸린 주소 자체를 디버거의 exception ignore 기능을 이용하면 되는데, 그렇게 해도 그 다음에도 예외가 걸리더라.

그 후에도 또 걸리고... 이거~ 일일히 예외 무시를 걸어주기도 지겨워서 OPEN 버튼 안 누르고 경로 이름 적는 곳에 파일 이름을
직접 적고 재생 버튼 클릭하니 예외 안 뜨고 재생이 잘만 된다. (괜히 예외 걸리는 부분 하나씩 다 해본 게 억울.. ㅋ)


또 한가지....


노래 1분 끝날 때 까지 언제 기다리나? 플레이 타임바를 1분 끝나는 지점 쪽으로 끌어다가 1분 재생을 순식간에 완료하려고 잔머리를 굴렸지만 디버깅 상태에서 그 짓을 하면.... 여지없이 EXCEPTION 에 또 걸린다.

이 부분은 편의를 위해서라도 역시나 예외가 걸리는 지점을 디버거 옵션에서 수동으로 ignore 시켜주자.

그렇게 하면 반복 분석을 위해 몇 번이나 타임바를 움직여도 예외가 걸리는 일은 없을 것이다.






[그림. 5] 메시지 함수로의 분기문



내 PC에서는 메시지 함수의 발생 주소가 4045D8로 나타났다. 1분이라는 조건에 걸려서 메시지 함수로 넘어왔으니 분명 그 쪽

주소로 태우는 분기문이 존재할 터... 코드를 위로 올려보니 40456B 분기문이 메시지 함수로 보낼지 말지를 결정하는 곳이다.


분기문 바로 위에 보니 EAX 레지스터와 0x0EA60 값을 비교하고 EAX 값은 [EBP-18] 위치로 저장시킨 후에 분기문을 태운다.

0x0EA60 을 10진수로 변환하면 60000이라는 값이 나온다. 이는 아마도 60000ms(60sec)를 의미하는 것일 터...

404563 주소에 BP를 걸고 처음부터 재생을 하면 그 위치에 걸리는 것을 보게 될 것이다.

F9를 눌러서 재생을 강행시킬 때 마다 EAX 값이 변하는 것을 볼 수 있다. 즉, 곡을 재생하면서 계속해서 시간을 체크한다는 뜻...

60초가 넘어가면 1분 메시지 발생 함수가 실행되니 아예 40456B의 JL문을 JMP로 바꿔서 계속 점프되도록 하자.






[그림. 6] 두 번째 분기문



[그림. 5]에서 수정된 점프문을 타고 4045FE 주소로 이동하는데, 여기서도 곧바로 EAX 값을 -1 (0xFFFFFFFF) 값과 비교한다.

0xFFFFFFFF 값과 같으면 점프하고 아니면 그냥 코드를 쭉 진행하게 되는데, 곡이 10분 짜리라고 해도 10분의 시간을 16진수로 변경하면 0x00927C0 이기 때문에 0xFFFFFFFF 과 같아질 수가 없기에 일반적으로 점프할 일은 없을 것이다.

만약 이 부분의 JE 코드를 그냥 JMP 시키면 곡이 줄창 끝날 때 까지 잘재생된다.

그러나 플레이바의 미동도 없고, 곡이 끝날 때 까지 정답은 나오지 않는다.

즉, 이 영역의 코드를 점프시키지 말고 원활하게 돌게해야 뭔 일이 일어나도 일어난다는 걸 예상할 수 있겠다.

암튼 코드 수정은 하지 않는 걸로 하고 이 상태로 곡을 1분간 재생시키면 곡이 다 끝났기 때문에 또 다시 EXCEPTION 이 발생하게 되는데 발생된 EXCEPTION 역시 ignore 처리를 해주고 재차 확인하자. 이 파일의 목적은 시리얼 키를 알아내는 것이니...






[그림. 7] Run time error 380 오류 발생


[그림. 6]에서 설명한 듯이 1분 재생 완료 시의 예외 처리를 ignore 시켜버리면 이렇게 오류 메시지가 발생하게 된다.

곡은 쭈욱 재생되고 있지만 확인 버튼을 클릭하면 프로그램 자체가 강제 종료된다.

그렇다면 코드 내에서 특정 조건 시에 예외 이벤트를 발생시키는 영역이 존재할텐데, 어디일지를 한 번 찾아보자.






[그림. 8] vbaHresultCheckObj 함수


[그림. 6]에서 언급된 분기문 아래 영역에 존재하는 나머지 코드들을 훑어보면 RETN 하기 전 까지 존재하는 함수들은 대개 스트링 관련 함수들과 vbaHResultCheckObj 라는 함수로 짜여져 있다.

HResult 라는 단어가 왠지 좀 걸리적 거렸는데, C언에서 try ~ catch에서 쓰던 걸로 기억한다.

try문에서 오류 발생 시에 MAKE_HRESULT 함수로 오류의 상태 정보를 만들어서 catch 문에 HRESULT이라는 타입으로 던져서 썼는데, VB에서의 해당 함수도 오류 관련 시에 쓰이는 것이라고 판단... 

(좀 더 정확히 알고 싶지만 구글신에서 검색이 잘 안 되고 있음. ㅠㅠ)


뭐 솔직히 애러난 부분에서 call stack 을 확인해보면 VB 엔진의 vbaHresultCheckObj 함수가 동작된 걸 볼 수 있다. -_-;


2개의 vbaHResultCheckOb 함수에 BP를 걸어보니, 곡 재생이 완료된 후에 첫 번째 vbaHResultCheckObj 함수에서 걸렸고,  그 함수를 실행하면 [그림. 7]의 오류 창을 발생시키는 것이 확인되었다.


해당 함수 위에 4046AB 주소를 보면 JGE 분기문이 보이는데, 1분 미만 재생 시에는 해당 점프문을 타고 점프하다가 1분이 넘어가면 점프를 하지 않고 vbaHResultCheckObj 함수로 진입을 해버리는 것이 확인이 되었기에 JGE 코드에 걸리지 않도록 무조건 점프를 시키도록 해보자.






[그림. 9] 패스워드 겟~


드디어 패스워드가 제목 표시줄에 표기되었다. 재생도 멈추지 않고 계속 된다. 이로서 성공~~


지문에도 나왔듯이 1분 체크 루틴을 bypass 시키면서 그 뒤에 벌어지는 현상들을 예외처리 하며 접근해오니 정답을 알게 됐지만 이렇게 푸는 건 글쎄~~~ 수학 문제를 꼼수로 풀준 알았을지언정 정석대로 착착 풀었다는 느낌이 들지 않은 것과 같다고 해야 할까? 그래서 좀 찝찝하긴 하다.


초기에는 시간 체크 방식에 대한 기준을 여러 방면으로 생각하다보니 너무 어렵게만 시작을 해버렸는데

막상 풀고나니  너무 헛짓을 많이 했다는 느낌이 든다. 그러나 정작 풀고나서도 그 과정이 상당히 찜찜하다.

정답만 맞추면 망고 땡~ 이라는 건 위험한 발상이긴 하니....


만약 제시된 문제가 1분만 재생되는 것을 해제하고 플레이 타임바도 시간에 맞게 움직이게 만들라고 했으면....

아마도 나는 혀를 내두르고 포기했을 것이다.

(UI쪽은 전혀 모르므로... -_-)


머 암튼 특정 비교 구간 및 예외(애러 발생)로 빠지는 부분들을 무력화 시키면 접근하지 못했던 코드들이 실행되면서 자연스럽게 몇 개로 쪼개져 있던 패스워드 문자열을 하나로 모아서 UI에 뿌려주는 구조였다.

블로그 이미지

slays

생각나는 것, 알게된 것을 끄적여보자.

댓글을 달아 주세요

세 번째 문제 풀이를 시작해보자.

제목으로 봐서는 packing 이 된 파일을 unpack 시켜야 하는 것 같은데, 같이 첨부된 ReadMe.txt 파일을 열어보면은...


ReversingKr UnpackMe


Find the OEP

ex) 00401000


이렇게 나온다. 즉, 언팩을 한 다음에 Original Entry Point의 주소를 찾아내라는 소리이다.




[그림. 1] Entry Point


언팩이 된 파일은 일반 PE 구조와는 좀 다르다.

PEViewer 로 확인해보면 Address of Entry Point 가 0x0000A04B라는 것을 알 수 있다.

(이 파일도 ImageBase가 0x00400000 부터 시작일테니, 로드가 된다면 0x0040A04B 주소가 Entry Point로 나온다는 얘기..)


근데 일반적인 파일이라면 Entry Point는 .text (코드 섹션)에 있어야 할텐데, 저 주소는 .GWan 이라는 이상한 이름의 섹션에 포함되는 주소이다.

보통 Packing이 된 파일들은 이렇 듯 언팩킹 전에는 죄다 EP 저따구로 나온다.






[그림. 2] PEiD로 확인 


PEiD 툴로 확인해보니 역시 Entry Point 와 EP Section이 엄한데로 나온다. 또, 뭘로 만들어졌는지 알 수가 없다고 나온다.






[그림. 3] Compressed code ??


디버거로 로딩하여 Entry Point로 가려고 하면 저런 메시지가 나온다. 말 그대로 코드 섹션이 압축 되었거나, 암호화 되었거나 해서 거시기 하다는 내용이다. 계속 분석하겠냐고 하는데 "예"를 눌러서 분석을 시작해보자.






[그림. 4] EP 코드 시작 부분


EP 영역의 코드 시작 부분이다. 여기를 어떻게 접근했냐고 하는 분들은 없길 바란다. 윈xp라면 그냥 바로 나올 것이고, 비스타 이후 계열 OS라면은 ntdll.dll 영역서 부터 디버깅이 시작되므로 Ctrl + F9 눌러서 가볍게 넘어와주면 된다.

코드를 보면 kernel32.dll을 의 주소를 얻기 위해서 LoadLibrayA 함수를 CALL 하는 게 보인다.

함수는 EAX에 kernel32.dll의 주소를 셋팅해주는데, 그 EAX 값이 40A644 주소에 넣어지는 것이 3번째 줄에 보인다.

40A644 값은 그 아래에 PUSH 되는 아스키 값인 GetModuleHandleA 와 FreeLibrary 와 함께 PUSH 되고 각각

GetProcAddress 함수가 호출되는 것이 보일 것이다.

그나저나 이유를 잘 모르겠지만 GetProcAddress 함수를 바로 호출하지 않고 apphelp.dll 쪽을 타고 들어가서 간접 호출을 한다. 왜 저녀석을 타고 들어가서 하는지 모르지만 뭐 아무튼 GetProcAddress 함수 호출인 건 변함이 없다.

여기서 알 수 있는 건?? GetModuleHandleA 함수와 FreeLibrary의 함수 주소를 알아와서 각각 40A648, 40A64C에 주소를 넣는다는 것이다. 저 함수들을 사용하기 위해 함수 주소를 예제 파일 자신의 영역에 넣는 것이다.


여기서 알아둘 Tip이라면... 보통 이런 패킹 파일들은 코드가 얽히고 섥혀 있다고 해도 파일의 온전한 동작을 위해서는 자체적으로 언패킹 과정을 거치며 필요한 함수들의 정보를 가져와야 하고, Encrypting 되어있는 코드들을 풀어서 어딘가에 넣어야 하기 때문에 꼭 특정 메모리 공간 등을 사용해야 한다는 것이 거의 기본적인 동작이 된다.

물론 이렇게 디버깅 되는 것을 막기 위해 중간 중간 안티 디버깅 기법이 걸리는 애들도 많겠지만, 이건 Ea~~~sy  한 샘플임을 잊지 말자. -_-






[그림. 5] VirtualProtect 함수 사용


좀 더 트레이싱을 해보면 VirtualProtect 함수의 주소를 알아낸 뒤에 그 함수를 바로 사용한다.

총 4개의 인자값을 PUSH 하고 함수를 호출한다.

대강 해석하면 0x00405000 의 주소에서 0x1000 크기만큼 Read/Write 속성으로 변경하겠다는 것이다. 현재 해당 영역은 읽기 전용으로 되어 있는데, 그 부분을 쓰기도 가능하도록 바꾸겠다는 얘기이다.

왜 쓰기를 할까? 당연히 뭔가를 쓰려고 저 영역의 설정을 바꾼 것이겠지...

이렇 듯 패킹 파일은 거의 대다수가 VirtualProtect 를 사용한다. 왜냐~~ 용량이 큰 영역이 필요하거나 할 땐 VirtualAlloc과 VirtualProtect  함수 등이 자주 쓰인다고 MS 쪽에서 그런 걸로 기억하기 때문에.. ㅡㅡ;






[그림. 6] 함수 주소 구하기


위에서 설명한 것들을 종합해보면 필요한 dll 주소도 구하고, 그 dll 내부에서 사용할 함수의 주소를 GetProcAddress 로 알아온 다음에 쓰기 가능한 영역에 쓰는 행위를 지속적으로 하게 될 것이다. 그렇기 때문에 영역에 대한 속성 변경이 필요하기 때문에 보통 VirtualProtect 함수가 자주 쓰이게 되며, 뭐 새로운 영역 생성이 필요할 때는 VirtualAlloc 함수도 같이 쓰이기도 한다.

GetProcAddress 쓰려면 함수 이름이 아스키 코드로 존재할텐데, 패커의 경우 보통 모두 Encrypting이 되어 있기 때문에 그것을 복호화 하기 위한 연산 작업이 수행이 된다.

그렇다면 아마 연산과 반복이 몰리는 코드 구간이 존재하겠지!? 대략적으로 보자면 [그림. 6] 처럼 뭔가 ADD 하고 CMP 하고 INC 하고 점프하고 그 사이에 LoadLibrayA도 하고, GetProcAddress 도 하고 하는 걸 보니 뺑뺑이 돌면서 뭔가 작업을 한다는 것을 알 수 있는데, 저런 연산 구간이 VirtualProtect 함수가 쓰이는 코드들의 전후로 몇 개 존재한다.

그 작업들이 코드의 복호화 작업 되시겠다.






[그림. 7] 함수 정리 후의 분기문


모든 루프문을 완료시키면 JMP 코드 2개를 보게 되는데, 위의 JMP는 아직 작업이 덜 끝났기 때문에 다시 루프를 돌리려는 JMP문이고, 작업이 다 완료가 되면 루프문이 돌다가 40A1FB 주소로 점프를 시켜버린다.

자.. 해당 주소의 JMP 코드가 가리키는 00401150 주소로 들어가보면...






[그림. 8] OEP 코드 영역??


이상하게도 어셈코드가 해석이 안 되어 있고 그냥 널부러져서 나온다.

당황하지 말고 Ctrl + A 를 눌러서 재해석을 시켜보자.






[그림. 9] Original Entry Point


드디어 OEP 코드가 등장했다.

해당 OEP 코드 화면은 1, 2번 예제 파일들과 똑같지 않은가?

매번 이런 식으로 패킹 파일을 풀다가는 실제 샘플 분석 시에 아주 많은 개고생을 할 것이다.

즉, "패킹 파일들에서 자주 사용되는 부분들을 중점으로 파팍~ 넘어가줘야 더욱 빠르게 OEP로 들어가서 메인 함수를 분석할 수 있을 터... "


라는 식으로 지인을 통해서 들었던 과거의 조언이 생각이 났다. -_-; 그 뒤로 VirtualProtect 함수에 BreakPoint 찍고 달리는 습관이 들었다는.. ㅎ






[그림. 10] PEiD 재확인


디버거를 이용해서 OEP 주소에 도달했다면 Dump 플러그인을 이용하여 Entry Point 주소를 fix 해주고 나서 메모리 영역을 덤프 떠주고 덤프 파일을 PEiD로 다시 확인한 결과 이제서야 제대로 표기가 된다.

블로그 이미지

slays

생각나는 것, 알게된 것을 끄적여보자.

댓글을 달아 주세요

두 번째 포스팅 시작..

분석할 파일을 디버깅 하기 전에 같이 따라온 txt 파일을 보면 다음과 같은 메시지가 적혀져 있다.


ReversingKr KeygenMe

Find the Name when the Serial is 5B134977135E7D13


시리얼 값이 5B134977135E7D13 일 때의 이름을 찾아내라는 건데, 한 번 찾아보자.






[그림. 1]  실행 화면


이번 예제는 Win32 Console 프로그램군. 적으라는 Name 부분에는 이번에도 어김없이

개인로 적어본다. ㅎ

보자~~ 이름을 적고 시리얼 키를 텍스트 파일에서 알려준데로 해봤지만 냉정하게도 "wrong" 이라고 결과를 출력해주고 있다.

뭐 시리얼 키는 이미 지정된 것이 틀렸을리는 없겠고 저 키에 해당되는 이름이 별도로 있을 것이라 추정할 수 있겠다.

화면상으로도 힌트는 몇 가지 추정이 된다. 바로 프로그램을 실행하면 콘솔 창에 친절히 나타나준 Input Name, Input Serial, Wrong 이 세 글자 되시겠다.

이전에 1번 문제를 풀 때와 뭔가 좀 비슷한 스멜이 느껴지지 않나? 시작해보자.







[그림. 2] 파일 내역


PEiD로 파일의 기본 내역을 확인해보니 Entry Point 영역과 어떤 언어로 만들어졌는지를 알 수 있다.

뭐 Entry Point 부분은 이뮤니티 또는 올리 디버거에서 Ctrl + F9를 치면 바로 이동해주기는 하니 굳이 PEiD 로 볼 필요는 없지만 그냥 어떤 언어로 만들어졌나 싶어서 한 번 봐본다.

Microsoft Visual C++ 로 만들어진 것이 보인다. (갠적으로 Visual Basic 보단 Visual C++ 로 만들어진 파일들이 젤 좋다. ㅠㅠ)

메인 함수 찾기가 좀 더 편해지겠군. 훗~






[그림. 3] 스트링 내역


솔직히 메인 함수를 트레이싱 하면서 찾는 것 보다는 [그림. 1]에서 얻을 수 있었던 문자열 내역으로 바로 보는 것이 좋다.

디버거의 기능인 search for -> all referenced text strings 으로 보면 해당 샘플이 사용하는 문자열 내역이 바로 보인다.

(초급 단계이다 보니 텍스트의 암호화가 되어있지 않음이 얼마나 고마운가.. ㅎ)

역시나 Input Name, Input Serial, Wrong 의 아스키 코드가 보인다.

그 외에도 다른 단어들이 눈에 띄는데, 바로 Correct! 라는 이 단어... 아마 성공 시의 메시지가 아닐까?

왠지 첫 번째 문제와 비슷해 보인다. 자... 코드로 직접 이동을 해보자.






[그림. 4] 문자열 출력 영역


Correct 와 Wrong 문자열 출력 영역으로 접근하니 둘이 인접해 있으며 그 둘 위의 주소 00401118 영역을 보면 분기점인

JNZ 코드가 보여진다. 해당 분기문을 기점으로 Correct로 갈지 Wrong으로 갈지 나뉘는데, 그렇다면 JNZ 분기점 윗 부분에는

입력 값이 맞는지 아닌지를 비교하거나, 또는 뭔가를 하는 부분이 있을 것이다.





[그림. 5] 키 입력 구간


[그림. 4]에서 화면을 좀 더 위로 올리면 [그림. 5]와 같은 눈에 익은 코드들이 나타나게 된다.

0040102E 주소에서 Input Name: 문자열에 해당되는 아스키 코드를 PUSH 하고 그 아래 즈음에 CALL 하는 함수가 있는데,

함수가 호출되면 콘솔 화면에 Input Name: 이라는 단어가 출력이 된다. 그렇다면 00401059 주소에서의 함수는 무엇일까?

C언어 기초에서 자주 나오는 문자 출력과 문자 입력 함수의 조합 같이 느껴지지 삘이~~

00401059 함수가 호출되기 전에 PUSH EAX, PUSH 0040805C 라는 코드가 보이는데 EAX에는 ESP+10에 해당되는

0018FE20 스텍 주소가 담겨져 있다. (참고로 스텍 주소는 디버거 환경마다 당연히 다르므로 절대적이지 않다.)

그 다음 %s 라는 아스키 코드가 PUSH 가 되는데, 이 조합을 코드를 C 언어로 구현한다면 scanf("%s", eax) 정도 되시겠다.

EAX 레지스터는 포인터로 사용이 된 것이며 ESP+10 해당되는 스텍 주소가 담겨있다.

함수를 호출하고 사용자 이름을 직접 적으면 그 입력 값이 EAX가 가리켰던 스텍 주소에 기록이 된다.






[그림. 6] 이름 길이 체크


이름을 입력하면 그 뒤에 나오는 코드이다. 그림에 보이는 모든 코드 중 일부를 간략히 설명하자면 EDI 레지스터에 이름이

입력된 스텍의 주소를 넣고, ECX를 0xFFFFFFFF 값과 OR 연산하여 -1 로 만들어 버린다. (카운팅을 하여 길이 체크에 사용 됨)

XOR EAX, EAX 명령어는 해당 레지스터의 값을 0으로 초기화 하는 아주 대표적이고 자주 쓰이는 명령어이다.

(참고로 MOV EAX, 0을 해도 되겠지만 XOR EAX, EAX 명령어의 길이가 더 짧기 때문에 이걸로 사용이 된다.)


위 코드에서 0040106E ~ 00401072 형식은 문자열의 길이를 체크하는데 자주 쓰이는 형식이다. (라고 어디선가 본 기억이 남)

0040106E 코드를 설명하자면 REPNE는 ZF 값이 0임과 동시에 ECX가 0보다 클 때만 동작을 반복한다.

반복마다 ECX는 1 감소.. (ZF나 ECX나 둘 중 하나라도 조건이 안 맞게 되면 반복을 중단하고 다음 코드로 넘어간다.)

SCAS는 EAX의 값을 EDI가 가리키는 BYTE 값과 비교하고 EDI를 1만큼 증가시킨다.

자 그렇다면 0040106E의 코드의 결과는 어떻게 될 것인가?


EDI 영역의 Hex 값은 내 PC 기준으로 아래처럼 나온다. 내가 입력한 이름이 저장되어있는 곳의 주소이다.


Addr        Hex dump                                   ASCII

18FE20  |  73  6C  61  79  73  00  00  00  00  |  slays....


SCAS 명령어를 통해 EAX의 0 값과 18FE20 주소의 알파벳 s에 해당되는 0x73을 같은지 아닌지 비교를 한다.

SCAS에서의 비교라는 건 혼자 삽질하며 반복 확인해보니 CMP 명령어가 두 값을 비교하는 행위와 같아 보인다.

즉 비교하는 두 값이 서로 같냐 아니냐를 판별하는 것... (한 번 판별하고 EDI 값을 1 증가시키는 걸 잊지 말자.)

0과 s를 비교하면 서로 다르기 때문에 ZF 값은 0이 될 것이고, ECX는 1 감소한 0xFFFFFFFE 이 될 것이다.

이번엔 0과 a를(EDI는 현재 1증가했으니 a 쪽을 가리킴) 비교하면 역시나 또 ZF는 0이 되고 ECX는 0xFFFFFFFD 가 되고,

반복하다 보면 NULL 값을 만날 때 까지 반복하겠지..

즉, slays 단어 끝의 NULL 값 까지 비교를 하고 나서야 ZF가 1이 되며 빠져나오고 ECX는 0xFFFFFFF9가 나올 것이다.

반복 명령문을 빠져나온 뒤에는 NOT ECX 라는 코드가 보인다. 0xFFFFFFF9 을 NOT 연산하면 0x06이 나오며

그 다음 코드인 DEC ECX 코드로 인해 ECX 값은 최종적으로 0x05가 될 것이다.

0x05 는 slays 라고 내가 입력한 값의 자릿수와 동일하지 않은가? 이런 방식으로 입력한 스트링의 길이를 알아내는 것이다.

자주 쓰인다고 하니 알아두도록... ㅋ


00401073 주소의 TEST ECX, ECX 와 00401075의 JLE 명령어를 통해서 ECX가 0 이하면 점프 시켜버리고 0보다 크면 점프 안하고 계속 진행하겠다는 것이다.

근데 입력을 받는 함수 특성 상 뭔가라도 입력해야 넘어가므로 JLE 분기문 땜에 점프할 일은 아마? 없을 것이다. -_-






[그림. 7] 이름 값 변환 구간


자.. 뭔가 조건부 루프문이 등장했다. 루프문 사이에 함수 호출이 하나 보이는데, 한 번 살펴보자.

0040107E 주소의 코드에서 BYTE PTR SS: [ESP+ESI+C] 이 코드는 앞서 언급하지 않았지만 0x10 값이 들어가 있다.

사진에는 안 보이지만 00401038 ~ 00401042 코드를 보면 사이좋게 BYTE 단위로 0x10, 0x20, 0x30을 각각 스텍에 넣어줬는데

그 구간이 이번 [그림. 7]의 40107E 코드에서 [ESP+ESI+C] 부분으로서 ECX에 셋트 되면서 루프 연산 내에서 각각 모두 이용된다.

00401083의 [ESP+EBP+10] 이 가리키는 위치에는 사용자가 입력한 이름, 즉 slays 라는 단어가 존재하는 영역이다.

이 영역의 문자열을 BYTE 단위로 EDX 레지스터에 넣는 것이다. (첫 시작은 s 부터이니 0x73 값이 EDX에 저장 된다.)

그 후에 XOR ECX, EDX 연산으로 ECX 값이 0x63으로 바뀌게 된다. (입력한 이름에 따라 값은 달라지니 절대값으로 착각은 금물..)

그리고 4개의 값이 PUSH가 되는데 PUSH 된 것들은 다음과 같다.


0040108E    PUSH ECX    <= 이 값은 이전의 XOR ECX, EDX 를 시킨 후의 값을 PUSH하는 것이다.

0040108F    PUSH EAX    <= 이 값은 특정 값이 저장 될 Buffer 영역이다.

00401094    PUSH 00408054    <= 이 값은 아스키 코드로서 %s%02X 으로 표현되는 코드를 PUSH 한다.

00401099    PUSH ECX    <= 이 값은 위의 ECX와는 다르다. 00401090에서 ECX에 다른 값이 들어간 후의 코드이다.

    Buffer 영역으로서 인자 값으로 PUSH 된 EAX가 가리키는 주소와 동일한 주소를 갖고 있다.


그 후 Call 을 하는데, 이 것을 C언어로 작성한다면 대략 sprintf(buf, "%s%02X", buf, 연산된 특정 값) 이 될 것이다.






[그림. 8] 16진수 자체를 문자열로 변환


첫 번째로 연산(XOR ECX, EDX 부분)되었던 특정값이란 16진수 0x63이었는데 해당 함수는 그 63을 자체를 문자열로 변환시키고 그 문자열에 해당되는 16진수 값을 다시 Buf에 입력시킨다.

즉 Buf에는 0x63이 그대로 저장되는 것이 아닌 63의 6과 3이 각각 문자열로 변환되어 저장 된 것이다.

[그림. 8]을 보면 ASCII 영역에 문자열 63이 보이고 그에 해당되는 16진수 값이 0x36, 0x33 으로 저장이 되어있다.

[그림. 7]에서의 이름 값 변환 루프는 이런 식으로 하여 문자열 길이만큼 루프가 돌고, 0x10, 0x20, 0x30의 값도 순차적으로 번갈아가면서 XOR ECX, EDX의 연산에 사용되어 차곡차곡 Buf에 쌓이게 된다.






[그림. 9] 이름 값 최종 변환


slays 문자열을 0x10, 0x20, 0x30과 문자열 길이만큼 루프 돌리면서 XOR 연산된 값이 최종적으로 저장된 Buffer의 상태이다.

루프문을 벗어나면 시리얼 키를 입력하라는 부분이 나오는데 텍스트 파일에서 말한 5B134977135E7D13 를 적으면 된다.






[그림. 10] 키 값 비교 루프문


루프문 진입 전에 박스 친 두 코드를 보자, ESI 와 EAX에 각각 이름 값을 변환시킨 값이 저장된 버퍼와 시리얼 키 값을 넣은 버퍼의 주소를 담고 있다.

이제 그 아래에 루프를 열심히 돌면서 각각의 값을 하나씩 비교하고 다를 경우에는 [그림. 10]의 맨 아래 쪽에 보이는 JNZ 분기문을 통해 Wrong을 출력하는 함수로 빠질지 아니면 Correct를 출력하는 함수로 빠질지가 결정이 된다.


루프문 비교 방식은 Easy CrackMe 때랑 방식이 크게 차이나지 않기 때문에 대략적으로 설명하겠다.

입력한 시리얼 키의 첫 번째 자리와 사용자 이름을 연산시켜서 변환된 버퍼의 값을 하나씩 비교하면서 같으면 패스~

다르면 Wrong 메세지로 빠이빠이 시켜버리는 구성이다.

즉 비교시켜야 할 문자열이 모두 시리얼 키와 똑같은지를 보는 것이니깐 EDI가 가리키는 버퍼의 값을 EAX가 가리키는 시리얼 값과 동일하게 미리 변경을 시켜둔 후에 루프문에 수행시키면 Correct의 출력을 맞이할 것이다.






[그림. 11] 이름 값을 시리얼 값과 동일하게 변경


내가 입력한 slays 라는 이름은 634C516953 이라는 아스키 코드로 변환이 되었는데, 시리얼 값으로 입력된 아스키 코드 길이에 한참 못 미친다. -_- Hex Dump 에서 그냥 개의치 말고 시리얼 값과 똑같이 변경시켜주자. 길이가 총 16바이트구만...

자.. 여기서 생각을 해보자. 앞서서 내가 입력한 slays 라는 5글자를 0x10, 0x20, 0x30 과 XOR 연산을 순차적으로 뺑뺑이 돌면서 값을 변환시켰다.

그렇다면 시리얼 값을 반대로 0x10, 0x20, 0x30과 XOR 연산 뺑뺑이를 시켜주면? 그 것이 사용자 이름으로 다시 되지 않겠는가?

이해 안 되시는 분들은 피연산자를 0x01로 두고 0x02와 XOR 연산을 시켜보자.

연산 후의 결과 값을 다시 0x02로 XOR 연산을 하면 어떻게 될까?


자.. 위의 원리대로 재연산을 해보면 해당 시리얼 키의 문자열을 16진수 아스키 코드라 생각하고 XOR 연산을 하면...


ex.1) 0x5B ^ 0x10

ex.2) 0x13 ^ 0x20.....


이렇게 연산시켜서 나온 16진수 값에 해당되는 아스키 코드의  8자리 문자열 값이 정답이 될 것이다.

에고! 아스키 2개를 한 자리의 16진수로 표기되게 코드 짜다가.. 변환 방법 떠오르질 않아서 수동으로 일일히.. ㅠㅠ

블로그 이미지

slays

생각나는 것, 알게된 것을 끄적여보자.

댓글을 달아 주세요

본격적인 글?을 처음으로 작성...

이미 아주 옛날부터 이쪽에 관심있는 사람들이라면 다 했던 것을 난 이제서야 해본다.


뭐! 목적은 내가 한 것을 남겨보고자 하는 것과 이렇게 남기면서 내 머릿속에 한 번 더 각인시키고자 하는 것이 목적인 만큼

활발히는 아닐지라도 애정을 갖고 한 번 진행해보는 것도 나쁘지 않을 것 같다.


지인의 추천으로 알게 된 Reversing.kr 사이트에서 제공된 첫 번째 예제 문제이다.

자~~ GoGo!~






[그림. 1]  Easy Crack Me 실행 화면


처음 실행 화면은 [그림. 1]과 같다.

심플하게 다이얼로그 박스 하나만 있는데 뭔가 입력하고 확인 버튼을 누르면 어떤 반응이 있겠지.

(쌩뚱맞게 이게 만약 악성코드 였다면 VM에서 돌렸을텐데... 라는 생각을 했지만, 뭐 믿고 그냥 리얼 환경에서 바로 실행 -_-)






[그림. 2]  Incorrect Password


시험 삼아서 개인 ID로 사용하는 slays 를 입력하고 확인 버튼을 누르면 부정확한 패스워드라는 메시지가

위의 [그림. 2] 처럼 발생한다.

(아.. 그냥 12345 수준으로 입력해서 풀이를 할 걸 굳이 ID를 쓰니 풀이 과정을 보기가 약간 불편해질 망조가.. -_-)


실행 결과만 보면 제대로 된 패스워드를 넣지 않았다는 건데, 그렇다면 본 예제 파일이 원하는 패스워드가 무엇인지를

알아내는 것이 이번 문제의 핵심일 터... 디버거로 한 번 까보도록 하자.






[그림. 3]  MessageBoxA 함수 사용 주소


디버거로 확인하기 전에 [그림. 2]에서 추정할 수 있는 힌트는 "Incorrect Password" 라는 메시지가 발생했다는 것이다.

일반적으로 윈도우 환경에서 메시지 박스를 출력할 때 쓰이는 함수는 MessageBox() 라는 API 함수이다.

해당 함수가 쓰이는지를 확인하기 위해서 본 예제의 코드 영역인 Entry Point 부분으로 접근 한 후,

Search for -> all intermodular calls 기능으로 코드에서 사용하는 함수 리스트를 조회하면 [그림 3] 처럼

MessageBoxA 함수를 사용하는 영역이 두 곳이 있음을 알 수 있다.






[그림. 4]  MessageBoxA 함수 영역


두 곳의 주소로 이동해보면 성공 시의 메시지를 출력해주는 함수와 실패 시의 메시지를 출력해주는 함수가 보이는데,

두 개의 메시지 함수가 각각 "Congratulation" 과 "Incorrect Password" 라는 메시지를 출력해준다.

실패 메시지 출력 코드 영역을 자세히 보면 00401142 주소로 점프시켜주는 구간이 총 4개가 된다는 것을 알 수 있다.

아마 그 주소들이 패스워드를 비교하는 영역에서 패스워드가 틀리면 이 쪽으로 점프시켜주는 주소들이 될 것이다.






[그림. 5]  GetDigitemTextA 함수


우선 결과 메시지 출력 이전에 사용자가 입력한 패스워드 값을 비교하는 곳이 있을테니 그 부분을 확인해야 할 것이다.

슬라이드 바를 이용해 메세지 박스 출력 코드의 위 쪽으로 화면을 위로 올리다 보면은 [그림. 5]와 같이 GetDigitemTextA

API 함수가 확인이 되는데, 본 함수는 다이얼로그 박스에 입력된 텍스트 값을 가져올 때 사용되는 함수이다.

이 함수가 존재하는 영역이 Easy Crack Me 의 [확인] 버튼을 눌렀을 때의 이벤트 헨들러 영역에 해당될 것이다.

예제 파일 실행 뒤, GetDigitemTextA 함수에 BP를 걸고 아무 패스워드 값이나 입력해서 확인 버튼을 누르면 해당 함수에서 멈추게 된다.

스텍 영역을 보면 함수의 인자값들이 보이는데 3번째 인자 값이 패스워드 값을 저장할 포인터를 가리키고 있다.






[그림. 6]  GetDigitemTextA 함수 결과 및 비교 구문


[그림. 6]을 보면 비어있던 0018F958 스텍 영역에 패스워드 값으로 입력한 slays 라는 단어가 저장된 것이 확인된다.

함수 호출 코드 다음에 004010B0에서 CMP 코드가 등장하고 그 밑의 코드는 JNZ 분기점이 보인다.

CMP로 [ESP+5] 영역의 BYTE 값을 0x61과 비교하는 것을 볼 수 있는데 스택 값을 보면 ESP+5의 BYTE값은

알파벳 l 인 것을 알 수 있다.

입력한 패스워드 값은 slays 인데 비교의 시작이 s가 아닌 l 부터 하는 게 의아하겠지만 그냥 뒤로 하고 보이는데로

분석을 속행하자.

CMP 명령어는 비교하는 두 개의 값이 같은지 아닌지 비교할 때 자주 쓰이는 어셈 명령어인데 둘이 다르면 JNZ 코드에 의해

실패 메시지 함수 출력 부분으로 점프를 하게 된다.

알파벳 l 은 16진수 값으로 하면 0x6C이니깐 당연히 실패하는 쪽으로 빠지겠지.. 

즉 여기서 입력된 패스워드 값 중 두 번째 값은 0x61에 해당되는 문자열이 입력되었어야 한다는 것을 알 수 있다.

실패하지 않으려면 저 분기문을 넘어가야 하니 CMP 실행 전에 l을 소문자 a로 미리 변경하여 우회를 시키자.

(0x61은 소문자 'a' 에 해당되는 값이다.)






[그림. 7]  문자열 비교 함수


00401089의 코드를 보면 [ESP+A] 해당되는 스텍 주소를 ECX 에 넣고 "5y" 라는 아스키 값과 ECX를 PUSH 한 뒤 특정 함수를 호출하는데, [ESP+A]가 가리키는 주소에는 slays 중 ays 가 존재한다. 이 전에 알파벳 l 을 비교한 후에 그 다음 문자열을 비교할 것으로 보인다.

참고로 004010B7 주소의 PUSH 2 라는 코드 역시 해당 함수에서 인자값으로 사용이 되는데 이건 함수 내에서 카운터 수치로 쓰이게 된다.

해당 함수로 들어가보자






[그림. 8]  3, 4번째 문자열 비교 수행


인자값으로 들어온 2는 ECX 레지스터에 저장하여 문자열 비교 횟수의 카운터 역할을 시키고

인자값으로 들어왔던 사용자의 입력 암호 나머지 부분과 "5y"라는 문자열을 비교한다.

솔직히 이 부분만 보더라도 5y를 비교한다는 것은 암호의 3번째, 4번째 자리가 5y여야 한다는 것을 짐작할 수 있다.

카운터도 친절하게 2 가 입력되는 걸 보면 두 자리의 비교라고 짐작할 수 있다.

[그림. 8]을 보면 함수 내부에서 00401164 주소의 REPNE 명령어와 0040116F 주소의 REPE 명령어를 이용하는데


두 명령어를 이용해서 특정한 두 개의 값(여기서는 뭐 당근 문자열 비교겠지)을 비교하는 것인데,

각 명령어의 역할은 다음과 같다.

(참고로 REP 류의 명령어는 단독으로 쓰이지 못하고 꼭 뒤에 추가로 명령어가 붙어야 함)


REPE -> ECX 값이 0보다 크고 ZF 레지스터 값이 1일 경우에 그 뒤의 명령어들을 반복시킴

REPNE -> ECX 값이 0보다 크고 ZF 레지스터 값이 0일 경우에 그 뒤의 명령어들을 반복시킴

(위 명령어는 두 가지의 조건 중 하나라도 어긋나면 코드를 빠져나오게 된다.)


EAX 를 0으로 먼저 초기화를 시킨 뒤 00401164 코드에서 REPNE 명령어와 SCAS 명령어를 이용하는데 이는 사용자 입력 암호 중 3, 4 번째 자리가 공란인지 아닌지를 확인하는 구간이다.

0040116F 코드에서 REPE CMPS 명령어는 사용자 암호 3,4 번째 부분과 "5y" 코드를 직접 비교하는 것이고,

그 밑의 점프 코드는 비교 결과에 대하여 암호가 작거나 클 경우의 동작을 위한 점프문으로 구성되어진다.

암호가 공란이거나 또는 5y 와 같지 않을 경우 점프 문을 통해 ECX 레지스트를 이용한 연산이 수행되며

MOV EAX, ECX 명령을 통해 EAX에 값을 세팅한다.

암호가 일치하지 않을 경우에는 JA, JE 점프문에서 걸려저서 ECX 레지스터에 0이 아닌 다른 값으로

세팅이 되고 그 값이 EAX에 저장이 된다.

암호가 제대로 됐다면 EAX에는 0이 세팅이 되며 함수를 빠져나가게 된다.

(EAX는 함수 완료 후 다음 분기문의 참조로서 사용이 된다.)






[그림. 9]  두 번째 암호 비교 성공


3, 4번째 항목을 5y로 맞춰주면 EAX는 0이 되며 함수를 빠져나온다.

004010CB의 TEST EAX, EAX 코드는 EAX가 0인지를 체크하는 코드인데, EAX 값에 0이 설정되었다면 패스워드 비교가 서로 맞았다는 뜻이고 0이 아니라면 패스워드 값이 서로 달랐다는 것을 의미한다.

만약 0이 아니라면 004010CD의 JNZ 코드로 인해서 실패 메시지 함수로 빠잉~ 하게 된다.

뭐~ 5y라는 비번까지는 맞춰줬으니 다행히 분기문은 그냥 지나쳐가겠지.






[그림. 10]  세 번재 암호 비교


이어지는 코드에서 갑자기 등장한 "R3versing" 아스키 코드....

그리고 [ESP + 10] 스텍 주소를 EAX에 셋팅하는데, 해당 위치를 가보면 입력했던 사용자 값의 5번째 위치를 가리키는 것이 확인된다.

slay 까지 왔으니 끝에 남은 s 의 위치를 EAX가 가리키게 된다.

(역시, 그냥 숫자로 예제 풀이할 걸... 하고 급 후회가 몰려오는 중 -_-)


그렇게 세팅을 하고 [그림 10]에서 처럼 문자열 비교 반복문들을 엄청 돌려댄다. 만약 비교 값이 다를 경우 JNZ 코드들로 인하여 00401102 로 빠지며 EAX가 -1로 세트되고 그림엔 안 보이지만 밑에 나올 코드에서 또 다시 JNZ 점프문으로 인해 실패 메시지 함수 출력으로 빠잉~ 하게 된다.






[그림. 11]  마지막 암호 비교


연산을 설명하긴 귀찮으니 자세한 설명은 생략하고...

앞서 얻은 힌트 "서로 값이 다르면 점프문 타고 빠이~" 한다는 공통적인 점들과 그리고 친절하게도

R3versing 이라는 아스키 코드가 평문으로 나타나주셨으니 최소한 현재까지 알려진 암호의 조합은 "a5yR3versing" 일 것이라 추정되겠다.

(참고로 맨 앞에 한 자리를 아직도 비교하지 않았으므로 그 부분이 빠졌다는 것을 염두하자)


해당 암호의 앞에 한 자리가 비었다는 것은 위의 분석 결과로 알 수 있지만 뒷 부분에도 뭔가 더 붙을지는 계속 분석해봐야

알 수 있겠지.

세 번째 암호 비교문을 탈출하면 [그림. 11] 의 CMP 코드를 보게 되는데, 자세히 보면 [ESP+4] 스텍 영역의 값과 0x45 값을 비교하고 있다. 그리고 그 다음 코드인 JNZ 코드가 있는데 CMP 코드 수행 결과, 두 비교 값이 다르다면 실패 메시지 박스를 출력하는 코드로 점프를 하고, 비교 값이 똑같다면 JNZ 분기문은 그냥 지나쳐 버리고 그 밑의 Congratulation 메시지를 출력하는 메시지 박스에 도달할 수 있다.

[ESP +5]의 영역을 확인해보면 암호의 맨 앞자를 가리키는 것을 알 수 있다. (처음에 비교하지 않고 지나쳤던 바로 그 부분...)

0x45에 해당되는 텍스트는 대문자 알파벳 E 이며 이 모든 것을 조합하면 암호는 "Ea5yR3versing" 으로 조합이 된다.






[그림. 12]  풀이 완료


Ea5yR3versing 을 입력한 결과, 풀이 완료~~~

블로그 이미지

slays

생각나는 것, 알게된 것을 끄적여보자.

댓글을 달아 주세요