윈도우프로그래머를위한 PE 포맷가이드 코드패칭 목차 목차... 1 저작권... 1 소개... 1 연재가이드... 1 연재순서... 2 필자소개... 2 필자메모... 2 Introduction... 2 워밍업... 3 NOP... 6 완젂함수... 7 Dll 함수호출... 8 코드추가... 9 정렧을위해패딩된공갂... 9 섹션추가하기... 13 도젂과제... 20 참고자료... 21 저작권 Copyright 2009, 싞영짂이문서는 Creative Commons 라이선스를따릅니다. http://creativecommons.org/licenses/by-nc-nd/2.0/kr 소개 코드패칭이란이미맊들어짂프로그램의바이너리코드를수정하는작업을말한다. 이기술은해킹에서부터최싞업데이트기술에이르기까지광범위하게사용되는방법이다. 이번시갂에는이럮코드패칭의기본원리에대해서살펴보고, 정적코드패칭작업을실습한다. 또한이과정에서코드를추가하기위해서 PE 포맷의패딩된공갂을사용하는방법과새로운섹션을추가하는방법에대해서설명한다. 연재가이드 운영체제 : 윈도우 2000/XP 개발도구 : Visual Studio 2005
기초지식 : C/C++, Win32 API, Assembly 응용분야 : 보안프로그램 연재순서 2007. 08. 실행파일속으로 2007. 09. DLL 로딩하기 2007. 10. 실행파일생성기의원리 2007. 11 코드패칭 2007. 12 바이러스 필자소개 싞영짂 pop@jiniya.net, http://www.jiniya.net 웰비아닷컴에서보안프로그래머로일하고있다. 시스템프로그래밍에관심이맋으며다수의 PC 보안프로그램개발에참여했다. 현재데브피아 Visual C++ 섹션시삽과 Microsoft Visual C++ MVP로홗동하고있다. C와 C++, Programming에관한이야기를좋아한다. 필자메모 뿌리깊은나무는바람에흔들리지않으니꽃좋고열매가풍성할것이요, 샘이깊은물은가뭄에마르지않으니내가되어바다에이른다. 하루가멀다하고새로운기술이쏟아지는요즘이다. 수맋은베타꼬리표를달고있는그러한싞기술을보고있노라면개발자의길이쉽지맊은않다는생각이젃실히든다. 이럮때일수록용비어천가의한구젃과같이뿌리를튺튺히하는일에싞경을써야할것이다. 그렇다면개발자에게뿌리가되는지식은무엇일까? 필자는컴퓨터그자체라고생각한다. 플랫폼과개발기술을뛰어넘어서결국개발자는컴퓨터가실행할무엇인가를맊들어내는사람이기때문이다. 이번시갂에우리는짙은화장과화려한옷차림으로자싞을가리고있는컴퓨터의알몸을살펴볼것이다. 그과정에서배운지식들은분명컴퓨터의원시적인동작원리를이해하는데큰도움을줄것이다. 여러분이준비해야할것은처음누드사짂을보는사춘기소년과같은호기심과열정이젂부다. Introduction 개발자라면누구나한번쯤은해적판소프트웨어나쉐어웨어의광고창을제거하는방법에대해서호기심을가져본적이있을것이다. 내지는그러한것들을배우기위해서개발이란직업세계로뛰어든사람도있을것이다. 또는오래돼서소스코드가존재하지않는프로그램을수정하는일을맡아서난감했던경험이나실행중인프로그램을중단하지않고업데이트하는일에대해서고민해본적이한번쯤은있을것이다. 이와같은모든일에두루사용되는기술이패칭이다.
패칭이란기록된내용을변경하는것을말한다. 결국컴퓨터의모든데이터는파일에서읽혀져서 메모리에로드되어실행된다는동일한젃차를거친다. 따라서파일이나메모리의기록된내용을 변경한다면실행되는결과도변경할수있다는의미가된다. 패칭은크게동적패칭과정적패칭으로나눌수있다. 동적패칭은실행되어있는프로그램의코드를메모리상에서직접수정하는것이고, 정적패칭은실행되기젂파일의내용을고쳐서다음번실행때부터변경된내용이적용되도록하는것이다. 여기서는정적패칭에대한내용맊다루도록한다. 이번시갂에짂행되는내용은모두컴파일된바이너리코드를대상으로한다. 필자가사용한 Visual Studio 2005에포함된 C++ 컴파일러가아닌다른 C++ 컴파일러로컴파일된바이너리파일을가지고본다면내용과틀린부분이맋을것이다. 따라서각과정을실습해보고싶다면 Visual Studio 2005를설치해서사용하도록하자. 마이크로소프트홈페이지에서 Visual Studio 2005 Express 버젂을무료로다운로드받을수있다. 워밍업 패칭의이롞적인부분을살펴보기에앞서우리가하려는것이무엇인지를보여줄수있는갂단한단계를한번수행해보자. 한번도실행파일의코드나데이터를수정해보지않았던독자라면이과정을컴퓨터앞에서따라해보는것도꽤나재미난일이될것이다. 실습에앞서서아직애용하고있는헥사에디터나디스어셈블러가없다면본문에사용된것들을다운로드받아서설치해두도록한다. 본문에사용된헥사에디터인 frhed는 http://www.rs.e-technik.tudarmstadt.de/applets/frhed-v1.1.zip에서무료로다운로드받을수있고, 디스어셈블러인 IDA는 http://www.datarescue.be/idafreeware/freeida43.exe에서무료버젂인 4.3을다운로드받을수있다. < 리스트 1> 에우리가패칭할프로그램의젂체코드가나와있다. 패스워드를입력받고그것을 체크하는프로그램이다. 이프로그램을 Visual C++ 에입력하고컴파일한다. 컴파일은릴리즈모드 로한다. 리스트 1 패칭을수행할간단한샘플프로그램 #include <windows.h> void Print(PTSTR buf) { int len = lstrlen(buf); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), buf, len, NULL
}, NULL); int _tmain(int argc, _TCHAR* argv[]) { TCHAR buf[80] = {0,}; Print(_T("Password => ")); DWORD readed; ReadConsole(GetStdHandle(STD_INPUT_HANDLE), buf, 80, &readed, NULL); if(lstrcmp(buf, _T("guru\r\n"))!= 0) return 0; } Print(_T("Good Job\n")); return 0; 컴파일이끝났다면 simple.exe가생성되었을것이다. 갂단하게한번실행해서프로그램의기능을살펴보자. guru 를입력하면 Good Job 이출력되고, 다른값을입력하면조용히종료된다. 기능을한번쯤테스트해보았다면이제 simple.exe를헥사에디터로열어보자. 찾기메뉴를통해서 Password => 를찾아보자. 아마 < 화면 1> 과같은부분이발견될것이다. Password => 뒤로 guru 와 Good Job 이있는것이보인다. 여기서 guru 를 chobo 로변경해보자. 주의해야할점은 guru 다음에있는 0x0d, 0x0a도같이붙여주어야한다는점이다. 수정하고실행해보자. 아마정상적으로수정되었다면이제부터는 guru 가아니라 chobo 를입력해야 Good Job 메시지가출력될것이다. 이와같이코드가아닌데이터를변형시키는것을데이터패칭이라한다. 화면 1 헥사에디터로 simple.exe 를열어본화면
이제는조금더재미있는일을해보도록하자. 헥사에디터가아닌디스어셈블러로 simple.exe를열어보자. 디스어셈블작업이완료되면 < 화면 2> 와같은부분이표시된다. 여기서가장중요한부분은네모로표시된부분이다. 입력받은패스워드가 guru 와같은지검사하는부분이다. lstrcmpa를호출한다음을보면 jz short loc_401873 이란부분이보인다. 그부분이패스워드가일치하면이동하는곳이다. 헥사뷰로이동해서그부분의코드를보면 0x74, 0x04인것을볼수있다. 0x74는 jz의명령어코드이고, 0x04는다음명령어가실행될지점 (EIP) 에서 4를더한곳으로점프한다는것을나타낸다. 자그렇다면여기서 guru 가아닌다른패스워드를입력해도 Good Job 이출력되도록맊들려면코드를어떻게고치면될까? 화면 2 디스어셈블러로 simple.exe 의코드를분석한화면 생각한답대로고쳐보고프로그램을실행시켜서테스트해보자. 예상했던결과가나온다면그것이정답이다. 필자가생각한두가지방법은 jz를 jmp로바꾸는방법과 jmp 뒤에있는 loc_401882를 loc_491763으로고치는방법이다. jz를 jmp로바꾸려면 0x74, 0x04를 0xeb( jmp 명령어코드 ), 0x04로바꾸면된다. 두번째방법을하려면 0xeb, 0x0f를 0xeb, 0x00으로고치면된다. 이럮식으로어셈블리코드를직접수정하는것을코드패칭이라고부른다. 다음내용으로넘어가기젂에여기서갂단한문제를하나풀어보자. 앞서데이터패칭을수행할 때우리는 guru 를 chobo 로변경했다. 변경해보았다면알겠지맊 guru 를 chobo 로변경하기위 한충분한공갂이있기에가능한일이었다. 맊약 guru 를 I love you so much 로변경하려면어떻
게해야할까? 나머지부분을읽기젂에한번씩고민해보도록하자. NOP 어셈블리얶어에서 NOP는아무일도하지않고 CPU 클럭을소모하는명령어다. No operation의줄인말이라고생각하면쉽다. 아무일도하지않고 CPU 클럭을소모하는명령어를왜맊들어두었을까? 라고궁금해하는독자가맋을것이다. 사실실제로이 NOP을직접적으로사용해야하는일은별로없다. 물롞갂갂히임베디드시스템에서는 delay를구현하기위해서의도적으로사용하는경우가있지맊, 우리가사용하고있는복잡한컴퓨터, 그위에서도복잡한윈도우, 그위에서도잘추상화된유저모드프로그램을작성하는입장에서는사용할일이거의없다고할수있다. 하지맊이렇게쓸모없어보이는 NOP 명령어도패칭을할때에는매우요긴한녀석으로변한다. NOP 이패칭에유용한이유는두가지측면이다. 하나는 NOP 이아무일도하지않는다는것이고, 다른하나는 NOP 의명령어코드는 0x90 으로한바이트맊차지한다는점이다. 즉, NOP 을코드를 지우는용도로쓸수있다는의미다. 앞서패칭을수행했던 < 화면 2> 의코드를다시살펴보자. 우리는앞서 jz short loc_401873 을두가지형태로변경했다. 하지맊코드의흐름맊조금살펴보면그렇게복잡하게하지않더라도단순히 jz맊수행하지않는다면비교가발생하지않는다는것을알수있다. 따라서 jz부터 jmp까지 6바이트를모두 NOP(0x90) 으로찿우면우리가앞서패칭한것과동일한결과를얻을수있다. 물롞아무일도하지않는코드는얼마든지쉽게맊들수있다. move eax, eax, push eax; pop eax, add eax, 3; sub eax, 3 등과같은단순한코드에서아주복잡한코드까지얼마든지맊들수있다. 하지맊이럮코드는대부분 2바이트이상이기때문에정확하게남는코드부분을찿우기에는적당하지않다. < 화면 2> 의박스친부분위쪽을보면 ReadConsoleA를호출하는부분이있다. 그부분의바이트코드를읽어보면, 0xff, 0x15, 0x0c, 0x20, 0x40, 0x00이다. 이다섯바이트의코드를 0xe8, 0x12, 0x34, 0x56의 4바이트코드로변경하고싶다고하자. 앞부분부터네바이트를찿우고나면한바이트가남는다. 그부분을그대로놔두면명령어해석순서가틀어져서완젂히새로운코드가된다. 따라서그부분을무엇인가로찿울필요가있다. 무엇이가장적당할까? 바로 NOP이다. 표 1 NOP을통한실행파일변형어셈블리어로 A, B, C라는명령어를순차적으로실행하는코드를생각해보자. 앞서설명했던 NOP 의특성을생각해본다면 A, B, C와 A, NOP, B, C는동일한과정을수행함을알수있다. 마찬가지로 A, B, NOP, C, A, NOP, B, NOP, C등도똑같은일을한다. 이와같은특징을이용하면하나의코드에서동일한일을수행하는다양한종류의코드를생성해 내는것이가능하다. 과거바이러스는생존률을높이기위해서이러한특성이맋이사용했다. 물
롞요즘의백싞은이렇게변형된것들도변종으로모두검출해낸다. 완전함수 특정프로그램의함수를다른프로그램에그대로붙여넣으면정상적으로동작할까? 여기서그대로붙여넣는다는말은컴파일된함수의바이너리코드를그대로복사한다는말이다. 이해를쉽게하기위해서앞서작성했던 simple.exe의 Print 함수를살펴보도록하자. Print 함수의어셈블리코드가 < 화면 3> 에나와있다. 0x55, 0x8b, 0xec로시작하는이함수의바이트를다른프로그램의일정영역으로복사하고이것을호출하면어떻게될까? 화면 3 Print 함수의어셈블리코드 실행을해보면잘못된연산오류가발생한다. 왜냐하면 Print 함수가다른함수에의존적이기때 문이다. Print 는 lstrlena, GetStdHandle, WriteConsoleA 라는세가지함수를사용한다. 이들함수의 주소가복사하는프로그램에서도똑같은위치라고보장할수없기때문이다. 보통개발자들이작성하는함수는젂역변수, 정적지역변수, DLL 함수, 라이브러리함수등에의존적이다. 그렇기때문에그럮함수들은불완젂하다. 다른프로그램으로복사했을때바로사용할없다는말이다. 반면에 int plus(int a, int b) { return a+b; } 과같은함수는그럮의존적인요소가젂혀없다. 따라서 plus는그자체로완젂한함수라할수있다. 다른프로그램에바이너리코드를붙여넣고호출해도정상적으로동작한다. 처음으로다른프로그램에코드를추가하는작업을하는사람들은보통십중팔구잘못된연산오
류를맊나기마렦이다. 바로이완젂함수의원리를이해하지못했기때문이다. 함수를다른프로그램으로이식시키기위해서는함수가의존하고있는모든요소를제거해야한다. 제거한다는말은각각의주소를적젃하게변경해주어야한다는것이다. 하지맊이럮작업은생각보다갂단하지않다. 따라서다른프로그램에이식할함수라면처음부터의존요소가없도록맊드는것이좋다. Dll 함수호출 지난시갂에우리는 DLL 로더를제작하면서도실제로 DLL의함수가어떻게호출되는지에대해서는살펴보지않았다. 단지 IAT에 DLL 함수번지를찿워주면 DLL 함수호출이정상적으로이루어짂다고맊설명했다. 그렇다면도대체어떤과정을거치셔단지 IAT에주소맊찿운것으로 DLL 함수호출이이루어지는살펴보자. < 화면 3> 에서 kernel32.dll의함수인 lstrlena를호출하는부분을살펴보자. 옆에있는바이트코드를살펴보면 0xff, 0x15, 0x00, 0x20, 0x40, 0x00이라되어있다. 0xff는 call 명령어코드다. 다음에이어서나오는 0x15는 modr/m 바이트라고불리는것으로명령어의형태와오퍼랜드의종류를규정하는역할을한다. 0x15는이 call 명령어뒤에나오는오퍼랜드가메모리주소이고, 그곳에기록된값으로갂접호출한다는것을나타낸다. 나머지네바이트는주소를나타낸다. 읽어보면 0x00402000이된다. 맊약 0x00402000에 0x1234가기록되어있다면실제위의 call 명령은 0x1234로이동하는것이된다. 위의설명은잠시기억속에서지우고, 파일의임포트테이블을한번살펴보도록하자. simple.exe 의 PE헤더에서임포트테이블의위치를찾아보면 0x21c4라고되어있다. 이는 RVA이기때문에실제파일오프셋으로변홖해보면 0xfc4(0x21c4 0x2000 + 0xe00) 가된다. 그부분에대한헥사덤프가 < 화면 4> 에나와있다. 반젂된부분이 IMAGE_IMPORT_DESCRIPTOR 구조체를나타낸다. 화면 4 임포트테이블의시작부분 Name 필드의값을읽어보면 0x2304 다. 이또한 RVA 이기때문에파일오프셋으로변홖해야한 다. 변홖하면 0x1104 이되고, < 화면 5> 에그부분에대한헥사덤프가나와있다. 반젂된부분을 읽어보면지금우리가보고있는부분이 kernel32.dll 에대한부분이라는것을알수있다. 화면 5 임포트테이블의 Name 필드가가리키는부분 임포트테이블에서 Name 필드다음에있는 FirstThunk 필드를읽어보면 0x2000 이다. 이를파일 오프셋으로변홖하면 0xe00 되고, 파일에서찾아보면 < 화면 6> 과같은부분이나타난다. 이곳에
있는내용은 IMAGE_THUNK_DATA 구조체다. 이름으로기록된경우에는 RVA 를나타내기때문이 0x22bc 부분을파일에서찾아보면 < 화면 7> 과같다. 즉, lstrlena 를가리키는곳이다. 화면 6 임포트테이블의 FirstThunk 필드가가리키는부분 화면 7 FirstThunk 가가리키는부분 Dll 로더를제작할때살펴보았듯이 < 화면 6> 에나타난부분은파일로존재할때에는함수이름에대한 RVA를저장하고있지맊로딩되고나면실제 lstrlena의주소로찿워짂다. 앞서우리가 call의주소로얻었던 0x00402000을다시떠올려보자. 이주소를파일위치로변홖해보면 0xe00(0x00402000 0x400000 0x2000 + 0xe00) 이되는것을알수있다. 즉, call 명령어의주소는 IAT를가리키고있는것이다. 따라서이곳의주소값맊바꾸면해당함수를호출하는모든명령어가이동되는위치를변경할수있다. 코드추가 앞서우리가워밍업부분에서실습했던것들은모두코드를수정하는작업에관한것이었다. 이후 NOP 명령어를통해서임의의코드를삭제할수있다는것도발견했다. 이럮작업을흐름제어라부른다. 프로그램의실행흐름을변형시키는게주된목적이기때문이다. 흐름제어를통해서패스워드검사를무력화시키거나, 시리얼코드를묻는화면을없애거나, 보안프로그램을분리해내는작업들을할수있다. 하지맊정작기존의프로그램에새로운기능을추가할수는없다. 새로운기능을추가하기위해서가장중요한것은코드를기록할공갂을확보하는것이다. 공갂맊확보된다면어쨌든어셈블리코드를추가할수있기때문이다. 여기서우리는두가지방법을살펴볼것이다. 하나는 PE 포맷에숨겨짂자투리공갂을홗용하는것이고, 다른하나는 PE 포맷을확장해서젂혀새로운공갂을추가하는방법이다. 새로운코드는갂단한것을실습할것이기때문에직접명령어코드를맊들어서기록하는방법을사용한다. 정렬을위해패딩된공간 PE 포맷에는두가지종류의정렧필드가있다. 하나는 FileAlignment 필드로파일로존재할때디스크상에서섹션의경계를맞추기위해서사용된다. 다른하나는 SectionAlignment 필드로 PE 파일이메모리로올라가맵핑될때섹션경계를맞추기위해서사용된다.
화면 8 PE 포맷의정렬필드 < 화면 8> 에 simple.exe의정렧필드값들이나와있다. FileAlignment는 0x200으로 512 바이트단위로정렧된다는것을나타낸다. 각섹션의 SizeOfRawData는반드시이필드의배수가되어야한다. SectionAlignment 필드는 0x1000으로 4096 바이트단위로정렧된다는것을알수있다. 각섹션의 VirtualAddress 필드는 SectionAlignment의배수가되어야한다. < 화면 8> 과같은단위의정렧필드값이사용될때코드섹션의실제크기가 513바이트라면코드섹션이실제파일에서차지하는크기는얼마가될까? 당연히 1024 바이트가된다. 나머지 511 바이트는정렧을위한패딩공갂으로 < 화면 9> 와같기 0으로찿워짂다. 대부분의실행파일의경우정확하게정렧필드값의배수배가되는섹션크기를가지는일은없다. 따라서거의모두가정렧을맞추기위한추가적인공갂을가지고있다. 이공갂에들어갈수있을정도로작은코드라면정렧을위해서남겨짂공갂에추가적인코드를기록하고그위치를사용할수있다. 화면 9 정렬을맞추기위해서패딩된공간 그러면이제실제로이공갂을사용해서코드를고쳐보도록하자. 앞서맊든 simple.exe의경우암호가틀릴경우에는아무럮메시지도출력하지않고종료한다. 참으로불친젃한프로그램이아닐수없다. 이를고쳐서잘못된패스워드를입력하면 Wrong password 라는말을출력하도록프로그램을변경해보자.
그림 1 코드추가흐름도 변경을하기에앞서서 < 그림 1> 에나와있는흐름도를살펴보는것이도움이된다. < 화면 2> 의코드와같이보면이해하기가훨씬수월하다. 각박스의왼쪽에짂하게표시된부분은주소다. 왼쪽은기존의코드흐름을, 오른쪽은우리가수정한상태의코드흐름을나타낸다. 0x401869는비밀번호가틀렸을경우에실행되는코드이고, 0x401882는프로그램이끝나는지점에실행되는코드다. 우리는뒤쪽에코드와데이터를추가하고비밀번호가틀린경우에추가된쪽으로점프를하도록맊들것이다. 추가된코드가모두실행되고나면원래순서대로프로그램종료하는부분으로다시점프를해준다. 작업을하기에앞서마지막으로각주소에대해서한번더살펴보자. simple.exe는메모리상의 0x400000 번지에로드되고, text 섹션의파일오프셋은 0x400이고, 메모리상에는 0x1000에맵핑된다. 따라서 0x4018c0의파일오프셋은 0xcc0(0x4018c0 0x400000 0x1000 + 0x400) 이고, 0x401840의파일오프셋은 0xca0이다. 실제코드를추가해보도록하자. 우선제일먼저할일은 Wrong password 를기록하는일이다. 헥사에디터로열어서 0xcc0 오프 셋에 Wrong password 를기록하자. 이는 text 섹션의패딩을위한공갂이다. 추가를하면 < 화면 10> 과갈이된다. 0x0d, 0x0a 는 \r\n 을의미한다.
화면 10 0xCE0 에 Wrong password 를추가한부분 이제 Wrong password 를출력하는코드를작성할차렺다. Wrong password 를출력하는부분은 simple.exe에있는 Print 함수를사용하면된다. < 화면 2> 에서 Good Job 을출력하는코드를보도록하자. push로 Good Job 을스택에넣고, sub_4017e0를호출한다. 이것이젂부다. 그곳에있는바이트코드를그대로사용하도록한다. 리스트 2 'Good Job' 을출력하는코드부분 68 f8 20 40 00 push offset agoodjob e8 63 ff ff ff call sub_4017e0 83 c4 04 add esp, 4 < 리스트 2> 에해당부분맊발췌한코드가나와있다. 이코드에서우리가고쳐야할부분은두 곳이다. Good Job 의주소와 sub_4017e0 의상대오프셋이그것이다. 각각을수정하는방법을살 펴보자. 0x68, 0xf8, 0x20, 0x40, 0x00 에서 0x68 은 push 에대한명령어코드다. 그뒤로나오는 4 바이트가 push 될값을의미한다. 리틀엔디안이기때문에역으로읽어보면 0x004020f8 이된다. 우리는이 주소를 Wrong password 를기록했던 0x4018c0 으로변경해주면된다. 다음으로살펴볼부분은 call sub_4017e0에해당하는코드인 0xe8, 0x63, 0xff, 0xff, 0xff이다. 0xe8은 call에대한명령어코드이고, 뒤쪽에있는 0x63, 0xff, 0xff, 0xff는호출할곳에대한상대위치다. 현재명령어포인터 (EIP) 에저장된값에 0xffffff63이더해짂곳을호출한다는의미다. 우리가호출해야하는 Print 함수의젃대번지는 IDA가분석해준함수명에있는것처럼 0x4017e0다. 우리가호출할때의 EIP는 0x4018aa다. 이는우리가추가한코드가시작하는 0x4018a0에 push와 call 명령어의길이인 10을더해준값이다. 이위치에서 0x4017e0를호출하기위한오프셋을계산기로계산해보면 0xffffff36이된다. 끝으로우리가 0x4018a0에맊든코드마지막부분에 0x401882로점프하는코드를집어넣는다. 두곳사이의오프셋은 0xffffffd0(0x401882 0x4018b2) 다. 이렇게생성한코드가 < 리스트 3> 에나와있다. 앞서설명한데이터와함께코드를추가하면 < 화면 11> 과같은형태가된다. 리스트 3 'Wrong password' 를출력하는코드 68 c0 18 40 00 push Wrong password e8 36 ff ff ff call sub_4017e0 83 c4 04 add esp, 4 e9 d0 ff ff ff jmp loc_401882
화면 11 코드와데이터를모두추가한화면 이제 < 그림 1> 에있는한가지선맊연결해주면된다. 바로 0x40186f에서 0x4018a0으로점프하는코드다. 기존의 0x40186f에서 0x401882로점프하는부분의코드는 0xeb, 0x0f다. 이명령어가실행될때의 EIP는 0x401873(0x40186f + 0x04) 다. 따라서우리가점프해야하는곳인 0x4018a0까지의오프셋은 0x2d(0x4018a0 0x401873) 가된다. 0x0f를 0x2d로고쳐주면된다. 이렇게모든부분을고친다음프로그램을실행해서엉뚱한패스워드를집어넣은것을캡쳐한것이 < 화면 12> 에나와있다. 화면 12 프로그램실행화면 섹션추가하기 사실패딩영역을사용해서추가할수있는코드에는한계가있다. 때로는패딩영역이아예존재하지않을수도있다. 그럮경우에는별도의섹션을추가해서그곳에코드를기록하는방법을사용하면된다. 이경우에는이롞적으로는거의모든코드를추가할수있다는장점이있다. 반면에섹션을추가하는복잡한작업을해야한다는불편함이있다. < 리스트 4> 에파일에섹션을추가하는 AddSection 함수가나와있다. 일단한번코드를쭉살펴 보도록하자. 함수중갂에나오는 WriteSection 과 FillSection 은파일끝에새로운섹션을기록하는 역할을한다. 리스트 4 파일에섹션을추가하는함수 BOOL AddSection(LPCTSTR inpath // 입력파일경로
{, LPCTSTR secpath // 섹션파일경로, LPCTSTR secname // 섹션이름, DWORD secsize // 섹션크기, DWORD secattr) // 섹션속성 HANDLE in = INVALID_HANDLE_VALUE; HANDLE out = INVALID_HANDLE_VALUE; TCHAR outpath[max_path]; PVOID buf = NULL; DWORD ntoffset, secoffset; BOOL ret = FALSE; DWORD hsize, rhsize; // 기존헤더크기, 새헤더크기 int secno; // 추가되는섹션번호 DWORD falign, salign; // 파일정렧, 섹션정렧 DWORD sizeofimage; // 이미지크기 // 1. 파일을열고, 헤더내용을검증한다. in = CreateFile(inPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if(in == INVALID_HANDLE_VALUE) return FALSE; IMAGE_DOS_HEADER dos; IMAGE_NT_HEADERS nt; IMAGE_SECTION_HEADER sec[maximum_section_number]; DWORD readed; if(!readfile(in, &dos, sizeof(dos), &readed, NULL)) goto $cleanup; if(dos.e_magic!= IMAGE_DOS_SIGNATURE) goto $cleanup; ntoffset = dos.e_lfanew; SetFilePointer(in, ntoffset, NULL, FILE_BEGIN); ReadFile(in, &nt, sizeof(nt), &readed, NULL); if(nt.signature!= IMAGE_NT_SIGNATURE) goto $cleanup; secno = nt.fileheader.numberofsections;
if(secno >= MAXIMUM_SECTION_NUMBER) goto $cleanup; secoffset = dos.e_lfanew + sizeof(nt.signature) + sizeof(nt.fileheader) + nt.fileheader.sizeofoptionalheader; rhsize = secoffset + (nt.fileheader.numberofsections + 1) * sizeof(image_section_header); SetFilePointer(in, secoffset, NULL, FILE_BEGIN); ReadFile(in, sec, secno * sizeof(image_section_header), &readed, NULL); falign = nt.optionalheader.filealignment - 1; salign = nt.optionalheader.sectionalignment - 1; // 2. 새로운헤더크기를계산한다. rhsize = MakeAlign(rhSize, falign); hsize = nt.optionalheader.sizeofheaders; if(hsize > sec[0].pointertorawdata) hsize = sec[0].pointertorawdata; if(rhsize > hsize) { if(rhsize > sec[0].virtualaddress) goto $cleanup; } // 3. 출력파일을열고, 기존섹션내용을기록한다. StringCbCopy(outPath, sizeof(outpath), inpath); StringCbCat(outPath, sizeof(outpath), _T(".nx")); out = CreateFile(outPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(out == INVALID_HANDLE_VALUE)
goto $cleanup; DWORD bufsize = GetFileSize(in, NULL); buf = VirtualAlloc(NULL, bufsize, MEM_COMMIT MEM_RESERVE, PAGE_READWRITE); if(!buf) goto $cleanup; DWORD datasize = bufsize - hsize; SetFilePointer(in, hsize, NULL, FILE_BEGIN); ReadFile(in, buf, datasize, &readed, NULL); SetFilePointer(out, rhsize, NULL, FILE_BEGIN); DWORD written = 0; WriteFile(out, buf, datasize, &written, NULL); // 4. 기존파일에새로운섹션내용을추가한다. if(secpath) { secsize = WriteSection(out, secpath, secsize, falign); if(secsize == 0) goto $cleanup; } else if(secsize) { secsize = FillSection(out, secsize, falign); if(secsize == 0) goto $cleanup; } else goto $cleanup; // 5. 헤더필드를계산해서갱싞한다. if(rhsize > bufsize) { VirtualFree(buf, 0, MEM_RELEASE);
} bufsize = rhsize; buf = VirtualAlloc(NULL, bufsize, MEM_RESERVE MEM_COMMIT, PAGE_READWRITE); ZeroMemory(buf, rhsize); SetFilePointer(in, 0, NULL, FILE_BEGIN); ReadFile(in, buf, hsize, &readed, NULL); PIMAGE_NT_HEADERS pnt; PIMAGE_SECTION_HEADER psec; pnt = (PIMAGE_NT_HEADERS) GetPtr(buf, dos.e_lfanew); psec = (PIMAGE_SECTION_HEADER) GetPtr(buf, secoffset); if(psec[0].pointertorawdata < rhsize) { DWORD diff = rhsize - psec[0].pointertorawdata; for(int i=0; i<nt.fileheader.numberofsections; ++i) psec[i].pointertorawdata += diff; } PIMAGE_SECTION_HEADER src = psec + secno - 1; PIMAGE_SECTION_HEADER dst = psec + secno; #ifdef _UNICODE char msecname[20]; WideCharToMultiByte(CP_ACP, 0, secname, -1, msecname, 20, NULL, NULL); StringCbCopyA((LPSTR)pSec[secNo].Name, sizeof(psec[secno].name), msecname); #else StringCbCopy(pSec[secNo].Name, sizeof(psec[secno].name), secname); #endif dst->characteristics = secattr; dst->misc.virtualsize = secsize; dst->pointertorawdata = src->pointertorawdata + src->sizeofrawdata;
dst->sizeofrawdata = MakeAlign(secSize, falign); dst->virtualaddress = src->virtualaddress + src->misc.virtualsize, salign; dst->virtualaddress = MakeAlign(dst->VirtualAddress, salign); ++pnt->fileheader.numberofsections; pnt->optionalheader.sizeofheaders = rhsize; sizeofimage = pnt->optionalheader.sizeofimage + dst->misc.virtualsize; sizeofimage = MakeAlign(sizeOfImage, salign); pnt->optionalheader.sizeofimage = sizeofimage; // 6. 새로운파일의시작위치에헤더를기록한다. SetFilePointer(out, 0, NULL, FILE_BEGIN); WriteFile(out, buf, rhsize, &written, NULL); ret = TRUE; $cleanup: if(buf) VirtualFree(buf, 0, MEM_RELEASE); if(in!= INVALID_HANDLE_VALUE) CloseHandle(in); if(out!= INVALID_HANDLE_VALUE) CloseHandle(out); } return ret; 코드가복잡해서코드를설명하는것보다는기본적인아이디어를설명하는것이좋겠다. PE 포맷은섹션단위로관리되고각섹션은독립적으로존재하기때문에섹션을삭제하고, 추가하는것이가능하다. 섹션을추가한다고생각했을때우리가해야할작업은무엇일까? 바로새롭게추가될섹션헤더와섹션데이터를파일에추가해주는것이다. 섹션헤더를추가할때주의해야할점은두가지다. 헤더가꽉차서섹션헤더를추가할맊한
공갂이존재하지않는경우다. 이때에는젂체헤더크기를증가시켜야한다. 또한젂체헤더크기를증가시킬때, 첫번째섹션이메모리에맵핑되는위치보다헤더의크기가커서는안된다는점을명심해야한다. 맊약섹션헤더를늘렸다면이후나오는섹션의본문부분이모두뒤로증가된맊큼이동돼야하기때문에섹션헤더의 PointerToRawData를뒤로이동시켜주어야한다. 두번째로주의해야할점은헤더내에서마지막섹션다음에정보가있는경우다. 주로바인드정보가이위치에기록된다. 이경우에는바인드정보는정보를새로운위치로옮겨주거나해당파일을지원하지않는형태로처리해야한다. 섹션내용을추가할때주의해야할점은섹션내용의크기는 FileAlignment의배수가되어야한다는점이다. 또한새롭게추가된섹션은마지막섹션다음에오도록 VirtualAddress를조정해주어야한다. 종종디버그정보가섹션에포함되지않고파일끝에독립적으로존재하는경우가있다. 이것은바인드정보와마찬가지로옮겨주거나지원하지않는형태로처리해야한다. 끝으로섹션을추가하게되면필연적으로 SizeOfImage가변경된다. 이크기는추가된섹션의크기를더해서 SectionAlignment에정렧이되도록조정해준다. 앞서섹션헤더를추가하는과정에서헤더크기를늘린경우라면 SizeOfHeaders 필드도같이수정해주어야한다. FileHeader에있는 NumberOfSections 필드도추가한섹션의개수맊큼증가시켜주어야한다. addsection 프로그램의젂체코드는이달의디스크에들어있다. addsection 프로그램을사용해서 simple.exe를변형해보도록하자. simple.exe는시작할때 Password => 를출력한다. 새로운섹션을추가해서 Password => 를출력하기젂에 Please enter the password 란문구를출력하도록맊들것이다. 필자가생각한답을보기젂에직접한번풀어보도록하자. < 리스트 5> 에필자가생각해본코드가나와있다. 앞서패딩공갂에코드를추가할때느꼈겠지맊컴퓨터에게상대주소는굉장히편리한방식이지맊사람에게는굉장히힘든방식이다. 그래서이번코드에는상대주소를사용하지않도록맊들었다. eax에 0x405030을넣는것을볼수있다. 0x405030에는미리 Print 함수의주소인 0x4017e0를기록해두었다. 이렇게하면 DLL 함수를호출하는것처럼 eax에 0x405030을넣고 call을하면얶제든지 Print를호출할수있게된다. 0x405000에는 Please enter the password 를기록해두었다. 이렇게젂체를구성하면 < 화면 13> 과같이구성된다. 리스트 5 'Please enter the password' 를출력하는코드 68 00 50 40 00 push 0x405000 b8 30 50 40 00 mov eax, 0x405030 ff 10 call dword ptr ds:[eax] 83 c4 04 add esp, 4 68 e0 20 40 00 push 0x4020e0 b8 30 50 40 00 mov eax, 0x405030 ff 10 call dword ptr ds:[eax] 83 c4 04 add esp, 4
c3 retn 화면 13 섹션에추가할데이터 섹션을 simple.exe에추가한다음에 simple.exe에서 Password => 를출력하는부분의코드를 0x405040을호출하는코드로변경한다. 0x68, 0xe0, 0x20, 0x40, 0x00, 0xe8, 0xa2, 0xff, 0xff, 0xff로되어있는부분을 0xb8, 0x20, 0x50, 0x40, 0x00, 0xff, 0x10, 0x90, 0x90, 0x90으로고쳐주면된다. 이과정이 < 화면 14> 에나와있다. 화면 14 수정한프로그램을실행한화면 도전과제 마지막에작성한 AddSection 함수는바운드정보나디버그정보에대한대비가되어있지않다. 해당정보가존재하는경우에는적젃히옮겨서충돌이나지않도록맊들어보자. 좀더관심이있는독자라면조금거리가있지맊 detour 라이브러리의소스코드를분석해보는것도도움이될것같다. 내용이이해하기는힘들지맊관렦분야를공부해보고싶다는생각이든다면참고자료에있는것 들을홗용하자. 거의바이블로불리는자료들이기때문에일독한다면내공향상에큰도움이될 것이다. 그리고참고자료에있는인텔매뉴얼의경우주문을하면나라에관계없이챀으로된매
뉴얼을보내준다. x86 시스템프로그래밍이나명령어코드를분석하는데큰도움이되는자료이 므로아직마렦하지못했다면꼭주문해서읽어보도록하자. 물롞무료다. 참고자료 Assembly Language For Intel-Based Computers 4/e KIP R. IRVINE 저, Prentice Hall Reversing: Secrets of Reverse Engineering Eldad Eilam 저, WILEY Hacker Disassembling Uncovered Kris Kaspersky 저, A-List Publishing Intel 64 and IA-32 Architectures Software Developer's Manuals http://www.intel.com/products/processor/manuals/index.htm What Goes On Inside Windows 2000: Solving the Mysteries of the Loader http://msdn.microsoft.com/msdnmag/issues/02/03/loader/ Windows 시스템실행파일의구조와원리 이호동저, 한빛미디어 An In-Depth Look into the Win32 Portable Executable File Format http://msdn.microsoft.com/msdnmag/issues/02/02/pe/ An In-Depth Look into the Win32 Portable Executable File Format, Part 2 http://msdn.microsoft.com/msdnmag/issues/02/03/pe2/ Peering Inside the PE: A Tour of the Win32 Portable Executable File Format http://msdn2.microsoft.com/en-us/library/ms809762.aspx