리버스 엔지니어링 바이블 : 코드 재창조의 미학

126

Post on 22-Jul-2016

1.050 views

Category:

Documents


266 download

DESCRIPTION

강병탁 지음 | 보안 & 해킹 시리즈 _ 002 | ISBN: 9788998139018 | 38,000원 | 2012년 09월 20일 발행 | 616쪽

TRANSCRIPT

Page 1: 리버스 엔지니어링 바이블 : 코드 재창조의 미학
Page 2: 리버스 엔지니어링 바이블 : 코드 재창조의 미학
Page 3: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

리버스 엔지니어링

바이블

Page 4: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

iv

차 례

[ 01부 ] 리버스 엔지니어링 기본

01 | 리버스 엔지니어링만을 위한 어셈블리 1

어셈블리의 기본 구조 ......................................................................................3

어셈블리의 명령 포맷 .......................................................................................4

레지스터, 복잡한 설명은 그만 .........................................................................6

EAX ....................................................................................................................7

EDX ....................................................................................................................7

ECX ....................................................................................................................7

EBX ....................................................................................................................8

ESI, EDI .............................................................................................................8

외울 필요가 없는 어셈블리 명령어 .................................................................15

PUSH, POP .............................................................................................16

MOV.........................................................................................................16

LEA ...........................................................................................................16

ADD .........................................................................................................17

SUB ...........................................................................................................17

INT ...........................................................................................................17

CALL ........................................................................................................18

INC, DEC ................................................................................................18

AND, OR, XOR .......................................................................................18

NOP ..........................................................................................................18

Page 5: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

v

CMP, JMP ................................................................................................18

리버스 엔지니어링에 필요한 스택 .................................................................19

함수의 호출 .......................................................................................................20

리턴 주소 ...........................................................................................................21

02 | C 문법과 디스어셈블링 23

함수의 기본 구조 ...............................................................................................24

함수의 호출 규약 ...............................................................................................26

if 문 .....................................................................................................................31

반복문 ................................................................................................................34

구조체와 API Call ............................................................................................36

결론 ....................................................................................................................44

03 | C++ 클래스와 리버스 엔지니어링 45

C++ 분석의 난해함 ...........................................................................................46

클래스 뼈대 .......................................................................................................47

클래스의 수명과 전역변수 ...............................................................................53

객체의 동적 할당과 해제 ..................................................................................55

생성자와 소멸자 ................................................................................................57

캡슐화 분석 .......................................................................................................62

다형성 구조 파악 ...............................................................................................63

Page 6: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

vi

04 | DLL 분석 65

DLL의 번지 계산법 ...........................................................................................66

재배치를 고려한 방법 .......................................................................................69

1) push 문 .................................................................................................70

2) 점프문 ..................................................................................................71

번지 고정 ...........................................................................................................72

익스포트 함수 ....................................................................................................72

DllAttach/DllDetach 찾기 ...............................................................................75

패킹된 DLL의 DllMain() 찾기 ........................................................................79

Disable�readLibraryCalls로 찾기 ................................................................80

[ 02부] 리버스 엔지니어링 중급 83

05 | PE 헤더(PE Header) 85

PE에 대한 상식적인 개념 .................................................................................88

빌드 과정 ...........................................................................................................89

PE 파일 구조......................................................................................................90

IMAGE_DOS_HEADER ......................................................................91

IMAGE_NT_HEADER .........................................................................94

IMAGE_FILE_HEADER ......................................................................95

IMAGE_OPTIONAL_HEADER .........................................................98

IMAGE_SECTION_HEADER .............................................................107

PE와 API 호출의 원리 ......................................................................................115

Page 7: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

vii

IAT에서 API를 계산하는 방법 ........................................................................117

익스포트 테이블(export table) ........................................................................125

마무리 ................................................................................................................125

06 | 흔히 사용하는 패턴 127

조건문의 기본 ....................................................................................................128

심화된 조건문 ....................................................................................................129

반복문 ................................................................................................................134

break가 들어가는 경우 .....................................................................................138

continue가 들어갈 경우 ...................................................................................139

return이 생기는 경우 .......................................................................................141

문자열 컨트롤 ....................................................................................................142

1. strcpy ....................................................................................................143

2. strcat .....................................................................................................147

3. strlwr ....................................................................................................150

정리 ....................................................................................................................152

07 | MFC 리버싱 153

리버스 엔지니어링에 필요한 MFC 구조 ........................................................154

MFC로 개발됐는지 여부 확인 .........................................................................156

MFC 라이브러리 등록 .....................................................................................157

MFC 초기화 루틴 찾기 ....................................................................................158

버튼 핸들러 찾아내기 .......................................................................................161

헤더 파일 활용 ...................................................................................................167

그 밖의 MFC 관련 리버싱 접근 방법 ..............................................................171

Page 8: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

viii

[ 03부 ] 연산 루틴 리버싱 173

08 | 시리얼 추출 방법 175

F-Secure Reverse Engineering 대회 ...............................................................177

Level 1 문제 풀이 ..............................................................................................179

1) 사전 정보 획득 첫 번째 – 파일 스캐너로 스캔 .................................180

2) 사전 정보 획득 두 번째 – API 확인 ..................................................181

3) 사전 정보 획득 세 번째 – 파일의 이미지 베이스 확인 ....................182

Level 2 문제 풀이 ..............................................................................................184

문자열 컨트롤 ....................................................................................................189

안티 디버깅 기법 ...............................................................................................193

WinMain보다 먼저 가동되는 코드 .................................................................196

09 | 코드 속이기 199

Level 3 문제 풀이 ..............................................................................................200

데이터 속에 숨은 코드 ......................................................................................203

리버싱으로 방정식 세우기 ...............................................................................207

바이너리 파싱 ....................................................................................................210

재사용이 어려운 코드 ......................................................................................215

디스어셈블러 속이기 ........................................................................................216

정리하며 ............................................................................................................219

Page 9: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

ix

[ 04부] 안티 리버스 엔지니어링 221

10 | 교과서적인 안티 디버깅 223

프로그램 개발 완료 후 다가올 리버싱의 위험 ...............................................224

리버싱에 대항하는 A씨의 반격, 그 후… ........................................................224

안티 디버깅이란? ..............................................................................................225

고전적인 안티 디버깅 .......................................................................................225

API 로 제공되는 안티 디버깅 기능 .................................................................226

다중 기능을 보유한 맥가이버칼, NtQueryInformationProcess ..................230

Debug Object Handle ......................................................................................232

또 핸들을 찾아라, NtQueryObject .................................................................232

NoDebugInherit ...............................................................................................233

디버거는 어디에, NtSetInformation�read .................................................235

int3을 이용한 디버거 감지 ...............................................................................237

SetUnhandledExceptionFilter ........................................................................239

0xCC 자체를 탐지하자 .....................................................................................241

소프트아이스 감지 ............................................................................................242

PEB를 이용한 방법 ...........................................................................................243

정리 ....................................................................................................................249

11 | 한 단계 높은 안티 디버깅 251

생각보다 활용도가 높은 프로세스 체크 .........................................................252

버전 체크 ...........................................................................................................253

부모 프로세스 체크 ...........................................................................................256

Page 10: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

x

SeDebugPrivilege 권한 체크 ...........................................................................259

WinDBG 검출 ...................................................................................................260

키보드 입력 봉쇄 ...............................................................................................261

CloseHandle()을 이용한 안티 리버싱 ............................................................262

OutputDebugString이 주는 두 가지 마술 ......................................................263

시간차 공격! 타이머를 이용한 방법 ................................................................265

PREFIX REP을 통한 예외 처리 .......................................................................267

API Hook을 이용한 디버깅 감지 ....................................................................268

리모트 디버깅 감지 ...........................................................................................270

생각해 볼 과제 ...................................................................................................271

12 | 패커가 사용하는 기술 273

패커? 대수롭지 않은 개념 ................................................................................274

패킹된 코드의 암호화/복호화 .........................................................................276

IAT의 변화 .........................................................................................................278

매뉴얼 언패킹(MUP) .......................................................................................281

파일 스캐너 .......................................................................................................283

디버거의 구조 ....................................................................................................284

Self Debugging .................................................................................................286

스택 세그먼트 레지스터 ...................................................................................288

int 0x2d ..............................................................................................................290

덤프 방지 ...........................................................................................................290

디버그 레지스터 ................................................................................................292

Page 11: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xi

13 우회 방법 297

API Hooking을 통한 우회 ...............................................................................299

OllyDBG 옵션을 이용한 우회 .........................................................................305

플래그 수정으로 우회 .......................................................................................306

[ 05부] OllyDBG 플러그인 309

14 | OllyDBG 플러그인 SDK 311

최고의 매력 OllyDBG 플러그인 .....................................................................312

플러그인 가동 원리 ...........................................................................................313

플러그인 SDK 사용 규약 .................................................................................316

초기화 코드 .......................................................................................................318

PreInit - ODBG_Plugindata() ........................................................................318

초기화 루틴 - ODBG_Plugininit() .................................................................319

메뉴 작성 - ODBG_Pluginmenu() .................................................................322

메뉴 핸들러 작성 - ODBG_Pluginaction() ...................................................326

종료 처리 ...........................................................................................................329

PDK 활용 ...........................................................................................................330

t_dump - 덤프 관련 구조체 .............................................................................330

t_memory – 메모리 관련 구조체 ....................................................................334

t_disasm – 디스어셈블 구조체 ........................................................................337

Readmemory ....................................................................................................337

구조체 종합 예제 - Extra Copy .......................................................................340

Page 12: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xii

15 | 리버싱 보조 플러그인 345

IsDebuggerPresent() 무력화 ...........................................................................346

디버거 떼어내기 ................................................................................................348

자동 어태치 .......................................................................................................350

단축키 - ODBG_Pluginshortcut() .................................................................351

OllyAdvance 분석 .............................................................................................352

Setbreakpoint()와 TLS CallBack 감지 ...........................................................355

커맨드라인 실행 바 ...........................................................................................358

디버깅 흔적 청소 ...............................................................................................362

게임 해킹 플러그인 – Game Invader .............................................................363

Game Invader의 구현 원리 ..............................................................................366

메모리 속성 옵션 ...............................................................................................368

암복호화 코드 분석 – Find Crypt ...................................................................369

FindCrypt의 구현 원리 ....................................................................................371

남은 과제 ...........................................................................................................373

16 | 메모리 스캔 플러그인 개발 375

아직 세상에 없는 플러그인을 만들어 보자 ....................................................376

분석 시간을 줄일 수 있다면? ...........................................................................377

플러그인 코어 루틴 컨셉 ..................................................................................377

UI 디자인 ...........................................................................................................379

버튼 핸들러 제작 ...............................................................................................380

후킹 탐지 기능 ...................................................................................................382

파일과 메모리의 기준 번지를 정하는 방법 ....................................................384

IAT가 코드 섹션에 포함된 경우 ......................................................................384

Page 13: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xiii

패킹 체크 ...........................................................................................................385

후킹 탐지 스캔 결과 ..........................................................................................386

암복호화 루틴 탐지 ...........................................................................................387

화이트 리스트 처리 ...........................................................................................388

OllyMemScan 평가 ..........................................................................................389

[ 06부 ] 보안 모듈 우회 393

17 | 바이러스 감지 회피 395

백신은 과연 바이러스만 감지하는가 ..............................................................396

기본적인 감지 원리 ...........................................................................................397

고전적인 우회 방법 ...........................................................................................399

쓰레기 코드를 통한 우회 방법 .........................................................................400

실행조차 되지 않는 코드 ..................................................................................403

헤더 변조 ...........................................................................................................405

문자열 패턴 우회 ...............................................................................................406

바이너리 패킹의 시대 .......................................................................................409

대체 가능한 어셈블리어 ...................................................................................410

패커의 특성을 고려한 회피 ..............................................................................411

섹션 이름 변경 ..................................................................................................413

백신사의 대응 ...................................................................................................415

Page 14: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xiv

18 | 백신을 공격하는 악성코드 417

백신을 공격하는 바이러스는 어느 정도 존재하는가 ....................................418

특정 백신 언급 증가 ..........................................................................................419

백신 프로세스 종료 ...........................................................................................420

트레이 속임 .......................................................................................................422

공식적으로 언급된 취약점 ...............................................................................423

업데이트 우회 ....................................................................................................424

보안업체 비방 ....................................................................................................427

드라이버 공격 ....................................................................................................428

몸체가 없는 공격 코드 ......................................................................................430

Con�cker ...........................................................................................................432

SDT 취약점 .......................................................................................................433

벤치마킹의 중요성 ............................................................................................434

19 | 실시간 감시 기능 취약점 435

바이러스 감지 후 필요한 행위 .........................................................................436

파일 시스템 필터 드라이버 ..............................................................................437

실시간 감시 기능의 구현 원리 .........................................................................439

콜백 함수 무력화 ...............................................................................................441

콜백 무력화 대응 방법 ......................................................................................443

노티파이 카운트 취약점 ...................................................................................444

화이트리스트 강제 주입 ...................................................................................445

파일 접근 통제 ...................................................................................................446

ZIP 취약점 .........................................................................................................447

가상 PC 감지 .....................................................................................................448

그외 안티바이러스 우회를 위한 다양한 방법 ................................................449

Page 15: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xv

20 | 키보드 보안 솔루션 우회 451

키보드 취약점이 되는 구간 ..............................................................................452

키보드 보안의 원리 ...........................................................................................454

폴링 취약점 .......................................................................................................455

IE의 추가 기능 관리 ..........................................................................................457

가장 피해 사례가 많은 ring3 영역 ..................................................................458

핵심 모듈 공격 ...................................................................................................460

언로드 DLL .......................................................................................................461

ring3에서 키보드 보안을 우회하는 이유 ........................................................463

키보드 보안의 문제점 .......................................................................................464

가상 키보드 취약점 ...........................................................................................464

인터넷 뱅킹 취약점 – 메모리 해킹 ..................................................................466

키보드 보안이 어려운 이유 ..............................................................................468

클라이언트에서 완전한 보안은? .....................................................................469

[ 07부] 한 차원 높은 바이너리 창조 473

21 | 코드 후킹 475

코드 후킹과 공동경비구역 ...............................................................................476

번지 계산의 고통 ...............................................................................................478

DLL 인젝션 코드 ...............................................................................................479

CreateRemote�read .......................................................................................480

DLL 인젝션 테스트 ...........................................................................................484

Page 16: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xvi

코드 후킹 기본 디자인 ......................................................................................485

여섯 바이트를 변조하는 이유 ..........................................................................487

DLL 번지의 강제 고정과 naked 함수 .............................................................490

패킷 버퍼 로깅 ...................................................................................................492

하드코딩에서 탈피하자 ....................................................................................494

후킹 코드의 확장 ...............................................................................................496

MakeJMP - 후킹 함수의 간결화......................................................................501

마무리 ................................................................................................................502

22 | 코드 패칭 503

동적 접근 방법과 정적 접근 방법 ....................................................................508

프로세스 검출 루틴 분석 ..................................................................................513

점프문 공격 .......................................................................................................515

파일에서 패칭할 때와 메모리에서 패칭할 때 ................................................516

상수값 패칭 .......................................................................................................519

스택 해킹 ...........................................................................................................521

거짓을 리턴 .......................................................................................................525

스레드 공격 .......................................................................................................526

마무리 ................................................................................................................531

23 | 난독화와 더미 코드 533

디스어셈블러 개론 ............................................................................................534

안티 디스어셈블링 ............................................................................................536

가비지 코드 속에 담긴 진짜 코드 ....................................................................539

ASPack이 사용하는 안티 디스어셈블링 .........................................................540

Page 17: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xvii

�emida에서 사용하는 안티 디스어셈블링 ..................................................543

ASProtect의 코드 카피 .....................................................................................546

�emida의 코드 리다이렉트 ...........................................................................548

상수 암호화 .......................................................................................................554

API 호출 감추기................................................................................................556

난독화 코드 찾아내어 우회하기 ......................................................................561

마무리 ................................................................................................................563

찾아보기..............................................................................567

Page 18: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xviii

감수자의 글

정보보안의 태동기에는 네트워크 보안, 시스템 보안 두 영역에서 방화벽과 같은 네트워크 단

에서의 필터링 기술과, OS 단에서의 아주 기본적인 요소기술이 발전했었다. 그 뒤 나온 보안

기술들을 보더라도 네트워크 기반 침입탐지시스템(N-IDS), 침입방지시스템(IPS), 웹애플리

케이션방화벽(WAF)과 같이 주로 이미 발생해서 인스턴스화된 공격에 대해 탐지와 예방을

하는 것들이 주류를 이뤘다. 예전부터 개념 역시 존재했고 소수 기업 및 인원들에 의해 이뤄

지고는 있었지만, 이미 개발된 프로그램 그 자체에 대한 취약점을 발견해 내기 위해 시큐어

코딩이나 개발보안에 사람들이 관심을 갖고 노력을 하기 시작한 것은 사실 10년도 채 되지

않았다고 볼 수 있다.

보안산업 역시 고도화 되어가는 과정에서 나타나는 현상 중 하나로, 일단 기본적인 기술

들이 먼저 발전한 뒤에야, 특화된 영역에 대한 요소기술로 깊이 연구가 이뤄지게 된다. 전통

적인 네트워크 보안 영역에 쏠렸고(Firewall, N-IDS), 그 후 OS 영역(H-IDS, SecureOS)에

주 초점이 맞춰져 있었다가, DB/Application 영역(DB보안, 웹보안 및 시큐어코딩 등의 개발

보안)으로 관심사가 옮겨감에 따라 리버스엔지니어링 기술의 중요성은 두말할 나위 없이 보

안 분야 종사가 반드시 갖춰야 할 스킬로 자리 잡았다.

다양한 보안직군 가운데 모의해킹, 취약점분석을 주로 하거나 또는 악성코드 분석 또는

안티리버스 엔지니어링(Anti-Reverse Engineering)을 통해 보안을 강화하고자 한다면 리

버스 엔지니어링 관련 지식을 꼭 쌓을 것을 권하고 싶다. 리버스 엔지니어링 지식과 더불어

Solaris, HP-UX, AIX, Linux, FreeBSD와 같은 다양한 OS를 익혀보고 각 환경에서의 프로

그래밍 스킬을 함께 익히게 된다면 보안 분야에서 “자신만의 고유한(unique) 능력을 갖춘 대

체 불가능한(irreplaceable) 인재”로 성장하리라 확신한다.

이 책은 리버스 엔지니어링에 대해 충실히 다룰 뿐 아니라, 익힌 리버스 엔지니어링 기법을

실제로 어떤 곳에 활용할 수 있는지에 대해서도 상세히 다룬다. 그간 저자가 오랜 기간 동안

Page 19: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xix

시행착오를 겪어오면서 익힌 소중한 지식을, 그다음에 배우는 사람들은 실수와 시행착오를

최소한으로 하고 배울 수 있게 여기저기에 배려하고 있으며, 복잡하고 어렵기만 한 내용을

편안하게 전달하는 저자 특유의 노하우가 담겨 있다.

저자는 이미 정보보안 분야 종사자 중에서도 실력이 완성에 이르렀다고 볼 만큼 시스템

보안 및 개발 보안에 능하고, 국내에서 여러 유수의 세미나 및 대학에서의 강의를 한 경험

이 있는 자기만의 철학과 유머를 갖춘 구루다. 독자들이 이 책을 통해서 많은 지식을 전달

받을 수 있을 것이라 기대한다.

- 김휘강

현 고려대학교 정보보호대학원 조교수

전 엔씨소프트 정보보안실장/TD (Technical Director)

전 A3 시큐리티컨설팅 창업자

Page 20: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xx

추천의 글 I

리버스 엔지니어링의 역사는 사실 깊다. 아마도 엔지니어링이 시작되었던 인류 역사 초기부

터, 리버스 엔지니어링은 존재했을 것이다. 리버스 엔지니어링은 누군가가 만들어 놓은 것을

분해해서 분석하고 재조합하는 과정을 말한다. 그 대상이 인간이 만든 기계 장치가 될 수도

있을 것이고, 자연에 존재하는 생명체나 유전자, 화학 물질 등이 될 수도 있겠다.

소프트웨어의 리버스 엔지니어링의 역사는 소프트웨어의 역사만큼이나 길 것이다. 기계

어로 코딩을 해야 하는 시기에는 엔지니어링에 사용하는 언어와 리버스 엔지니어링으로 분

석하는 언어도 같았다. 하지만 컴퓨터 언어 체계가 발전하고 다분화하면서 엔지니어링 과정

에 비해 리버스 엔지니어링 과정은 현저히 복잡한 과정이 되었다. 단 한줄의 코드도 컴파일

러의 종류, 작동 방식과 최적화 알고리즘 등에 의해 다른 형상을 가진 비트(bit)를 생성해 낸

다. 이러한 복잡성의 증가로 인해 리버스 엔지니어링은 완벽함을 실현할 수 있는 분야가 아

니다. 의도되거나 의도되지 않은 수많은 가지치기된 기술의 출현으로 바이너리들은 온갖 형

상을 취할 수 있기 때문이다.

소프트웨어 리버스 엔지니어링은 보안 분야나 악성 코드 분석에만 한정된 것이 아니다.

엔지니어링을 할 때도 리버스 엔지니어링은 필수적인 과정의 일부다. 특히 요즘처럼 개발을

위한 요소가 모듈화돼 있는 경우 소스코드를 얻기 어려운 모듈의 작동 방식에 대한 이해도

를 높이기 위해서는 리버스 엔지니어링은 필수다. 또한 자신이 직접 작성한 코드라도 작동

방식의 문제를 해결하거나 최적화 등을 이루기 위해서는 리버스 엔지니어링은 필수적인 과

정이다.

본질적으로 리버스 엔지니어링을 가장 빠르게 습득하는 방법은 엔지니어링을 먼저 섭렵

하는 것이다. 엔지니어링에 관한 각 분야별 정보는 넘치도록 있으니 일단 그 부분을 섭렵하

고 들어 가는 것이 좋을 것이다. 하지만 이미 그러한 과정을 거치고 지나간 선생들의 가르침

을 들어 보는 것도 좋을 것이다. 이 책의 의미가 바로 거기에 있다. 책 한권으로 리버스 엔지

니어링을 모두 이해하겠다는 것은 어불성설이다. 하지만 이 책을 통해 현업에서 실제로 쓰이

Page 21: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxi

는 기술과 전략, 트릭 등에 대한 지식을 습득할 수 있을 것이라고 본다. 책을 읽다 보면 저자

가 실무에서 풍부한 경험을 쌓아 왔다는 것을 책을 통해 쉽게 느낄 수 있었다.

책 자체는 심심풀이용으로 읽을 수 있을 만큼 재미 있게 쓰여져 있다. 예전에 어렸을 적에

흔히 보던 퍼즐 문제집을 읽는 것처럼 흥미진진하다. 저자의 의도대로 책을 읽으면서 예제를

따라해 본다면 어느덧 리버스 엔지니어링의 세계에 빠져 있는 자신을 발견할지도 모른다. 흥

미로운 전개 방식은 첫 장을 읽고 어딘가에 처박아 놓고 몇 달, 몇 년이 지나도 다시 펴 보지

않는 책과의 차별 요소라고 할 것이다. 그러한 재미를 담는 여유 또한 지은이의 오랜 연륜에

서 나오는 것이라 보고 싶다.

이제 독자 여러분도 맛있는 리버스 엔지니어링의 세계에 푹 빠져 볼 것을 권한다. 물론 맛

있게 요리된 지식을 소화해서 자신의 것으로 만드는 것은 전적으로 독자의 몫일 것이다.

이렇게 재미와 내용의 균형이 잡힌 책인 한국에서도 나왔다는 사실을 기뻐하며, 이 서평

을 마칠까 한다.

- Matt 오정욱

현) Microsoft Malware Protection Center(미국 시애틀) 근무

블랙햇, 데프콘, 캔섹 웨스트(CanSecWest), EuSecWest,

ShmooCon, ToorCon, BayThreat, CARO, XCON,

CodeGate 스피커 활동, 다른 그림(http://darungrim.org) 개발자

전) Websense, eEye Digital Security 근무, BugTruck 운영진

Page 22: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxii

추천의 글 II

우리는 지금 총성 없는 전쟁, 바로 사이버 전쟁 시대에 살고 있다. 국가에서도 사이버 사령부

를 창설하고 보안 인재를 육성하기 위해 다양한 시도를 하고 있으며 언론이나 사람들의 관

심도 날로 높아지고 있다. 한편 보안 전문가라 하면 보안이라는 용어가 지닌 지나치게 광범

위한 의미 탓에 과연 어떤 지식을 겸비해야 보안 전문가로 칭할 수 있는지가 애매한 부분이

있고, 때문에 보안 전문가를 만나보면 문제 풀이나 이론적인 부분에만 치중해 있어 실무적

인 감각은 떨어지는 경우가 종종 있다.

이 책은 국내 최고 보안 전문가이면서 넥슨 미국지사 보안팀을 책임지고 있는 강병탁 님의

실전 노하우가 녹아있는 책이다. 저자가 진행한 세미나와 블로그를 통해 그의 노하우가 공개

된 적은 있지만 접하기 어려운 사람들이 많았기에 이번 책이 특히 기대되는 이유이기도 하

다. 이 책은 수많은 보안 기술 분야 중에서 ‘리버싱’을 중점적으로 설명하고 있다. 리버싱은 보

안 전문가라면 반드시 마스터해야 하는 기술로 소위 ‘분석’ 작업의 기초 기술이기도 하다. 하

지만 기계가 이해할 수 있는 있는 언어로 만들어진 코드를 인간이 이해할 수 있는 논리도 변

환하기는 쉽지 않다. 그뿐만 아니라 패킹과 같은 안티-리버싱 기법의 범용화로 더 이상 예제

수준의 리버싱 기법만 배워서는 실무에서 어떤 정보도 얻지 못하고 좌절하고 말 것이다. 이

책은 저자가 실무에서 경험하고 해결했던 수많은 사례를 통해 리버싱의 기초부터 안티-리버

싱 기법에 이르는 전방위적인 리버싱 기법에 대해 설명한다.

모든 보안 분야가 그렇듯이 사용하는 사람에 따라 칼이 될 수도 있고 방패가 될 수도 있

다. 이 책의 내용은 기업의 자산을 외부로부터 보호하기 위해서지만 ‘6장 보안 모듈 우회’와

같은 기법은 해킹 도구를 만드는 사람들에게는 악용될 소지가 있다. 하지만 그럼에도 불구

하고 이 책에서 상세하게 보안 모듈 우회 기법을 설명함으로써 오히려 음지의 기법을 양지로

드러내면 해당 분야가 더욱 발전할 수 있는 기회가 될 것이다.

끝으로 리버싱이 기술적 난이도로 인해 설명하기 쉽지 않은 분야지만 저자만의 위트 있

는 설명과 다양한 도식, 예제 코드 등을 통해 독자 여러분도 소설책 읽듯이 리버싱을 이해

Page 23: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxiii

할 수 있을 것이다. 오랜만에 국내 저자가 집필한 완성도 높은 보안 서적을 만나게 되어 정

말 기쁘다.

- 서우석

현) 파인드스티브 대표이사

전 디버그랩 운영진 / Microsoft C# MVP

전 국가기관 보안 연구원, 안철수 연구소 엔진팀 근무

Page 24: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxiv

추천의 글 III

인간은 살아가면서 한번쯤 모순적인 선택을 할 수밖에 없는 상황에 부딪히게 된다. 보안과

해킹! 절대로 뚫릴 수 없다는 보안과 무엇이든 뚫을 수 있다는 해킹, 보안을 천직으로 하는

사람들은 이 창과 방패의 모순 속에서 치열하게 싸움을 하고 있다. 보안과 해킹은 그 태생이

같다. 기술적인 기본이 같다는 얘기다. 그 기술을 사용하는 사람과 적용하는 대상에 따라 선

이 될 수도 있고 악이 될 수도 있다. 그렇기 때문에 보안과 해킹은 더더욱 모순과의 싸움이

될 수밖에 없다고 생각한다.

어찌보면 이 책의 저자인 강병탁 님도 이 책을 통해 그 모순에 대한 스스로의 고민을 말하

고 싶었는지도 모르겠다. 리버싱의 기본부터 방어기능, 공격기능을 차례로 설명하면서, 선과

악에 대한 선택을 독자에게 맡기는 잔인함도 보인다. 자칫 양날의 검이 될 수도 있는 기술들

중에서 가장 기본이 되는 것이 바로 리버싱이다.

그동안 리버싱에 대해서는 많은 번역서가 출간되었다. 그러나 지금까지의 리버싱 관련 서

적은 현업에서의 경험과 지식에 기반을 두고 있다기보다는 대부분 이론적인 내용을 기반으

로 한다. 리버싱을 완벽하게 이해하려면 운영체제, 프로그래밍, 컴파일러 등 여러 가지 기본

적인 지식이 필요하기 때문에 이론에만 치우칠 경우 독자들이 쉽게 다가갈 수 없는 벽이 느

