기술문서 Anti Reversing Techniques 박지훈 sonicpj@nate.com
Preface 프로그램개발완료후다가올리버싱의위험 새로운트랜드에대한열망은가득차있으나아이디어고갈에허덕이며새로운프로그램이등장하지않는요즈음. 어느날개발자 A씨는몇년간의고된개발기간을거쳐 COREA' 라는프로그램을만들어상용화하는데성공했다. 이제품은현재의인터넷사업풍토에서분위기를완전히뒤엎어준획기적인솔루션으로이름이알려지기시작했다. 당연히프로그램으로인한매출은급등하였으며유료사용자들은나날이늘어갔다. 몇년간의힘든고생이빛을발하는순간이었으며 A씨의순이익도덩달아늘어만갔다. 그러던어느날 COREA' 라는프로그램의크랙버전이라는것이유포되기시작했다. 크랙버전을사용하면유료등록을하지않아도 COREA' 를얼마든지이용할수있다. 매출과직결되는위험한이크랙버전은인터넷여기저기퍼지기시작하면서 A씨를힘들게하였다. 정보를입수해본결과 'COREA' 의크랙버전은 C라는해커가개발했으며 COREA' 를리버스엔지니어링하여유료등록루틴을무력화시켜버렸다는것을알게된다. 당연히그때문에무료이용자라도누구든지사용할수있는상태가되며이크랙버전이퍼지면서매출은급감하고만다. 그리고 A씨와회사의목줄은점점조여만갔다. 리버싱에대항하는 A 씨의반격, 그후... A씨는유료등록루틴을좀더어렵게변경해다음버전을릴리즈했다. 힘들게수정한만큼이제기존크랙방식은사용되지않을거라고확신했다. 그러나너무안일한생각이었다. 새버전을발표하자마자하루만에또다시크랙버전이등장했다. A씨는크랙버전을입수해다시유심히살펴봤다. 그리고자신이코딩한등록루틴이이번에도무력화된것을발견한다. 어떻게이런일이가능한것일까? 어떻게해야크랙버전의유포를막을수있을까? A씨는고민한결과자신과친분이있는 B씨가몇몇해커와도교류가있다는것을알고그루트를통해해커를만나컨설팅을받는다. 해커가해놓은답은하나였다. 당신의프로그램은너무분석하기쉽습니다. 각종디버거등에완전히무방비상태여서아무리어렵게등록루틴을개발해봤자리버스엔지니어링을통해금방유린이됩니다. 라는소리를들었다. 즉내가만든코드를보호하기위해디버깅방지, 리버싱방지를위한기능이필수적이라는사실을깨닫게된다. 이후 A씨는 안티디버깅 (Anti-Debugging) 이라는기술에대해알게된다. 그리고관련기법을총동원하여솔루션에적용시키기에이르렀다. Micro Software 잡지 2008 년 8 월 실전강의실 3 - 안티리버싱과리버스엔지니어링
Content 1. 리버싱개념 2 2. 안티리버싱개념 2 3. 기본안티리버싱방법 2 3.1. 심볼정보제거 2 3.2. 패킹 3 4. 안티디버거기술 5 4.1. 안티디버거개념 5 4.2. IsDebuggerPresent API 5 4.3. NtGlobalFlag, HeapFlags, ForceFlags 9 4.4. Trap Flag 11 5. 참조 14-1 -
1. Reversing 리버싱 (reverse + engineering) 이란이미완성된소프트웨어를역으로분석하는과정이다. 개발자가소스코드를작성하고자신이사용하는컴파일러로컴파일시켜바이너리파일 ( 소프트웨어 ) 을만든다. 이런바이너리파일은사람이읽기힘들도록되어있지만리버싱을이용해사람이알아볼수있도록기존의소스를복원하는것이다.( 디컴파일 ) 아무리리버싱을한다고해도기존의소스를전부알아내기는힘들지만어셈블리코드를분석해원래소스의구조와원리정도는알아내게되는것이다. 예를들어해당소프트웨어에서종료를누르면어느부분에서어떤코드가어떻게실행되는지는알수있다. 위설명을간단히그림으로표현해보겠다. 이렇듯크래커들은리버싱으로개발자가만들어놓은소프트웨어를역분석해코드를알아내버려서인증하는부분을우회해버린다거나코드를바꿔서부당한이익을취한다거나할수있다. 그렇기때문에개발자들은크래커들이자기소프트웨어를리버싱하지못하도록방어하는기술이안티 (Anti) 리버싱기술이다. 2. Anti Reversing 앞에서설명했듯이안티리버싱이라는것은크래커들로부터리버싱을차단하려는것이다. 하지만완벽하게리버싱을차단하는것은불가능하다. 그렇기때문에안티리버싱기법을많이적용시켜역분석을하는크래커들을지치게해서포기하게하는것이다. 안티리버싱기술도신뢰성있고안전함에따라많은비용이들어간다. 안티리버싱기술이필요한경우는예를들어파일복제방지할때나저작권관리, 사용자들이돈을지불하고정품인증하는소프트웨어등이런것들에서는꼭필요한기술이다. 안티리버싱에도심볼제거, 패킹, 코드체크섬, 안티디버깅기술등여러가지가있는데간단한안티리버싱기술부터알아볼것이다. 3. Simple anti reversing techniques 3.1. Remove the symbol information 첫번째로소개할것은심볼정보를제거하는것이다. 리버싱을해본사람들은알겠지만문자열, dll이익스포트하는함수만으로도해당바이너리파일에서패스워드를검증하는부분이어디인지오류창을띄워주는부분이어디인지를쉽게알수있다. 그렇기때문에바이너리파일안의문자열정보를제거하는것이다. 혹은문자열들을의미없는문자열로바꾸는것이다. C기반언어들은일반적으로바이너리파일에심볼정보가포함되지않지만바이트코드기반의언어인경우에는내부주소대신이름을참조하기때문에리버싱하기가아주쉬워진다. 그렇기때문에바이트코드기반인경우는꼭심볼정보는제거, 변경해줘야할 - 2 -
것이다. 3.2. Packing 두번째로소개할것은패킹이다. 리버서들이코드분석을방지하기위해코드를패킹하는것이다. 이렇듯바이너리파일을패킹해놓으면원래소스를볼수없기때문에정적분석은불가능해진다. 실제소스를봐보자. #include <stdio.h> int main(void) { int a = 10; int b = 20; int result; result = a + b; } return 0; < 그림 1> 위소스를디버거로열어보면위와같이보이는데패킹을하지않고봤을때의코드이다. 다른부분의코드를보더라도암호화되지않고전부읽을수있는코드이다. 이제패킹되었을때의코드를봐보자. 패커의종류만해도엄청나게많고사용자가직접패커를만들수도있지만여기선간단한패킹인 UPX를사용하겠다. < 그림 2> 패킹이끝났고다시디버거로코드를열어보겠다. - 3 -
< 그림 3> 아까와코드가완전히다르다. Comment 부분에도아무것도없을뿐더러코드자체도우리가알던소스가전혀아니다. 이렇듯패킹이되어버리면정적분석으로는알수가없다. 그렇다고해서절대로못푸는것은아니다. 크래커들이파일을분석할시간을늘릴수있다는것이다. 뒤에서도말할것이지만안티리버싱이란기술이아무리뛰어나도많은시간과인내가있으면뚫리는것이다. 하지만얼마나많은끈기를요구하게하느냐가관건이다. 실력이안되는크래커들은안티리버싱기술이강력할수록늪에빠지게되고결국은리버싱을포기하게되는것이다. 위에서보여준패킹은언패킹하는방법이널리퍼져있고툴도만들어져있다. 위패킹된바이너리파일의정보를봐보자. < 그림 4> 간단하게어떤방식으로패킹이되어있는지를확인할수있다. 하지만 PE확인툴을우회하는기능을사용한다거나사람들이알지못하는패커을사용한다거나자기가직접패커를만들어서사용하게된다면리버서들은어떤패킹방법으로패킹이되어있는지의정보를얻지못하게될것이고코드를분석하는데많은시간을투자하게될것이다. - 4 -
< 그림 5> 위그림은잘알려지지않은패커로패킹했을때의결과이다. 어떠한정보도얻지못하고라이브디버깅을해봐야한다. 그리고잘알려져있으면서도언패킹하기어려운패커로는 Themida, Winlicense, VMProtect 같은패커들이있다. 이패커들을가지고패킹을할경우 UserMode, KernalMode 디버거로분석을하려고 Attach하면안티디버거기술때문에디버거가꺼지는현상, PE확인툴로도해당패커를확인할수없는것등을볼수있을것이다. 또한가지장점으로는패킹을잘이용하면바이너리파일용량도줄일수있다. 위에서설명한 UPX패킹전후의용량을비교해보겠다. 파일은 Starcraft.exe 파일로해보겠다. < 그림 6> 패킹전패킹후 < 그림 7> 패킹전후의바이너리파일정보와용량을나타낸것이다. 용량을보면알겠지만절반이상의용량이줄어든것을볼수있다. 어떤바이너리파일들은 4배까지도줄어든다. 이렇듯패킹이라는것은잘사용하면크래커들이리버싱을못하도록방해도할수있을뿐더러방대한양의바이너리파일을용량도줄일수있다는것이다. 4. Anti debugger techniques 4.1. Anti debugger 안티디버거라는것은안티리버싱기술에속한다. 대부분리버싱을하기위해서는디버거를사용하고디버거안에서브레이크포인트를설정하고트레이스해가면서분석을한다. 이렇듯안티디버거라는것은이러한디버거를탐지하는기술이다. 이런기술들을사용해디버거가탐지될경우해당디버거를꺼버릴수도있고바이너리파일을꺼버릴수도있다. 또한파일을패킹시켜놓을경우크래커들은라이브디버깅을해가며언패킹을해야하므로디버거를사용해야한다. 그렇기때문에패킹과함께안티디버깅기술을사용하면리버싱을방어하는데효과를더욱볼수있다. 4.2. IsDebuggerPresent API 안티디버거를기술을사용하기전에이런기술들이들어있는여러 API 함수들이있다. 얼마나많은안티리버싱기술과안티디버깅 API 함수가있는지봐보자. - 5 -
< 그림 8> 보시는바와같이많은안티리버싱기술과안티디버깅기술이있지만위의기술이전부는아니다. 이제우리가이번에알아볼안티디버깅 API함수로는 IsDebuggerPresent() 함수이다. 위에서보여준패킹과마찬가지로위함수도디버거를탐지하는함수들중에가장기본적인함수이다. 2가지를보여줄것인데함수를사용한코드와코드안에인라인어셈을한코드를가지고설명을할것이다. 먼저위함수를사용한간단한코드를짜보자. ( 참고, IsDebuggerPresent() 함수는디버깅시리턴값이 1이다.) - 6 -
정상실행 < 그림 9> 위코드를정상실행시옆그림과같이출력이된다. 정상실행을하였으므로 IsDebuggerPresent() 의리턴값은 0이돼서 Nomal Operating을출력하게된다. ( 위소스에서 #define _WIN32_WINNT 0x501이라고한부분이있는데어떤함수를사용할때해당프로그램이실행될 OS를지정해주는것이다. 위소스에서는 IsDebuggerPresent() 라는함수를사용하기위해 define시켜준것이다. #include<windows.h> 헤더위쪽에선언을해주어야한다.) _WIN32_WINNT 표 _WIN32_WINNT WINVER Windows Server 2008 0x0600 Windows Vista 0x0600 Windows Server 2003 SP1, Windows XP SP2 0x0502 Windows Server 2003, Windows XP 0x0501 Windows 2000 0x0500 < 그림 10> ) 위코드를디버깅상태에서실행을시켜보겠다. 그리고그때 IsDebuggerPresent() 에서리턴되는값을봐보겠다. 디버깅중 < 그림 11> - 7 -
디버깅상태로넘어오니 IsDebuggerPresent() 리턴값이 1 이되면서 Debuggering 이라는 문자열이출력되는것을볼수있다. 이제 IsDebuggerPresent() 함수가어떤일을하는지는알게되었고 Assembly 코드로어떻 게수행을하고있는지봐보겠다. mov eax,fs:[0x30] lea edx,[eax]; xor eax,eax mov al,byte ptr [edx+0x2] RETN < 그림 12> 위그림이 IsDebuggerPresent() 함수내부를봐본것이다. 설명을하기전에기본적으로 TEB 구조체와 PEB 구조체를알아야한다. TEB 구조체 : 운영체제가동작하기위해서필요한스레드정보를포함하는구조체 PEB 구조체 : 유저레벨에서프로세스에대한정보를저장하고있는구조체 먼저 TEB구조체를 Offset 0x30까지나열해보자. 0x000 NtTib : _NT_TIB 0x01c EnvironmentPointer : Ptr32 Void 0x020 ClientId : _CLIENT_ID 0x028 ActiveRpcHandle : Ptr32 Void 0x02c ThreadLocalStoragePointer : Ptr32 Void 0x030 ProcessEnvironmentBlock : Ptr32_PEB < 그림 13> Offset 0x30에 PEB구조체를가리키고있는것을볼수있다. PEB 구조체도 Offset 0xC 까지봐보자. 0x000 InheritedAddressSpace : UChar 0x001 ReadImageFileExecOptions : UChar 0x002 BeingDebugged : UChar 0x003 SpareBool : UChar 0x004 Mutant : Ptr32 Void 0x008 ImageBaseAddress : Ptr32 Void 0x00c Ldr : Ptr32_PEB_LDR_DATA < 그림 14> 여기까지정보를알고있으면 < 그림 12> 에서의코드를이해할수있다. mov eax,fs:[0x30] 참고로 FS:[0] 은 TEB 구조체처음을가리키고있다. 그러므로 FS:[0x30] 은 PEB 구조체를 가리키게된다. lea edx,[eax]; xor eax,eax 위두줄은크게볼건없다.( 그주소를레지스트리 edx 에넣는과정 ) - 8 -
mov al,byte ptr [edx+0x2] RETN PEB 를가리키는주소에서 0x2 만큼이동하면 BeingDebugged 변수를가리키게되고그안에들어있는값을리턴해주는것이다. 결론을내려보면 IsDebuggerPresent() 함수는 PEB 구조체안의 BeingDebugged 변수안의값을가지고현재파일이디버깅중인지아닌지를판단하는것이다. 디버깅중이면 BeingDebugged 변수에 1 값이들어갈것이고아니라면 0 값이들어가게되는것이다. 위정보를토대로코드를인라인어셈을사용하여코드를만들어실험해보겠다. < 그림 15> 아까와같이디버거가탐지되고있는것을알수있다. 4.3. NtGlobalFlag, Heap.HeapFlags, Heap.ForceFlags 이번에는 NtGlobalFlag, Heap.HeapFlags, Heap.ForceFlags 값을이용한안티디버깅기술이다. 위에서소개한 IsDebuggerresent() 와큰차이는없다. NtGlobalFlag값이정상일땐 0으로되어있고 WinDBG, OllyDebugger, ImmunityDebugger, IDA, Visual studio 등에서프로세스를디버깅하게되면 NtGlobalFlag 값이 0x70으로값이설정된다. 코드를짜서봐보자. 리턴값은 Ollydebugger로봐보겠다. ( NtGlobalFlag는 PEB구조체에서 0x68만큼떨어진곳에존재한다. ) - 9 -
< 그림 16> 0x004113B8 코드를실행후 EAX 에 0x70 이들어간것을볼수있다. 올리디버거로디버거 중이므로당연히 0x70 값이들어간것이다. Heap.HeapFlags, Heap.ForceFlags 도마찬가지로디버깅중이면 0x50000062, 0x40000060 이라는값을갖게될것이다. Heap.HeapFlags 위치 mov eax,fs:[0x30] mov eax,[eax+0x18] mov eax,[eax+0xc] Heap.ForceFlags 위치 mov eax,fs:[0x30] mov eax,[eax+0x18] mov eax,[eax+0x10] < 그림 17> 위두개도간단한코드로실험을해보겠다. 이번에는올리디버거에서의값만보여주겠다. Heap.HeapFlags Heap.ForceFlags < 그림 18> 디버깅중이므로 0x50000062, 0x40000060값을지니게된것을볼수있다. 이런안티디버깅기술이엄청나게많이있지만여기서는간단하게어떤식으로안티디버깅기술이적용되는지를보여주기위해서몇몇예제를보여주는것이다. 개발자들은이제탐지를하기만하면안되고탐지가된후어떤대처를할것인가에대한코드도만들어야할것이다. 이런안티디버깅기술에관련해서는마지막으로하나만더보여줄것이다. - 10 -
4.4. Trap Flag 마지막으로소개할안티디버깅기술은트랩플래그 (TF) 값을설정해예외발생여부로디버거탐지를하는것이다. 이안티디버깅장점은유저모드디버거와커널모드디버거모두탐지를할수있기때문이다. 이번에도인라인어셈블리로코드를짜볼것이다. 그전에몇가지알아두면이해하기편하다. try - except : SEH( 예외 ) 를처리하기위한키워드 try { // Guarded code } except (EXCEPTION_EXECUTE_HANDLER) // Exception filter { // Exception handling code } try 부분에예외를검사할코드를넣습니다. 예외가발생하면 except 부분에서지정한코드가실행되게됩니다. EXCEPTION_CONTINUE_EXECUTION (-1) 예외를무시하고, 예외가발생한지점에서부터프로그램을계속실행한다. 예를들어 10 / i 에서 i 가 0 이라서예외가발생한경우, 예외처리필터가이값이라면, 다시 10 / i 부터실행한다는말이다. EXCEPTION_CONTINUE_SEARCH (0) except 블록안의코드를실행하지않고, 상위에있는예외처리핸들러에게예외처리를넘긴다. EXCEPTION_EXECUTE_HANDLER (1) except 블록안의코드를실행한후, except 블록아래의코드를실행한다. < 그림 19> < 그림 19> 와같은예외처리루틴을사용할것이다. - 11 -
< 그림 20> pushfd 명령어는모든플래그값들을스택에넣으라는명령어이고, popfd 명령어는스택에있는값들을플래그에넣으라는명령어이다. 빨간색네모친곳을설명하겠다. 처음에 check() 함수에들어오고 pushfd 명령어를수행후의플래그와스택을봐보겠다. Flag Stack < 그림 21> 플래그값들이전부스택에들어간다고했는데스택을봐보니 0x206 이라는숫자만들어갔다. 그이유는 C, P, A, Z, S, T, D, O 라고된부분이플래그인데빨간네모친부분 EFL 부분이위플래그들을전부관리하는값이라고할수있다. 즉, 플래그들중값하나만바뀌어도 EFL 도다른값으로변경이된다는것이다. 그리고위플래그를보면 T 라고된부분이 TF 이고현재 0 으로되어있는것을볼수있을것이다. 다음줄을봐보자. esp 에들어있는값과 0x100 을 OR 연산을하고있다. 연산을해보자. 0x206 0x100 0x306 < 그림 22> - 12 -
2 진수 9 번째자리를 1 로셋팅해주고있다. 이제 0x306 이라는값을 EFL 에들어가게된다면 TF 값은 1 로셋팅이될것이다. 마지막줄인 popfd 라는명령어수행후를봐보자. < 그림 23> EFL 값은 0x306 으로되어있고 TF 는 1 로셋팅이되어있는것을볼수있다. 즉, TF 가 1 로셋팅이되었다는것은예외가발생했다는것이다. 이제바로예외처리루틴으로넘어가는가싶더니아래 nop 이라는게나왔다. nop 은크게신경쓸건없지만실험해본결과 TF 가 1 로셋팅되고난후코드한줄을더실행후예외처리루틴으로가게된다. 그렇기때문에아무의미없는 nop 을한줄집어넣은것이다. 만약그한줄에프로세스를종료시키는코드를넣었다면예외처리루틴을가지도못하고프로세스가종료될것이다. nop 까지실행하고난후의코드를보자. < 그림 24> 그림의아래를보면예외가발생했고예외처리루틴으로가려면 Shift+F7/F8/F9 를사용하라고하였다. 아마예외처리루틴으로가서트레이스해보면빨간색네모친부분으로가게될것이고결국 check() 는리턴값 true 로 Nomal Operating 이라는문자열을출력하게될것이다. 하지만디버거에서이런트레이스과정을거치지않고바로실행시켰다고해보자. 그러면디버거는예외처리루틴을먹어버리고아래 JMP 문을수행해서예외처리루틴을건너뛰어버리게된다. 직접봐보자. 현재예외처리루틴으로가려면 Shift+F7/F8/F9 를사용하라고했는데그냥 F8( 코드한줄실행, Visual Studio 에서 F10 과동일 ) 을눌러보자. F8 한번눌렀을때예외처리먹어버림 계속트레이스후 예외처리루틴건너뛰어버림 < 그림 25> 설명한대로이다. 여기서설명하고싶은것은결국이런종류의안티리버싱기술을찾아내기위해서는차근차근트레이스를해보는수밖에없다. 이부분을찾아내질못하고그냥지나치게된다면디버거탐지가될것이고결국역분석에실패를하게될것이다. - 13 -
5. References [1] http://www.openrce.org/reference_library/anti_reversing [2] 리버스엔지니어링역분석구조와원리 - 박병익 / 이강석공저 [3] 리버스엔지니어링비밀을파헤치다 - 엘다드에일람 - 14 -