Home | Info | Research | Blog | Repos | Messages | Contact Me

 


CPU는 mov eax, 0x100 같은 명령을 그대로 알아듣지 못합니다. 그래서 CPU가 알 수 있는 2진수 형태의 기계어로 변환해서 실행을 하게 됩니다.

오늘은 그 CPU가 실행하는 기계어를 사람이 알아 볼 수 있는 어셈블리 형태로 해석하는 방법을 알아보도록 하겠습니다.

먼저 인텔 CPU 매뉴얼이 필요합니다.(이건 인텔 웹사이트에서 받으세요, 이번 글에서는 Volume 2A: Instruction Set Reference, A-M"를 보면 됩니다.) 각 기계어가 의미하는 뜻을 알아보려면 매뉴얼을 봐야 합니다. 이 글에서는 mov 명령을 예로 들면서 설명하도록 하겠습니다.

B8 00 01 00 00   mov         eax, 100h

mov 명령을 인텔 매뉴얼에서 찾아보면 다음과 같이 B8입니다.


mov 명령도 B8만 있는 것이 아니고 뒤에 오는 오퍼랜드의 종류에 따라서 여러가지가 있습니다. 이것은 인텔 매뉴얼 Instruction Set Reference의 mov 부분에 목록이 나와 있습니다. 그런데 mov 도표에 보면 B8+라고 되어 있는데 이+는 목적지 오퍼랜드의 종류에 따라 바뀐다는 것을 뜻합니다. 이 순서는 eax, ecx, edx, ebx, esp, ebp, esi, edi 순으로 지정되어 있는데, 따라서 mov eax는 B8, ecx는 B9, edx는 BA, ... edi는 BF가 됩니다.

그런데 mov 명령 중에서도 뒤에 오퍼랜드가 eax, ecx등 여러가지로 바뀌는데도 기계어는 똑같은 경우가 있습니다. 이것은 레지스터에 주소의 값을 대입하는 경우인데, ModR/M 바이트를 보고 판별합니다.

ModR/M 바이트는 3부분으로 구성되어 있는데, Mod 2비트, Reg/Opcode 3비트, R/M 3비트로 구성되어 있습니다. 이것은 인텔 매뉴얼 Instruction Set Reference의 앞부분에 보면 "32Bit Addressing Forms with the ModR/M Byte"라고 표 형태로 되어 있습니다.

8B 07            mov         eax, [edi]



위의 예에서 mov는 8B이고 eax, [edi] 부분은 07입니다. 이것은 ModR/M 표에서 찾으면 되는데, 첫번째 오퍼랜드는 세로줄이며, 두번째 오퍼랜드는 가로줄입니다. eax 줄에서 [edi]를 찾으면 07이 나옵니다. (이 경우에는 목적지가 레지스터기 때문에 mov가 8B입니다. 목적지가 메모리주소가 될 경우 mov는 89로 바뀌고, 표에서 찾을 때에도 첫번째 오퍼랜드는 가로줄이 되고 두번째 오퍼랜드는 세로줄이 됩니다. 그렇기 때문에 오퍼랜드의 종류와 방향에 따라 명령어의 기계어가 바뀌게 됩니다.)

8B 59 04         mov         ebx, [ecx+4]



이것 역시 mov는 8B이고 ebx, [ecx+] 부분은 59입니다. 4는 그 바로 뒤에 나오고 있습니다. 이걸 ModR/M 표에서 찾으면 ebx 줄에서 [ecx]+disp8에 있습니다. +disp8은 +4와 같은 오프셋을 뜻합니다. 8은 8비트 숫자를 나타냅니다. +disp32는 32비트 오프셋이겠죠.

8B 0C 50         mov         ecx, [eax+edx*2]



이번에 것을 찾아보면 ecx 줄에 [eax+edx*2]라는 것이 없을 겁니다. 이런 경우는 [--][--]를 찾으면 됩니다. 이건 0C인데 eax+ebx*2는 어디에 있느냐? 50이 바로 eax+ebx*2를 뜻하고 있습니다. 이것은 ModR/M 표에서는 안나오고 ModR/M 바로 뒤에 보면 "32Bit Addressing Forms with the SIB Byte" 표에 나와 있습니다. eax 줄에서 edx*2를 찾으면 50이 나옵니다.



 이 표를 찾는 것만 알고 있으면 어떤 기계어가 나와도 인텔 매뉴얼에서 다 찾을 수 있을 것입니다.



앞에 올렸던 리버스 엔지니어링이란? 1부에서 이어지는 글입니다.

윈도우용 바이너리를 리버스 한다고 하면 윈도우 API를 알아야 하고 기타 리눅스나 BSD라면 해당 OS의 API를 알아야 합니다. 그리고 당연히 인텔 어셈블리를 알고 있어야 하고, 인텔 CPU가 돌아가는 방식을 알아야 합니다. 프로그램은 단순히 로직만으로 이루어져 있지 않고 시스템의 API를 호출하여 여러가지 동작을 하기 때문에 각 API의 사용방법과 동작결과를 알고 있어야 어셈블리 코드에서 C소스 코드로 옮기고 다시 소스를 재구현할 수 있기 때문입니다. 어플리케이션을 리버스하는 경우 윈도우 커널에 대한 지식이 없어도 리버스가 가능하지만 커널 레벨로 동작하는 드라이버나 기타 서비스는 윈도우 커널에 대한 지식이 필요합니다.

어셈블리에서 C소스 코드로 옮기는 작업은 한마디로 단순 반복작업입니다. 디스어셈블된 코드에서 call은 함수이고 각종 점프 명령어들은 if, for, while, switch, goto 등의 C언어 제어문입니다. mov 명령어는 변수에 값을 대입한다거나 하는 것이고 push, pop 명령은 함수를 호출하면서 인자값을 넘겨줄 때 사용합니다. 이 어셈블리 명령의 조합을 읽어 C코드로 구현을 하면 됩니다.


각종 도구들

리버스 엔지니어링에서는 디스어셈블러라는 도구가 매우 필수적입니다.
자주 쓰이는 것들로는 리버스계에서 아주 유명한 소프트아이스(SoftICE, 이하 소아)라는 프로그램이 있습니다. 소아의 특징으로는 윈도우가 돌고 있는 상태에서 Ctrl+D를 누르면 윈도우가 멈추고 소아 창이 떠서 현재 실행되고 있는 어셈블리 코드를 보여줍니다. 정말 강력하고 편리한 기능입니다. 이 기능이 소아를 쓰는 이유이기도 합니다. 물론 그냥 바이너리를 열어서 디스어셈블도 가능하며 요즘은 비주얼 소프트아이스라고 나와서 원격 디버깅도 가능합니다. 단 비쌉니다.

WinDBG(Windows Debugger)는 MS에서 배포하는 무료 디버거인데 윈도우 내부를 분석하는데 매우 유용한 도구입니다. 심볼 서버에서 심볼 파일을 받아와서 윈도우 내부 DLL들의 함수이름과 구조체 등을 볼 수 있습니다. 물론 이것도 그냥 바이너리를 열어 디스어셈블이 가능합니다.

OllyDBG, 아주 편리한 도구입니다. 리버스하기에는 딱 알맞은 도구가 아닌가 싶습니다. 디스어셈블 뿐만 아니라 이 디버거가 어셈블리 코드를 분석하여 사용자에게 많은 정보(함수 이름, 인자값 이름, 서브루틴끼리 묶어주는 기능, 점프명령의 도착점 표시 기능 등)를 제공해 줍니다.

W32dasm는 OllyDBG처럼 인터렉티브 하지는 않지만 상당히 쓸만한 도구입니다. 기타 IDA나 PE Browse등의 프로그램이 있는데 사용자 취향에 따라 골라 쓰면 되겠습니다.

거의 모든 디버거에서 레지스터, 스택, 메모리 상태 등을 표시해 주고 있으므로 코드 분석에 많은 도움이 됩니다.


마지막으로 주의할 점은 같은 바이너리를 디스어셈블한다고 하더라도 디스어셈블러마다 분석해 내는 코드가 조금씩 다른 경우가 있습니다. 그래서 한가지 디버거만 쓰다 보면 엉뚱한 코드를 보기 쉽습니다. 여러가지 디버거를 돌려 가면서 코드를 분석하는 것도 좋은 방법입니다.




리버스 엔지니어링이란?

리버스(reverse)라는 말은 반대, 역(逆)의 뜻을 가지고 있는데, 리버스 엔지니어링을 역공학이라고 쓰기도 합니다. 리버스 엔지니어링은 목표가 되는 프로그램이나 프로토콜을 분석하여 똑같은 동작을 만들어 내는 것을 말합니다.


리버스 엔지니어링의 종류

통상적으로 컴파일된 바이너리(EXE, DLL, SYS 등)를 디스어셈블러라는 도구를 이용하여 어셈블리 코드를 출력한 후 그것을 C언어 소스형태로 다시 옮겨 적고 적당한 수정을 통해 리버스하고 있는 파일과 동일한 동작을 하는 프로그램을 만드는 것이 있습니다.

모든 어셈블리 코드를 소스 형태로 옮기지 않고 그냥 동작 방식만을 알아낸다거나 일정 부분만 수정하는 것들도 리버스 엔지니어링이라고 할 수 있습니다. 예를 들면 바이러스를 분석하는 일은 모든 코드를 알아낼 필요가 없기 때문에 동작 방식만 알아내면 됩니다. 그리고 크랙처럼 일정 부분만 수정하여 사용제한을 푸는 것 등도 이에 해당됩니다.

실행파일을 디스어셈블 하지 않고도 그 실행파일이 만들어내는 데이터 파일이나 패킷등을 분석하여 똑같이 재구현하는 것도 리버스 엔지니어링입니다. 예를 들면 오래전 PC 게임에서 많이 하던일인데, HEX 에디터 등으로 세이브 파일을 분석하여 에디트를 만들거나 게임자체를 조작하는 것이 있고, 당나귀와 호환되는 이뮬 같은 프로그램은 당나귀 프로토콜의 패킷을 분석하여 동일한 동작을 하도록 만들어낸 것입니다.


리버스 엔지니어링에서 가장 많이 사용되는 방식은 첫번째로 이야기 했던 바이너리를 디스어셈블 하여 코드를 얻어내는 것입니다. 이것을 하기 위해서는 먼저 인텔 어셈블리를 배워야 하고, 물론 C언어도 알아야 됩니다.

그런데 여기서 컴파일된 바이너리가 VC나 gcc등으로 컴파일한 것이 대부분이지만 비주얼 베이직으로 컴파일 한 것도 있고 델파이(파스칼)로 컴파일 한 것도 있을 것입니다. 이 바이너리들은 모두 CPU에서 직접 실행되는 것들이기 때문에 디스어셈블 해보면 모두 똑같은 방식으로 되어 있습니다. 그래서 VC, VB, 델파이(파스칼)등으로 컴파일 된 것도 디스어셈블 한 뒤 C 소스 코드로 옮길 수 있습니다. 물론 리버스 하는 사람이 VB나 파스칼로 옮겨 적을 수도 있을 것입니다. 하지만 C언어로 하는 것이 가장 간편합니다.

대부분 리버스해서 얻어내는 것들은 프로그램의 로직(알고리즘)이기 때문에 어느 언어로 표현하든 결과는 똑같기 때문입니다.


자바나 닷넷으로 컴파일 된 바이너리는 CPU에서 직접 실행되지 않고 자바 가상머신이나 닷넷 프레임워크를 통해서 실행됩니다. 그래서 자바로 컴파일된 바이너리를 열어보면 자바 바이트코드 문법으로 되어 있고 닷넷으로 컴파일 된 것은 MSIL이라는 문법으로 되어 있습니다. 이런 것을은 인텔 어셈블리와 문법이나 명령어가 다르므로 따로 자바 바이트코드나 MSIL을 공부해서 리버스 하면 되겠습니다.