껴질 수밖에 없다.

그러나 이 책은 현업에서 보안전문가로서의 저자의 경험을 바탕으로 기본적인 지식이 조

금 부족하더라도 리버싱을 이해하는 데 전혀 어려움이 없을 정도로 매우 친절하게 설명돼

있다. 또한 저자 특유의 감각이 돋보이는 제목과 설명으로 자칫 지루하고 딱딱하게 느낄 수

있는 내용을 공부하고 싶은 호기심으로 바꾸고 있다. 이 책을 발판으로 실무 경험을 바탕으

로한 좋은 내용의 국내 서적들이 많이 출간되어 보안을 꿈꾸는 많은 이들의 길잡이가 되기

를 희망한다.

Page 25: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxv

마지막으로, 언제나 새로운 목표를 향해 도전하는 강병탁 님에게 마음속 깊은 응원의 박

수를 보낸다.

- 이호웅

현) 안철수연구소 ASEC 센터장

Application Hacking 감수

Page 26: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxvi

베타리더 후기

한 권의 명저가 탄생했다. 기존의 책과는 차별화된 방식으로 리버스 엔지니어링에 접근하는

이 책은 리버스 엔지니어링의 현대판 바이블이라고 생각한다. 저자의 노고에 무한한 박수를

보낸다.

- 이홍선, 넥슨 코리아 게임보안팀

실무자로서의 경험과 연구에서 비롯된 리버스 엔지니어링에 관한 다향한 기술, 기법들을 집

대성한 주옥같은 학습서다. 쉬운 예제와 친절한 설명으로 이 새로운 세계에 쉽게 적응할 수

있도록 도와주며 구체적인 설명으로 기술에 대한 이해를 높여주고 이에 더해 기존에는 잘

다뤄지지 않았던 부분에 대한 전문적인 노하우를 알려 주고 있어 중급 이상의 과정으로 나

아가려는 이들이라면 시행착오를 줄이고 시간을 절약하는 데 도움될 것이다.

- 이진석, 넥슨 코리아 게임보안팀

리버스 엔지니어링을 강병탁 님 특유의 위트있는 어투로 재미있게 풀어낸 책이다. 그동안 게

임보안 일을 하며 많은 책을 봐왔지만 이렇게 군더더기 없이 필요한 것만 설명해주는 책은

처음이다. 이제부터 이 책은 지금까지와는 다른 리버스 엔지니어링 입문서가 될 것이다.

- 김동현, 넥슨 코리아 게임보안팀

리버스 지니어링의 기초부터 고급기술까지 다루고 있는 책이다. 베타리더를 진행하면서 새

롭게 알게 된 부분도 많고, 기초를 다시 다지는 계기가 되었다. 이 책을 읽는 독자들 또한 실

력이 두 단계 업그레이드될 것이라 장담하며, 리버스 엔지니어링의 세계를 경험해 보시기 바

란다.

- 전동기, 넥슨 코리아 게임보안팀

Page 27: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxvii

강력한 비급이 담겨 있을수록 어려운 내용으로 인해 주화입마에 빠지고 마는 경우가 많다.

하지만 이 책은 그러한 주제를 특유의 위트와 재치로 풀어낸 비급서가 아닐까 생각한다.

- 최종학, 넥슨 코리아 게임보안팀

아직 많은 부분에서 부족한 내가 이 책을 통해 얻게 된 것, 그것은 저자가 수년간 게임 보안

업계에서 쌓아온 실무 노하우였다. 해커들과 수없이 공방을 치뤄야 하는 나에게 이 책은 전

세계 해커들과 맞짱을 뜰 수 있게 해준 무기였다.

- 김지호, 넥슨 코리아 게임보안팀

처음 시작조차 막막하고 높은 벽처럼 느껴졌던 리버싱 기술을 어떻게 접근해야 하는지, 그

기술의 핵심이 무엇인지 보여주는 지침서라고 생각한다. 처음 리버싱을 공부했을 때 어셈블

리 책을 읽다가 좌절했던 경험을 겪어본 한 사람으로서 지식과 실무 경험이 녹아 있는 이 책

으로 보안 분야에 입문하기를 감히 추천한다.

- 김창규, 넥슨 아메리카 정보보안팀

Page 28: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxviii

시작하며

지금 이 책을 보고 계신 분들은 여러 유형의 사람들이 아닐까 한다. 리버스 엔지니어링이라

는 학문을 처음 시작하게 된 분, 또는 리버스 엔지니어링이라는 것에 대해서는 이미 알고 있

지만 얕은 지식에 불과했던 수준에 깊이를 더하기 위해 새롭게 시작하신 분, 이미 리버스 엔

지니어링의 고수 반열에 올라와 있지만 어디 한두 개 건질 만한 내용이 없을까 하여 책을 빠

르게 훑어보는 분 등, 필자는 이 모든 분들을 환영한다. 처음 시작하는 분에게나, 이미 가진

실력을 향상시키고 싶으신 분들 모두에게 이 책을 선택한 것이 후회없는 선택이 되도록 만

들어 줄 것을 약속한다. 이 책은 필자가 그간 리버스 엔지니어링과 연관된 업무상 겪어왔던

수년간의 경험(나 경력 몇 년이야~ 라고 라고 내세우는 걸 그다지 좋아하지 않아서 “경험”으

로 대체했다. 보통 그런 발언을 서슴치 않는 분들은 헛된 경력이 많이 쌓여 있는 경우가 많기

도 하기 때문에) 또는 개인적으로 연구해서 파악한 내용에 대해 글로 묶은 내용이다. 보통의

책에 흔하게 나와 있는 내용 또는 인터넷에서 쉽게 검색해서 찾을 수 있는 딱딱하고 하품나

는 공대 서적 특유의 냄새는 모두 제거하고, 실제 경험을 토대로 얻은 지식을 말랑말랑한 표

현과 농담을 섞어가며 위트있는 가르침을 내려주는 선배들에게 한 수 배우듯이 친근하게 썼

다. 공대생들은 무뚝뚝한 사람투성이며, 기술서적은 기계가 작성한 글을 보는 듯한 느낌이

라는 고정관념에서 최대한 탈피하여 지식을 전달하려고 노력했다.

리버스 엔지니어링이란 무엇인가

타사 서적을 폄하하려는 의도는 아니지만, 흔히 볼 수 있는 “C++ 시작하기”, “엑셀 시작하기”

등과 같은 누구나 아는 소재/주제의 책은 그 내용을 어렵지 않게 짐작할 수 있지만 리버스

엔지니어링이라는 것은 직접 접해본 사람이 아니라면(생각외로, 일반적인 애플리케이션 프

로그래머 중에서도 리버스 엔지니어링에 대해 모르는 사람이 아직도 꽤 많다) 리버스 엔지

니어링이 어떤 것에 사용되고, 무엇을 위한 학문인지 모를 수도 있다 싶어 먼저 개론적인 설

명부터 시작하겠다.

Page 29: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxix

리버스 엔지니어링이라는 것은 소스코드를 역추적하는 것을 말한다. 요즘에야 보안 트렌

드가 많이 다양해지면서 리버스 엔지니어링이라는 과목을 정식으로 개설하게 된 학교도 있

지만 학번이 지긋하게 높으신 분들은 소프트웨어공학 시간에 간략히 다루어진 정도로만 기

억할 것이다. 예를 들어 어떤 C 코드가 있다. 개발자가 코딩을 완료하고 빌드를 하게 되면 이

소스코드를 컴퓨터가 이해할 수 있는 언어인 기계어로 번역하게 된다. 그렇게 컴파일이라는

과정을 거치며 오브젝트(obj) 파일이 만들어지며, 여러 링크된 라이브러리와 이 오브젝트 파

일을 결합해 최종적으로 EXE나 DLL 등이 생성된다. 이렇게 빌드된 파일은 (일반적인 상식

으로는) 당연히 원래의 소스코드를 알 수 없게 되는 형태가 된다.

하지만 여기서 리버스 엔지니어링이라는 기술을 사용하게 되면 빌드된 EXE, DLL 등의 바

이너리 분석을 거쳐서 원래의 소스코드가 어떤 식으로 만들어져 있는지 파악하는 것이 가

능해진다. PE Header 분석을 통해 어떤 라이브러리가 링크돼 있는지 분석하고, 버튼을 눌렀

을 때 가동되는 코드의 흐름을 추적해 원래의 코드가 어떤 식으로 제작돼 있는지 알 수 있게

된다. 물론 컴파일러가 오브젝트 파일을 만들고 바이너리로 빌드된 이후의 내용을 어셈블리

코드로 재분석하는 것이라 원래의 소스코드와 완전히 일치하는 내용이 나오는 것은 아니

다. 따라서 분석가의 능력에 따라 “리버스 엔지니어링을 제대로 하긴 한 건지?”라는 의구심

이 들 정도로 아주 적은 정보만 파악해낼 수도 있고, “혹시 이거 소스코드를 직접 보고 온 거

아니야?”라는 감탄이 들 정도로 원래의 소스코드에 매우 근접한 결과치를 뽑아내는 결과가

나오기도 한다.

어떤 곳에 리버스 엔지니어링을 사용할 것인가

자, 그럼 이런 리버스 엔지니어링을 통해 어떤 것들을 할 수 있는지 알아보자. 원본 소스코드

의 내용을 뽑아낼 수 있다는 결과론적 내용에 의거하면 파악할 수 없는 내용 또는 함부로 열

어봐서는 안 되는 내용에 대한 검증을 한다는 맥락에서 봤을 때 왠지 뒤가 구린 행위에 활용

Page 30: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxx

할 수 있지 않을까 생각이 든다. 정답이다. 리버스 엔지니어링은 타인의 기술을 얻어내거나

타 프로그램의 행위에 끼어드는 데 사용된다. 그래서 리버스 엔지니어링은 해킹과 보안 분야

에서 많이 사용될 수밖에 없는 면모를 지니고 있다.

[1] 모의해킹, 취약점 발견

대표적으로 애플리케이션의 취약점을 분석하는 데 리버스 엔지니어링이 이용될 수 있다.

소스코드가 없는 상태에서 현재 서비스가 되고 있는 애플리케이션은 리버스 엔지니어링을

통해 소스코드의 구조를 분석해야 한다. 버퍼 오버플로우가 일어날 가능성이 있는지, 코드

변조를 통해 허가되지 않은 액션이 가능한지, 패킷 분석을 통해 암복호화 알고리즘에 취약

점이 있지는 않은지 등이 대표적인 경우다. 또한 악성코드를 검출하는 안티바이러스가 혹

시 검출하지 못하는 형태의 로직이 있는지 취약점을 분석할 수 있고, 키 입력을 보호해야

하는 키보드 보안 솔루션이 보호하지 못하는 영역을 찾아낼 수도 있다. 이러한 관점에서 살

펴보면 리버스 엔지니어링은 모의해킹 등 보안 검수를 할 때, 분석가의 필수적 역량 중 하나

가 된다.

[2] 보안코드 개발

역으로 취약점 발견이나 보안검수 외에도, 리버스 엔지니어링을 이용해 보안기능 개발에도

활용할 수 있는데, 예를 들어 1000개의 악성코드가 등장하면 이를 모두 발견하기 위해서는

1000개의 시그니처를 뽑아 이 악성코드를 탐지해야 한다(물론 일반적인 대응을 할 때). 하

지만 만약에 이 악성코드에 공통적인 비정상 행위가 있다는 것을 리버스 엔지니어링을 통해

발견했다면 그 한 가지를 탐지하는 하나의 시그니처로 1000개의 악성코드를 한방에 차단하

는 효과를 보여줄 수 있다. 즉, 해커의 침입 여부를 역분석해서 그에 상응하는 보안 모듈을

개발할 수 있다. 이렇게 발견된 취약점은 리버스 엔지니어링을 통해 원인 분석을 하게 되고,

강화된 모듈을 개발하는 것으로 마무리할 수 있다.

[3] 버그 수정

버그가 발생했을 때 그 원인을 발견하는 목적으로도 리버스 엔지니어링은 사용된다. 소스

코드상에서는 문제가 없지만 다른 모듈이 결합했을 때 또는 필드상에서 외부 프로그램과

Page 31: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxi

충돌이 발생했을 때 또는 예상하지 못한 상황에서 예외가 발생했을 때 등에 대해 리버스 엔

지니어링을 통해 원인을 파악할 수 있고, 심지어 소스코드 없는 상태에서 수정도 가능하다.

아주 바람직한 사례는 아니지만 어떤 해커들은 프로그램의 버그를 찾아서 해당 벤더사에

수정 요청을 했는데 그 처리가 늦어질 경우, 직접 리버스 엔지니어링을 통해 바이너리를 수

정하여 버그를 고친 후 프로그램을 사용하기도 한다.

[4] 신기술 연구와 학습

리버스 엔지니어링은 소스코드 없이 소스코드를 파악하는 작업이므로 실행되는 프로그램

만 있으면 그것이 어떤 기술로 만들어져 있는지 분석해낼 수 있다. 어떤 진기한 프로그램이

세상에 등장했고, 그 기술이 어떻게 구현돼 있는지는 책이나 인터넷 사이트를 뒤져도 알 수

없을 때 그것을 리버스 엔지니어링하면 기술에 대한 궁금즘을 해결할 수 있다. 리버스 엔지

니어링은 이렇게 타 바이너리를 분석하며 신기술 연구에 큰 도움을 가져다 준다.

개발 지식은 필수

자, 지금까지의 내용을 생각해 보면, 리버스 엔지니어링은 바이너리 분석을 통해 원본 소스

코드의 내용을 파악하는 것이 한 줄 요약이자 가장 핵심적인 기술임을 알 수 있다. 따라서

훌륭한 리버서가 되기 위해 반드시 갖춰야 할 자질은 무엇일까? 그것은 바로 풍부한 개발 능

력이다. 코드를 작성해본 경험이 있어야 빌드된 바이너리가 원래 어떤 식으로 만들어졌는지

파악할 수 있기 때문이다. 보안 컨설턴트가 개발 능력을 뭐 하러 쌓느냐고 반문하시는 분들

이 종종 있는데, 보안업계에서 프로그래밍 스킬은 가장 기본적으로 갖춰야 할 자질임과 동

시에 필수적인 기반 기술이다. 진짜 고수 해커들은 웬만한 개발자보다 훨씬 더 훌륭한 코딩

능력을 보이는 경우도 많다. 필자 역시 개발자로 시작해서 보안 컨설턴트로 전향한 사례지만

필자 주변에는 컨설턴트에서부터 시작했으면서 필자보다 백배는 더 코드를 잘 작성하는 사

람이 수없이 많다. 보안쟁이들은 개발을 할 필요가 없다고 말한 사람이 누군지 모르겠지만

실제로 개발 능력이 출중한 보안 컨설턴트나 모의해커들은 지천에 널려 있다.

또한 리버스 엔지니어링을 배울 때 첫걸음을 잘못 내디딘 나머지 동적 디버깅에만 의지해

점프가 나올 때만 어디로 갈 것인지 고민만 하며 트레이싱을 하는 사람이 있는데, 그렇게 점

Page 32: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxii

프코드에만 의지해 크래킹만 연마한 사람들은 리버스 엔지니어링을 아무리 많이 해 봤자 코

드 분석이나 알고리즘 파악 등에 대한 지식은 쌓이지 않고 오직 점프 연산 같은 잡기술만 쌓

고 만다. 이는 곧, 영어 문제를 풀 때 문제에 대한 해답은 잘 찾으면서 정작 회화나 영작은 하

지 못하는 사례와 일맥상통한다. 리버스 엔지니어링을 할 때 크래킹이라는 마력에 빠질 수

있는데(스타워즈 매니아들의 표현으로, 해킹에 빠진 이들을 가리켜 다크 사이드 포스에 중

독됐다고 한다) 이런 사람이 되지 않아야 한다.

따라서 리버스 엔지니어링을 하기 전에는 C/C++에 대한 코드 작성 능력, 각종 라이브러리

를 컨트롤하는 스킬, 또한 Win32 API와 시스템 프로그래밍에 대한 기반지식이 충분해야 한

다(이 책의 중간 즈음에 나오는, 일상 생활에서의 역공학이라는 코너에서 이에 대한 얘기를

깊이 있게 설명했으니 해당 코너가 나오면 참고하기 바란다).

리버스 엔지니어링은 합법인가

리버스 엔지니어링은 이미 빌드된 바이너리를 역분석하는 과정을 거치며 공개돼서는 안 되

는 원본 소스코드를 추적하는 작업을 진행한다. 이런 면만 봐도 리버스 엔지니어링이라는

것은 과연 법적인 문제가 없을까, 라는 생각이 드는 분도 당연히 계실 것으로 안다. 사실, 리

버스 엔지니어링을 전파하는 입장에서 이 문제에 대해 법적인 문제가 전혀 없다고 하기에는

가슴이 조금 찔리고, 불법이 맞다, 라고 하기에는 “그럼 이 책은 불법을 저지르는 짓을 가르

치는 것인가?”라는 명제에 대해 대답하기가 애매해진다. 필자 개인적인 가치관을 떠나서 합

법이냐 불법이냐에 대한 잣대와 관련해서 현재의 분위기에 대해 간략히 얘기하면, 일단 바

이너리 파일 변조의 경우는 무조건 불법이다. 빌드된 바이너리는 어디까지나 바이너리를 만

든 회사에 지적재산권이 있다. 따라서 리버스 엔지니어링을 통한 바이너리 변조는 지적재산

권을 침해, 파괴하는 행위에 해당하므로 이것은 엄연한 불법이며, 이 책을 읽는 분들도 상용

소프트웨어에 대해서는 절대 리버스 엔지니어링을 수행하지 않기를 바란다. 반면 메모리에

로딩된 코드에 대해서 변조하는 것은 불법이다 합법이다가 아직도 의견이 분분한 편인데, 합

법이라고 외치는 분들은 내가 내 돈 주고 산 내 메모리에서 실행되는 내용을 좀 변경했다고

해서 어디가 문제냐는 논지를 펼치고 있고, 불법이라는 분들은 당신의 메모리를 변조했어도

그 메모리에 올라간 바이너리는 우리의 제품이며, 결국 우리 회사의 자산을 침해하기 위한

Page 33: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxiii

의도로 자행한 일이므로 불법이라는 의견을 내세우고 있다. 필자는 법률 수행가가 아니므로

어느 의견이 맞다는 가치 판단은 보류한다.

첨언해서 이야기하자면, DMCA(Digital Millenium Copyright Act)라는 기술이 있다. 이

것은 저작권 보호 기술을 보호하기 위한 법으로서, 보호 기술을 보호하는 법인데, DRM 기

술을 이용한 바이너리에 그것을 우회하거나 회피하기 위한 시도를 했을 때 법적인 처벌을 받

는 것을 말한다. 이런 DMCA 개념이 리버스 엔지니어링을 통한 법적 분쟁이 발생했을 때 중

요한 개념으로 사용된다. 물론 아직까지는 리버스 엔지니어링이 법률에 대해서는 미개척 분

야라 변호사의 역량대로 또는 판사의 판단대로 결과가 나올 수 있으므로 무엇이 답이다, 라

고 정확히 결론내리기는 어려운 편이다. 따라서 이런 때에는 당연히 이전의 사례를 살펴보기

마련이고, 결국은 과거에 어떤 선례가 있었는지 판례가 중요해진다. 물론, 완전히 동일한 판

례는 아직 많지 않고 IT와 보안과 관련된 법적 분쟁에 대해서는 변호사들도 문외한인 경우

가 많으므로 기업 내 보안을 담당하는 보안 컨설턴트들도 이러한 법에 대해 풍부한 지식을

쌓아두는 것이 좋다. 이와 관련해 좀더 자세한 내용을 공부하고 싶다면 Gerald R. Ferrera

외 6인이 작성한 ‘CyberLaw: Text and Cases’라는 책을 추천하니 살펴보기 바란다.

이 책은 어떤 내용을 설명하는가

자, 그럼 서론은 이쯤 하고, 이 책에서 여러분들에게 전파할 지식은 어떤 내용일지 간략히 정

리해 보겠다. 이 책은 필자가 월간 마이크로소프트웨어에서 약 3년간 해킹/보안 칼럼에 연재

한 내용을 근간으로 만들었다. 책에 정리된 약 60%의 내용은 완전히 새로 작성했고, 40%의

내용은 기존의 기고물을 보충하거나 새로운 기술을 알릴 수 있는 데 주력해 철저히 개편했

다. 설명이 불친절하거나 최소화됐던 내용은 바이블이라는 제목에 걸맞게 최대한 독자들이

알기 쉽고 흥미를 유발하며 따라 할 수 있게 각색해 두었다.

[1부] 리버스 엔지니어링 기본

어셈블리, C, C++, DLL 등의 매우 기본적인 코드의 생성 흐름과 규칙을 정리한 내용이다.

리버스 엔지니어링도 결국 정해진 문법 안에서 기본적인 규칙을 따르면 풀어낼 수 있는 것에

불과하기 때문에 그러한 규범을 알아보자는 이야기라고 보면 될 것 같다. 차를 달이거나 마

Page 34: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxiv

실 때의 방식이나 규칙을 말하는 다도(茶道)라는 것이 있다. 한 잔에 불과한 맹물을 마실 때

도 이 같은 엄격한 규율이 있듯이 리버스 엔지니어링을 할 때도 기본적으로 알아둬야 할 규

칙이 있다는 것을 이해하기 위한 내용이다.

[2부] 리버스 엔지니어링 중급

PE Header는 리버스 엔지니어링을 공부하는 분들이 가장 고통스러워하기도 하고, 공부하

기도 싫어했지만 어쩔 수 없이 결국에는 익혀야만 되는 필수 요소 중 하나인데, 그런 선배 경

험자들의 고충을 밑바탕으로 이론에 불과한 내용을 철저히 배제하고 현업과 실무에 맞게

재각색해서 설명했다. “6장 흔히 사용하는 패턴”에서는 1부 리버스 엔지니어링 기본에서 다

루지 못한 코드가 생성되는 과정과 계속 반복해서 나오는 패턴에 대해 분석하며 어셈블리

언어의 감을 익히기 위한 내용을 준비했다. 그리고 마지막으로 MFC 리버싱을 통해 애플리

케이션 분석의 집중도를 높일 수 있게 여러 가지 기법 소개를 준비했다.

[3부] 연산 루틴 리버싱

일반적으로 수학 공부를 시작할 때 집합과 명제가 나오고 수와 식을 익히며 방정식을 공부

한 뒤 함수, 지수로그, 삼각함수 등으로 넘어가는 과정이 있듯이, 사실 대부분의 리버스 엔

지니어링 커리큘럼에서 가장 먼저 가르치는 것은(집합과 명제 코너에 해당하는 것은) 키젠

풀이나 시리얼 연산 방법에 대한 리버싱이다. 하지만 개인적으로 필자는 키젠 풀이를 그다지

좋아하지 않고 현업에서도 업무 연관성이 상당히 적기 때문에 리버스 엔지니어링에 있어 가

장 불필요한 학습단계라고 본다. 따라서 우선순위를 낮추다 못해 아예 생략할까 하기도 했

지만 그래도 리버스 엔지니어링한다는 사람치고 키젠 하나 못 푸는 것도 우스운 일이므로

결국 타협하고자 한 게 중간 즈음에 내용을 준비하는 것이었다. F-Secure 백신사의 리버스

엔지니어링 대회를 통해 연산 루틴을 분석하는 과정을 설명하는데, 흔히 볼 수 있는 점프 패

치를 중점적으로 설명하기보다는 왜 이 루틴을 분석해야 하고 어떤 접근 방식을 써야 하는

지 등, 생각하는 리버스 엔지니어링에 초점을 맞춰 단순한 키 빼내기 해설이 되지 않도록 노

력했다.

Page 35: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxv

[4부] 안티 리버스 엔지니어링

필자는 나무에 열리는 밤을 무척 좋아하는데, 특히 밤송이의 가시 속에 먹음직한 알밤이 들

어있을 때 알밤을 빼 먹을 욕심으로 가시를 제거하면서 긴장감을 느끼는 아슬아슬한 순간

을 은근히 즐겼던 것 같다. 안티 리버싱은 리버스 엔지니어링을 막는 기술, 이른바 밤송이에

들어있는 가시 같은 역할이다. 이것을 제거해야만 리버스 엔지니어링을 순탄하게 진행할 수

있고 결국에 알밤을 빼먹을 수 있는 결과까지 도달하는 것이다. 안티 리버싱 기술은 계속해

서 발전을 거듭해, 안티 리버싱을 분석, 격파하는 그 자체가 하나의 학문으로 될 만큼 성장

해오고 있다. 인터넷에 흔히 등장하는 안티 디버깅 코드는 실제로 가동되지 않는 것도 많고

예제 코드도 충분하지 않아 익히기에 어려운 감이 있지만, 이 4부에서는 모든 기법을 100%

실제 코드로 제작했으며 각 밤송이 가시 기술에 대한 강력함의 정도, 우회 방법 등의 한계

점, 그리고 보완 방법 등에 대해 다각도로 설명했다.

[5부] OllyDBG 플러그인

OllyDBG 플러그인은 리버스 엔지니어링을 좀 더 쉽게 하기 위해 만든 보조 도구라고 볼 수

있다. OllyDBG에서는 누구나 플러그인을 제작할 수 있게 훌륭한 인터페이스를 제공하고 있

으므로 SDK의 사용법과 자신만의 아이디어가 결합하면 리버스 엔지니어링을 몇 배 이상 효

율적으로 증가시킬 플러그인을 제작할 수 있다. 필자는 가끔 OllyDBG 자체보다 OllyDBG

플러그인 자체에 더욱 더 큰 감동을 느낄 때가 있다. 5부를 공부하고 난 후의 누군가가, 또 다

른 사람들에게 감동을 줄 플러그인을 만들어 세상에 알려주었으면 하는 바람이다.

[6부] 보안 모듈 우회

보안 관계자에게 가장 두려운 것은 방어장치가 없는 상태에서 해킹을 하는 사람이 아니고

존재하는 보안 모듈을 뚫어서 해킹 시도를 하는 이들이다. 그리고 대부분의 취약점은 보안

설계 자체의 실수보다는 어이없는 한두 줄의 취약점 때문에 발생하는 것들이 상당수를 이룬

다. 세계 1, 2차대전 때 대형 항공모함이 침몰하던 사례를 봐도 격렬한 전투에 의해 항공모함

이 격추됐던 것이 아니라, 자신이 죽는 것을 각오하고 취약점을 향해 덤비는 가미가제의 전

투기 한두 대에 의해 침몰되는 경우도 허다했다는 점도 단순한 공격이 커다란 사고까지 이

Page 36: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxvi

어질 수 있다는 사실을 반증한다. 6부에서는 일반적으로 보안 모듈을 우회하는 기법 말고도

굉장히 단순한 한두 가지 취약점을 이용해 보안 모듈이 공격 당하는 여러 가지 사례를 보여

준다.

[7부] 한 차원 높은 바이너리 창조

7부에서는 마지막 내용으로, 코드 후킹, 코드 변조, 난독화 등의 내용을 다루며 리버스 엔지

니어링을 목표로 하는 당신에게 “초짜”의 늪에서 “중수” 이상으로 가는 동력이 됐으면 하는

바람으로 작성했다. eax는 리턴값, ecx는 카운터 라는 등의 시시껄렁하고 반복적인 지식의

답습에서 벗어나 리버싱의 깊이를 좀더 맛볼 수 있게 노력했다. “6부 보안 모듈 우회” 편의 연

장이라는 느낌을 살렸기 때문에 6부의 내용을 반드시 숙지한 후에 살펴보길 바란다.

이 책은 어떤 분들에게 필요한가

서두에 간략히 소개했다시피 이 책은 리버스 엔지니어링을 처음 접하는 분들, 또는 어느 정

도 수준을 높이고 싶은 분들을 대상으로 한다. 특히 친근한 설명과 가벼운 대화체를 사용했

으므로 접근하는 데 벽이 높지 않으리라 생각한다. 따라서 리버스 엔지니어링이 너무 어려워

서 중도에 포기했던 분들은 대환영이다.

그리고 필자의 인생관 중 하나인데, 굳이 리버스 엔지니어링에 국한된 내용은 아니지만 어

떤 내용을 공부하건 발전하는 본인을 느낀다면 그것에 기뻐하고 거침없이 본인에게 칭찬을

해준다면 정말 중요한 사기 증진이 되며 아주 간단하게 자기성찰을 할 수 있는 방법이 아닐

까 한다. 처음부터 너무 높은 벽을 목표로 잡았다면 벽 근처에 가기도 전에 좌절하게 되고,

시작하기도 앞서 포기하고픈 마음이 더 쉽게 들 것이다. 한 초등학생이 방정식을 풀었다며

선생님이나 부모님께 자랑스럽게 칭찬받을 것을 기대하면서 성적표를 들고왔을 때, “야, 달

나라에 가려면 그 정도로는 어림도 없어”라고 핀잔을 주는 모습은 잠재력 있는 사람을 영원

