Did you know that you can navigate the posts by swiping left and right?

HelloWorld 리버싱! / OllyDbg 사용법

27 Feb 2017 . category: Reversing . Comments
#reversing #hacking #helloworld #ollydbg

본 글은 이승원이 집필한 「리버싱 핵심원리」 를 읽고 공부(정리)한 내용을 바탕으로 쓰여졌습니다.

HelloWorld를 리버싱하기!

처음 코딩을 배울 때 C언어로 HelloWorld 라는 프로그램을 작성한다. 그때의 설렘과 감탄은 아무도 잊지 못할 것이라고 생각한다. 아래의 코드로 HelloWorld의 소스코드를 보자.

#include "stdio.h"

int main(void) {
	printf("Hello World! \n");
	return 0;
}

위의 소스코드를 Visual Studio에 작성하고 컴파일하면 Hello World! 라는 문자열이 적힌 콘솔창이 뜬다. 아니, 뭐.. 실행해보면 무슨 창이 떳다가 바로 사라지지만 Ctrl+F5를 누르면 “계속하려면 아무 키나 누르십시오.”라는 말과 함께 콘솔창이 오래 떠있다. 모든 개발자가 그렇듯 자신이 제작한 최초의 프로그램일 것이다. 기념비적인 프로그램이라고 할 수 있다. 이제 그 프로그램을 패치(Patch)하여 다른 문자열이 출력되도록 만들어 볼 것이다.

참고로, Release 모드로 빌드(컴파일)하길 권한다. Debug 파일로 빌드할 경우, 어셈블리 코드가 길고 복잡해진다.

목표

문자열패치: Hello World! 라는 문장을 Hello Reversing! 으로 바꾸자.

준비물

  1. OllyDbg (올리디버거): Win32용 전문 디버거. 직관적인 인터페이스와 강력한 확장기능을 가지고 있다. 설치는 이곳에서 할 수 있다.
  2. 매의 눈: 우리가 사용할 디버깅 프로그램인 OllyDbg의 글씨는 매우 작다. (한 6pt 쯤 되지 않을까..) 작은 글씨들 사이에 숨어있는 함수들을 찾아야한다.
  3. 끈기: 끈기는 매우 중요한 덕목(?)중에 하나이다. OllyDbg를 처음 사용할 때에는 매우 어렵고 짜증나게 생긴 프로그램이라고 생각되기 때문.. 필자는 그랬다. 지금도 그렇다.

OllyDbg를 사용해보자.

OllyDbg를 설치하고 실행해보면 다음과 같은 화면이 나온다. (창의 크기는 본인이 적절하다 싶을 정도로 조절해주면 된다.)

Imgur

▲ OllyDbg 처음 켰을 때의 화면
  • Code Window: 기본적으로 Disassembly Code를 표시하여 주석이나 Label을 보여준다. 코드를 분석하여 loop나 jump 위치 등의 정보를 표시한다. (기계어를 어셈블리어로 보여준다는 의미)
  • Register Window: CPU register 값을 실시간으로 표시하며 특정 Register 값을 수정할 수도 있다.
  • Dump Window: 프로세스에서 필요한 메모리 주소 위치를 Hex(16진법)와 ASCII/Unicode로 보여준다. 수정과 저장을 할 수 있다.
  • Stack Window: ESP register가 가리키는 프로세스 스택 메모리를 실시간으로 표시하고 수정도 가능하다.

단축키

Imgur

▲ OllyDbg 자주 사용하는 단축키

Label과 Comment가 헷갈릴 수 있을거같은데.. Comment는 그냥 해당하는 한 줄에 주석을 다는 것이다. 우리가 흔히 아는 주석 Label은 특정 메모리 값에 이름을 붙여주는 것이다. 그래서 JMP 00754ABD 라는 명령어가 존재한다고 할 때, 00754ABD에 Hello World 라는 레이블을 붙여준다면 JMP <Hello World> 라고 표현된다.

리버싱 방법을 생각해보자.

어떤 프로그램이든 메모리 주소를 호출하고, 반환하는 행위를 반복한다. 그렇다면, “Hello World!”를 출력하는 하는 함수(printf)를 호출하는 어셈블리 명령어를 찾아야한다. 만약 명령어를 찾았다면, “Hello World!”라는 문자열이 담긴 메모리 배열을 당연히 해당하는 함수(printf)에서 사용할 것이다. 그렇다면 2가지를 생각해볼 수 있다. “Hello World!”라는 문자열 자체를 변환하는 방법과 “Hello World!” 문자열을 참조하는 명령어를 변환하여 다른 메모리를 참조하는 방법이 있다.

정리해보면 방법은 다음과 같다.

  1. 함수(printf)를 호출하는 명령어를 찾는다.
  2. “Hello World!”가 담긴 메모리 자체를 변환한다.

또는

  1. 함수(printf)를 호출하는 명령어를 찾는다.
  2. “Hello World!” 문자열을 참조하는 명령어를 변환하여 다른 메모리를 참조하도록 한다.