히 매장하는 행위가 될 수도 있다. 우주 항공선을 만든 사람도 방정식을 공부하는 것이 어려

웠을 수 있다. 결코 겁내지 말고 바이너리를 한 바이트 한 바이트 들여다 보며 리버스 엔지니

어링에 눈을 떠가는 자신을 흐뭇한 표정으로 비춰보기 바란다.

Page 37: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxvii

서두르지 말 것

실제로 공부를 시작해 보면 리버스 엔지니어링이라는 분야는 해야 할 것들이 너무도 많은

분야라고 느껴질 때가 올 수도 있다. 이쯤에서 드리고 싶은 말씀은 절대 서두르지 말라는 것

이다. 대나무가 하늘로 쭉쭉 휘어지지 않고 곧바로 올라갈 수 있는 것은 적당히 자랐을 때마

다 매듭으로 봉하고, 또 자라면 또 매듭으로 봉해서 위로 올라갔기에 가능한 것이다. 인간도

마찬가지다. 어떤 대나무도 단 한번에 성장할 수 없는 것처럼, 하나의 목표를 휘지 않고 달성

하기 위해서는 흔적의 매듭이 반드시 필요하다. 실력이 바로 늘지 않는다고 해서, 내용이 어

렵다고 해서 본인을 자책하지 말고 조금씩 한걸음씩 매듭을 봉해 나간다면 이 책을 읽은 후

에는 1년 후 반드시 훌륭한 대나무가 돼 있을 것이라고 말해주고 싶다.

생각하는 책이 되고 싶다.

많은 사람들이 학문이나 책을 접할 때 지식의 전달 수단에 충실한 책은 많이 볼 수 있지만

실질적인 조력자나 어드바이서의 역할에 충실했던 책을 꼽아보라면 과연 얼마나 될까, 라

는 생각이 든다. 요즘 같은 정보의 홍수 시대에 데이터는 넘쳐난다. 하지만 어떤 정보를 어떻

게 활용할 것이며, 정보를 대할 때의 자세나 정보를 활용하는 사고력 등과 같은 것을 길러주

는 것이 오히려 더 중요하지 않을까. 예를 들어 수학 문제를 풀 때 이 문제는 이런 공식을 통

해 답을 낸다는 결과주의적 접근에 의거해 공부를 하는 사람보다는 왜 이런 답이 나오는지,

왜 이런 상황에는 그 공식을 써야만 하는지, 다른 공식을 썼을 때 답은 나오지 않는 것인지,

이 문제를 접할 때 적용해 볼 수 있는 접근 방법에는 최소한 몇 가지가 있는지 등을 생각하

는 사람이 훨씬 더 다양한 사고력을 펼칠 수 있으며, 새로운 문제에 대한 해답 역시 쉽게 찾

을 수 있다. 물론 전자에 해당하는 사람은 많은 문제를 공부했을 때 그만큼의 경험이 존재하

기 때문에 이미 접해본 유형에 대해서는 별 무리 없이 풀어나가겠지만, 새로운 문제를 마주

했을 때 후자에 속하는 사람보다 훨씬 응용력이 떨어질 것이다. 수집된 데이터는 많겠지만

사고력을 기르는 연습이 부족했기 때문이다.

그런 부분에 의거해 이 책 역시 가급적이면 많은 양의 정보를 전달하기 위한 목적보다는

원리에 중점을 두고, 리버스 엔지니어링을 할 때의 “접근 방법”과 “사고력”을 우선적으로 설

명하려고 노력했다. 점프문, push 문, call 문 등 다양한 코드가 나오는데, 왜 이곳의 점프문

Page 38: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxviii

을 봐야 하는 것인지, A 상황에 B 기법을 써야만 하는 이유 등 독자로 하여금 스스로 질문을

던지고 한 차원 수준 높은 사고를 통해 해답을 찾을 수 있게 최대한 배려했다. 즉, 필자가 말

하고 싶었던 것은 물고기를 잡는 법에 대한 가르침, 아니 그것을 넘어서 일반적인 물고기가

아닌 네 발 달린 물고기가 나왔을 때, 지렁이를 먹지 않는 물고기가 나왔을 때 등 다양한 상

황에 능동적으로 사고할 수 있게 접근 기술과 생각의 전환법을 알리려고 최대한 노력했다.

정제되지 않은 표현에 대한 양해를 부탁한다

이 책에서는 딱딱한 정보전달 위주의 설명이 아닌 접근 방법과 사고력에 대해 선임에게 가르

침을 받듯 차근차근 설명을 듣는 분위기로 책을 구성했다, 라고 이야기했다. 모든 것을 갖춘

상태에서 예의범절과 매너까지 갖추면 얼마나 좋겠느냐만은 위와 같은 모토로 문장이 나가

다 보니 다소 직설적인 표현이나 과도한 은유법 등이 책 곳곳에 등장하게 된 점은 어쩔 수 없

던 부분이었던 것 같다. 따라서 어쩌면 이 책에 등장하는 이런 거친 표현은 일부 독자에겐 눈

살을 찌푸리게 할 수도 있다. 일반적으로 화술의 재료는 풍성한 지식에서 나오지만, 그렇다

고 해서 화술이라고 포장될 만큼 집대성된 지식이 흔해 빠진 잡학을 의미하는 것은 아니라

고 생각한다. 따라서 필요한 상황에 적합한 이야기를 하는 것이 중요하고, 불필요하게 길지

도 않으며 속 깊은 말을 써야 한다는 것 역시 중요하다고 생각한다. 필자가 비록 글 재주는 없

지만 나름 위와 같은 철학을 가지고 글을 쓰고 있다는 점을 조금이라도 눈여겨봐 주었으면

하며, 결코 도발적이며 파괴적인 언어로 일시적인 주목을 끌기 위함이 아니라는 점을 알아주

길 바란다. 따라서 품위 없는 위트나 질 떨어지는 농담이 한두 번씩 나오더라도 너그러운 마

음으로 이해해 주었으면 하는 바람이다.

당부의 말

이 책의 제목은 바이블이고, 책의 두께도 상당히 두껍다. 처음 책을 쓸 때와의 마음과는 달

리 이것도 다루고 싶었고 저것도 다루고 싶었지만, 지면상의 한계에 부딪혀 내용의 난이도 조

절에 실패해 싣지 못한 내용이 많았다. 개인적으로 매우 안타까운 부분이다. 또한 리버스 엔

지니어링은 어둠의 예술이다. 해킹과 보안이라는 것은 양면성이 존재하기 때문에 자칫 나쁜

곳에 사용하다가는 진짜 나쁜 악당이 될 수도 있다. 이 책을 읽는 사람들 가운데 리버스 엔

Page 39: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xxxix

지니어링을 크래킹이나 해킹에만 사용하는 분이 없기를 바란다.

마지막으로 필자는 항상 재미 위주로 모든 현상을 접하려는 나름의 주관이 있다. 리버스

엔지니어링과 연관된 보안 업무를 할 때도 언제나 즐거웠으며, 새로운 코드를 만드는 기쁨,

해커가 만든 코드를 리버스 엔지니어링해 보며 참지 못할 정도로 웃음을 터뜨린 적이 한두

번이 아니었던 것 같다. 이 학문을 본인의 천직으로 여기고 앞으로 살아갈 삶의 원동력으로

삼겠다면 진심으로 즐기길 바란다. 괴로운 듯이 스트레스 받으며 코드를 분석하고 짜증내는

모습은 지양하고, 이 일을 통해 얻는 보람과 즐거움을 느끼며, 그 즐거움을 주위 사람들에게

널리 전파해 주길 바란다.

감사의 말

본래는 더욱 일찍 출간했어야 할 책이었지만 여러 가지 사정이 겹쳐 다소 늦어졌다. 그와 더

불어 주변에서 필자의 책을 전설의 책이라 부르곤 했는데, 그 이유는 소문만 무성하고 실체

는 존재하지 않아 그렇기 때문이라는 후문이 있다(내용의 깊이가 있어 전설이라는 것이 절

대 아니다). 아무튼 책이 늦어진 만큼 주변에서 도와주시고 걱정해 주신 분들께 죄송한 마

음을 감출 길이 없다. 그 모든 분들께 감사의 마음을 전한다. 이 책을 감수해 주셨고 필자 인

생의 나침반이자 영원한 스승님이신 김휘강 교수님, 새로운 도전을 할 수 있게 언제나 뒤에

서 후원해주신 채은도 부사장님, 김대훤 본부장님, 이희영 본부장님, 이경엽 부실장님, 신용

석 센터장님, 어려운 미국생활에 큰 힘이 되어주셨던 다니엘 대표님, 이 책을 베타리딩 해 준,

가장 힘든 시절을 함께 했고 이제는 전우였다고 말할 수 있는 넥슨 코리아 게임보안팀 일동

이홍선, 이진석, 최종학, 김동현, 조경화, 전동기, 김지호, 전옥희, 권오석, 이예진, 김응주 그

리고 이혜원, 새로이 보안 업무를 준비해 가며 함께 고생중인 넥슨 아메리카 정보보안팀 김

창규, 유동규, 감동적인 추천사를 작성해주신 Matt 형님, 이호웅 실장님, 서우석 사장님, 보

안팀의 영원한 동반자 우철이형, 소수정예 최강의 군단인 우리 고려대학교 정보보호학과 해

킹대응연구실 일동, 언제나 큰 힘이 되어주는 김명현, 정국이형, 박사장님, 이유경, 노수민,

정박커플, 넥슨밴드 밴드원들, 돌아이 모임(+진형), 그리고 번번히 마감일을 어겨 마음고생

많이 시켜드려 끝없이 죄송한 마음 금할 길 없는 김윤래 팀장님, 항상 응원해주시는 아버지,

어머니 그리고 장인어른과 장모님, 마지막으로 이 책을 집필하는 동안 옆에서 끊임없이 격려

해주고 채찍질 해준, 사랑하는 나의 아내에게 이 책을 바친다.

Page 40: 리버스 엔지니어링 바이블 : 코드 재창조의 미학
Page 41: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

[ 01부 ]

리버스 엔지니어링 기본

Page 42: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

xlii 리버스 엔지니어링 바이블

Page 43: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

01

리버스 엔지니어링만을 위한 어셈블리

어셈블리 책이나 개론서, 또는 각종 바이블을 찾아보면 사실 그런 식이다. 레지스터(register)를

설명하고 플래그(flag)에 대한 굉장히 긴 표가 따라붙는다. 그리고 mov니 add니 하는 명령어에

대한 기계적인 설명이 굉장히 길게 늘어지기 시작하는데, 대부분의 입문자가 여기서 머리를

쥐어짜거나 책을 덮어버리는 모습을 보이며 좌절을 겪는다. 그들에게 필요한 건 어셈블리로 실제

프로그래밍을 하기 위한 지식이 아니라 리버스 엔지니어링을 위해 어셈블리 코드를 해석하는

방법이다. 다시 말해, 리버서로 첫걸음을 내딛는 사람들에게 필요한 것은 많은 양의 어셈블리

문법이 아니라 리버스 엔지니어링이라는 조명을 밝혀줄 만한 한줄기 빛이다.

C 코드와 어셈블리 비교, 옵코드(opcode), 오퍼랜드(operand), 레지스터(register), 리틀 엔디

언(little endian), 스택(stack), 함수의 호출, 리턴 주소(return address)

Page 44: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2 리버스 엔지니어링 바이블

어셈블리는 리버스 엔지니어링을 하기 위한 가장 기초적인 도구와도 같다. 비유하자면 영어 공부를 시

작했다면 알파벳, 일어 공부로 치자면 히라가나쯤 된다. 따라서 가장 먼저 시작해야 할 기초 이론이며,

필수로 수강해야 할 과목이다. 그러나 이제 막 시작한 초보에게 어셈블리란 가장 처음 느끼는 커다란

벽과 같은 느낌이다. 필자가 후배들에게 리버스 엔지니어링을 가르칠 때 가장 많이 들은 질문은 바로

다음과 같다.

“어셈블리는 대체 어떻게 접근해야 할지 모르겠어요.”

이 친구들이 이런 질문을 하는 심정이 이해되는 이유는, 사실 필자 역시 같은 고민을 해 본 시절이

있었기 때문이다. 어려운 어셈블리를 대체 어떻게 접근해야 할까? 나는 어떻게 했을까? 그리고 요즘

리버스 엔지니어링 입문자들은 리버스 엔지니어링을 처음 공부할 때 어셈블리와는 어떤 식으로 첫인

사를 나누게 될까? 지식을 쌓고 문제를 해결하려면 일단 책이 가장 적당하니 어셈블리 책으로 먼저

접근해 본다.

필자가 처음 어셈블리를 접한 책은 황희융 선생님의 《MS-DOS 매크로 어셈블리》라는 책이었다. 이

책은 정말 훌륭하고 많은 내용이 담긴, 틀림없는 어셈블리의 바이블이다. 많은 부분을 이 책을 통해

알게 됐으며, 어셈블리 지식을 쌓는 데 아주 큰 공헌을 한 책으로 기억한다. 하지만 단점은 이해하기가

쉽지 않았으며, 어셈블리 프로그래밍이나 언어 자체를 위한 내용이 중심이라서 리버스 엔지니어링을

위한 어셈블리 입문서로는 적합하지 않았다. 그래서 기계를 다루는 공대생이 아닌 이상 보안 업계 종

사자가 어셈블리에 입문하는 데 이 책을 추천하기가 여의치 않았다.

그렇다면 조금 수준이 떨어지는 책은 어떨까? 아쉽게도 그 밖의 책들도 크게 사정이 다르지 않았다.

일부 번역서나 32비트 어셈블리만을 따로 추려 놓은 책도 역시 예외는 아니었다. 리버서를 표방하는

이들에게 필요한 것은 어셈블리를 목적이 아닌 수단으로 활용하기 위한 방법이지만, 사실 그런 책은

없었다. 대부분의 책에서는 순수하게 어셈블리 언어에 대해서만 다뤘다. 우리가 알고 싶어 하는 지식

과는 접근법 자체가 완전히 방향이 다르며, 대체 어디서부터 시작해야 할지 감이 오지 않는 경우가 대

부분이다. 물론 사람에 따라 그것을 자기 입맛에 맞게 소화하며, 모래 속에서 바늘을 찾아가는 식으로

지식을 연마하는 사람들도 있었지만, 입문하는 모든 사람에게 그런 일취월장을 기대하기는 어려웠다.

그래서 리버스 엔지니어링을 처음 공부할 때 “리버스 엔지니어링만을 위한 어셈블리” 지식을 따로

마련하는 건 어떨까, 라는 생각이 들었다. 불필요한 어셈블리 프로그래밍 기반의 지식을 배제하고, 코

드 분석을 할 때 각 레지스터의 어떤 부분에 주안점을 두고 살펴봐야 하고 컴파일러가 코드를 만들어

Page 45: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 3

내는 양상을 비롯해 어셈블리 명령어는 어디까지 암기해야 할지에 관한 내용을 주요 고민거리로 삼아

정리한다면 입문자에게 아주 유용한 지침서가 되지 않을까 한다.

그래서 리버스 엔지니어링 바이블의 첫 시작으로, 순수 리버스 엔지니어링만을 위한 어셈블리를 설

명하는 코너를 마련했다. 어셈블리를 쉽게 이해하고 싶은 분, 그리고 아직도 이해가 되지 않는 분을 위

해 더욱 친숙한 어셈블리의 세계로 안내하는 내용을 담고 있다. 이번 장은 말 그대로 “리버스 엔지니어

링만을 위한 어셈블리”라는 주제로 풀어간다. 이 장에서는 어셈블리 구문이나 모든 명령어를 설명하

지 않는다. 단지, 앞으로 새로운 명령어를 만나도 얼마나 쉽게 이해할 수 있느냐에 중점을 뒀다. 즉, 지

금까지 고민한 내용에 대해 어느 정도 해답에 근접한 방식으로 접근하는 것을 일차적인 목표로 삼고,

어셈블리를 좀 더 쉽게 생각하자는 것을 이차적인 목표로 삼았다. 어셈블리에 이미 자신 있는 분들은

건너뛰어도 무방하지만 다소 유치한 위트를 부분부분 섞어놓았으므로 재미로 읽어봐도 크게 손해 볼

것은 없을 것이다. 그럼 “리버스 엔지니어링만을 위한 어셈블리”라는 거창하지만 다소 별거 없는 듯한

주제를 지금부터 시작한다.

어셈블리의 기본 구조

어셈블리는 매우 간단하다. 매우 쉽고 간결하다. 하지만 어셈블리가 어려운 이유는 바로 그 단순함에

있다. C/C++ 코드와 비교해서 얼마나 단순한 것일까? 예를 하나 들어보자. 냉장고에서 물을 꺼내 마

시는 작업을 한다고 생각해 보자. C/C++ 코드라면 다음과 같이 진행될 것이다.

void 물마심(){ BOOL bOpen = 냉장고문오픈(); if (bOpen) { 물을꺼냄(); 마심(); }}

다소 유치한 코드지만, 어쨌든 대략의 흐름은 저렇다. 냉장고문오픈()을 통해 냉장고 문을 열고, 문

을 여는 데 성공(bOpen == TRUE)하면 물을꺼냄()을 이용해 물을 꺼내고 마심()을 통해 물을 마시는

아주 일목요연한 코드가 된다. 하지만 이것을 어셈블리로 표현한다면 어떨까?

Page 46: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4 리버스 엔지니어링 바이블

__asm{ 냉장고앞으로간다

냉장고문을잡는다

냉장고문을연다

오픈성공: 냉장고안을본다

손을든다

냉장고안에넣는다

물병을잡는다

물병을꺼낸다

뚜껑을연다

물을컵에따른다

컵을손에든다

컵에든것을마신다

C/C++ 코드와 비교했을 때의 가장 결정적인 차이는 뭘까? 그것은 한 번의 동작에 몇 가지 액션을

할 수 있느냐다. 그리고 어셈블리로 바뀌었을 때의 착안점은 “한 가지 동작”이다. 어셈블리는 한 번에

한 가지 동작밖에 하지 못한다. 그래서 냉장고에서 물을 꺼낼 때도 먼저 시선을 냉장고로 향해야 하

며, 물병을 꺼낼 손을 들어야 하고, 냉장고 안에 손을 넣는 것까지 세세하게 다 지정해야 한다. 이것이

바로 어셈블리의 단순하다는 명제에서 오는 맹점이다. 어셈블리는 이처럼 간단명료하기 때문에 코드

한두 줄만 봐서는 해당 프로시저가 무슨 목적으로 만들어졌는지 알 수 없다. 즉, 전체적인 흐름을 봐

야지, 지엽적인 부분만 봐서는 대체 무엇을 하는 코드인지 파악할 수 없다.

게다가 추가적인 난제로, 이렇게 많은 부분을 일일히 지정해야 하므로 코드가 굉장히 길어진다. 그

래서 C/C++에서는 몇 줄 안 되는 코드가 어셈블리로 바꾸면 수십 수백 줄로 바뀌는 이유가 여기에

있다.1 그래도 크게 걱정할 필요는 없다. 이 책의 2부에서는 그러한 세부적인 코드에 연연하지 않고 대

략적인 흐름만 파악해서 코드의 목적을 알아내는 여러 가지 방법을 설명하겠다. 일단 그 부분은 나중

에 다시 살펴보기로 하고 먼저 어셈블리에 대해 알아둬야 할 만한 것부터 좀더 생각해 보자.

어셈블리의 명령 포맷

어셈블리에도 여러 종류가 있지만 지금부터 설명할 어셈블리는 x86 CPU의 기본 구조인 IA-32를 기

본 플랫폼으로 삼아 소개할 예정이다. 그 이유는 사실상 대부분의 PC가 Intel CPU를 사용하고 있고

(AMD의 경우도 실제로 대부분의 코드가 Intel Processor와 호환되므로) 요즘 어셈블리를 익힌다는

1  물론 이것도 사실 간단해진 것이다. 그 전에는 0과 1로 된 바이너리였을 테니...

Page 47: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 5

것은 IA-32를 익힌다는 것과 동일한 표현으로 봐도 무방하기 때문이다. 따라서 이후부터 리버스 엔지

니어링을 위한 어셈블리는 IA-32에 초점이 맞춰져 있다는 것을 알아두자.

먼저 IA-32의 기본 형태는 매우 단순하다. 한 줄을 짤 때 단지 하나 또는 두 개의 코드가 있을 뿐

이다.

"명령어 + 인자"

명령어는 어셈블리를 한 번이라도 본 적이 있다면 알 수 있는 mov나 push 같은 것을 말하며, 좀더

우아한 표현으로 옵코드(opcode)라고 한다. 그리고 인자는 명령어 다음에 “어떤 장소로 값을 넣을 것

인지”, 또는 “명령어에 해당하는 값” 등이 된다. 이것은 다시 고급스러운 표현으로 오퍼랜드(operand)

라고도 한다. 예를 들어 보자.

push 337

이 코드에서 옵코드는 push가 되며, 337이 오퍼랜드가 된다. push는 스택에 값을 넣으라는 명령어

이며, 337은 넣을 값으로서 인자로 볼 수 있다. 예를 하나 더 보자.

mov eax, 1

이번에는 오퍼랜드가 2개인 경우다. 이 코드에서 옵코드는 mov가 되며, 오퍼랜드는 eax와 1이 된다.

eax에 1을 넣으라는 얘기로, 앞의 오퍼랜드가 목적지 오퍼랜드가 되며, 뒤의 오퍼랜드가 출발지가 된

다. mov 문뿐 아니라 모든 오퍼랜드는 앞의 것이 destination이고 뒤의 것이 source라는 점을 잊지 않

길 바란다(memcpy나 strcpy 등과 같다고 생각하자). 이런 식으로 어셈블리는 “명령어 1개 + 인자 1

개 또는 2개” 구조로 한 줄의 코드가 끝난다. 3개가 넘는 것은 거의 없다. 그래서 어셈블리가 단순하다

고 하는 것이며, 그 단순함 때문에 어렵다는 얘기가 나오는 것이다. 냉장고에서 물을 꺼낼 때도 이 모

든 작업을 한 번에 해줄 수 없기 때문에 문을 열고 손을 올리는 등등 세부적인 명령이 따라붙을 수밖

에 없으며, 그러한 명령 하나에는 인자가 2개까지밖에 지원하지 않는 탓에 간단한 처리를 위해서도 굉

장히 많은 양의 코드가 필요하다. 어쨌든 모든 어셈블리는 이런 구조를 따른다. 대략 언어의 구조를 살

펴봤으므로 이번에는 어셈블리 입문자의 최대 난관이라는 레지스터에 대해 알아보자.

Page 48: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

6 리버스 엔지니어링 바이블

레지스터, 복잡한 설명은 그만

수많은 어셈블리 책에서는 이런 식으로 접근한다. “IA-32 레지스터(register)는 범용 레지스터가 8개

있으며, 각 역할은 다음과 같다. EAX는 정수 연산, ECX는 카운팅….” 사실 필자는 처음 어셈블리를

공부할 때 레지스터 자체를 이해하는 데 너무도 오랜 시간이 걸렸던 기억이 있다. 그리고 레지스터가

뭔지 이해했을 때 이렇게 생각했다. “만약 레지스터가 그냥 ‘변수’에 불과하다, 라는 말을 누군가 해줬

다면 얼마나 좋았을까…” 그것이 포인트다. 레지스터를 어렵게 생각하지 말자! 레지스터는 그냥 변수

일 뿐이다.

물론 변수라는 것에 원론적으로 접근했을 때 실제 변수와는 개념 자체가 완전히 다르지만, 쉽고 친

근하게 접근하기 위해서는 변수라고 생각하는 것이 가장 좋다. 그럼 이렇게 생각해 보자.

“변수는 변수인데, CPU가 사용하는 변수다.”

다만 CPU가 사용하는 변수라서 개수가 몇 개 안 되고, 그래서 메모리의 힘을 빌려서 연산을 시작

하는 것이라고 생각하자. 그리고 C/C++ 코드를 작성할 때 변수로 더하고 빼고 나누는 연산을 할 수

있는 것처럼, 어셈블리에서도 역시 연산을 할 때는 레지스터끼리 연산을 하게 된다. 레지스터는 변수

이며, 그 변수끼리 계산을 할 수 있다. 대략 이런 전제를 머리에 깔아두고 EAX, EBX, ECX, EDX에 대

한 설명을 들어보면 좀 더 간단하게 레지스터가 뭔지 이해할 수 있을 것이다.

자, 그럼 EAX 등의 레지스터가 무슨 역할을 하는지 지금부터 알아보자. 레지스터는 EAX, EBX,

ECX, EDX, ESI, EDI, EBP, ESP로 총 8개가 있다. 한 가지 알고 넘어가야 할 것은 EAX부터 EDX까지

A, B, C, D 순으로 늘어나기 때문에 보통은 이 순서대로 알고 있는 편인데, 사실 EBX는 여분의 레지스

터가 생겨난 것이며, 실제로는 EAX, EDX, ECX, EBX 등의 순서로 기억하는 것이 맞다. 물론 뭐가 제

대로 된 순서인지 안다는 것만으로 어셈블리 실력이 느는 것은 아니다. 하지만 적어도 A, B, C, D의 의

미가 알파벳 순서가 아니라 그 나름의 약어로 표현한 것이라고 알아두면 좀 더 깊이 있는 지식이 되지

않을까 한다.

Page 49: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 7

EAX

EAX로 가장 많이 들은 설명으로는 “산술 계산을 하며, 리턴값을 전달한다”였다. 물론 이것은 맞는

말이며, EAX를 축약했을 때 가장 제대로 된 정의라고 볼 수 있다. 하지만 처음에는 쉽게 와닿지 않는

다. EAX를 쉽게 생각해 보자. EAX는 단지 “변수”이며, 조금 더 구체적인 표현으로는 “가장 많이 쓰는

변수”라고 할 수 있다. 그리고 변수라서 당연히 계산식에 사용되며, 더하기, 빼기, 곱셈, 나눗셈 등에

EAX가 자주 등장한다. 그리고 함수의 리턴값이나 return 100, return FALSE 등의 코드를 사용할 때

이러한 100이나 FALSE에 해당하는 값이 바로 EAX에 기록된다고 생각하면 된다. 별거 없다. 결국 변

수의 하나일 뿐이다. 단지 CPU에서 사용하는 변수라서 어떤 개념서에서도 변수라는 부연 설명을 하

진 않지만 전혀 와닿지 않는 분들은 그냥 쉽게 변수라고 생각하자. 그것을 이해할 만한 쉬운 예제는

이후 등장할 EDI까지 대략적으로 설명한 후 풀어나가겠다. EAX의 A는 Accumulator의 약자다.

EDX

이것 역시 변수의 일종이라고 생각하자. EAX와 역할은 같되, 리턴 값의 용도로는 사용되지 않는다.

EDX 역시 각종 연산에 쓰이며, 더하고 빼고 곱하는 용도로 이용된다. 가끔은 곱하기나 나누기 등에

서 좀더 복잡한 연산이 필요할 때 덤으로 쓰이기도 한다. 하지만 역시 변수의 용도로 쓰인다고 생각하

자. EDX에서 D는 Data의 약자다.

ECX

C의 약자부터 얘기하는 것이 더 이해하기 쉽겠다. 이것은 Count의 약자로, 루프문을 수행할 때 카운

팅하는 역할을 한다. for 문에서 int i라고 선언할 때 i의 역할이라고 생각하면 쉽다. 다만 보통 우리가

사용하는 for 문에서는 i++;를 많이 사용하며, i가 특정 조건에 도달할 만큼 커지면 루프를 중단하는

데, 반대로 ECX는 미리 루프를 돌 값을 넣어놓고 (예를 들어, 5바퀴를 돈다면 먼저 5를 넣어놓고, i--;

를 한다. 즉, 감소시키며 루프 카운터가 0이 될 때까지 카운팅한다. 카운팅할 필요가 없을 때는 변수로

사용해도 무방하다.

Page 50: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

8 리버스 엔지니어링 바이블

EBX

EBX도 사실 별거 없다. 어떤 목적을 가지고 만들어진 레지스터가 아니므로 레지스터가 하나쯤 더 필

요하거나 공간이 필요할 때 등 적당한 용도를 프로그래머나 컴파일러가 알아서 만들어서 사용하고 있

다. EAX, EDX, ECX가 부족할 때 사용하기도 한다.

ESI, EDI

역시 마찬가지로 CPU가 사용하는 변수의 일종이라고 생각하면 쉽다. 다만, EAX ~ EDX는 주로 연

산에 사용되지만 ESI는 문자열이나 각종 반복 데이터를 처리 또는 메모리를 옮기는 데 사용된다. 보통

이런 설명이 많다. “ESI는 시작지 인덱스(Source Index), EDI는 목적지 인덱스(Destination Index)로

사용된다” 이 말 역시 제대로 축약한 의미이며, 정확한 설명이라고 볼 수 있다. 하지만 역시 쉽게 와닿

지 않는다. 이렇게 생각하자. memcpy(void *dest, void *src, size_t count)는 두번째 인자(source)에

서 첫번째 인자(destination)로 메모리를 복사한다. 마찬가지로 ESI와 EDI 역시 source와 destination

으로, ESI에서 메모리를 읽어 EDI로 복사한다고 생각하면 간단하다. 실제로 strcpy()나 memcpy()에

서도 ESI와 EDI를 이용한다(나중에 다시 설명하겠지만 물론 복사할 메모리 크기가 아주 크지 않은

경우는 굳이 ESI, EDI를 사용하지 않는다). Source와 Destination을 기억해 두자.

그리고 경우에 따라 al, ah 등의 레지스터도 보이는데, 이것은 16비트 레지스터로 크기가 반 정도 작

다고 생각하면 쉽다. 아래 그림을 보자.

EAX 등의 레지스터는 32비트, 즉 4바이트의 크기다. 하지만 AX는 16비트, 즉 2바이트이며 AH와

AL은 각각 8비트, 즉 1바이트의 크기다. 예를 들어, EAX가 0xaabbccdd라면 ccdd는 ax에 해당하고,

cc는 ah이며, dd는 al에 해당한다. 실제 디버거 화면을 보면서 알아보자.

Page 51: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 9

지금 EAX는 0x78563412라는 변수가 담겨져 있다. 자, 이제 아래와 같은 코드를 가동해 보자.

mov ah, byte ptr ds:[esi]mov al, byte ptr ds:[esi]mov ax, word ptr ds:[esi]

먼저 간단하게 해당 3줄의 어셈코드가 어떤 의미인지 해석해 보자. esi 주소에 담긴 값을 바이트 단

위로 (즉 1바이트만 가져와서) ah에 넣으라는 것이 첫 번째 라인이고, 같은 값을 al에 넣으라는 것이 두

번째 라인이다. 마지막 라인은 ax에 값을 넣으라는 코드다. 위 그림을 다시 보면서 아래의 디버깅 화면

과 비교해 보자. ah와 al은 각각 1바이트이며, ax는 워드로 2바이트의 크기를 차지한다는 것을 감안하

며 실제 트레이싱을 하겠다(현재 ESI에 담긴 0x401020 번지에는 83 EC 40 56라는 값이 담겨있다. 따

라서 이 값들이 eax에 옮겨지는 모습이 지금부터 포착될 것이다).

esi : 0x401020 – 83 EC 40 56

첫 번째 라인이 실행된 후에 EAX를 1바이트 단위로 잘라서 4바이트로 생각해 볼 때 두 번째 바이트

인 0x34 값이 0x83로 바뀌며, 0x78563412가 0x78568312으로 변경되었다. ah에 해당하는 위치에 새

로운 값이 들어갔다.

Page 52: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

10 리버스 엔지니어링 바이블

esi : 0x401020 – 83 EC 40 56

두 번째 라인이 실행된 후에는 네 번째 바이트인 0x12가 0x83으로 바뀌었다. 이곳이 al에 해당하는

곳이다. 그래서 EAX는 0x78568383이 되었다.

esi : 0x401020 – 83 EC 40 56

세 번째 라인이 실행된 후에는 0x8383이었던 값이 EC83으로 바뀌었다. ax에 해당하는 위치로 2바

이트가 바뀌었음을 알 수 있다. 이제 EAX, AX, AL, AH 등이 어떤 관계인지 쉽게 알 수 있지 않은가?

자, 그러면 이와 같은 16비트 레지스터가 각 EAX, EDX…별로 어떤 것이 있는지 표로 정리해 보자.

외우기 힘들다는 분도 계신데, A, B, C, D로 시작하는 각 레지스터명에 L과 H만 붙이면 된다는 점을

착안하면 굳이 외우지 않고 레지스터 이름만 봐도 충분히 어떤 녀석인지 판별할 수 있다(L은 low, H

는 high의 약자다).

32비트 16비트 상위 8비트 하위 8비트

EAX AX AH AL

EDX DX DH DL

ECX CX CH CL

EBX BX BH BL

Page 53: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 11

리틀 엔디언

바이트 저장 순서는 엔디언(endian)이라고 한다. 쉽게 설명하면 우리가 흔히 사용하는 순서의

숫자는 빅 엔디언(big endian)이라고 하며, 이것의 반대 방향은 리틀 엔디언(little endian)이

라고 한다. 쉽게 예를 들어보자. 0x12345678이라는 DWORD 값이 있다. DWORD는 4바이트

값이며, 0x12345678이라는 숫자는 1바이트씩 총 4바이트 값을 저장하게 된다. 그렇다면

12 34 56 78

로 4바이트가 된다. 그리고 이것은 빅 엔디언 방식이다. 반면 리틀 엔디언은 오른쪽부터 읽

는 방식이다. 쉽게 생각해서 한자를 읽을 때 오른쪽으로 읽을 때가 있는데, 그것과 비슷하다.

0x12345678을 리틀 엔디언으로 읽는다면

78 56 34 12

가 된다. 이것은 인텔 CPU에서 채택한 방법이고 포인터 컨트롤 속도와도 연관이 있지만, 복잡한

설명은 그만두고 오른쪽부터 읽는 것을 리틀 엔디언, 보통의 순서대로 읽는 것을 빅 엔디언이라

고 생각하면 된다. 리버스 엔지니어링을 할 때 대부분의 2바이트 또는 4바이트 값은 리틀 엔디

언을 사용한다고 생각하면서 바이너리를 해석하는 습관을 들이자.

그럼 여기까지 해두고 이번에는 실제로 EAX ~ EDX가 변수에 불과하다는 증거를 실제 코드로 살

펴보자. 다음 코드를 보면 Plus()라는 간단한 함수가 있다. a, b라는 인자를 두 개 받아 더한 결과를 반

환하는 아주 단순한 코드다.

int Plus(int a, int b){ return a + b;}

이것을 빌드한 후 디스어셈블해 보면 아래와 같다.

mov eax, dword ptr ss:[esp+8]mov ecx, dword ptr ss:[esp+4]add eax, ecxretn

Page 54: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

12 리버스 엔지니어링 바이블

이미 알고 있는 분들은 알겠지만 아직 어셈블리를 마저 공부하지 않은 사람들을 위해 설명을 덧붙

이면, 첫 번째 파라미터 a는 [esp+4]에 담겨 있으며, 두 번째 파라미터 b는 [esp+8]에 담겨 있다. 이것

을 각각 eax와 ecx에 mov로 담는다.

add eax, ecx

그리고 add 명령어는 두 오퍼랜드의 값을 더해서 첫 번째 오퍼랜드에 담는다는 내용이다. 앞에서 연

산은 레지스터를 이용한다고 했다. 만약 메모리 스택 안에서 연산이 가능했다면 다음과 같이 연산을

시도했을 것이다.

add [esp+8] , [esp+4]

하지만 분명히 어셈블리에서 연산은 레지스터를 이용한다고 했다. 따라서 위와 같은 코드는 존재할

수 없다. 메모리끼리는 연산할 수가 없다. 따라서 [esp+8]과 [esp+4]에 들어 있는 값을 레지스터인 eax

와 ecx에 담고 그것끼리 add 연산을 시작했다.

retn

그리고 eax에는 이미 리턴값인 두 인자의 덧셈 결과가 담겨 있다. 이대로 리턴해 버리면 된다. 자, 간

단하지 않은가? 그럼 이 상태에서 레지스터의 역할을 보기 위해 몇 가지 바꿔 보자. 다음 코드로 이어

진다.

mov ebx, dword ptr ss:[esp+8]mov edx, dword ptr ss:[esp+4]add edx, ebxmov eax, edxretn

구조는 똑같지만 레지스터가 바뀌었음을 알 수 있다. 첫 번째 파라미터인 [esp+4]를 ecx에 넣었던

것을 edx에 넣도록 바꾸고, 두 번째 파라미터인 [esp+8]를 eax에 넣었던 것을 ebx에 넣도록 바꿨다. 그

리고 이것을 edx에 더한 다음 그것을 다시 eax에 넣고 리턴하게 했다. 결과는 어떨까? 비교를 위해 앞

에서 본 Plus() 함수와 함께 실행되도록 위 어셈블리 구문을 이용해 PlusAsm()라는 함수를 만들어

실제로 호출해 보았다.

Page 55: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 13

어셈블리로 구현한 PlusAsm()

#include <windows.h>#include <stdio.h>

int Plus(int a, int b){ return a + b;}

__declspec(naked) PlusAsm(int a, int b){ __asm { mov ebx, dword ptr ss:[esp+8] mov edx, dword ptr ss:[esp+4] add edx, ebx mov eax, edx retn }}

void main(int argc, char *argv[]){ int value = Plus(3,4); printf("value: %d\n", value); int value2 = PlusAsm(3,4); printf("value2: %d\n", value2);

return;}

결과가 똑같다는 것을 알 수 있다. esi와 edi를 사용해도 마찬가지다. 결과는 동일하다.

mov esi, dword ptr ss:[esp+8]mov edi, dword ptr ss:[esp+4]add esi, edimov eax, esiretn

Page 56: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

14 리버스 엔지니어링 바이블

물론 컴파일러가 이런 식으로 레지스터를 목적에 부합하지 않게 엉망으로 사용해 가며 코드를 만들

어내진 않을 것이다. 각 레지스터에도 당연히 각기 고유한 목적이 있으므로 그 목적에 맞게 사용되는

것이 사실이다. 하지만 그 목적 자체를 너무 중요하게 생각하다 보니 기본 개념 자체를 이해하지 못하고

있는 경우가 있어서 이런 웃기는 코드를 한번 만들어서 레지스터가 별거 아니라는 내용을 증명해 보았

다. 레지스터는 이처럼 단순하다. 레지스터는 단지 CPU가 사용하는 변수에 불과하다. 연산과 값 처리

를 위해 존재하므로 그냥 보이는 그대로 해석하면 된다. 레지스터를 결코 어렵게 생각하지 말자.

__declspec(naked)

맨 처음 등장한 코드 앞에 접두어로 붙은 것이 무엇인지 궁금한 분이 계실지도 모르겠다. naked

라는 의미는 ‘나체가 되었다’라는 뜻으로 코드가 벌거벗은 형태라고 생각할 수 있다. 실제로 함

수 하나를 만들어서 빌드해 보면 컴파일러는 내부적으로 해당 함수에서 변수를 몇 개 사용하고,

구조체를 몇 개 사용하는지 등에 대한 내용을 분석해 관련 데이터 덩어리를 사용할 수 있는 만

큼의 스택을 준비한다. 그래서 이후 함수의 구조에서도 나오겠지만 함수의 엔트리 포인트에서

는 개발자가 작성한 코드의 첫 줄이 등장하는 것이 아니라 컴파일러가 자체적으로 생성한 스택

을 확보하는 작업에 대한 코드부터 등장한다. naked는 그것을 방지하기 위한 접두어다. naked

를 사용하면 이제부터 이 함수 안에서는 부수적인 코드를 전혀 사용하지 않을 것이라고 지정하

게 되며, 컴파일러는 이 함수 안에 어떤 자체적인 코드도 생성하지 않는다. 심지어 리턴값조차

컴파일러가 만들어주지 않는다. 따라서 naked 함수를 만들려면 개발자가 스택이나 변수 할당,

레지스터 사용 등의 모든 처리 내용을 모두 작성해야 한다. 보통 이번 예제처럼 순수 어셈블리로

만들어진 코드를 작성할 때 이와 같이 naked를 사용한다. 앞으로도 naked 함수는 이 책 곳곳에

등장할 것이다. 다시 한번 나올 때 또 설명하도록 하겠다.

Page 57: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 15

외울 필요가 없는 어셈블리 명령어

지금까지 설명한 내용을 제대로 숙지했다면 어셈블리를 처음 보는 사람이더라도 대략 어셈블리가 어

떤 식으로 굴러가는지 대략은 파악했을 것이다. 이제는 옵코드에 해당하는 명령어를 살펴보자. 필자

는 개인적으로 악기를 좋아하는데, 악기나 음악에 대한 부분을 프로그래밍과 결부시켜 비교하는 작

업을 즐겨 쓰는 편이다(아마 책을 읽다 보면 앞으로도 계속 나올 것이다). 많은 사람들이 통기타를 한

번 배우고 싶어한다. 통기타는 코드 몇 개만 외우면 바로 연주가 가능할 정도로 친숙하지만, 코드가

그 상황에 잘 잡히지 않는다는 어려움이 있다. 그래서 통기타를 처음 익히는 사람들을 보면 일단 코드

를 잡는 것이 먼저라고 보고 기계적으로 코드 책을 펼쳐보면서 여섯 줄의 기타 위에 손가락을 올려가

며 각 손가락이 짚는 자리를 외우려 애쓴다. 결과는 어떨까? 필자의 경험상 이런 식으로 접근하는 통

기타 입문자들은 90% 이상 중도에 포기하는 모습을 많이 봤다. 실제로 통기타 책에 나오는 각종 코드

는 입문자가 어떤 노래를 연주하느냐에 따라 평생 한 번도 짚어보지 않을 코드가 수두룩하다. 그러나

책에 나온 순서대로 기계적으로 코드를 외워 가고 있으니 힘겹게 외운 코드가 나중에 잘 쓰이지도 않

아서 결국은 잊어버리게 되고, 오히려 진짜 중요한 코드를 익히는 데에 써야 할 시간을 엉뚱한 곳에 낭

비하고 만다.

// scan all dll if tr->dwPid = dwDebugeePid; tr->dwScanOption = DC_MEMCMP; tr->hDlg = hDlgWnd; tr->hShowText = hShowText; tr->dwModuleCount = dwModuleCount; HANDLE hThread = CreateThread(NULL, 0, ScanAllDllThread, (LPVOID)tr, 0, &dwThreadId); }

(SendMessage(hSelectAllDll, BM_GETCHECK, 0, 0) == BST_CHECKED) { // …… PTHREAD_PARAM tr; tr = (PTHREAD_PARAM)malloc(sizeof(THREAD_PARAM));

// scan all dll if (SendMessage(hSelectAllDll

필자는 바로 이러한 경우가 어셈블리 명령과 유사하다고 생각한다. 어셈블리 명령어는 정말 너무나

많다. 그래서 사실 그것을 다 암기하기란 사실상 불가능에 가깝다. 필자도 당연히 다 외우고 있지 못하

다. 심지어 어셈블리 입문서나 인터넷에서 찾을 수 있는 어떤 리버스 엔지니어링 관련 자료에서 소개

한 필수라고 붙여놓은 어셈블리 명령어 중에서도 필자가 모르는 것이 수두룩하다. 기타 코드도 마찬

가지로 책에는 필수 코드라고 나와 있지만 실제로는 거의 쓰지도 않는 코드일 때도 많다. 물론 그렇게

잘 몰라도 기타를 연주하거나 리버스 엔지니어링 관련 업무를 하는 데는 크게 지장이 없다.

Page 58: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

16 리버스 엔지니어링 바이블

그렇다고 모든 명령어와 코드를 무시하라는 이야기는 아니다. 가장 기본적이며 필수적인 몇 개만

확실히 이해해 두고, 나머지는 필요할 때마다 찾아서 보는 방법으로도 충분하다는 의미다. 그리고 기

본적으로 익혀야 할 옵코드는 구조가 서로 다른 것들끼리 외워두는 것이 좋다. 필수 명령어만 머릿속

에 집어넣고 나머지는 실제 리버스 엔지니어링을 할 때 해당 어셈블리 구문이 나오기 전까지는 굳이

펼쳐보지 않아도 무방하다.

PUSH, POP

스택에 값을 넣는 것을 PUSH, 그리고 스택에 있는 값을 가져오는 것이 POP이다. 프로그래밍해본 사

람치고 스택을 모르는 분은 없으므로 굳이 설명할 필요는 없는 부분이다(PUSHAD, POPAD는 모든

레지스터를 PUSH하고 POP하라는 명령어다). 오퍼랜드는 push eax, push 1, push edx… 등과 같이 1

개만 있으면 된다.

MOV

지금까지 설명한 내용을 열심히 읽었다면 역시 아주 쉬운 명령어일 것이다. MOV는 단지 값을 넣는 역

할을 한다. 가령 MOV eax, 1은 eax에 1을 넣는 코드가 되고, MOV ebx, ecx는 ebx에 ecx를 넣는 코

드가 된다.

LEA

LEA는 MOV와 헷갈려 하는 분들이 많아서 좀더 예를 들어가며 상세히 설명하겠다. LEA는 아주 단

순하다. 주소를 가져오라는 얘기다. MOV가 값을 가져오는 것이라면 LEA는 주소를 가져온다. 따라서

LEA는 가져올 src 오퍼랜드가 주소라는 의미로 대부분 [ ]로 둘러싸여 있다. 몇 가지 예를 들어보자.

가정 : 레지스터와 메모리에 다음과 같은 값이 들어 있다.

esi : 0x401000 (esi에는 0x401000이라는 값이 들어 있다)

*esi : 5640EC83 (esi가 가리키는 번지에는 5640EC83라는 값이 들어 있다)

esp+8 : 0x13FF40

*(esp+8) : 33

Page 59: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 17

lea eax, dword ptr ds:[esi]

: esi가 0x401000이므로 eax에는 0x401000이 들어온다.

mov eax, dword ptr ds:[esi]

: esi가 0x401000이므로 eax에는 0x401000 번지가 가리키는 5640EC83이라는 값이 들어온다.

lea eax, dword ptr ss:[esp+8]

esp+8은 스택이며, eax에는 0x13FF40라는 값이 들어온다

mov eax, dword ptr ss:[esp+8]

esp+8은 스택이며, eax에는 0x13FF40가 가리키는 값인 33이 들어온다

MOV와의 차이점을 확실히 알 수 있을 것이며, 사실 MOV보다 간단하다.

ADD

위의 예제에서도 나왔다시피 src에서 dest로 값을 더하는 명령어다.

SUB

ADD와 반대되는 뺄셈 명령어로, ADD와 쌍으로 생각하자.

INT

인터럽트를 일으키는 명령어다. 뒤의 오퍼랜드로 어떤 숫자가 나오느냐에 따라 각기 다른 처리가 일

어난다. MS-DOS 시절에는 애플리케이션에서 즉시 인터럽트를 일으킬 수 있는 무풍지대의 공간이

라 INT로 갖가지 응용이 가능했지만 현재의 32비트 시대에서는 애플리케이션 레벨에서의 인터럽트

는 한계가 있고, ring0 레벨로 내려가기 전까진 거의 사용할 일이 없는 명령어가 되었다(네이티브 API

를 호출할 때 이용되지만 그 내용은 이 장의 수준을 벗어나므로 나중에 다루겠다). 아마 리버스 엔지

니어링을 하다 보면 가장 많이 만나는 것은 INT 3 명령어로 옵코드가 0xCC인 DebugBreak() 정도일

것이다.

Page 60: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

18 리버스 엔지니어링 바이블

CALL

함수를 호출하는 명령어다. CALL 뒤에 오퍼랜드로 번지가 붙는다. 해당 번지를 호출하고 작업이 끝

나면 CALL 다음 번지로 되돌아온다. 왜냐하면 CALL로 호출된 코드 안에서는 반드시 RET를 만나

게 되어 다시 호출한 쪽으로 돌아오기 때문이다. 나중에 “함수의 구조” 부분에서 좀 더 자세히 설명

한다.

INC, DEC

INC는 i++;이고 DEC는 i--;라고 생각하자.

AND, OR, XOR

dest와 src를 연산한다. 다만 XOR은 dest와 src를 동일한 오퍼랜드로 처리 가능한데, 예를 들어 XOR

EAX, EAX를 수행하면 EAX가 0이 된다. 같은 값으로 XOR을 하면 0이 되기 때문에 XOR로 같은 오

퍼랜드를 전달했을 때 이것은 변수를 0으로 초기화하는 효과를 줄 수 있다. 그 밖에 AND, OR, XOR

를 모르는 분은 없을 것으로 생각한다. 이 책은 전산개론 서적이 아니므로 만약 비트 연산에 대해 모

르는 사람은 인터넷을 참고하자.

NOP

아무것도 하지 말라는 명령어다. 해킹이나 리버스 엔지니어링에서 가장 많이 쓰이는 명령어이기도

하다.

CMP, JMP

비교해서 점프하는 명령어

Page 61: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 19

이 정도만 훑어봐도 이후 나오는 어셈블리 명령어는 사실 그때그때 검색하면서 찾는 것으로도 충분

히 보완할 수 있다. 영어 원서를 보면서 사전 없이 100% 모두 읽을 수 있는 사람이 있는가? 한국어로

된 책을 볼 때도 사전이 없으면 무슨 뜻인지 모를 때가 있다. 그런 상황을 위안 삼아 모르는 명령어가

나올 때 굴하지 말고 구글을 띄워 어셈블리 명령어를 검색하자. 인텔 매뉴얼을 가지고 있어도 좋다. 잘

찾아보면 사전처럼 전체 명령어가 정리된 훌륭한 문서를 발견하는 행운을 맛볼 수도 있다.

리버스 엔지니어링에 필요한 스택

스택(stack)은 이미 자료구조에서 충분히 배웠을 것이다. 스택 하면 생각나는 것은 LIFO(Last In First

Out)으로 먼저 넣은 것이 나중에 나오고, 나중에 넣은 것이 먼저 나오는 구조라고 할 수 있다. 리버스

엔지니어링을 위해 스택에 관해 알아야 할 지식은 일단 다음과 같다. 하나씩 알아보자.

1. 함수 호출 시 파라미터가 들어가는 방향

2. 리턴 주소

3. 지역 변수 사용

다음 장에서 좀더 상세히 설명하겠지만 함수 안에서 스택을 사용하게 되면 보통 다음과 같은 코드

가 함수의 엔트리 포인트에 생성된다.

push ebpmov ebp, espsub esp, 50h

이 코드를 한번 해석해 보자. 먼저 ebp 레지스터를 스택에 넣는다. 그리고 현재 esp의 값을 ebp에 넣

는다. 자, 이제 ebp와 esp가 같아지면서 이제 이 함수에서 지역변수는 ebp에서부터 얼마든지 계산할

수 있다. ebp를 기준으로 오프셋을 더하고 빼는 작업으로 스택을 처리할 수 있게 된다는 이야기다.

그리고 sub esp, 50h는 esp에서 50h만큼을 뺀다는 의미인데, 스택은 LIFO 특성으로 인해 아래로

자란다. 따라서 특정 값만큼 뺀다는 것은 그만큼 스택을 사용하겠다는 이야기가 된다. 즉, 50h만큼 지

역변수를 사용하겠다고 해석할 수 있다.

Page 62: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

20 리버스 엔지니어링 바이블

그렇다면 이제 생각해 보자, ebp가 현재 함수에서 스택의 맨 위가 되었고, 첫 번째 번지가 되었다. 그

리고 사이즈를 빼가며 자리를 확보하고 있으므로 결국 지역 변수는 “-” 마이너스의 형태로 계산이 가

능하다. 4바이트 단위로 움직이는 변수라고 가정했을 때 ebp-4라면 첫 번째 지역변수가 될 것이고,

ebp-8은 두 번째 지역변수가 될 것이다. 즉 ebp-x 형태로 변수를 계산할 수 있다.

함수의 호출

그렇다면 이번에는 파라미터에 대해 알아보자. 예를 들어, HelloFunction이라는 함수가 있다.

DWORD 타입으로 3개의 인자를 받는 함수 타입이다.

DWORD HelloFunction(DWORD dwParam1, DWORD dwParam2, DWORD dwParam3)

이 함수를 다음과 같이 호출했다고 해보자.

HelloFunction 호출

main(){ DWORD dwRet = HelloFunction(0x37, 0x38, 0x39); if (dwRet) // ……}

위 코드를 리버스 엔지니어링해 보면 다음과 같다.

push 39hpush 38hpush 37hcall 401300h

함수의 인자는 스택에 값을 LIFO 순서대로 넣기 때문에 실제 소스 코드에서 호출한 것과는 반대로

들어간다. 따라서 위와 같은 양상을 보인다. call 401300h 안으로 들어가서 생각해 보면 mov esp, ebp

코드를 거치기 때문에 아까 지역 변수를 봤을 때는 ebp-x 등과 같이 마이너스로 스택에 보관된 변수

를 사용했는데, 파라미터를 push로 넣어 놓았기 때문에 이 값들에 접근하려면 ebp에서 오프셋을 더

하는 방식으로 계산해야 한다. 즉 파라미터는 ebp+x 형태로 계산할 수 있다. ebp+8이 첫 번째 인자인

37h 이며, ebp+0xc가 두 번째 인자인 38h, ebp+0x10이 세번째 인자인 39h가 된다.

Page 63: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

1장_리버스 엔지니어링만을 위한 어셈블리 21

리턴 주소

그렇다면 거론하지 않은 ebp+4에는 무엇이 있을까? ebp+4에는 이 함수가 끝나고 돌아갈 리턴 주소

가 담긴다. 직접 눈으로 확인해 보자. HelloFunction 함수 안에 다음과 같이 리턴 주소를 가져오는 어

셈블리 코드를 삽입해 보자.

리턴 주소 출력

DWORD HelloFunction(DWORD dwParam1, DWORD dwParam2, DWORD dwParam3){ DWORD dwRetAddr = 0; __asm { push eax mov eax, [ebp+4] mov dwRetAddr, eax pop eax } printf(“dwRetAddr: %08x\n", dwRetAddr);}

결과를 보면 dwRetAddr은 “if (dwRet)”의 위치를 출력한다는 사실을 알 수 있다. HelloFunction()

을 호출한 뒤 호출한 쪽의 다음 번지가 바로 리턴 주소다.

지금까지의 내용은 아래 그림으로 요약할 수 있다.

이상으로 리버스 엔지니어링에 필요한 어셈블리를 간략하게 살펴봤다. 설명을 누락한 부분은 플래

그 부분인데, 이 부분은 기계적인 설명이 들어가기보다는 2부, “리버스 엔지니어링 중급”에서 실전 코

드와 함께 좀더 상세하게 다룰 예정이니 성격이 급하신 분들은 이후 장을 건너뛰고 바로 그쪽부터 살

펴보고 와도 무방하다.

Page 64: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

22 리버스 엔지니어링 바이블

Page 65: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

02

C 문법과 디스어셈블링

프로그래밍의 기본은 C 문법이며, 리버스 엔지니어링의 기본은 C 코드가 어셈블리로 바뀌었을 때

어떻게 해석하느냐다. 어셈블리 코드를 눈으로 살펴볼 때 머릿속에서는 그와 동시에 어셈 코드

덩어리가 C 코드로 변환되는 현상이 일어나야 한다. C에도 문법이 있듯이 리버스 엔지니어링에

사용되는 어셈블리 코드에도 정해진 문법이 있다. 마구잡이로 흐트러져 있는 듯한 어셈블리

코드의 홍수 안에서 C 코드를 추출해 내는 과정이 몸에 배어야 한다.

함수의 기본 구조, 함수의 호출 규약(__cdecl, __stdcall, __fastcall, __thiscall), 조건문, 반복

문, 구조체와 API 호출

Page 66: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

24 리버스 엔지니어링 바이블

대부분의 사람들은 어셈블리보다 C 소스에 강한 법이다. 문법 사용이 제한적이며, 언제나 하나의 명

령어로 수많은 문장을 조합해서 코딩해야 하는 어셈블리는 확실히 보통 사람들에게 친숙해지기 어려

운 면이 있다. 특히 C 문법을 먼저 공부한 사람들에게는 C 코딩의 스타일대로만 생각하도록 굳어져 있

기 때문에 어셈블리 방식으로 생각하기가 쉽지 않은 편이다. 하지만 어셈블리는 앞서 살펴본 대로 생

각 외로 간단한 언어이며, 접근 방식에 따라 오히려 더 쉽게 읽어내려갈 수도 있다. 그 이유는 뭘까? 사

실 C 코드에서 자주 사용되는 코드가 항상 비슷하듯이 어셈블리 역시 코드에 일정한 패턴이 있기 때

문이다. 따라서 그런 유형과 패턴을 잘 알아놓는다면 어셈 코드를 역분석하는 리버스 엔지니어링 작

업도 아주 어려운 작업으로 느껴지지 않을 수 있다. 이번 장에서는 C 문법뿐 아니라 어떤 언어에서든

가장 기본적으로 등장하는 조건문과 반복문 등에 대해 어셈블리로 어떻게 표현되는지 살펴보겠다.

함수 규약, 구조체 등에 대해서도 그 구조를 알아보고, 개발자들이 흔히 사용하는 코드 패턴이 어셈

블리로 어떻게 변환되는지 그 규칙을 몸소 느낄 수 있을 것이다.

함수의 기본 구조

리버스 엔지니어링의 기본을 익히기 위해서는 먼저 함수에 대해 알아야 한다. 소스 코드는 당연히 수

많은 함수로 구성돼 있으며, 각 함수의 역할을 파악하는 것이 필수다. 함수가 하는 기능이 무엇인가에

대한 굵은 가지부터 파악하는 것으로 시작해, 세세하게는 파라미터가 몇 개이고, 리턴값으로 어떤 것

들이 전달되는지까지도 분석할 수 있어야 한다. 그리고 그러한 작업이 가능하려면 함수의 몸체가 어

셈블리 코드로 어떻게 구성되는지부터 습득할 필요가 있다.

리버스 엔지니어링을 할 때 가장 골치 아픈 것 중 하나가 개발자가 직접 코딩한 부분이 아닌 빌드 시

에 컴파일러가 자동으로 생성해내는 코드를 필터링하는 작업이다. 따라서 훌륭한 리버서가 되려면 이

러한 군더더기 코드는 가볍게 넘어갈 수 있는 능력이 필요하다. 지면에서 그러한 내용을 다 다룰 수는

없지만 최소한 함수를 제작할 때 앞뒤로 붙는 코드에 대해 간단하게나마 살펴볼 필요가 있다. sum()

이라는 간단한 함수와 그것을 디스어셈블링한 코드를 살펴보자.

함수의 기본 구조

int sum(int a, int b){ int c = a + b; return c; }

Page 67: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 25

push ebpmov ebp, esppush ecxmov eax, [ebp+arg_0]add eax, [ebp+arg_4]mov [ebp+var_4], eaxmov eax, [ebp+var_4]mov esp, ebppop ebpretn

단지 2줄의 코드인데도 (선언부를 포함해도 3줄), 디스어셈블된 코드를 보니 10줄이나 된다. 이것이

리버스 엔지니어링의 가장 기본적인 어려움이다. 짧은 코드도 어셈블리 구문으로 바뀌면 코드의 양

도 길어지고, 더불어 특유의 문법으로 인해 가독성도 매우 떨어진다. 그래서 컴파일러가 자동으로 생

성하는 코드를 걸러내고 중요한 곳만 포착해내는 능력이 필요하다.

자, 시작부터 1장, “리버스 엔지니어링만을 위한 어셈블리”에서 예습한 내용이 나온다. 일단 자세한

의미 파악은 나중으로 돌리고 예제 코드의 앞뒤 구문을 보자. push ebp와 pop ebp로 끝나는 코드가

보인다. 결론부터 얘기해서 이것이 바로 함수의 처음과 끝이다. 스택을 사용하지 않아도 될 만큼 간단

한 함수라면 예외이긴 하지만 대체로 함수를 만들면 대부분 이러한 인터페이스를 갖게 된다. 그 이유

를 생각해보자

push ebpmov ebp, esp

ebp는 스택 베이스 포인터다. 그리고 push ebp를 통해 지금까지의 베이스 주소를 스택에 보관한다.

그리고 mov ebp, esp를 통해 현재의 스택 포인터인 esp를 ebp로 바꾼다. 즉, 지금까지의 기준이 될 스

택 베이스 포인터를 일단 백업해 두고, 새로운 포인터를 잡는 것이다. 함수 안에서 스택을 통해 계속 메

모리를 이용할 것이므로 함수의 시작 번지에서는 항상 이 같은 초벌 작업을 진행한다. 다시 말해 함수

의 시작은 곧 새로운 스택을 사용한다고 생각할 수 있다. 그래서 스택 베이스 포인터를 보관해 놓고, 현

재의 스택 포인터를 베이스로 잡아두며 새 삶을 시작한다.

그리고 종료 코드는 아래와 같이 나타난다.

mov esp, ebppop ebp

Page 68: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

26 리버스 엔지니어링 바이블

함수가 종료되면 지금까지 사용한 스택 위치를 다시 원래대로 돌려놓는다. 그 부분이 push ebp로

시작해 pop ebp로 끝나는 코드다. 대부분의 함수는 저렇게 시작과 종결을 맺는다. 따라서 저런 모양

을 보면 ‘아 함수의 시작과 끝이구나’라고 생각하면 된다. 스택을 사용하지 않는 간단한 함수의 경우에

는 이 같은 패턴을 밟지는 않지만 대부분의 함수는 push ebp를 통해 함수의 명줄을 계산한다.

함수의 호출 규약

리버스 엔지니어링을 하기 위해 디버거를 켜고 바이너리를 올려놓았다. 어셈블리가 눈앞에 주르륵 펼

쳐진다. 가장 먼저 뭘 생각해야 할까?

“아, 지금 보는 이 함수의 역할은 무엇이고 파라미터는 이런 구조로 넘어가는구나!”

우리가 리버스 엔지니어링을 할 때 반드시 필요한 것 중 하나는 각 함수의 역할을 파악하는 것이다.

코드의 목적을 알아낸다면 리버스 엔지니어링 작업의 50%는 달성한 것이나 다름없다. 그리고 그것의

선행 작업으로 함수가 어떻게 생겼고 인자가 몇 개인지 등에 대한 정보를 추출해 낼 수 있어야 한다.

이제부터 그 내용을 확인할 수 있는 함수 호출 규약에 대해 알아보자.

함수 호출 규약에는 여러 가지 방식이 있다. 대표적으로 __cdecl, __stdcall, __fastcall, __thiscall 네

가지가 있다. 여기서 우리가 확인할 것은 디스어셈블된 코드를 보고 이것이 어떤 콜링 컨벤션(calling

convention)에 해당하는지 파악하는 것이다. 이를 확인하는 목적은 리버스 엔지니어링을 할 때 call

문을 보고 이 함수의 인자가 몇 개이고 어떤 용도로 쓰이는지를 분석하기 위해서다. 간단한 하나의 함

수를 각 호출 규약별로 정의해서 빌드해 보자. 아래에 디스어셈블된 코드가 나와 있다. 먼저 __cdecl

부터 확인해 보자.

__cdecl을 사용한 함수의 디스어셈블

int __cdecl sum(int a, int b){ int c = a + b; return c; }

int main(int argc, char* argv[]){ sum(1,2); return 0;

Page 69: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 27

}

sum:push ebpmov ebp, esppush ecxmov eax, [ebp+arg_0]add eax, [ebp+arg_4]mov [ebp+var_4], eaxmov eax, [ebp+var_4]mov esp, ebppop ebpretn

main:push 2push 1call calling.00401000add esp, 8

함수 본체 말고 call calling.00401000이라고 돼 있는, 함수를 호출하는 곳을 살펴보는 것이 함수

역분석의 1차 과제다(이 소스에서는 main() 함수에 해당한다). 항상 call 문의 다음 줄을 살펴서 스택

을 정리하는 곳이 있는지 체크해야 한다. 이 코드처럼 add esp, 8과 같이 스택을 보정하는 코드가 등

장한다면 그것은 __cdecl 방식의 함수라고 생각할 수 있다(__cdecl 방식은 함수 밖에서 스택을 보정

한다). 그리고 해당 스택의 크기로 함수 파라미터의 개수까지 확인할 수 있다. 인자는 4바이트씩 계산

되므로 스택을 8바이트까지 끌어올린다는 점에서 파라미터가 2개인 함수라는 점까지 파악할 수 있

다. 자, 지금까지 알아낸 정보를 정리해 보자.

1. __cdecl 방식

call calling.00401000 밑에 add esp, 8을 하는 것으로 봐서 함수를 호출한 곳에서 스택을 보

정하는 __cdecl 방식임.

2. 파라미터는 2개

add esp, 8 그리고 push 문이 2개라는 점을 봐서 4바이트 파라미터가 두 개라는 것을 확인.

3. 리턴 값이 숫자

함수의 맨 마지막 부분에 eax에 들어가는 값이 숫자라는 것을 봐서 리턴 값은 주소 같은 값

이 아닌, 숫자임을 확인.

Page 70: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

28 리버스 엔지니어링 바이블

3)번을 빼더라도 이처럼 스택을 처리하는 코드에서 두 가지 정보를 알 수 있다는 것을 확인했다. 이

번에는 위 함수를 __stdcall 방식으로 바꿔서 다시 디스어셈블해 보자. 다음을 보자.

__stdcall을 사용한 함수의 디스어셈블

int __stdcall sum(int a, int b){ int c = a + b; return c; }

sum:push ebpmov ebp, esppush ecxmov eax, [ebp+arg_0]add eax, [ebp+arg_4]mov [ebp+var_4], eaxmov eax, [ebp+var_4]mov esp, ebppop ebpretn 8 main: push 2push 1call calling.00401000

똑같은 C 코드인데도 __cdecl를 붙였느냐 __stdcall를 붙였느냐에 따라 어셈블리 코드가 미묘하

게 달라졌다. 이전 예제와의 차이점으로 add esp, 8을 사용한 코드가 보이지 않는다는 것을 알 수 있

다. 이것은 main() 안에서 sum()을 사용한 뒤 어떠한 스택 처리도 없다는 이야기다. 대신 sum()의 본

체의 후반부의 리턴문에 그냥 retn이 아닌 retn 8을 했다는 사실을 알 수 있다. 즉, 이 경우에는 함수

안에서 스택을 처리한다는 것을 알 수 있다. 이런 식으로 __stdcall 방식은 함수 안에서 스택을 처리한

다. 그래서 8바이트의 스택 보정과 파라미터가 2개라는 판단은 함수 내부에서 확인해야 한다. 대표적

으로 Win32 API는 __stdcall 방식을 이용한다. Win32 API 하나를 직접 분석해 보자.

Page 71: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 29

MessageBoxA()를 디스어셈블한 모습이다. 77D30830 번지에서 retn 10을 한다는 것을 알 수 있다.

retn 10은 16진수이며, 10진수로는 16즉, 4 x 4가 된다. 여기서 볼 수 있듯이, 또 한 가지 참고할 만한

점은 retn을 보면 알 수 있는 정보들이다. retn이 보이고 retn 10 같은 별도의 숫자가 보이지 않는 상태

에서 call 후에 add esp, x도 보이지 않는다면 이 함수는 __stdcall 방식이자 파라미터가 없는 경우라

고 볼 수 있다.

아무튼 MessageBox()의 경우 인자가 4개이므로 정확히 16바이트만큼 스택 처리를 한다. 아래는

MessageBox()의 프로토타입이다. 역시 인자가 4개라는 것을 다시 확인할 수 있다.

int MessageBox( HWND hWnd, // handle to owner window LPCTSTR lpText, // text in message box LPCTSTR lpCaption, // message box title UINT uType // message box style);

이번에는 __fastcall을 살펴보자. 다음 예제를 보자.

__fastcall을 사용한 함수의 디스어셈블

int __fastcall sum(int a, int b){ int c = a + b; return c; }

sum:push ebpmov ebp, espsub esp, 0Chmov [ebp+var_C], edxmov [ebp+var_8], ecxmov eax, [ebp+var_8]add eax, [ebp+var_C]mov [ebp+var_4], eaxmov eax, [ebp+var_4]mov esp, ebp

Page 72: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

30 리버스 엔지니어링 바이블

pop ebpretn

main:push ebpmov ebp, espmov edx, 2mov ecx, 1call sub_401000xor eax, eaxpop ebpretn

sub esp, 0Ch로 스택 공간을 확보하고 edx 레지스터를 사용한 것을 알 수 있다. __fastcall은 함수

의 파라미터가 2개 이하일 경우, 인자를 push로 넣지 않고 ecx와 edx 레지스터를 이용한다. 메모리를

이용하는 것보다 레지스터를 사용하는 것이 속도가 훨씬 빠르다. 따라서 __fastcall을 사용하는 경우

에는 인자가 2개 이하면서 빈번히 사용되는 함수에 쓰이는 편이다. 그러므로 리버스 엔지니어링을 할

때 함수 호출 전에 edx와 ecx 레지스터에 값을 넣는 것이 보이면 __fastcall 규약의 함수라고 생각할

수 있다.

다음으로는 __thiscall이다.

Class CTemp{public: int MemberFunc(int a, int b);};

mov eax, dword ptr [ebp-14h]push eaxmov edx, dword ptr [ebp-10h]push edxlea ecx, [ebp-4]call 402000

__thiscall은 주로 C++의 클래스에서 이용되는 방법이다. 특징으로는 현재 객체의 포인터를 ecx에

전달한다는 것이 있다. 이 같은 구조를 설명하자면 클래스에 대한 이론적인 내용을 되새겨보는 것이

좀더 효과적일 듯하다. 먼저 클래스는 객체지향 프로그래밍의 개념이며, 하나의 클래스만 정의해 두

면 얼마든지 여러 개의 독립적인 객체를 만들 수 있다. 따라서 모양은 완전히 동일한 클래스더라도 실

제 오브젝트 입장에서 생각해 보면 이 클래스는 서로 다른 메모리 번지에 존재하게 된다. 그리고 그것

Page 73: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 31

을 각각 구분하기 위해서는 현재 자신이 어떤 객체를 이용하고 있는지 구분해줄 값이 필요하다. 그것

이 C++에서는 this 포인터로 사용되고 있다. 이쯤에서 감이 오지 않는가? 바로 ecx로 전달되는 값이

this 포인터가 된다. 그리고 해당 클래스에서 사용하고 있는 멤버 변수나 각종 값은 다음과 같이 ecx

포인터에 오프셋 몇 번지를 더하는 식으로 사용할 수 있다.

ecx+xecx+yecx+z

이것이 __thiscall의 특징이다. 물론 인자 전달 방법이나 스택 처리 방법은 __stdcall과 동일하

다. 이번 장에서는 C 문법과 리버스 엔지니어링을 다루고 있으므로 일단은 이만큼만 살펴보고, __

thiscall은 다음 장의 C++ 리버스 엔지니어링 부분에서 좀더 상세하게 다룰 예정이니 잠시 후에 다시

알아보자.

알아두기

간단한 C 코드를 콘솔에서 코딩하고 디스어셈블할 때 책이나 인터넷 등에서 나온 것과 조금 다르

거나 뭔가 코드가 생략되는 것 같다는 질문을 받을 때가 종종 있다. 그 이유는 컴파일러 최적화 옵

션을 어떻게 사용하느냐에 따라 바이너리의 크기와 생성되는 코드의 모습이 미묘하게 달라지기

때문이다. Optimizations가 기본적으로는 Maximize Speed로 되어 있지만(VC 6.0 기준) 그대

로 사용하면 콘솔에서 간단한 함수를 작성하는 경우 최적화 옵션 때문에 push ebp, mov ebp,

esp 등의 코드가 생성되지 않는다. 또 이 경우에는 몸체만 있고 사용하지 않는 함수는 컴파일러가

필요없는 코드라고 간주해서 아예 빌드에서 제외해 버리기도 한다. 따라서 자신이 작성한 코드를

원본 그대로 확인하고 싶다면 Optimizations 옵션을 Default로 변경해야 한다.

if 문

지금까지 함수의 규약을 대강 살펴봤으니, 이번에는 모든 언어에서 기본 중의 기본이 되는 조건문에

관해 알아보자. 먼저, C 코드의 조건문이 디스어셈블됐을 때 어떻게 바뀌는지 살펴보겠다. 다음 코드

는 간단한 조건문의 예다. 이 코드가 바이너리로 바뀐 뒤 디스어셈블된 부분을 유심히 살펴보자.

Page 74: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

32 리버스 엔지니어링 바이블

조건문 디스어셈블링

int Temp(int a){ int b = 1;

if (a == 1) { a++; } else { b++; }

return b;}

int main(int argc, char* argv[]){ Temp(1);}

.text:00401000 push ebp

.text:00401001 mov ebp, esp

.text:00401003 push ecx

.text:00401004 mov dword ptr [ebp-4], 1

.text:0040100B cmp dword ptr [ebp+8], 1

.text:0040100F jnz short loc_40101C

.text:00401011 mov eax, [ebp+8]

.text:00401014 add eax, 1

.text:00401017 mov [ebp+8], eax

.text:0040101A jmp short loc_401025

.text:0040101C loc_40101C:

.text:0040101C mov ecx, [ebp-4]

.text:0040101F add ecx, 1

.text:00401022 mov [ebp-4], ecx

.text:00401025

.text:00401025 loc_401025:

.text:00401025 mov eax, [ebp-4]

.text:00401028 mov esp, ebp

.text:0040102A pop ebp

.text:0040102B retn

자, 지금까지는 매우 간단한 코드였지만 이제 양이 좀 많아졌다. 하지만 겁먹을 필요는 전혀 없다. 리

버스 엔지니어링의 난제는 코드량이 방대하다는 것이지만, 사실 한줄한줄씩 분석해 가면 어셈블리보

다 쉬운 언어는 없기 때문에 누구나 충분히 해석할 수 있다는 사실에 자신감을 갖고 코드를 들여다보

자. 지금부터 두 코드를 비교해가며 한 줄씩 천천히 살펴보려고 한다. 분석을 마치고 나면 리버스 엔지

Page 75: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 33

니어링에 좌절하던 사람들의 고통이 사실 알고보니 별거 아니었음을 알게 될 것이다. 그럼 첫 줄부터

따라가 보자.

push ebp mov ebp, esp

이젠 여기가 함수의 머리부분이라는 것은 쉽게 파악할 수 있을 것이다.

push ecx

다음으로 보이는 코드로 ecx를 스택에 보관한다. C 소스를 보면 현재 지역 변수는 int b로 1개뿐이

다. 이처럼 변수의 숫자가 적은 경우에는 굳이 스택을 확보할 필요없이 레지스터만 이용해 연산을 처

리한다. 이 b라는 변수를 앞으로 ecx 레지스터에서 사용하기 위해 push 문으로 기존 값을 일단 보관

해놓는 것이다. 맨 처음 “리버스 엔지니어링을 위한 어셈블리”에서 레지스터는 변수에 불과하다고 설

명했다. ecx를 변수로 사용하기 위해 보관해둔다고 가정하자. 참고로 보통 함수의 초반부에 레지스터

를 push 문으로 스택에 넣는 코드가 등장한다면 앞으로 이 레지스터를 이 함수에서 계속 연산 목적으

로 사용할 것이기 때문이라고 생각하면 된다.

mov dword ptr [ebp-4], 1

다음 코드부터는 스택에 직접 값을 넣는다. [ebp-4]도 앞으로 ecx와 더불어 연산으로 사용될 b 변

수에 해당하는 값이다. 즉, int b=1;라는 초기화 코드에 해당하는 부분이다.

cmp dword ptr [ebp+8], 1 jnz short loc_40101Cmov eax, [ebp+8]add eax, 1

자, 이곳이 바로 직접적인 if 문이다. if (a == 1)에 해당하는 코드이며, [ebp+8]은 첫 번째 파라미터

를 가리킨다. 보통 스택을 사용할 때는 “ebp 마이너스” 형식만 봐왔는데 처음으로 플러스가 등장했다.

이전 장의 내용을 기억하는가? 다시 복습해 보자. [ebp+8]이 첫 번째 파라미터임을 확인하는 방법은

간단하다. 함수가 호출되면 스택을 사용하는 규약이 있는데, 맨 아래에는 함수가 끝나고 돌아갈 리턴

주소인 [ebp+4], 그리고 첫 번째 파라미터는 [ebp+8], 두 번째 파라미터는 [ebp+C] 등으로 4바이트씩

늘어난다는 점을 알아둬야 한다. 따라서 [ebp+8]은 Temp(1)로 넣어준 첫 번째 인자라는 사실을 알 수

Page 76: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

34 리버스 엔지니어링 바이블

있다. 아무튼 이와 같은 식으로 그 값이 1인지 비교(cmp)한다. 그래서 그 결과가 0이면(즉 if (a == 1)에

해당하면) 바로 아랫줄로 가서 [ebp+8]를 eax에 넣고 인자였던 a에 1을 더하게 된다. eax에 넣고 add

를 한 이유는 메모리에서는 바로 연산이 되지 않기 때문에 레지스터를 이용한 것이다.

mov eax, [ebp-4]mov esp, ebppop ebpretn

그리고 0x401025 번지로 가서 eax에 b 변수의 값을 넣어주고(mov eax, [ebp-4]) 리턴해서 함수를

끝낸다. eax에는 함수의 리턴값이 들어가기 때문에 void 형이 아닌 함수의 후반부에는 항상 eax 값을

설정하는 코드가 등장한다. 그리고 cmp dword ptr [ebp+8], 1로 다시 돌아가서 결과가 non zero라면

(즉 else라면) b 변수에 1을 더한 후 끝낸다.

우리의 목적은 cmp 이후에 jnz 등이 어떻게 넘어가는지 살펴보는 것이기 때문에 뒷부분까지 분석

하진 않았다. 조건문은 이렇게 간단히 구분할 수 있다. 궁극적으로 jnz, jz 등을 처리하기 위한 코드가

대부분이며, 변수의 처리를 위해 레지스터를 이용한다는 사실을 확인할 수 있다. 현재 위 예제 코드에

는 지역 변수가 많은 경우는 다루지 않았지만, 그러한 경우에도 마찬가지로 스택을 이용해 각 역할에

해당하는 변수를 x바이트씩 나눠가며 한줄한줄 트레이스하면 실제 소스와 유사한 수준으로 분석할

수 있다.

반복문

루프문은 for나 while, goto 등이 있지만, 컴퓨터가 보기에는 결국 카운터 레지스터를 이용한 반복 행

위일 뿐이다(실제로도 while 문을 for 문 등으로 얼마든지 바꿔서 코딩할 수 있지 않은가?). 따라서 for

나 while로 구분할 필요가 없고, 현재 트레이스 중인 코드가 반복문이라는 것만 알아낼 수 있으면 된

다. 반복 구문이 어떤 식으로 구성되는지 이번에도 실제 C 소스와 비교해 가면서 살펴보자. 다음 예제

를 보자.

반복문 디스어셈블링

int loop(int c){ int d;

Page 77: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 35

for (int i=0; i<=0x100; i++) { c--; d++; }

return c+d;}

.text:00401000 push ebp

.text:00401001 mov ebp, esp

.text:00401003 sub esp, 8

.text:00401006 mov dword ptr [ebp-8], 0

.text:0040100D jmp short loc_401018

.text:0040100F mov eax, [ebp-8]

.text:00401012 add eax, 1

.text:00401015 mov [ebp-8], eax

.text:00401018 cmp dword ptr [ebp-8], 100h

.text:0040101F jg short loc_401035

.text:00401021 mov ecx, [ebp+8]

.text:00401024 sub ecx, 1

.text:00401027 mov [ebp+8], ecx

.text:0040102A mov edx, [ebp-4]

.text:0040102D add edx, 1

.text:00401030 mov [ebp-4], edx

.text:00401033 jmp short loc_40100F

.text:00401035 mov eax, [ebp+8]

.text:00401038 add eax, [ebp-4]

.text:0040103B mov esp, ebp

.text:0040103D pop ebp

.text:0040103E retn

인자를 하나 받고, 0x100번 만큼 해당 인자에서 1씩 뺀 후, 별도 변수에 0x100번 만큼 1씩 더하는

간단한 코드다. 기본 코드는 이미 앞에서 모두 살펴봤으니 여기서는 for 문의 핵심 코드만 살펴보자.

.text:0040100F mov eax, [ebp-8]

.text:00401012 add eax, 1

.text:00401015 mov [ebp-8], eax

0x40100F 번지가 for 문에서 i++에 해당하는 부분이다. 현재 지역변수 i에 해당하는 코드는[ebp-

8]에 위치해 있고, 그것을 eax 레지스터를 이용해 1을 더하고, 그 값을 다시 i 지역변수인 [ebp-8]에

넣었다.

.text:00401018 cmp dword ptr [ebp-8], 100h

.text:0040101F jg short loc_401035

Page 78: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

36 리버스 엔지니어링 바이블

그리고 0x401018 번지가 for 문에서 넣었던 반복문의 시작이다. 방금 설명한 대로 dword ptr [ebp-

8]이 int i로 선언한 지역변수이며, 이 값이 현재 0x100인지 비교해 0x100보다 크면 0x401035 번지로

점프한다.

.text:00401035 mov eax, [ebp+8]

.text:00401038 add eax, [ebp-4]

.text:0040103B mov esp, ebp

.text:0040103D pop ebp

.text:0040103E retn

0x401035 번지에서는 return c + d에 해당하는 코드가 된다. 메모리끼리 바로 연산을 수행할 수 없

으므로 각각 c에 해당하는 [ebp+8]을 eax에 넣고, 그 eax와 d 변수에 해당하는 [ebp-4]를 더한다. 그

리고 eax에 결과값이 들어 있는 상태에서 리턴한다.

.text:00401021 mov ecx, [ebp+8]

.text:00401024 sub ecx, 1

.text:00401027 mov [ebp+8], ecx….text:00401030 mov [ebp-4], edx.text:00401033 jmp short loc_40100F

만약 jg short loc_401035의 조건에 부합하지 않는다면(즉, 0x100이 되지 않는다면) 바로 아래로 내

려가 for 문 안의 코드를 수행할 것이다. 이때 0x401033 번지에서 보이는 것처럼 다시 위 코드로 올라

가는 경우, 그리고 그 위치에 해당하는 코드가 적당한 값을 더하거나 빼면서 어떤 특정한 값과 cmp한

다면 이 디스어셈블한 코드는 반복문으로 봐도 좋다. 대부분의 for 문에는 이러한 패턴과 규칙이 있으

므로 확실히 기억해 두자. 기본 반복문은 이렇다. 몇 가지 변칙 패턴이 있긴 한데, 그와 관련된 내용은

2부, “리버스 엔지니어링 중급”에서 좀더 자세히 다루겠다.

구조체와 API Call

C 문법에서 또 중요한 것이 구조체의 사용이다. 구조체의 각 멤버 변수가 어떤 식으로 사용되는지 살

펴볼 필요가 있다. 더불어 함수를 사용할 때의 규약을 위에서도 잠시 살펴봤지만 인자가 들어가는 상

황에서는 디스어셈블된 코드가 어떻게 변경되는지 알아둬야 한다. 리버스 엔지니어링을 할 때는 스택

포인터만 보고 구조체의 크기가 얼마이고 이 API의 인자로는 어떤 것이 들어가는지 파악하는 것이

Page 79: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 37

필수다. 양쪽 모두를 간단히 알아볼 수 있는 것과 동시에 분석하기에 크게 부담되지 않는 예제 코드를

하나 살펴보자. 다음 예제코드는 STARTUPINFO와 PROCESS_INFORMATION 구조체를 이용해

CreateProcess()로 새 프로세스를 생성하는 코드다. 구조체도 2개가 사용되며, CreateProcess()의 파

라미터 개수도 적당하며, 코드 후반부에는 구조체 멤버 변수를 활용하는 부분까지 있다. 분석하기에

부담도 없으며 초보자가 공부하기에 적격인 예제다. 다음 코드를 보자.

프로세스 생성 코드

void RunProcess(){ STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); // Start the child process. if( !CreateProcess( NULL, "MyChildProcess", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi ) ) { printf( "CreateProcess failed.\n" ); return; } // Wait until child process exits. WaitForSingleObject( pi.hProcess, INFINITE ); // Close process and thread handles. CloseHandle( pi.hProcess ); CloseHandle( pi.hThread );}

C 코드부터 간단히 해석해 보면 STARTUPINFO와 PROCESS_INFORMATION 구조체

를 선언한 뒤 CreateProcess()를 호출한다. 그러면 두 구조체에는 생성된 새 프로세스와 관련된

값이 들어오며, 해당 구조체의 멤버 변수인 프로세스 핸들을 이용해 프로세스가 종료될 때까지

Page 80: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

38 리버스 엔지니어링 바이블

WaitForSingleObject()로 대기한다. 그리고 프로세스가 종료되면 관련 핸들을 닫아 주는 것이 전부

다. 다음은 코드를 보자. 위 코드를 디스어셈블한 코드다.

디스어셈블한 프로세스 생성 코드

0x401000 PUSH EBP0x401001 MOV EBP,ESP0x401003 SUB ESP,540x401006 PUSH 440x401008 PUSH 00x40100A LEA EAX,DWORD PTR SS:[EBP-54]0x40100D PUSH EAX0x40100E CALL calling.004011A00x401013 ADD ESP,0C0x401016 MOV DWORD PTR SS:[EBP-54],440x40101D PUSH 100x40101F PUSH 00x401021 LEA ECX,DWORD PTR SS:[EBP-10]0x401024 PUSH ECX0x401025 CALL calling.004011A00x40102A ADD ESP,0C0x40102D LEA EDX,DWORD PTR SS:[EBP-10]0x401030 PUSH EDX0x401031 LEA EAX,DWORD PTR SS:[EBP-54]0x401034 PUSH EAX0x401035 PUSH 00x401037 PUSH 00x401039 PUSH 00x40103B PUSH 00x40103D PUSH 00x40103F PUSH 00x401041 PUSH calling.004070300x401046 PUSH 00x401048 CALL DWORD PTR DS:CreateProcessA0x40104E TEST EAX,EAX0x401050 JNZ SHORT calling.004010610x401052 PUSH calling.004070400x401057 CALL calling.0040116F0x40105C ADD ESP,40x40105F JMP SHORT calling.004010810x401061 PUSH -10x401063 MOV ECX,DWORD PTR SS:[EBP-10]0x401066 PUSH ECX0x401067 CALL DWORD PTR DS:WaitForSingleObject0x40106D MOV EDX,DWORD PTR SS:[EBP-10]0x401070 PUSH EDX0x401071 CALL DWORD PTR DS:CloseHandle0x401077 MOV EAX,DWORD PTR SS:[EBP-C]0x40107A PUSH EAX0x40107B CALL DWORD PTR DS:CloseHandle

Page 81: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 39

0x401081 MOV ESP,EBP0x401083 POP EBP0x401084 RETN

다시 얘기하지만, 디스어셈블된 코드로 바뀌었을 때 내용이 많다고 해서 결코 겁먹을 필요가 없다.

한줄씩 분석하다 보면 머릿속에서 C 소스로 바뀌어가는 모습이 차츰 그려질 것이다. 먼저 첫 줄부터

살펴보자.

함수의 시작

0x401000 push ebp0x401001 mov ebp, esp

이제 많이 본 코드이므로 가볍게 넘어가자. 함수의 시작이라고 생각하면 된다.

스택 확보

0x401003 sub esp, 54

중요한 부분이다. 지금까지 살펴본 코드는 변수가 몇 개 되지 않았기 때문에 레지스터로 충분히 충

당할 수 있었지만 지금은 구조체가 등장한 상황이다. 따라서 레지스터만으로는 이 크기를 다 감당할

수 없으며 스택을 늘려서 공간을 확보해야 한다. 그럼 0x54바이트만큼 스택을 늘린 이유는 무엇일까?

우리가 사용한 구조체는 2개다. 두 구조체의 레이아웃을 보자.

STARTUPINFO와 PROCESS_INFORMATION 구조체

typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2;

Page 82: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

40 리버스 엔지니어링 바이블

LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;

typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION;

멤버 변수의 개수를 세어서 데이터 타입의 바이트 크기대로 계산해 보면 STARTUPINFO 구조체

의 크기는 0x44바이트이고 PROCESS_INFORMATION은 0x10바이트다. 두 값을 더하면 0x54바이

트라는 숫자가 나온다. 그렇다. sub esp, 54로 늘려놓은 스택 크기는 바로 저 두 구조체의 메모리 공간

을 할당하기 위한 것이다. 리버스 엔지니어링할 때 저 스택 확보 코드만 보고 구조체라는 것을 바로 판

단할 수는 없겠지만(왜냐하면 구조체가 아닌 지역 변수만으로 0x54 바이트만큼 사용할 수도 있으므

로) 적어도 0x54바이트만큼의 메모리를 사용한다는 점은 파악할 수 있어야 한다. 지금은 일단 0x54

바이트만큼의 메모리 덩어리를 쓴다고 생각하며 다음으로 넘어가 보자.

0x401006 PUSH 440x401008 PUSH 00x40100A LEA EAX,DWORD PTR SS:[EBP-54]0x40100D PUSH EAX0x40100E CALL calling.004011A00x40102A ADD ESP,0C

ZeroMemory( &si, sizeof(si) );

이 코드가 바로 ZeroMemory()에 해당하는 코드다. STARTUPINFO 구조체의 크기는 위에서 살

펴봤듯이 0x44바이트였다. 0x40100A에서는 STARTUPINFO 구조체인 [EBP-54]의 포인터를 eax에

넣고 ZeroMemory()의 인자로 전달한다. 그런데 push 문이 3개인 것으로 보아 파라미터는 3개라고

분석되는데, ZeroMemory()의 인자는 2개뿐이다. 이유는 무엇일까? 이는 ZeroMemory()가 매크로

함수라서 바이너리로 변환된 실제 프로토타입과 다르기 때문이다. 즉 다음 코드와 같이 인자가 3개인

memset()으로 전처리된 구문이며, 궁극적으로는 memset()을 이용하게 된다. 따라서 바이너리로는

memset()의 인자 개수대로 변환된 것이다.

Page 83: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 41

ZeroMemory 전처리문

#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))#define ZeroMemory RtlZeroMemory

어쨌든 0x44바이트만큼을 0으로 바꾸는 것으로 봐서 그만한 크기로 데이터를 초기화하는 것

은 구조체라는 예상에 좀더 근접한다. 다음으로 등장하는 CALL calling.004011A0 함수 호출은

memset()에 해당하는 함수다. 여기서 함수의 규약 부분에서 배운 내용이 다시 등장한다. 함수를 호

출한 후 ADD ESP,0C로 스택을 보정하는 것으로 봐서 memset() 함수는 __cdecl 규약의 함수라고

생각할 수 있다. memset()의 인자가 3개이므로 4 * 3 = 12로, 0xC바이트만큼 스택을 정리해 준다.

구조체의 첫 번째 멤버 변수 처리

0x401016 MOV DWORD PTR SS:[EBP-54],44

이번에는 MOV 명령어가 등장했다. 본격적으로 어떤 값을 넣는다고 생각할 수 있다. [EBP-54]

는 이미 특정 구조체의 선두 번지라는 1단계 분석을 마친 상태다. 그곳에 4바이트만큼 0x44라는

값을 넣고 있으니, 이는 구조체의 첫 번째 멤버 변수에 0x44를 넣으라는 것으로 판독할 수 있다.

STARTUPINFO 구조체의 첫 번째 멤버 변수는 DWORD cb다. 따라서 이 코드는 si.cb = sizeof(si)

가 된다.

PROCESS_INFORMATION 초기화

0x40101D PUSH 100x40101F PUSH 00x401021 LEA ECX,DWORD PTR SS:[EBP-10]0x401024 PUSH ECX0x401025 CALL calling.004011A00x40102A ADD ESP,0C

STARTUPINFO 구조체의 ZeroMemory()에 해당하는 코드와 동일하므로 자세한 설명은 생략한

다. 다만 push로 넣는 크기만 0x10바이트로 다르며, 앞에서는 0x44바이트, 이번에는 0x10바이트 규

모를 초기화했다. 그리고 맨 처음 sub esp, 54라는 코드로 인해 메모리 덩어리는 0x54바이트의 공간

을 사용한다는 것을 확인할 수 있었다. 이 정도쯤이면 다음과 같이 한방에 눈치를 채야 한다.

Page 84: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

42 리버스 엔지니어링 바이블

“이 함수 안에서는 크기가 각각 0x44바이트와 0x10바이트인 구조체 두 개를 사용한

다. 그리고 모두 memset()으로 초기화한 것이 전체 스택의 크기와 일치하는 것으로 봐

서 별도의 지역변수는 존재하지 않는다.”

이번 구조체는 PROCESS_INFORMATION에 대한 초기화임을 알 수 있다. 이 구조체는 [EBP-10]

에 위치해 있다.

CreateProcess

0x40102D LEA EDX,DWORD PTR SS:[EBP-10]0x401030 PUSH EDX0x401031 LEA EAX,DWORD PTR SS:[EBP-54]0x401034 PUSH EAX0x401035 PUSH 00x401037 PUSH 00x401039 PUSH 00x40103B PUSH 00x40103D PUSH 00x40103F PUSH 00x401041 PUSH calling.004070300x401046 PUSH 00x401048 CALL DWORD PTR DS:CreateProcessA

이것이 바로 CreateProcess()를 호출하는 코드다. 앞에서 구체적으로 설명하진 않았지만 CALL 문

이전에 사용되는 PUSH가 어떤 것인지 눈치가 빠른 사람은 이미 이해했으리라 생각한다. CALL 문 이

전에 호출되는 PUSH는 바로 함수의 인자다. 그리고 이것은 원래 순서와 반대로 들어간다. 그 이유는

스택의 LIFO(Last In First Out) 특성 때문이다. 따라서 쉽게 생각해서 CALL 문 밑에서부터 거꾸로

위치를 세면 그것이 인자가 전달되는 순서가 된다. 0x40102D 번지를 보자. [EBP-10]의 번지를 EDX에

넣고 PUSH를 함으로써 CreateProcess()의 마지막 인자에 넣는다. [EBP-10]는 어디일까? 스택을 54h

만큼 뺐고 거기서 -44h만큼 제외한 위치는 앞에서 계산한 STARTUPINFO 구조체의 크기와 일치한

다. 따라서 STARTUPINFO 구조체의 크기만큼 빼면, 그 위치는 PROCESS_INFORMATION이다.

마찬가지로, [EBP-54]는 STARTUPINFO의 위치다. 즉, 두 개의 포인터를 넣는 &si, &pi라는 코드가

된다.

NULL 리턴 시 에러 처리

0x40104E TEST EAX,EAX0x401050 JNZ SHORT calling.00401061

Page 85: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

2장_C 문법과 디스어셈블링 43

0x401052 PUSH calling.004070400x401057 CALL calling.0040116F0x40105C ADD ESP,40x40105F JMP SHORT calling.00401081

함수의 리턴값은 EAX에 들어오므로 CreateProcess()의 리턴값을 체크해서 0이 아니면 점프해서

계속 진행하고, 그렇지 않으면 “CreateProcess failed.\n”을 출력하는 코드다. CALL calling.0040116F

은 printf()가 된다.

대기 후 클로즈 루틴

0x401061 PUSH -10x401063 MOV ECX,DWORD PTR SS:[EBP-10]0x401066 PUSH ECX0x401067 CALL DWORD PTR DS:WaitForSingleObject0x40106D MOV EDX,DWORD PTR SS:[EBP-10]0x401070 PUSH EDX0x401071 CALL DWORD PTR DS:CloseHandle0x401077 MOV EAX,DWORD PTR SS:[EBP-C]0x40107A PUSH EAX0x40107B CALL DWORD PTR DS:CloseHandle

위 코드를 보면 WaitForSingleObject()에 두 개의 인자를 넣고 대기하는 코드가 보인다. 첫 번

째 인자에는 핸들이 들어가야 하는데, 첫 번째로 넣는 [EBP-10]은 아까 얘기했다시피 PROCESS_

INFORMATION 구조체이고 거기서 DWORD PTR SS:로 4바이트를 넣으니 이것은 즉 첫 번째 인자

인 HANDLE hProcess가 된다. 그리고 두 번째 인자로는 -1을 전달하는데, 이것은 INFINITE로 선언

돼 있는 기정의 값이다. 이 값은 어디에 선언돼 있을까? 플랫폼 SDK에 설치돼 있는 WinBase.h 파일의

선언부를 찾아보자.

이처럼 헤더를 통해 인자를 찾는 법은 나중에 살펴볼 “MFC 리버스 엔지니어링”에서 계속 설명

하기로 하고, 다음 부분을 마저 살펴보자. WaitForSingleObject()가 리턴된 이후 각각의 인자로

CloseHandle()을 실행하는데, [EBP-10]은 방금 확인한 프로세스의 핸들이다. 그럼 [EBP-C]는 무엇

Page 86: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

44 리버스 엔지니어링 바이블

일까? 0x10이 PROCESS_INFORMATION니까 거기서 0xC바이트만큼 이동하기 위해서는 4바이트

를 증가시켜야 한다. 구조체 순서를 살펴보니 그것은 HANDLE h�read 다. 즉 스레드의 핸들을 닫는

코드로 판독할 수 있다.

함수 종료

0x401081 MOV ESP,EBP0x401083 POP EBP0x401084 RETN

스택을 원래대로 복구해 놓고 함수를 종료한다.

설명이 좀 길었지만 생각 외로 어렵지 않았을 것이다. 실제로 디버거나 디스어셈블러 등이 문자열

이나 API Call 등에 대해서도 상세한 정보가 될 수 있게 표기해 주므로 그쪽으로 방향을 잡아서 분석

하는 것도 도움이 될 수 있다. 더불어 리버스 엔지니어링 경험이 늘어날수록 API 콜이나 파라미터의

PUSH 구조만 봐도 이것이 어떤 역할을 하는 함수이고, 현재 사용되는 스택에 어떤 모양의 구조체가

있는지 파악하는 수준까지도 도달할 것이다.

결론

이상으로 C 문법의 기본인 함수의 구조와 콜링 컨벤션, 조건문, 반복문, 구조체, API CALL 등에 대

해 살펴봤다. 어느 정도 정형화된 것만 골라 봤는데, 이것만으로는 사실 부족한 부분이 많다. 리버스

엔지니어링에 능숙해지려면 일단 코딩 경험이 풍부해야 하며, 더불어 많은 코드를 디스어셈블해 봐

야 한다. 어셈블리에 원래 강한 사람이 아니라면 C 소스와 역분석된 코드를 매치시키는 가장 좋은 방

법은 역시 원본 소스코드와 비교해 가면서 직접 확인해 보는 것이다. 변환된 부분을 그냥 눈으로 훑지

말고 레지스터 변수 하나라도 가벼이 여기지 않는다는 자세로 스택 처리도 바이트 단위까지 샅샅히

조사해야 한다. 그러다 보면 디스어셈블된 코드를 보는 눈이 한 단계씩 업그레이드될 것이다.

Page 87: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

03

C++ 클래스와 리버스 엔지니어링

call, 조건문, 점프 구문 등으로 어느 정도 직관적인 예측을 할 수 있었던 C 문법에 비해 C++는

멤버 함수, private 함수, 생성자, 소멸자, 상속 등 분석하기가 어려운 다양한 개념이 많은 편이다.

이번 장에서는 리버스 엔지니어링을 할 때 객체지향 개념을 어떻게 확인할 수 있을지 C++

문법에 견주어 하나씩 소개한다.

클래스 멤버 변수 분석, this 포인터 분석, 전역 변수, 동적 할당 했을 때의 코드 분석, 생성자

와 소멸자 코드, 캡슐화, 다형성

Page 88: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

46 리버스 엔지니어링 바이블

사실 C 코드의 경우에는 함수, 조건문, 반복문이라는 일정한 규칙이 있고, 이것은 어셈블리에서 call

문, Jmp 문 등으로 쉽게 대입된다. 하지만 C++로 넘어오면 얘기가 달라진다. 일단 C++에서는 클래스

가 등장하고, 멤버 변수와 멤버 함수가 필요하며, 캡슐화나 상속 등의 개념까지도 필요해진다. 이러한

것들이 개발자 입장에서는 좀더 쉽고 간편한 코딩을 위해 기반이 마련된 것이지만, 리버스 엔지니어링

을 할 때는 오히려 걸림돌이 되는 사항이다. 일단 컴파일러가 빌드를 하고 나면, 결국 0과 1의 조합으

로 코드가 만들어지고, 어셈블리로 읽을 때 현재 보고 있는 함수가 그냥 함수인지 특정 클래스의 멤

버 함수인지, 그리고 이 변수는 전역변수인지 생성자는 어떤 식으로 구성돼 있는지 등 파악하기 어려

운 부분이 많아진다. 즉 구조적 뼈대가 C++과 어셈블리는 1:1로 매칭되지 않으므로 직관적으로 파악

하기가 쉽지 않은 편이다. 그렇다면 객체지향 프로그래밍은 바이너리 코드로 바뀌었을 때 어떻게 접근

해야 할까?

C++ 분석의 난해함

C++ 바이너리를 디스어셈블했을 때 해당 어셈블리 코드는 이것이 멤버 변수이고 저것은 클래스 선언

부라고 친절하게 가르쳐 주지 않는다. 메모리 사용량과 함수의 구조적인 부분을 살펴보면서 클래스라

는 것을 리버서 스스로 판단해야만 한다. 그리고 그것은 절대로 쉬운 작업이 아니다. C 코드를 분석할

때처럼 눈에 확 들어오는 정형화된 모습이 없기 때문이다. 또 오히려 어떤 부분은 클래스 멤버 함수인

지 그냥 함수인지 상당히 파악하기 힘든 경우도 있다. 그렇다면 C++ 코드를 분석하기 위해 대체 어떤

것을 준비해야 할까?

먼저 가장 필요한 것은 리버서의 풍부한 코딩 경험이다. C++로 작성된 코드의 특징을 알고 나서 해

당 바이너리를 객체지향적 프로그래밍에 근거한 단서를 하나씩 발견해 나가는 작업이 필요하다. 그리

고 다음으로 필요한 것은 C++을 디스어셈블할 때의 “감”이다. 현재 코드는 클래스의 어떤 부분이고,

어떤 역할을 하고 있다는 것 정도는 파악할 수 있는 힘을 길러야 한다. 리버서의 코딩 경험은 가르쳐서

되는 것이 아니므로 스스로 쌓으라고 얘기하겠지만, C++ 리버스 엔지니어링 시에 “감”을 얻을 수 있는

방법들은 정형화된 것이 있으므로 학습을 통해 얼마든지 업그레이드할 수 있다. 자, 그럼 C++ 리버스

엔지니어링의 “감”을 익히는 방법을 지금부터 소개한다.

Page 89: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 47

클래스 뼈대

Employee 클래스

#include "stdafx.h"#include "windows.h"#include "tchar.h"

class Employee{ public : int number; char name[128]; long pay; void ShowData(); void Test();};

void Employee::ShowData(){ printf("number: %d\n", number); printf("name: %s\n", name); printf("pay: %d\n", pay); Test();

return;}

void Employee::Test(){ printf("Test fuction\n"); return;}

// Employee kang;int main(int argc, char* argv[]){ Employee kang;

printf("size: %X\n", sizeof(Employee));

kang.number = 0x1111; _tcscpy(kang.name, _T("강병탁")); kang.pay = 0x100;

Page 90: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

48 리버스 엔지니어링 바이블

kang.ShowData(); return 0;}

고용인이라는 매우 간단한 클래스를 만들었고, 각 클래스의 멤버 변수에 다양한 값을 넣어보았다.1

그리고 하나의 멤버 함수를 만들고 그 함수를 호출하는 코드까지 추가했다. 지금부터 이 클래스를 리

버스 엔지니어링해 보며 객체지향 프로그래밍에 근거한 각 특성을 파악해 보겠다. 먼저 이 코드를 빌

드한 후 어셈블리 코드로 변환된 코드를 보자. ShowData(), Test(), main()로 크게 3개의 함수가 있으

므로 각 함수에 해당하는 디스어셈블 코드를 분류했다.

ShowData()

.text:00401000 sub_401000 proc near ; CODE XREF: _main+46 p

.text:00401000

.text:00401000 var_4 = dword ptr -4

.text:00401000

.text:00401000 push ebp

.text:00401001 mov ebp, esp

.text:00401003 push ecx

.text:00401004 mov [ebp+var_4], ecx

.text:00401007 mov eax, [ebp+var_4]

.text:0040100A mov ecx, [eax]

.text:0040100C push ecx

.text:0040100D push offset aNumberD ; "number: %d\n"

.text:00401012 call sub_4010BA

.text:00401017 add esp, 8

.text:0040101A mov edx, [ebp+var_4]

.text:0040101D add edx, 4

.text:00401020 push edx

.text:00401021 push offset aNameS ; "name: %s\n"

.text:00401026 call sub_4010BA

.text:0040102B add esp, 8

.text:0040102E mov eax, [ebp+var_4]

.text:00401031 mov ecx, [eax+84h]

.text:00401037 push ecx

.text:00401038 push offset aPayD ; "pay: %d\n"

.text:0040103D call sub_4010BA

.text:00401042 add esp, 8

.text:00401045 mov ecx, [ebp+var_4]

.text:00401048 call sub_401051

.text:0040104D mov esp, ebp

1  디스어셈블된 코드에서 확인하기 쉽게 16진수로 표기했다. 가끔 이런 리버싱용 예제 코드는 10진수로 만들어 놓고, 디스어셈블하며 계산기를 꺼내 다시 16진수로 바꾸는 사람들이 있다. 우리의 목표는 16진법 공부가 아니므로 예제 애플리케이션을 만들 때는 바이너리상에서 찾기 쉽게 처음부터 16진수로 코딩하는 습관을 들여놓자.

Page 91: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 49

.text:0040104F pop ebp

.text:00401050 retn

Test()

.text:00401051 sub_401051 proc near ; CODE XREF: sub_401000+48 p

.text:00401051

.text:00401051 var_4 = dword ptr -4

.text:00401051

.text:00401051 push ebp

.text:00401052 mov ebp, esp

.text:00401054 push ecx

.text:00401055 mov [ebp+var_4], ecx

.text:00401058 push offset aTestFuction ; "Test fuction\n"

.text:0040105D call sub_4010BA

.text:00401062 add esp, 4

.text:00401065 mov esp, ebp

.text:00401067 pop ebp

.text:00401068 retn

main()

.text:00401069 ; int __cdecl main(int argc, const char **argv, const char *envp)

.text:00401069 _main proc near ; CODE XREF: start+AF p

.text:00401069

.text:00401069 var_88 = dword ptr -88h

.text:00401069 Dest = byte ptr -84h

.text:00401069 var_4 = dword ptr -4

.text:00401069 argc = dword ptr 8

.text:00401069 argv = dword ptr 0Ch

.text:00401069 envp = dword ptr 10h

.text:00401069

.text:00401069 push ebp

.text:0040106A mov ebp, esp

.text:0040106C sub esp, 88h

.text:00401072 push 88h

.text:00401077 push offset aSizeX ; "size: %X\n"

.text:0040107C call sub_4010BA

.text:00401081 add esp, 8

.text:00401084 mov [ebp+var_88], 1111h

.text:0040108E push offset Source ; "강병탁"

.text:00401093 lea eax, [ebp+Dest]

.text:00401099 push eax ; Dest

Page 92: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

50 리버스 엔지니어링 바이블

.text:0040109A call _strcpy

.text:0040109F add esp, 8

.text:004010A2 mov [ebp+var_4], 100h

.text:004010A9 lea ecx, [ebp+var_88]

.text:004010AF call sub_401000

.text:004010B4 xor eax, eax

.text:004010B6 mov esp, ebp

.text:004010B8 pop ebp

.text:004010B9 retn

C++ 코드를 처음 리버스 엔지니어링 해보는 이들에게 클래스를 분석할 때 가장 먼저 무엇부터 고

민해야 할지 물어보면 다음과 같이 답하곤 한다.

“클래스 선언부가 어디에 있는지부터 찾아야 하지 않을까요?”

남이 만든 C++ 소스 코드를 볼 때는 클래스 선언부부터 찾아볼 수도 있겠지만, 아쉽게도 프로그래

밍과 리버스 엔지니어링은 완전히 다른 것이기에 클래스 선언부를 찾으려 했다가는 평생 아무것도 찾

지 못할 것이다. 클래스 선언부의 모습은 전혀 없다. 당연하다. 선언부의 코드는 개발자의 이해를 증가

시키기 위해 편의에 맞춘 C++의 문법일 뿐이며, 그것이 컴파일됐을 때 뼈대의 모습은 보이지 않는다.

마치 변수를 여러 개 선언해도 디스어셈블된 상태에서는 그 형태를 알기 힘든 것처럼 클래스도 마찬

가지로 인터페이스가 쉽게 관찰되리라는 안일한 생각은 하지 않는 것이 좋다.

이럴 때는 컴파일러가 빌드하는 순서를 생각해 봐야 한다. 간단하게 생각해서 코드상의 맨 윗줄부

터, 즉 최초로 등장하는 함수부터 디스어셈블된다고 보면 된다. 위의 디스어셈블된 코드를 봐도 맨 처

음 등장하는 것은 바로 Employee 클래스의 ShowData()라는 함수다. 컴파일러는 우리가 클래스를

만들어 놓아도 해당 클래스의 직접적인 몸통이 있는 함수부터 빌드한다. 따라서 그 부분에 착안해 더

듬어 가야 한다.

Page 93: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 51

알아두기

이번 장의 디스어셈블된 코드를 보면 스택 부분이 var_4, var_88 등으로 표현돼 있다는 것이 의아

하게 여겨질 수도 있다. 이것은 IDA가 만들어낸 코드의 해석이라고 보면 된다. IDA는 디스어셈블된

코드를 구체적으로 상세화해서 표현하기로 유명한 툴이다. 예를 들어 아래와 같은 코드를

mov dword ptr ss:[esp+10], 1

다음과 같은 식으로 변환한다.

mov [esp+var_10], 1

그리고 그런 부분은 IDA의 각 함수의 맨 위에 설명돼 있다.

var_F4 = byte ptr -0F4hvar_90 = byte ptr -90hvar_50 = byte ptr -50hvar_C = dword ptr -0Chvar_4 = dword ptr -4

var_88이 클래스 혹은 구조체라는 생각까지 왔다. 그렇다면 당연히 해당 메모리 영역에 값을 넣

을 것이다. 0x401084 번지를 보면 var_88 번지에 직접 0x1111을 넣고 있다. 해당 객체의 첫 번째 멤버

변수가 되며, 원본 소스상에서 kang.number = 0x1111;에 해당하는 코드다. 중요한 부분은 그다음

0x4010A9 번지의 코드다(일단 직관적으로 필요한 부분만 설명하기 위해 두 내용 사이의 중간 코드는

생략한다). 해당 객체의 변수에 해당하는 var_88의 번지를 ecx에 넣고 sub_401000 함수를 호출하고

있다. 보통 클래스의 멤버 함수를 호출할 때는 현재 메모리 덩어리의 포인터를 ecx에 넣고 함수 호출

을 하므로 이런 코드로 클래스임을 짐작할 수 있다. 만약 ecx에 포인터를 넣는 것이 왜 클래스인지 가

물가물한 분들은 이전 장에서 다룬 함수의 호출 규약을 다시 참고하길 바란다.

어쨌든 ecx에 들어간 var_88은 당연히 C++에서의 this 포인터가 된다. 이처럼 클래스 멤버 변수는

ecx에 객체의 포인터를 넣고 함수 호출을 하는 형태가 일반적이다. 함수 호출 직전 ecx에 어떤 값을 넣

는 모습을 눈여겨봐야 한다. 초보 리버서들은 ecx를 카운터라고만 외우고 있지만 능숙한 리버서라면

이런 루틴이 왔을 때 한눈에 클래스 멤버 함수라는 것을 온몸으로 느낄 수 있어야 한다. 그런데 혹시,

실제 개발할 때는 거의 사용하지도 않는 this 포인터를 꼭 찾아야 하나, 라는 생각이 들지 않는가? 그

답은 조금 뒤에 나온다.

Page 94: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

52 리버스 엔지니어링 바이블

이전에 나왔던 ShowData()를 다시 보자. sub_401000은 ShowData()에 해당하는 함수가 된다. 그

리고 ecx에는 this 포인터가 전달돼 왔으므로 이를 염두에 두고 한줄씩 살펴보자.

클래스의 첫 번째 멤버 변수 사용

.text:00401004 mov [ebp+var_4], ecx

.text:00401007 mov eax, [ebp+var_4]

.text:0040100A mov ecx, [eax]

.text:0040100C push ecx

.text:0040100D push offset aNumberD ; "number: %d\n"

.text:00401012 call sub_4010BA

ecx에 들어온 포인터는 IDA가 해석해준 var_4 변수에 들어가며, 이제부터 이 변수의 위치를 늘려

가며(즉 포인터를 증가시켜 가며) 클래스의 해당 변수를 사용하게 된다. 앞 장에서 이야기한 대로 this

포인터를 전달하고, 해당 포인터에 오프셋을 더하는 방법으로 클래스의 멤버 변수를 컨트롤하게 된

다. 개발자들은 잘 알다시피 코드를 작성할 때 this 포인터는 클래스 멤버 함수 내에서는 생략하고 잘

표기하지 않지만 실제로는 내부적으로 this가 사용된다는 사실을 잘 알 것이다. 다음 코드는 그와 관

련된 내용이다.

클래스의 두 번째 멤버 변수 사용

.text:0040101A mov edx, [ebp+var_4]

.text:0040101D add edx, 4

.text:00401020 push edx

.text:00401021 push offset aNameS ; "name: %s\n"

첫 번째 변수는 int number였으며, 두 번째 변수는 char name[128]이었던 것으로 기억할 것이다.

int 형이 4바이트니까 var_4를 edx에 넣고, 거기서 4바이트를 증가시켜 다음 변수를 가리키게 한다는

것을 확인할 수 있다. 그리고 문자열이 들어가는 것으로 보아 현재 변수의 크기를 확인할 필요도 없이

다음 변수는 수십 바이트만큼 더 증가시킬 것이라는 예측도 할 수 있다.

클래스의 세 번째 멤버 변수 사용

.text:0040102E mov eax, [ebp+var_4]

.text:00401031 mov ecx, [eax+84h]

.text:00401037 push ecx

.text:00401038 push offset aPayD ; "pay: %d\n"

.text:0040103D call sub_4010BA

.text:00401042 add esp, 8

Page 95: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 53

.text:00401045 mov ecx, [ebp+var_4]

.text:00401048 call sub_401051

역시 예측한 대로 mov ecx, [eax+84h] 코드가 보인다. eax를 84바이트만큼 더 증가시킨다. 첫 번째

멤버 변수가 4바이트였고, 0x84에서 4를 빼면 0x80, 즉 10진수로 128바이트가 된다. 문자열이 들어가

는 배열의 크기는 128바이트라는 것을 알 수 있다. char name[128]과 일치한다. 그리고 0x401045 번

지에서 클래스임을 확정짓는 결정적인 단서가 또 등장한다. 이 메모리 덩어리의 시작 번지였던 var_4

를 ecx에 넣고 sub_401051 함수를 호출하고 있다. this 포인터를 전달하며 해당 클래스의 또 다른 멤

버 함수를 호출한다는 것을 파악할 수 있다. sub_401051 함수는 Test()다. 왜 this 포인터에 신경 쓰는

지 조금은 파악이 되는가? 이처럼 ecx를 사용하는 this 포인터는 해당 바이너리 코드와 확보된 스택이

클래스인지 구조체인지 파악하는 데 큰 도움이 된다.

클래스의 수명과 전역변수

다음으로 생각해 볼 것은 클래스의 수명이다. 클래스를 선언하고 나면 개발자는 해당 클래스의 각

멤버에 접근할 수 있으며, 목적에 맞게 각 멤버를 사용할 수 있다. 그렇다면 그 클래스의 수명은 언제

까지일까? 당연히 이것은 클래스를 어디에 선언했느냐에 따라 달라진다. 맨 처음 코드 같은 경우에

는 main() 안에서 Employee 클래스를 선언했다. 그러므로 main() 함수가 끝날 때 클래스의 객체

는 사라진다. 그러면 리버스 엔지니어링으로 확인할 때는 어떠한 점을 중점적으로 살펴봐야 할까?

Employee 클래스 객체의 유효범위(scope)를 전역으로 바꾼 다음 다시 살펴보자. // Employee kang;

이라고 주석 처리된 부분에서 주석 표시를 없애고 main() 안의 Employee kang; 코드는 주석 처리한

후 다시 빌드해 보자. 다음은 빌드한 결과다.

클래스 객체를 전역으로 만든 뒤 살펴본 main()

.text:00401049 ; int __cdecl main(int argc, const char **argv, const char *envp)

.text:00401049 _main proc near ; CODE XREF: start+AF p

.text:00401049

.text:00401049 argc = dword ptr 8

.text:00401049 argv = dword ptr 0Ch

.text:00401049 envp = dword ptr 10h

.text:00401049

.text:00401049 push ebp

.text:0040104A mov ebp, esp

.text:0040104C mov dword_409908, 1111h

Page 96: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

54 리버스 엔지니어링 바이블

.text:00401056 push offset Source ; "강병탁"

.text:0040105B push offset Dest ; Dest

.text:00401060 call _strcpy

.text:00401065 add esp, 8

.text:00401068 mov dword_40998C, 100h

.text:00401072 mov ecx, offset dword_409908

.text:00401077 call sub_401000

.text:0040107C xor eax, eax

.text:0040107E pop ebp

.text:0040107F retn

달라진 부분이 어디인가? 다음 두 부분이라고 볼 수 있다.

.text:0040104C mov dword_409908, 1111h….text:00401068 mov dword_40998C, 100h.text:00401072 mov ecx, offset dword_409908

원래 이 코드는 어땠을까? 클래스를 전역으로 선언하기 전의 코드를 다시 발췌했다.

.text:00401084 mov [ebp+var_88], 1111h….text:004010A2 mov [ebp+var_4], 100h.text:004010A9 lea ecx, [ebp+var_88]

원래는 객체의 포인터가 스택에 있었지만 전역으로 선언한 이후에는 dword_40998C 같은

.data 섹션에 있음을 알 수 있다. ecx에 전달되는 this 포인터도 마찬가지다. 즉, dword_40998C,

dword_409908 등은 해당 클래스의 멤버 변수이며, .data 섹션에 들어 있는 것으로 보아 전역 변수임

을 확인할 수 있다. 이처럼 클래스를 전역으로 만들면 public:에 선언된 각종 멤버 변수는 모두 전역변

수가 된다. C 언어를 처음 배울 때 전역변수를 남발하지 말라고 배웠는가? 전역변수를 만들지 않으려

고 갖은 애를 쓰는 학생들을 본 적이 있다. 그냥 전역변수를 사용한 것이나 C++ 클래스에서 public으

로 선언한 변수나 실제로 내부적으로는 다 똑같은 구조가 된다는 점을 기억하기 바란다.

Page 97: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 55

객체의 동적 할당과 해제

이번엔 malloc이나 new 등으로 클래스의 객체를 동적으로 할당했을 때를 살펴보자. 클래스를 동적

으로 할당한 후 번지 접근은 어떻게 살펴볼 수 있는지 확인하는 것이 과제다. 사실 외형상으로는 크

게 다르지 않지만 굳이 차이점을 생각해 보자면 크게 두 가지로 축약할 수 있다. 우선 처음부터 스택

을 클래스 크기만큼 확보하는 것이 아니고, 해당 포인터를 하나 선언해서 사용한다는 점이다. 그리고

메모리 할당 코드가 필요하다는 점이다. 따라서 클래스의 크기는 스택 크기가 아닌 new를 할 때 확인

할 수 있다. 코드를 작성할 때 new 연산자에는 크기를 기록할 필요가 없지만, 내부적으로는 크기를 계

산해서 집어넣는 작업이 일어난다. 따라서 컴파일러가 만든 코드가 디스어셈블된 부분이 보이는 것은

물론이다. 동적 할당으로 변경해서 코드를 작성한 main() 함수를 보자. 다음 두 코드를 보자.

동적 할당으로 변경한 main

int main(int argc, char* argv[]){ Employee *pkang; pkang = new Employee;

pkang->number = 0x1111; _tcscpy(pkang->name, _T("강병탁")); pkang->pay = 0x100;

pkang->ShowData();

delete pkang; return 0;}

동적 할당으로 변경한 main()을 디스어셈블한 코드

.text:004010A9 ; int __cdecl main(int argc, const char **argv, const char *envp)

.text:004010A9 _main proc near ; CODE XREF: start+AF p

.text:004010A9

.text:004010A9 lpMem = dword ptr -0Ch

.text:004010A9 var_8 = dword ptr -8

.text:004010A9 var_4 = dword ptr -4

.text:004010A9 argc = dword ptr 8

Page 98: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

56 리버스 엔지니어링 바이블

.text:004010A9 argv = dword ptr 0Ch

.text:004010A9 envp = dword ptr 10h

.text:004010A9

.text:004010A9 push ebp

.text:004010AA mov ebp, esp

.text:004010AC sub esp, 0Ch

.text:004010AF push 88h ; unsigned int

.text:004010B4 call ??2@YAPAXI@Z ; operator new(uint)

.text:004010B9 add esp, 4

.text:004010BC mov [ebp+var_8], eax

.text:004010BF mov eax, [ebp+var_8]

.text:004010C2 mov [ebp+var_4], eax

.text:004010C5 push 88h

.text:004010CA push offset aSizeX ; "size: %X\n"

.text:004010CF call sub_401121

.text:004010D4 add esp, 8

.text:004010D7 mov ecx, [ebp+var_4]

.text:004010DA mov dword ptr [ecx], 1111h

.text:004010E0 push offset Source ; "강병탁"

.text:004010E5 mov edx, [ebp+var_4]

.text:004010E8 add edx, 4

.text:004010EB push edx ; Dest

.text:004010EC call _strcpy

.text:004010F1 add esp, 8

.text:004010F4 mov eax, [ebp+var_4]

.text:004010F7 mov dword ptr [eax+84h], 100h

.text:00401101 mov ecx, [ebp+var_4]

.text:00401104 call sub_401030

.text:00401109 mov ecx, [ebp+var_4]

.text:0040110C mov [ebp+lpMem], ecx

.text:0040110F mov edx, [ebp+lpMem]

.text:00401112 push edx ; lpMem

.text:00401113 call sub_401152

.text:00401118 add esp, 4

.text:0040111B xor eax, eax

.text:0040111D mov esp, ebp

.text:0040111F pop ebp

.text:00401120 retn

.text:00401120 _main endp

new 연산자에서 push 88h를 인자로 전달함으로서 클래스 크기인 0x88바이트만큼 할당한다는 것

을 알 수 있으며, eax에 들어오는 할당한 영역은 var_8에 넣는다. 이것이 바로 Employee *pkang;에 해

당하는 pkang 포인터다. 이제부터 이 포인터를 이용해 pkang -> number 등의 작업을 진행한다. 그래

서 해당 메모리나 스택에 직접 값을 넣지 않고, new 로 메모리를 할당해서 받은 heap 공간에 값을 넣

게 된다. 따라서 다음과 같은 차이점이 생긴다.

Page 99: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 57

예를 들어, 0x100을 넣는 코드도 동적 할당을 받기 전에는 멤버 변수에 값을 넣을 때 이와 같이 스

택에 바로 값을 삽입했다.

.text:004010A2 mov [ebp+var_4], 100h

하지만 동적 할당한 후에는 스택에 직접 값을 넣는 부분은 없고 아래처럼 eax+84h라는 오프셋을

더하는 모습을 볼 수 있다. 즉, 객체가 가리키는 번지에 오프셋을 더하는 방식으로 해당 클래스의 멤

버 변수를 찾고 값을 넣는 작업이 이뤄진다.

.text:004010F4 mov eax, [ebp+var_4]

.text:004010F7 mov dword ptr [eax+84h], 100h

생성자와 소멸자

C++ 입문서를 보면 가장 먼저 등장하는 것이 생성자와 소멸자이며, 우리 또한 순서상으로는 이것부

터 다뤘어야 할 것 같지만 이 책에서는 C++의 문법을 설명하는 것이 목표가 아니라서 순서를 조금 달

리했다. 즉, 디스어셈블된 코드상에서 클래스의 구조를 파악하고 this 포인터를 이해하는 것이 우선적

으로 해야 할 일이었기에 생성자와 소멸자를 찾는 작업은 약간 후반부에 배치했다.

그럼 이제부터 생성자와 소멸자를 살펴보자. 코드는 방대하지만 설명할 부분은 그다지 많지 않으므

로 금방 이해할 수 있게 구구절절한 설명 없이 직접 눈으로 디스어셈블된 코드를 살펴보는 것이 좋겠

다. 먼저 지금까지 사용한 Employee 클래스에 생성자와 소멸자를 넣어 보자.

생성자와 소멸자 추가

class Employee{ public : int number; char name[128]; long pay; Employee(); ~Employee(); void ShowData(); void Test();};

Employee::Employee(){

Page 100: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

58 리버스 엔지니어링 바이블

printf("constructor!! \n");}

Employee::~Employee(){ printf("Desstructor!! \n");}

당연히 main() 함수의 코드는 바뀌지 않았다. 그러나 이제 디스어셈블해 보면 main() 함수에 수많

은 코드가 추가돼 있는 모습을 확인할 수 있다. 이는 컴파일러가 클래스의 생성자와 소멸자를 처리하

기 위해 내부적으로 생성해서 집어넣은 코드다. 다음 코드를 보자.

main 함수의 디스어셈블링 코드

.text:004010DC ; int __cdecl main(int argc, const char **argv, const char *envp)

.text:004010DC _main proc near ; CODE XREF: start+AF p

.text:004010DC

.text:004010DC var_28 = dword ptr -28h

.text:004010DC var_24 = dword ptr -24h

.text:004010DC var_20 = dword ptr -20h

.text:004010DC var_1C = dword ptr -1Ch

.text:004010DC var_18 = dword ptr -18h

.text:004010DC var_14 = dword ptr -14h

.text:004010DC var_10 = dword ptr -10h

.text:004010DC var_C = dword ptr -0Ch

.text:004010DC var_4 = dword ptr -4

.text:004010DC argc = dword ptr 8

.text:004010DC argv = dword ptr 0Ch

.text:004010DC envp = dword ptr 10h

.text:004010DC

.text:004010DC push ebp

.text:004010DD mov ebp, esp

.text:004010DF push 0FFFFFFFFh

.text:004010E1 push offset unknown_libname_9 ; Microsoft VisualC 2-8/net runtime.text:004010E6 mov eax, large fs:0.text:004010EC push eax.text:004010ED mov large fs:0, esp.text:004010F4 sub esp, 1Ch.text:004010F7 push 88h ; unsigned int.text:004010FC call ??2@YAPAXI@Z ; operator new(uint).text:00401101 add esp, 4.text:00401104 mov [ebp+var_18], eax.text:00401107 mov [ebp+var_4], 0.text:0040110E cmp [ebp+var_18], 0.text:00401112 jz short loc_401121.text:00401114 mov ecx, [ebp+var_18].text:00401117 call sub_401091 ; 생성자

Page 101: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 59

.text:0040111C mov [ebp+var_24], eax

.text:0040111F jmp short loc_401128

.text:00401121

.text:00401121 loc_401121: ; CODE XREF: _main+36 j

.text:00401121 mov [ebp+var_24], 0

.text:00401128

.text:00401128 loc_401128: ; CODE XREF: _main+43 j

.text:00401128 mov eax, [ebp+var_24]

.text:0040112B mov [ebp+var_14], eax

.text:0040112E mov [ebp+var_4], 0FFFFFFFFh

.text:00401135 mov ecx, [ebp+var_14]

.text:00401138 mov [ebp+var_10], ecx

.text:0040113B push 88h

.text:00401140 push offset aSizeX ; "size: %X\n"

.text:00401145 call sub_4011EE

.text:0040114A add esp, 8

.text:0040114D mov edx, [ebp+var_10]

.text:00401150 mov dword ptr [edx], 1111h

.text:00401156 push offset Source ; "강병탁"

.text:0040115B mov eax, [ebp+var_10]

.text:0040115E add eax, 4

.text:00401161 push eax ; Dest

.text:00401162 call _strcpy

.text:00401167 add esp, 8

.text:0040116A mov ecx, [ebp+var_10]

.text:0040116D mov dword ptr [ecx+84h], 100h

.text:00401177 mov ecx, [ebp+var_10]

.text:0040117A call sub_401030

.text:0040117F mov edx, [ebp+var_10]

.text:00401182 mov [ebp+var_20], edx

.text:00401185 mov eax, [ebp+var_20]

.text:00401188 mov [ebp+var_1C], eax

.text:0040118B cmp [ebp+var_1C], 0

.text:0040118F jz short loc_4011A0

.text:00401191 push 1

.text:00401193 mov ecx, [ebp+var_1C]

.text:00401196 call sub_4011C0 ; 소멸자

.text:0040119B mov [ebp+var_28], eax

.text:0040119E jmp short loc_4011A7

.text:004011A0

.text:004011A0 loc_4011A0: ; CODE XREF: _main+B3 j

.text:004011A0 mov [ebp+var_28], 0

.text:004011A7

.text:004011A7 loc_4011A7: ; CODE XREF: _main+C2 j

.text:004011A7 xor eax, eax

.text:004011A9 mov ecx, [ebp+var_C]

.text:004011AC mov large fs:0, ecx

.text:004011B3 mov esp, ebp

.text:004011B5 pop ebp

.text:004011B6 retn

.text:004011B6 _main endp

Page 102: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

60 리버스 엔지니어링 바이블

코드가 상당히 방대해졌다. 중요한 부분만 집어서 살펴보자. 먼저 초기 부분이다.

초기 코드

.text:004010DF push 0FFFFFFFFh

.text:004010E1 push offset unknown_libname_9 ; Microsoft VisualC 2-8/net runtime.text:004010E6 mov eax, large fs:0.text:004010EC push eax.text:004010ED mov large fs:0, esp

기존에는 없었던 fs 레지스터를 이용한 익셉션 처리 코드가 만들어져 있다. 생성자가 추가되면서 컴

파일러가 내부적으로 예외 관련 코드를 생성했음을 알 수 있다.

생성자

.text:00401117 call sub_401091 ; 생성자

.text:0040111C mov [ebp+var_24], eax

.text:0040111F jmp short loc_401128

.text:00401121

.text:00401121 loc_401121: ; CODE XREF: _main+36 j

.text:00401121 mov [ebp+var_24], 0

.text:00401128

.text:00401128 loc_401128: ; CODE XREF: _main+43 j

.text:00401128 mov eax, [ebp+var_24]

.text:0040112B mov [ebp+var_14], eax

.text:0040112E mov [ebp+var_4], 0FFFFFFFFh

.text:00401135 mov ecx, [ebp+var_14]

.text:00401138 mov [ebp+var_10], ecx

call sub_401091이 생성자에 해당하는 함수 호출이다. 우리는 생성자만 만들었고 생성자는 당연히

리턴값이 없는 함수지만, 이 함수는 분명히 리턴값이 있으며 해당 리턴값을 이용해 클래스 객체를 사

용한다는 것을 알 수 있다. 이처럼 할당 코드 직후에 어떤 함수가 등장하고, 그 함수가 리턴하는 값을

컨트롤하며 여러 변수를 제어하는 모습이 보인다면 그 변수는 클래스의 객체이고 그 함수는 클래스

의 생성자라고 볼 수 있다.

Page 103: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 61

알아두기

생성자를 찾는 노하우를 조금만 더 얘기해 볼까 한다. 먼저 클래스를 찾는다. 이 클래스는 엄청나게

방대하다. 그래서 스택에 어지럽게 널브러진 각종 값이 눈에 쉽게 들어오지는 않을 것이다. 하지만

역으로 생각해서 클래스가 방대하다는 점을 이용해 보자. 덩치가 클수록 당연히 변수도 많아진다.

변수가 많아지고 코드가 길어지면 당연히 초기화하는 작업 역시 많이 필요해진다. 경험상 생성자를

어떤 용도로 사용했는가? 아래와 같은 코드를 즐겨 넣지 않았을까?

dwMoney = 0;dwHuman = 0;dwCount = 0;dwSize = 0;memset(lpBuffer, 0, BUFFER_SIZE);bMoney = FALSE;bSucccess = FALSEpTest = NULL;

이런 부분을 생각해서 생성자를 찾을 때는 집단으로 여러 변수를 0으로 초기화하는 코드를 찾아보

길 권장한다. 그리고 그 동네에 memset()이나 malloc() 등의 친구들이 함께 거주하고 있다면 해당

함수는 90% 이상 생성자라고 생각할 수 있다. 물론 아닌 코드도 있었지만 필자의 경험상 이런 식

으로 접근하는 방법이 성공할 확율이 높았다.

소멸자

.text:00401193 mov ecx, [ebp+var_1C]

.text:00401196 call sub_4011C0 ; 소멸자

.text:0040119B mov [ebp+var_28], eax

.text:0040119E jmp short loc_4011A7

.text:004011A0

.text:004011A0 loc_4011A0: ; CODE XREF: _main+B3 j

.text:004011A0 mov [ebp+var_28], 0

소멸자는 보통 몸체의 맨 뒷부분에 위치하는 편이다. call sub_4011C0이 소멸자에 해당하는 함수

호출이며, 역시 그 전에 클래스의 객체의 포인터를 ecx에 넣고 있다. 실제 모듈에서도 이런 경우 함수

의 안으로 들어가 보면 각종 변수의 메모리를 해제하거나 Close 관련 API를 호출하는 모습이 등장하

는 편이다. 이렇게 생성자와 소멸자의 코드를 살펴봤다.

Page 104: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

62 리버스 엔지니어링 바이블

캡슐화 분석

캡슐화(encapsulation)된 구조는 리버스 엔지니어링할 때 어떻게 파악해야 할까? 사실 여기엔 정답이

없는 것 같다. private 연산자 등은 컴파일러 내부에서 개발자에게 주의를 주는 것이지 바이너리로 변

경되면 그것도 역시 평범한 함수가 될 뿐이기 때문이다. 따라서 기존 함수와 다를 바 없다. 직접 살펴

보자. Employee 클래스 private 함수를 넣어 보았다. 아래의 클래스는 수정한 소스코드이고 이어서

등장하는 두 코드는 디스어셈블한 코드다.

private 함수 추가

class Employee{ public : int number; char name[128]; long pay; Employee(); ~Employee(); void ShowData(); void Test(); private: void TestPrivate();};

void Employee::TestPrivate(){ printf("Private fuction\n"); return;}

private 함수와 해당 함수 호출부를 디스어셈블한 코드

.text:00401000 sub_401000 proc near ; CODE XREF: sub_401030+50 p

.text:00401000

.text:00401000 var_4 = dword ptr -4

.text:00401000

.text:00401000 push ebp

.text:00401001 mov ebp, esp

.text:00401003 push ecx

.text:00401004 mov [ebp+var_4], ecx

.text:00401007 push offset aPrivateFuction ; "Private fuction\n"

.text:0040100C call sub_4011EE

.text:00401011 add esp, 4

.text:00401014 mov esp, ebp

Page 105: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

3장_C++ 클래스와 리버스 엔지니어링 63

.text:00401016 pop ebp

.text:00401017 retn

.text:00401068 push offset aPayD ; "pay: %d\n"

.text:0040106D call sub_4011EE

.text:00401072 add esp, 8

.text:00401075 mov ecx, [ebp+var_4]

.text:00401078 call sub_4010C4

.text:0040107D mov ecx, [ebp+var_4]

.text:00401080 call sub_401000

ecx에 객체의 포인터를 넣고 함수를 호출하는 것이 일반 멤버 함수를 호출하는 경우와 다를 바 없

다. 즉, 바이너리상에서는 이것이 private 함수인지 일반 멤버 함수인지 파악할 방법이 없다. 그래서

분석상의 한계가 있다. 아쉽지만 캡슐화된 부분을 리버스 엔지니어링으로 100% 찾기는 불가능에

가깝다.

다형성 구조 파악

리버스 엔지니어링을 할 때 다형성(polymorphism)의 경우는 조금 특이하므로 확실히 기억해 둬야

한다. 가상 함수의 경우는 일반적인 함수가 만들어지는 것과는 달리 별도의 테이블이 생성된다. 그리

고 이러한 가상 함수는 상속과 관련돼 있어서 테이블에 대한 정보를 생성자와 소멸자에서도 관리한

다. 직접 코드를 보자. Employee 클래스의 Test() 멤버 함수를 가상 함수로 변경했다. 디스어셈블한

생성자를 살펴보자.

virtual 함수 추가

class Employee{ public : int number; char name[128]; long pay; Employee(); ~Employee(); void ShowData(); void virtual Test();};

Page 106: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

64 리버스 엔지니어링 바이블

가상 함수 테이블 정보가 추가된 생성자

.text:00401094 push ebp

.text:00401095 mov ebp, esp

.text:00401097 push ecx

.text:00401098 mov [ebp+var_4], ecx

.text:0040109B mov eax, [ebp+var_4]

.text:0040109E mov dword ptr [eax], offset off_4070C0

.text:004010A4 push offset aConstructor ; "constructor!! \n"

.text:004010A9 call sub_4011FE

.text:004010AE add esp, 4

.text:004010B1 mov eax, [ebp+var_4]

.text:004010B4 mov esp, ebp

.text:004010B6 pop ebp

.text:004010B7 retn

mov dword ptr [eax], o�set o�_4070C0라는 코드가 추가돼 있으며, 소멸자에도 같은 코드가 만

들어져 있음을 확인할 수 있다(지면상 코드는 생략한다). 이것이 바로 가상 함수의 테이블이다. 가

상 함수는 상속 문제가 있어서 이렇게 따로 관리되고 있으며, 그 테이블은 .rdata 섹션에 위치한다.

따라서 .rdata 섹션에 보관된 함수 포인터를 발견할 때는 가상 함수라 분석할 수 있다. PE(Portable

Executable)를 아시는 분들은 .rdata니 .data니 하는 것들이 매우 익숙하겠지만, 아직 PE Header를

공부하지 않으신 분은 설명도 없이 갑자기 나오는 용어에 어리둥절할지도 모르겠다. 일단 1부에서 리

버스 엔지니어링에 대한 코드 숙지법을 살펴보고 나서 PE 부분은 2부에서 설명할 예정이니 급하신 분

들은 그쪽부터 읽어보길 바란다.

sub_4010D9가 Employee::Test() 함수다. .rdata 섹션에 보관돼 있다.

Page 107: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

04

DLL 분석

DLL도 C/C++로 코드가 작성되기 때문에 기본적인 사항은 C/C++에 대한 디스어셈블과 별반

차이가 없다. 다만 WinMain() 대신 DllMain()이 존재한다는 것을 비롯해 각 ul_reason_for_call에

대한 분류 코드가 들어가며, 익스포트 함수가 있다는 것(물론 없는 경우도 있다) 정도의 간단한

차이점을 생각해볼 수 있다. 따라서 이번 장에서는 실제 어셈블리 코드나 스택의 계산법 ,

레지스터 이용 등에 대한 세부적인 설명보다는 DLL 분석과 관련된 굵직굵직한 내용을 다루겠다.

DLL 번지 계산 방법, 재배치(re-allocation)를 고려한 번지 계산, 익스포트 함수(export function),

DllAttach/Detach 찾기, DisableThreadLibrary 찾기

Page 108: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

66 리버스 엔지니어링 바이블

DLL(Dynamic Link Library)이 EXE와 다른 점은 무엇일까? DLL에도 같은 PE Header가 있으며, 함

수와 조건문, 반복문 등으로 구성된 코드 덩어리다. DLL이라고 해서 EXE와 아주 큰 차이를 보이며

리버스 엔지니어링할 때 크게 영향을 받는 것은 아니다. 다만 실행 중 라이브러리에 있는 함수를 호출

하고(물론 경우에 따라 EXE에서도 익스포트 함수가 있는 경우도 있지만) 로드되는 번지가 고정돼 있

는 EXE에 비해 메모리에 올라갈 때마다 위치가 바뀐다는 점 등을 생각해 봤을 때 DLL은 속성에 따

라 리버스 엔지니어링할 때도 큰 차이가 있다.

DLL의 번지 계산법

초보자들이 가장 많이 헷갈려 하기도 하고, 질문 또한 많이 하는 부분은 바로 DLL의 번지를 계산하

는 방법인 것 같다. IDA 등으로 디스어셈블했을 때와 DLL이 실제로 메모리에 올라갔을 때의 번지가

다른 경우가 있기 때문이다. 사실 그것이 혼동되는 이유는 단지 Image Base가 달라졌기 때문이다. 그

것 말고는 차이점이 없으니 실제로는 어려운 것도 아니며 계산 방법이라고 얘기하기에도 부끄러울 만

큼 단순하다.

Image Base와 Base of Code

Page 109: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 67

디스어셈블러가 가리키는 번지

두 화면을 보면 DLL의 Image Base는 0x10000000 번지로 돼 있다. 이것은 DLL을 제작할 때 컴파

일러가 기본적으로 지정해주는 번지다. EXE 파일은 보통 0x400000 번지에 올라온다는 점을 떠올려

보자. 보통 이렇게 빌드 옵션을 변경하지 않는다면 로딩되는 번지가 중복될 수 있는데, EXE는 한 프로

세스마다 한 개씩만 올라오니까 그렇다 쳐도 0x10000000 번지가 엔트리로 돼 있는 다른 DLL이 올라

온다면 어떻게 될까? 불안감과는 달리 별다른 큰 문제는 없다. 그때는 내부적으로 다른 영역을 찾는

작업이 이뤄지며, 비어 있는 메모리 공간에 DLL을 로드하게 된다. 그리고 더불어 CALL 문과 JMP 문

의 상대 주소를 계산하기 위한 재배치 작업도 이뤄진다. Data Directory의 BASERELOC이 여기에 해

당한다. EXE 파일의 경우는 이 필드가 0으로 비어 있지만 DLL 파일의 경우에는 상대 주소와 크기 필

드에 값이 채워진다.

자, 다음과 같이 saslLOGIN.dll이라는 DLL이 있다. 이 DLL의 PE Header를 보니 ImageBase가

0x10000000임을 알 수 있다.

== Optional Header ==Magic : 0x10BLinker Version : 6.00SizeOfCode : 0x3000SizeOfInitializedData : 0x4000SizeOfUninitializedData : 0x0AddressOfEntryPoint : 0x2F69BaseOfCode : 0x1000BaseOfData : 0x4000ImageBase : 0x10000000

Page 110: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

68 리버스 엔지니어링 바이블

메모리에 올라갔을 때의 DLL 번지

하지만 위 그림을 보면 Base Address가 0x10000000였던 saslLOGIN.dll이 0x15100000 번지에

로드돼 있다. 현재 이 프로세스의 0x10000000에는 이미 다른 DLL이 들어가 있기 때문에 이 공간에

saslLOGIN.dll이 로드된 것이다. 이 경우 올라간 번지가 달라져 버려서 IDA상에서 디스어셈블된 번

지와 메모리상에서의 번지를 매치시키지 못해 초보자들이 질문할 때가 많다. 이것은 매우 간단하다.

Image Base와 Base of code만 살펴보면 되기 때문이다. 먼저 첫 번째로 현재 DLL이 메모리에 올라가

있는 번지를 확인하고, 두 번째로는 그 파일의 PE Header를 살펴보고 Base of code를 더해주면 된다.

즉, 다음과 같은 간단한 공식이 성립된다. PE Header는 다음 장에서 다루겠지만 지금은 간단히 Base

Address와 Base of code라는 멤버가 있다는 것만 기억하자.

번지 계산 방법

Base Address + Base of code0x10000000 + 0x1000 (파일)-> 0x1510000 + 0x1000 (메모리)

ex) 0x10001234 번지의 코드라면 0x1511234 번지에 로드돼 있음.

메모리상의 엔트리

물론 예외는 있지만 대부분은 이렇게 간단하게 계산할 수 있다.

Page 111: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 69

재배치를 고려한 방법

사실 앞의 과정은 너무 쉬운 단계이므로 콧방귀도 나오지 않을 수 있다. 그럼 한 단계 더 나아가 이번

에는 번지가 매번 바뀌는 경우를 살펴보자. 바로 재배치(re-allocation)를 통한 DLL 번지 확인 방법이

다. 재배치? 앞에서도 거론되긴 했지만 무엇을 재배치한다는 걸까? 어셈블리는 옵코드로 표현되고 각

호출 번지나 상대 번지의 개념이 들어가게 된다. 여기서 재배치라는 용어가 사용된다. 먼저 재배치가

어떤 곳에서 일어나는지 알아보자. 다음은 dll.dll이라는 테스트용 dll이다. 먼저 PE를 보자.

Optional Header의 ImageBase 항목을 보면 0x10000000 번지로 되어 있다. 이 dll이 로딩된다면

메모리의 0x10000000 번지에 올라간다는 이야기다. 실제로 dll을 LoadLibrary()로 로드해보자. 테스

트 프로그램에 올린 결과는 다음과 같다.

0x00400000 D:\_code\test\_always\Ma\Release\test.exe0x10000000 D:\_code\test\_always\Ma\Release\dll.dll

dll.dll이 0x10000000 번지에 잘 올라간 것을 볼 수 있다. 자, 만약 여기서 dll.dll이 현재 이 프로세

스에 또 LoadLibrary()된다면 어떨까? 지금 이미 ImageBase인 0x10000000 번지에는 dll.dll이 올라

가 있다. 누가 공간을 차지하고 있는 상황이다. dll2.dll이라는 이름으로 바꿔서 파일을 하나 더 복사한

뒤, 다시 메모리에 올려 보았다.

0x00400000 D:\_code\test\_always\Ma\Release\test.exe0x10000000 D:\_code\test\_always\Ma\Release\dll.dll0x00BA0000 D:\_code\test\_always\Ma\Release\dll2.dll

자, 이번에는 0xBA0000 번지에 로딩된 것을 알 수 있다. d l l .d l l은 분명히 ImageBase가

0x10000000였으며, dll2.dll 또한 같은 파일을 복사한 것이므로 ImageBase는 동일하다. 그리고

Page 112: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

70 리버스 엔지니어링 바이블

0x10000000 번지에 뭔가가 올라가 있는 상황에서 전혀 다른 번지의 메모리에 올라간 것이다. 대체 어

떻게 된 일일까?

PE 헤더에서는 이처럼 기준 주소에 로드되지 못했을 경우에 대비해 재배치 섹션이라는 것을 제공

한다. 이미지 베이스를 바꾼 후, 주소 계산이 필요한 부분은 재배치 섹션을 통해 모두 새로 배치해 준

다. 물론 이 과정은 운영체제에서 자체적으로 처리하기 때문에 개발자나 사용자가 따로 신경 쓸 부분

은 없지만, 리버스 엔지니어링을 할 때는 이야기가 달라진다. 변경되는 바이트가 무엇인지 확실히 알

아둘 필요가 있다.

그렇다면 로딩되는 번지가 이렇게 바뀌었을 때 어떻게 영향을 받는지 지금부터 살펴보겠다. 어디가

변경되는 것일까? 어셈블리의 옵코드를 보면서 생각해 보자. 가장 두드러지게 변화가 일어나는 곳은

크게 세 부분이다. DLL이 두 개가 된 dll.dll, 그리고 dll2.dll이 메모리에 올라갔을 때를 비교해 보자.

먼저 정상적으로 0x10000000 번지에 로딩됐을 때의 상황이다.

Dll Base : 0x10000000

10012655 68 00900110 push dll.10019000100126D7 A1 C4F70110 mov eax, dword ptr ds:[1001F7C4]1001274E FF25 98300110 jmp near dword ptr ds:[<&MSVCRT._vsnprin>]

Dll2.dll Base : 0xBA0000

00BB2655 68 0090BB00 push dll2.00BB900000BB26D7 A1 C4F7BB00 mov eax, dword ptr ds:[BBF7C4]00BB274E FF25 9830BB00 jmp near dword ptr ds:[<&MSVCRT._vsnprin>]

1) push 문10012655 68 00900110 push dll.10019000

10012655 번지에서 push의 인자로 10019000 번지에 있는 전역변수를 넣었다. 여기서 push에 해당하

는 옵코드는 위에서 보다시피 68이며, 오퍼랜드는 00900110, 리틀 엔디언으로는 10019000이다. 이때

사용되는 오퍼랜드는 절대 주소인데, 이미지 베이스가 10000000이기 때문에 더하기 오프셋 19000

해서 10019000이라는 값이 나오는 것이다. 여기서 dll2.dll처럼 전혀 다른 메모리 번지에 로드되어 이

미지 베이스가 바뀐다면 어떻게 될까? 위 예제처럼 BA0000 번지에 로딩된다고 가정하고 옵코드를

보자.

Page 113: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 71

00BB2655 68 0090BB00 push dll2.00BB9000

push에 해당하는 옵코드인 68은 당연히 그대로겠지만 오퍼랜드에 해당하는 값이 00900110에서

0090BB00으로 바뀌었다. 이미지 베이스가 바뀌었으니 절대 주소도 바뀌는 것이 당연하다. 전역변수

의 예를 하나 더 보자.

Dll.dll - 100126D7 A1 C4F70110 mov eax, dword ptr ds:[1001F7C4]Dll2.dll - 00BB26D7 A1 C4F7BB00 mov eax, dword ptr ds:[BBF7C4]

역시 mov eax,에 해당하는 A1은 불변이지만 오퍼랜드에 해당하는 절대주소가 바뀌었다.

2) 점프문

달라지는 값은 또 있다. 이번에는 점프문을 살펴보자. 백문이 불여일견. 코드부터 바로 등장한다.

Dll.dll - 1001274E FF25 98300110 jmp near dword ptr ds:[<&MSVCRT._vsnprin>]Dll2.dll - 00BB274E FF25 9830BB00 jmp near dword ptr ds:[<&MSVCRT._vsnprin>]

역시 jmp near에 해당하는 FF25는 그대로지만, 점프할 지점이 표시된 절대 주소는 값이 변경됐다

는 사실을 알 수 있다.

이 부분은 무엇을 의미할까? 리버서가 옵코드로 어떠한 패턴을 찾을 때 전역변수나 저런 점프문에

해당하는 절대번지의 부분은 건너뛰어야 한다는 것이다. 메모리에 올라갈 때마다 값이 달라질 수 있

으므로 모든 옵코드를 몽땅 찾는 것만으로는 아무런 검색 결과가 나오지 않을 수 있다.

또한, CRC(Cyclic Redundancy Check)나 체크섬(checksum) 검사를 할 때도 이 부분이 메모리에

로딩될 때마다 값이 달라지기 때문에 마찬가지로 계산할 때마다 다른 값이 나올 수 있다. 따라서 DLL

의 재배치를 통해 이러한 특성이 있다는 사실을 파악하고 있어야 한다. 메모리의 패턴을 찾는 내용은

이 장의 후반부에서 다루는 DllAttach/detach 찾기 부분에서 좀더 자세히 설명한다.

Page 114: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

72 리버스 엔지니어링 바이블

번지 고정

그렇다면 한번 생각해보자. DLL이 로딩되는 위치를 항상 유동적으로 바뀌지 않게 할 수는 없을까?

#pragma comment()의 /base: 옵션과 /�xed: 링커 옵션을 사용하면 DLL이 로딩되는 위치가 재배치

되지 않고 개발자가 지정한 번지에 들어가게 된다. 예를 들어, 내가 만든 DLL을 항상 0x23400000 번

지에 로드시키고 싶다면 다음과 같은 작업이 필요하다.

#pragma comment(linker, "/base:0x23400000 /fixed" )

따라서 DLL을 자신이 원하는 위치에 로드시키고 싶으면 이 linker 옵션을 사용하면 된다. 이렇게 되

면 재배치 작업이 필요하지 않기 때문에 DLL의 특징 중 하나인 IMAGE_DIR_ENTRY_BASERELOC

필드의 RVA와 Size에 값이 할당되지 않는다. 일반 EXE 파일처럼 0으로 채워져 있다.

0으로 채워져 있는 DLL의 IMAGE_DIR_ENTRY_BASERELOC

이처럼 /base와 /�xed 옵션을 사용하면 DLL 로딩 번지를 고정시킬 수 있다는 특징이 있지만 실제

로 이 옵션을 필드에서 남용하는 것은 권장하지 않는다. 왜냐하면 해당 번지에 이미 다른 DLL이 들어

와 있으면 내가 만든 DLL이 제대로 로드되지 않는 사태가 발생하기 때문이다. 따라서 이 옵션을 사용

하고 싶으면 다른 DLL과 겹치지 않게 번지 위치를 선정하는 고민도 할 필요가 있다.

익스포트 함수

DLL에는 외부에서 호출하기 위한 익스포트(export) 함수가 있다. 바이너리상에서 확인해볼 때는 이

것도 하나의 엔트리 포인트(Entry Point)가 될 수 있다. EXE 같은 경우에는 시작 지점이 기본적으

Page 115: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 73

로 WinMain()밖에 없지만(TLS Callback 등을 사용한 경우는 일단 논외로 한다) DLL인 경우는

익스포트 함수의 개수만큼 엔트리가 존재한다. RuntimeCRC.dll이라는 DLL을 예로 들어보자. 다

음 화면처럼 디펜던시 워커(Dependency Walker)로 확인해본 결과 이 DLL에는 InstallHook()과

UnInstallHook()이라는 두 개의 익스포트 함수가 있다는 것을 알 수 있다.

RuntimeCRC.dll의 익스포트 테이블

이것을 이제 IDA 등의 디스어셈블러로 돌려보자. 그리고 Jump to Entry Point (CTRL + E) 메뉴를

선택해 선택할 수 있는 엔트리 포인트를 살펴보자. 아래 화면에서 확인할 수 있다시피 DllMain()과 더

불어 InstallHook(), UninstallHook() 등까지 3개의 진입 가능한 엔트리 포인트를 선택할 수 있음을

알 수 있다. 이처럼 DLL에 익스포트 함수가 있는 경우에는 디펜던시 워커나 기타 PE Tools 등으로 함

수의 이름까지 확인할 수 있으며, 그 함수의 엔트리는 번지가 제공되므로 디스어셈블러나 디버거 등

에서 쉽게 찾아갈 수 있다.

Jump to Entry Point

Page 116: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

74 리버스 엔지니어링 바이블

IDA와 OllyDBG

최고의 디스어셈블러라고 하면 누구나 IDA를 꼽고 최고의 디버거라고 하면 OllyDBG나

WinDBG라는 이름을 떠올리기 마련이다. 리버스 엔지니어링을 할 때는 디스어셈블러를 더 많

이 사용할까, 아니면 디버거를 더 많이 사용할까? 어떤 사람들은 디스어셈블러가 더 편하다고

얘기하고 다른 이들은 디버거가 더 편하다고 말한다. 이때 편하다는 표현이 필자에겐 상당히 모

호하게 들린다. 왜냐하면 편하다는 말은 두 가지 프로그램을 사용하는 사람에게 상당히 상대적

으로 들리기 때문이다. IDA는 상당히 많은 정보를 상세히 알려 주고 분기문에 대한 구분이 정확

하다. 따라서 머릿속으로 if를 그려가며 따라가 볼 수 있다. 아울러 역참조에 대한 링크를 확실히

분석하기 때문에 여러 가지 플로우 차트를 가정해보며 많은 상황을 예상해 볼 수 있다. 이런 구

조학적인 분석에서는 상당히 “편하다”고 할 수 있다. 반면 디버깅하기가 상당히 어렵다(물론 할

수는 있지만 속도가 상당히 느리다는 것을 비롯해 많은 문제가 있다). 따라서 EIP를 옮겨가거나

스택 트레이스를 해볼 수 없으므로 그런 면에서 볼 때 불편하다고 할 수 있다.

그에 반해 OllyDBG는 어떨까? OllyDBG는 IDA만큼 구조학적으로 분석하는 능력은 없다. 단지

바이너리를 디스어셈블할 뿐이다. 만약 OllyDBG를 단지 디스어셈블러 용도로 사용한다면 각종

점프문에선 리버서 스스로 번지로 뛰어가보며 확인해 봐야 하는 등 여러 가지 불편함이 따른다.

API에서 사용하는 구조체나 각종 파라미터에 대한 정보를 IDA만큼 친절하게 보여주지도 못한

다. 그런 면에서 “불편하다”고 할 수 있다. 그러나 OllyDBG의 디버깅, 즉 트레이싱 기능은 강력

하다. 스택에 들어오는 값을 눈에 잘 들어오는 인터페이스로 쉽게 확인할 수 있으며, 레지스터의

변화무쌍한 내용도 실시간으로 확인할 수 있다. 이런 면에서 볼 때는 상당히 “편하다”고 할 수

있다.

그렇다면 어떤 것을 사용해야 할까? 어떤 것을 더 우선적으로 사용해야 할까? 둘 중 하나가 유일

한 정답은 아니다. 필자는 디버거와 디스어셈블러는 엄연히 다른 것이라 생각한다. 따라서 트레

이싱 용도로 디버거를 쓰고, 깊은 고민과 함께 코드를 이리저리 옮겨볼 때는 디스어셈블러를 활

용하는 것이 적절하다고 본다. 따라서 반드시 하나를 더 많이 사용해야 한다는 의무감은 필요없

을 것 같다. 필요할 때, 그리고 원하는 작업을 해야 할 때 적합한 툴을 사용하면 된다. OllyDBG

가 최고라느니, IDA가 최고라느니 같은 주장은 그다지 설득적이지 못하다. 디버깅이 필요하면

OllyDBG를 사용하면 되고, 각종 세부적인 정보를 찬찬히 뜯어보고 싶을 때는 IDA의 강력한 기

능을 빌리면 된다. 그래서 리버서는 두 가지 툴을 병행해서 적절히 활용해야 하며, 그와 관련해

IDA와 OllyDBG를 연동하는 플러그인 등도 인터넷에서 쉽게 찾을 수 있다는 사실이 그러한 사

실을 반증한다. OllyDBG만을 예로 든 관계로 조금 시각이 좁아 보일 수도 있겠지만 WinDBG와

Win32DASM도 마찬가지다. 본인이 손에 익은 장비를 구미에 맞게 사용하면 되는 것이며 굳이

타인의 취향에 억지로 플랫폼을 맞출 필요는 없다. 어쨌든 디버거와 디스어셈블러는 엄연히 다

르다. 그리고 특정 도구만 고집할 필요는 없다. 사실 리버스 엔지니어링 자체에도 골치 아픈 일

이 너무나 많은데 툴을 선정하는 데까지 골머리를 썩인다면 리버스 엔지니어링 작업이 그다지

즐거울 것 같지는 않다.

Page 117: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 75

DllAttach/DllDetach 찾기

DLL을 리버스 엔지니어링할 때 생각해야 할 점은 메인 함수를 찾으려고 불필요하게 노력하지 않아도

된다는 것이다. DLL은 라이브러리 용도로 사용되는 함수의 집합이다. 따라서 초기화 코드에서 윈도

우 등 GUI를 만들 필요가 없는 경우도 있고, main()에 해당하는 코드가 거의 존재하지 않을 수도 있

다. 윈도우가 없다면 메시지 처리에 대해서도 생각할 필요가 없다. 따라서 WndProc()에 해당하는 함

수도 작성할 필요가 없다. DLL을 개발하는 입장에서 한번 생각해보기 바란다. DllMain()에 대체 얼

마나 많은 코드를 넣었는가를. 메인 코드를 DllMain()에 넣은 경우는 거의 없을 것이다. 더구나 C 런

타임 라이브러리(C Runtime Library)를 링크한다면 C 런타임이 DllMain()을 대신 제공하므로 엔트

리 포인트를 굳이 작성하지 않아도 무방하다. 하지만 그래도 혹시 외부 라이브러리가 호출되기 전에

DLL이 로딩되자마자 해야 할 작업이 있다면 DllMain()에 코드를 넣을 수밖에 없다. 그럴 경우에 대비

해 어쩔 수 없이 DllMain()을 리버스 엔지니어링할 때가 많은데, 지금부터 이 부분을 한번 살펴보자.

단, 숙지해야 할 내용이나 분석해야 할 내용이 그렇게 많지는 않으므로 가벼운 마음으로 읽어도 무방

할 것 같다.

일단 DllMain()의 원형은 대체로 다음과 같다.

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes);

가장 중요한 것은 fdwReason 값이다. 대부분 잘 알고 있듯이 4개의 값이 있다. 그래서 DllMain()

안에는 fdwReason을 이용한 스위치 케이스 문이 존재한다.

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes){ switch(fdwReason) { case DLL_PROCESS_ATTACH: break; case DLL_PROCESS_DETACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; } return TRUE;}