공통문제 :: 함수(printf)를 호출하는 명령어를 찾자

OllyDbg를 켜고 File-Open(단축키: F3)을 이용해서 HelloWorld.exe를 열어보자. HelloWorld.exe는 우리가 만든 파일이다. 소스코드를 잘 떠올려보면, main 함수가 시작되고 그 안에서 printf 함수가 실행된다는 것을 알 수 있다. 그렇다면 일단 먼저 main 함수를 찾아야한다. OllyDbg 옵션(단축키: Alt + [O]) - Event - Entry point of main module의 설정을 켜놓자. main 함수로 접근하는 것이 더 쉬울 것이다.

방법 1. 무작정 찾아본다. :: 노가다

처음 Open을 하면 이상한 어셈블리 코드들의 한가운데 에서 시작한다. 이 이상한 어셈블리 코드들을 흔히 Stub Code1라고 부른다. 여기서 실행(단축키: [F9])을 눌러보자. 어떤 함수는 호출하는 명령어에서 멈출 것이다. 거기서부터 왼쪽에는 OllyDbg를 켜놓고 오른쪽에는 콘솔창(OllyDbg로 Open 했을면 HelloWorld.exe 창도 뜰 것이다.)을 띄워놓는다. 그래놓고 한 줄씩 실행(단축키: [F8], 함수 내로 들어가고 싶으면 [F7])해서 콘솔창에 Hello World! 라는 문장이 나오면 그 때의 명령어를 BP(BreakPoint, 단축키: [F2])해놓고 재실행(단축키: Ctrl + [F2])을 하고 [F9]를 눌러서 Hello World!가 출력되는 함수를 찾는다.

CALL main
    PUSH 1052100 ; ASCII "Hello World!
    CALL DWORD PTR DS:[1052090]
    ADD ESP,4
    XOR EAX,EAX
    RETN

위의 코드가 보이는가? 위에 보이는 CALL main 이라는 곳을 실행하면 Hello World! 라는 문자열이 출력된다. 여기서 재실행을 해서 CALL main에서 [F7]을 눌러서 main 함수 안으로 들어가면 위의 코드처럼 5줄이 보인다.2 PUSH 1052100 이라는 명령어에서 우리는 Hello World 라는 문자열을 메모리에서 꺼내서 Push 했다는 것을 알 수 있다. 즉, PUSH 1052100 이라는 부분에서 printf 문장이 시작된다는 것을 (정확히는 printf 함수의 기초공사가 시작되었다는 것3을) 알 수 있다. 이렇게 문장을 하나하나 실행해가면서 실행파일에 어떤 변화가 있는지를 눈으로 확인하고, 어떤 명령어에서 변화가 생겼는지를 파악하는 것이 바로 노가다 방법이다. 노가다라고 해서 나쁜 것만이 아니다. 물론, 자주하면 코드를 보는 실력도 많이 향상될 뿐만 아니라 Stub Code가 거의 비슷하기 때문에 속도도 매우 향상된다. 고 하더라…. 나는 그 수준이 언제쯤 되려나.

방법 2. 검색을 활용한다. :: Ctrl + [G]

Open을 하고나서 일단 무작정 Ctrl + [G]를 눌러서 main을 검색해본다. 우리는 소스코드를 알기 때문에 main 함수가 존재한다는 사실을 알고있다. 그러므로 main을 검색해본다. main이 존재한다면 커서가 어떤 줄을 향해 갔을 것이다. 그곳이 main 함수의 시작이다. 방법 1에서 언급한 어셈블리 코드와 똑같은 글자를 볼 수 있다. 정말 편한 기능이다. 하지만, 항상 Ctrl+[G]가 되는 것이 아니다. 디버깅을 하다가 특정 CALL 명령에 의해서 외부에서 불러온 파일(DLL같은거)같은 것들로 이동했을 때는 검색이 안될 수가 있다. 그러므로 노가다를 잘 해봐야한다..

방법 3. 문자열 검색을 활용한다.

우리는 문자열을 수정할 것이기 때문에 문자열을 바로 검색해서도 원하는 위치를 찾을 수 있다. (목표가 뚜렷하기 때문이라고 하자.) Open을 하고나서 마우스 우클릭 - Search for - All referenced text strings 를 누르면 프로그램에서 사용되는 모든 문자열이 나온다. OllyDbg에서 어떤 프로그램을 Open 할때 빠르게 스캔해서 정보를 저장해두는데 이때 문자열도 따로 저장해두기 때문에 이런 편리한 기능을 사용할 수 있다. 그러므로, All referenced text strings를 누르면, Hello World! 라고 적힌 한 줄의 명령어를 볼 수 있고 (그 외에도 여러 명령어들이 같이 있는데, 잘 읽고 찾아내야한다.) 더블클릭 하면 해당하는 줄을 보여준다.

문자열을 변환하자.