Page 118: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

76 리버스 엔지니어링 바이블

물론 이 상태에서 빌드하면 컴파일러 최적화 옵션 때문에 모든 코드가 없어지고 return TRUE를

하는 코드만 생성될 것이다. 한번 테스트 해보길 바란다. 아래와 같이 eax에 1을 넣고 리턴하는 코드

만 덩그러니 생성될 것이다.

그럼 본격적으로 코드를 넣고 한번 빌드해보자. DLL이 로드됐을 때와 프리된 상황에 대한 매우 간

단한 코드를 넣어 보았다. 물론 DLL_THREAD_ATTACH와 DLL_THREAD_DETACH에는 코드

를 넣지 않았다.

case DLL_PROCESS_ATTACH: lpBuffer = (LPBYTE)malloc(sizeof(LPBYTE)); break;case DLL_PROCESS_DETACH: free(lpBuffer); break;

그리고 빌드하면 대략 다음과 같은 코드가 생성된다.

00BB1030 />mov eax, dword ptr ss:[esp+8]00BB1034 |>sub eax, 0 ; Switch (cases 0..1)00BB1037 |>je short Dll.00BB105300BB1039 |>dec eax00BB103A |>jnz short Dll.00BB106100BB103C |>push 4 ; Case 1 of switch 00BB103400BB103E |>call Dll.00BB10B1 ; malloc00BB1043 |>mov dword ptr ds:[BBAD08], eax00BB1048 |>add esp, 400BB104B |>mov eax, 100BB1050 |>retn 0C00BB1053 |>mov eax, dword ptr ds:[BBAD08] ; Case 0 of switch 00BB103400BB1058 |>push eax00BB1059 |>call Dll.00BB11EB ; free00BB105E |>add esp, 400BB1061 |>mov eax, 1 ; Default case of switch 00BB103400BB1066 \>retn 0C

자, 그럼 하나씩 코드를 해석해 보자. 맨 첫 줄부터 시작한다.

00BB1030 />mov eax, dword ptr ss:[esp+8]

Page 119: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 77

[esp+8]은 두 번째 인자로, fdwReason 값이다. 이 값을 eax에 넣는다.

00BB1034 |>sub eax, 0 ; Switch (cases 0..1)00BB1037 |>je short Dll.00BB1053

그리고 fdwReason이 들어간 eax에서 0을 빼 보고, 값이 0과 같으면 Dll.00BB1053로 점프한다.

Dll.00BB1053 번지 안에 들어간 코드는 무엇일까.

00BB1053 |>mov eax, dword ptr ds:[BBAD08] ; Case 0 of switch 00BB103400BB1058 |>push eax00BB1059 |>call Dll.00BB11EB ; free00BB105E |>add esp, 400BB1061 |>mov eax, 1 ; Default case of switch 00BB103400BB1066 \>retn 0C

BBAD08 번지에 있는 전역변수를 eax에 넣고, 그것을 인자로 준 뒤 free() 함수를 부르는 곳이다. 그

리고 스택을 보정한 뒤 eax에 TRUE를 넣고 리턴하며 함수를 끝낸다. 즉, 이곳은 DLL_PROCESS_