우리의 위의 방법들에서 어떻게 우리가 원하는 함수(printf)를 찾았는지를 알 수 있었다. (정확히는 printf 함수가 아니라 printf 함수를 실행하기 전의 문자열 메모리 주소값을 찾는걸 원했던 거지만.) 이제, 메모리 주소를 알 수 있게 되었으니 문자열을 변환해야한다. 앞서 말했듯이 문자열을 변환하는 방법에는 2가지가 있다.

  1. 메모리 자체를 수정하는 방법
  2. 문자열을 참조하는 명령어를 변환하여 다른 메모리를 참조하는 방법

방법 1. 메모리 자체를 수정하는 방법

우리는 PUSH 1052100이 문자열을 호출하는 함수라는 것을 알 수 있었고, 1052100 이라는 메모리 주소가 문자열의 메모리 주소값이라는 사실을 알 수 있었다. 1052100이 포인터 값이라고 생각하면 이해하기 쉬울 것 같다. 그럼 이제 OllyDbg의 3번째 창인 Dump Window를 클릭, 검색(단축키: Ctrl + [G])을 눌러서 1052100을 검색해보면 우측 ASCII 라고 적힌 부분에 Hello World라고 띄엄띄엄 적혀있는 것을 볼 수 있다. 이제 그만큼 드래그(마우스 쭈욱~)해서 Hex Code를 수정(단축키: [SpaceBar])하면 메모리가 패치되고, 그렇다면 Hello World 라는 문자열이 아닌 수정된 문자열을 출력할 것이다. (왜냐하면, 코드는 변함없이 1052100의 주소에 있는 문자열을 출력하는 것이었기 때문이다.) 그리고 패치된 메모리를 드래그하여 마우스 우클릭 - Copy to executable file 클릭 - (새창) 마우스 우클릭 - Save file 을 클릭하면 우리가 패치한 새 파일이 생긴다.

그러나, 이 방법에는 단점이 있다. “Hello World!” (Null 바이트 포함 14바이트)라는 컴파일 될 때 배정받은 바이트만큼만 수정할 수 있다. 그 이상을 수정할 수도 없고 수정한다면 뒤에 메모리가 오염되기 때문에 정상적으로 프로그램이 실행될 수가 없다. (켜지지 않을 수도 있다.)

방법 2. 다른 메모리 주소를 참조하도록 하는 방법

방법 1에서 보았듯이 1052100이라는 메모리 주소에는 Hello World! 라는 문자열이 존재했고, PUSH 1052100 이라는 명령어를 통해서 Hello World! 가 출력될 수 있었음을 알 수 있었다. 그렇다면 PUSH 1052100라는 명령어에 1052100 대신에 다른 주소를 참조하게 하면 어떻게 될까? 아까 봤던 Dump Window에서 아래로 쭈욱 드래그 해보면 00 으로 된 엄청 많은 공간이 있음을 알 수 있다. (전문용어로 이 공간을 Null Padding 이라고 한다.) 그 공간에 우리가 원하는 문자열을 삽입하고, PUSH 명령어로 호출하게 하면 방법 1과는 다르게 원하는 만큼 메모리를 사용할 수 있다.

빈 메모리 공간 10525D0 부터 10525EF 까지 총 32바이트를 “Good Day This is Hello Reversing” 이라는 문자열로 메모리를 패치(변환)하고 PUSH 1052100자리에 1052100 대신에 10525D0로 변환(단축키: [SpaceBar])한다. 그렇게 하면 1052100 자리에 있던 “Hello World!” 대신에 10525D0 자리에 있는 다른 문자열을 호출하게 되고 그러므로 내가 임의로 적은 문자열이 출력된다.

보통 문자열을 직접적으로 패치하는 것보다 이 방법으로 다른 메모리를 참조하는 방식이 흔하게 사용된다. 하지만, 이 방법은 치명적인 단점이 있다. 컴파일러에 의해서 허용되지 않은 메모리 주소를 참조하였기 때문에 메모리 패치가 적용이 안될 수 있고, 다시 이전으로 돌아갈 수 있다. (즉, 무용지물이 된다는 것이다.) (허용된 메모리를 참조하냐에 따라 될 수도 있고 안 될수도 있다.)




  1. 컴파일러가 프로그램을 만들 때 집어넣는 코드이다. 컴퓨터에 설치되어있는 컴파일러, 버전마다 Stub Code는 달라질 수 있다. 

  2. 그렇게 안 보일 수도 있다. PUSH OFFSET ??_C@_0P@LNOHKOJD@Hello?5World?$CB?5?6?$AA@ 라고 적혀있는데, 저 이상한 문자열을 더블클릭하면 1052100이라고 나온다. 그리고 옆에 친절하게 Hello World가 적혀있다고 Comment 달려있지 않는가. 의심의 여지가 없다. 

  3. 직접적인 연관관계는 없지만 레지스터를 통해서 문자열이 담긴 메모리 주소를 주고받았기 때문에 기초공사라고 할 수 있다. 


Me

Coding Future, Decoding Society.