DETACH가 자리한 곳이다. DLL_PROCESS_DETACH가 상수로는 0이 된다.

00BB1039 |. 48 dec eax00BB103A |. 75 25 jnz short Dll.00BB1061

다시 원래 코드로 돌아와서, 만약 eax가 0이 아니라면 1,2,3,4 등 다른 값이 올 수 있다. 각값을 체크

하기 위해 어셈블리 코드에서는 dec eax 명령어로(1을 빼라는 명령어로) fdwReason 값을 하나씩 빼

보며 비교하고 있다. 이번에 1을 뺀 뒤, 그 값이 0이라면 Dll.00BB1061로 이동시키는 코드가 보인다.

즉 앞에서 본 케이스 문에서는 fdwReason이 0일 때의 분기문으로 이동시켰지만, 이번에는 들어온 값

에 1을 빼보고 0일 경우에 이동시키고 있으므로 fdwReason가 1이라고 들어온 경우를 처리하는 코드

가 된다. 하지만 아까는 jz로 0일 때를 처리하고 있지만, 이번에는 jnz로 0이 아닌 경우를 처리하고 있

다. 즉, 이 바이너리에서는 fdwReason이 0과 1일 때의 분기만 처리하고, 그 밖에는 ELSE로 간주하고

jnz로 모조리 넘겨버리겠다는 의도가 깔린 것이라 생각하면 된다. 따라서 jnz로 이동해 보면

00BB1061 |>mov eax, 1 ; Default case of switch 00BB103400BB1066 \>retn 0C

return TRUE로 함수를 끝낸다는 것을 알 수 있다.

Page 120: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

78 리버스 엔지니어링 바이블

00BB103C |>push 4 ; Case 1 of switch 00BB103400BB103E |>call Dll.00BB10B1 ; malloc00BB1043 |>mov dword ptr ds:[BBAD08], eax00BB1048 |>add esp, 400BB104B |>mov eax, 100BB1050 |>retn 0C

자, 이제 분기를 타지 않고 jnz의 바로 아래로 내려가면(즉, fdwReason이 1이라면) 4를 사이즈

로 지정한 malloc 코드가 나타난다. 그것이 뱉어낸 리턴값인 번지 주소는 eax에 담겨와서 전역변수

인 BBAD08로 복사된다. 그리고 스택을 보정한 뒤 함수를 끝낸다. 이곳이 바로 DLL_PROCESS_

ATTACH의 코드라고 볼 수 있다. 즉, DLL_PROCESS_ATTACH의 상수값은 1이라고 생각할 수

있다.

요약하면, 바이너리의 해석 결과는 다음과 같다.

1. DLL_PROCESS_DETACH는 fdwReason으로 0이 왔을 때의 값이며, 메모리를 해제하는

코드를 가지고 있다.

2. DLL_PROCESS_ATTACH는 fdwReason이 1이며, 메모리를 할당하는 코드가 보인다.

DllMain()을 찾기 위해 아주 단순한 내용을 복잡한 설명으로 풀어서 살펴봤지만 사실 패킹되지 않

은 DLL의 경우에는 지금까지 설명한 복잡한 내용 없이 즉석에서 DllMain을 찾을 수 있다. 특히 IDA

같은 경우에는 DllMain()의 위치를 친절하게 분석해 주기 때문에 DllMain()의 위치를 찾아 헤맬 필

요가 없다. 아래는 IDA가 DllMain()을 보여주는 화면이다.

Page 121: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 79

패킹된 DLL의 DllMain() 찾기

하지만 패킹(packing)된 바이너리인 경우는 다르다. 리버서가 혼자 힘으로 DllMain을 찾아야 하므로

번거로움이 따를 수 있다. 그래서 지금까지와 같은 DllMain 구조에 따른 고달픈 과정을 살펴봤는데,

이제 그러한 내용을 활용해 아예 패킹된 바이너리에서도 IDA 등의 힘을 빌리지 않고 DllMain을 찾

아보는 방법에 대해 생각해 보자. 패킹에 대해서 전혀 모르는 분들은 “6부 보안 모듈 우회” 편에 상세

히 설명해 두었으므로 그쪽을 먼저 참고하고 와도 될 것 같다.

먼저 개발자가 DllMain()을 작성할 때는 가장 먼저 스위치 케이스 문을 만들 것이므로 아래 패턴과

같은 어셈블리 코드가 생성될 것이다.

.text:100010C0 8B 44 24 08 mov eax, [esp+8]

.text:100010C4 83 E8 00 sub eax, 0

.text:100010C7 74 2A jz short loc_100010F3

기억나는가? [esp+8]은 fdwReason 값이며, 이것을 eax에 넣은 뒤 0에서부터 값을 빼가며 1,2,3,4에

대한 케이스 문을 찾는다고 앞에서 설명한 바 있다. 이 어셈블리 코드를 옵코드로 변환했을 때 mov

eax, [esp+8]은 8B 44 24 08이 되며, sub eax, 0은 83 E8 00, 그리고 jz short_xxxx는 74 XX가 된다.

이 옵코드를 모아서 검색해보자.

8B 44 24 08 83 E8 00 74

jz에 해당하는 74 뒤에 붙을 2A 바이트는 빌드 환경에 따라 달라질 것이므로 그대로 검색했다가는

아무런 결과도 나오지 않을 수 있다. 따라서 마지막 바이트는 무시하고 74까지만 입력하는 것이 좋다.

요약하면 우리의 목적은 바이너리 코드 섹션에서 8B 44 24 08 83 E8 00 74라는 옵코드를 지닌 패턴을

찾는 것이다.

Page 122: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

80 리버스 엔지니어링 바이블

OllyDBG를 켜서 CPU 창에 마우스 오른쪽 버튼을 누른 뒤 Search for Binary String 메뉴로 가

보자. 그러면 위와 같은 창이 등장할 텐데, HEX+08 창에 방금 모아둔 옵코드를 입력한 뒤 OK 버튼을

누르자.

조금 전 우리가 분석한 어셈블리 코드와 정확히 일치하는 함수를 찾아준다는 것을 알 수 있다. 분

석이 어렵다는 데미다(�emida)로 패킹해도 결과는 마찬가지다. 개발자가 리버스 엔지니어링에 노출

되지 않으려고 고의로 코드를 더미 상태로 만든다면 모르겠지만 웬만해선 이 패턴을 피해갈 순 없다.

참고로 이 방법은 보통 백신에서 휴리스틱 패턴(heuristic pattern)이라고 하는 기법을 응용한 것 중

하나다. 특정 기능을 위해서는 어떤 코드를 사용할 수밖에 없고 그와 같은 패턴을 찾아서 해당 코드

의 존재 여부를 파악하는 방법으로, 바이러스가 자주 사용하는 중요 패턴을 백신에 포함시켜 놓고 해

당 패턴을 보유하고 있는지 찾는 방법을 활용한 것이다.

Disable�readLibraryCalls로 찾기

바이너리로 장난치는 듯한 느낌이 들지 않는가? 이왕 장난을 쳐보는 김에 조금만 더 해보자.

DllMain()을 찾는 힌트를 하나 더 소개한다. Disable�readLibraryCalls()를 알고 있는가? 이 API

는 DLL_THREAD_ATTACH/DLL_THREAD_DETACH와 연관된 것으로, DLL_PROCESS_

ATTACH/DLL_PROCESS_DETACH가 프로세스에 DLL이 로딩될 때 각각 호출되는 것이라고 한

다면 DLL_THREAD_ATTACH/DLL_THREAD_DETACH는 스레드가 생성되거나 종료될 때마

다 한 번씩 호출된다. 이 DLL에 스레드가 엄청 많거나, 경우에 따라 생성/소멸을 반복한다면 시스템

내부에서는 계속 DllMain()이 호출될 것이고, 결국엔 프로세스에 부하가 올 수도 있다.

Page 123: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

4장_DLL 분석 81

따라서 마이크로소프트에서는 스레드가 생성되거나 호출될 때 DllMain()이 호출되지 않는 함수

를 제공하는데 그것이 바로 Disable�readLibraryCalls()다. 많은 개발자들이 습관처럼 이 함수를

DllMain()의 DLL_THREAD_ATTACH에 넣는 편이다. 그리고 이 함수는 그 위치가 아니면 호출

될 일도 거의 없으므로 Disable�readLibraryCalls()을 호출하는 곳을 찾으면 그곳은 DllMain()이

거의 확실하다! IAT를 뒤져서 Disable�readLibraryCalls()가 어디서 호출되는지 찾을 수도 있고,

Disable�readLibraryCalls()에 API 후킹을 걸고 실행한 뒤 리턴주소를 걸러내면 해당 리턴 주소가

DllMain() 안이라고 생각할 수 있다.

Page 124: 리버스 엔지니어링 바이블 : 코드 재창조의 미학
Page 125: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

[ 02부 ]

리버스 엔지니어링 중급

Page 126: 리버스 엔지니어링 바이블 : 코드 재창조의 미학

84 리버스 엔지니어링 바이블