레지스터와 어셈블리 그리고 스택

레지스터와 어셈블리 그리고 스택

가장 먼저 공부해야할 부분을 CPU로 잡았다.

이유는 윈도우 구조 분석에 있어 대다수 코드가 CPU가 처리하는 과정을 보는 과정이기 때문이다.

CPU 처리 과정의 이해는 윈도우 구조를 이해하는데 매우 중요하다.

윈도우라는 운영체제는 사용자와 하드웨어에서 중간 다리 역활을 해주는 장치라 할 수 있다.

예로 사용자가 엑셀을 이용해서 문서를 만들때 입력 장치인 키보드를 사용할 수 있고 모니터에 엑셀 화면을 표현해줄 수 있도록 하는 역활이 바로 운영체제가 하는 중요 역활이다.

이러한 역활을 수행할 때 CPU에 요청을 하고 처리를 진행하게 된다. 이러한 요청 내용을 제대로 확인하기 위해서는 CPU의 처리 내용을 명확히 이해하여야 한다.

레지스터와 어셈블리 그리고 스택

그럼 CPU의 중요 처리 경로인 레지스터, 그리고 스택에 대해서 알아보도록 하자.

레지스터는 CPU내에 존재하는 메모리로 요청을 처리하는 데 필요한 데이터를 일시적으로 저장하는 공간이다.  CPU 처리를 위해 데이터를 레지스터라는 기억 공간에 기억해 두었다가, CPU에서 처리를 하고 해당 결과값 역시 레지스터에 저장하게 된다. 레지스터는 공간은 작지만, CPU와 직접 연결되어, 연산 속도가 메모리보다도 수십 배 혹은 수백 배 빠르다.

그리고 CPU는 데이터 연산을 위해서는 반드시 레지스터를 거처야 하며, 레지스터는 특정 주소를 가리키거나 메모리로부터 값을 불러올 수 있다(64비트는 예외).

스택이 필요한 이유는 CPU는 자료를 처리하기 위해 레지스터를 사용하지만, 제공하는 용량이 매우 적다. 따라서 스택을 이용해서 레지스터의 내용을 바꿔가며 처리하게 된다.

스택과 레지스터는 메모리와 CPU를 연결해주는 관계?라 할 수 있는데 CPU에서 일을 하기 위해서는 레지스터 저장을 한다. 그리고  CPU는 단순히  레지스터에 있는 내용을 비교하거나 연산을 하고 그 결과를 다시 정해진 레지스터에 저장하게 된다.  결과가 저장된 레지스터는 다시 스텍으로 옮겨서 저장하게 된다. 이것이 CPU가 하는 모든 작업에 전부라고 간단히 말할 수 있을 정도로 실제 CPU가 하는 동작은 단순하다.

그리고 이러한 CPU의 처리를 어셈블리 언어를 이용해 처리한다. 어셈블리라는 기계어를 인간이 이해할 수 있는 코드로 재해석한 것이 어셈블리이다. 실제 인텔에서 제공하는 X86 개발자 메뉴얼을 보면 어떻게 어셈블리로 명령을 내려야 CPU가 처리되는지 자세하게 설명이 되어 있다.

그리고 윈도우는 X86기반으로 제작되었기 때문에 X86 CPU 처리가 가능한 것이다. 여기서도 어셈블리 명령 중 자주 접하게 되는 명령들에 대해 정리할 것이다.

이번 장을 통해 레지스터들이 쓰이는 용도를 알아보고 실습을 통해 어셈블리를 이해해 보도록 하자.

2.1 실습 준비

대부분의 실습들은 VMware Player내에서 진행된다. 따라서 여기서 애기하는 프로그램들도 VMware Player로 설치한 가상 머신인 윈도우 7에 설치하기 바란다.

Masm32

http://www.masm32.com

어셈블러. 어셈블리 언어로 작성한 프로그램을 컴파일 할 수 있다. Masm32 11R 버전을 사용하였다.

Notepad

http://notepad-plus-plus.org/

메모장 대용 프로그램으로 메모장의 빈약한 기능을 보완해준다.

Ollydbg

http://www.ollydbg.de

커널 디버깅에 Windbg가 유용하지만, 유저 모드 디버깅에는 Ollydbg가 유용하다. 여러 편리한 기능과 직관적인 인터페이스를 가지고 있어 많은 역분석 전문가들이 IDA와 함께 프로그램 역분석에 애용하고 있다. 필자의 경우 Ollydbg 2.0 버전을 사용하였다.

Masm32는 64비트 운영체제를 제대로 지원하지 못하므로, 가상 머신을 이용하여 32비트 운영체제에 설치하기 바란다(여기서는 일반적으로 리버스 엔지니어들이 많이 사용하는 Masm32를 통해 설명하고자 한다. 만약 32/64비트 환경에서 이용하고자 한다면, 비주얼 스튜디오에서 제공하는 파일인 VCbin의 Ml64.exe(64비트) 혹은 Ml.exe(32비트)를 이용할 수 있지만, 어셈블리 코드 작성 방법이 Masm32와 다소 차이가 있다).

[그림] Masm32 SDK 설치 화면

그럼 Masm32를 설치하자. 필자는 C:masm32에 설치하였다. 설치가 완료될 쯤 Masm32 에디터에 대한 단축 아이콘을 생성할 것인지 묻는데, 어셈블리 코드 작성에 큰 장점을 제공하지는 않으니 별도의 에디터를 사용하는 것이 좋다.

기본으로 제공하는 편집기나 메모장을 사용해도 되지만, Notepad , Notepad2와 같은 무료 공개용 메모장을 사용하는 것도 괜찮다. 메모장보다 기능도 확장되어 있으며, 프로그램 코드 작성시 해당 확장자를 인식하여, 프로그램 코드를 구분해주므로 편리하다.

[그림] 무료 공개 메모장 Notepad , 코드에 알맞게 색상을 구분해주어 편리하다

2.2 레지스터

아래 그림은 Ollydbg라는 디버깅 도구를 통해 확인한 CPU 레지스터 화면이다(어셈블리와 4부 16장에서 Ollydbg에 대해서 자세히 다룬다).

여기서 Windbg가 아닌 Ollydbg를 함께 배우는 이유는 Windbg는 커널 분석 외에 유저 편의성이 많이 부족하기 때문이다(가독성이 떨어진다). 따라서 일반 프로그램 역분석 용도로는 Ollydbg를 사용을 권장한다.

[그림] Ollydbg에서 제공하는 레지스터 정보 창

레지스터의 총 개수는 잘 사용되지 않는 디버그(8개)와 제어(8개), FPU(8개) 레지스터를 제외하고 일반적으로 16개라 생각하면 된다. 이 레지스터는 CPU 개발사에 따라 달라지는데 기본적인 범용, 인덱스, 포인터, 세그먼트, 플래그 레지스터는 공통으로 쓰이기에 유지하게 때문이다. 좀 더 설명하자면, 인텔은 MMX, AMD는 3D Now!라는 기술에 이용되는 레지스터를 독립적으로 가지고 있듯이, CPU 제조사에 따라, 개발연도에 따라 레지스터가 달라지게 된다. 그리고 레지스터는 CPU에 있고, CPU의 동작 비트에 따라 크기가 달라지는데, 64비트/32비트 머신을 구분하는 기준이 바로 이 레지스터가 한번에 처리할 수 있는 레지스터 비트 수를 의미한다. 즉 한번에 레지스터 별로 저장 가능한 공간의 크기를 통해 32비트인지 64비트인지를 구분하게 된다(32비트 머신의 메모리가 4기가바이트인 이유도 32비트 레지스터가 한번에 표현할 수 있는 주소공간이 4기가바이트이기 때문이다).

32비트 컴퓨터가 나오기 전에 16비트 컴퓨터는 0~15비트까지 레지스터 공간을 제공했다. 그리고 각 레지스터에 사용 방식에 따라 이름을 통해 구분하였고 32비트로 환경이 변화하면서 ‘확장되었다’는 의미로 ‘E'(Extension)를 각 레지스터의 이름에 추가하였다.

본격적으로 각각의 레지스터에 대해 알아보기 전에 레지스터의 데이터 단위에 대해 간단하게 집고 넘어가보자.

데이터는 다음과 같은 단위를 사용한다.

BIT:

데이터를 표현할 수 있는 제일 작은 단위로 0과 1로 구분한다. 2진수의 표현과 같다.

00000001= 1, 00000010= 2, 00000011= 3

BYTE:

8개의 비트가 모이면 하나의 바이트라 한다. 최대로 표현할 수 있는 값은 0xF(255)까지 표현이 가능하다. 레지스터에서 8비트와 대응하는 레지스터는 AL, AH, BL, BH, CL, CH, DL, DH가 있다.

11111111= 255

WORD:

2개의 바이트가 모이면 워드(WORD)라 하며, 16비트로 표현할 수 있다. 표현할 수 있는 최대값은 0xFF(65535)로, 16비트로 표현할 수 있는 레지스터는 AX, BX, CX, DX, SI, DI, BP, SP, IP가 있다.

11111111 11111111= 65535

DOUBLE WORD:

워드 2개가 모이면 더블워드(DOUBLE WORD)라 한다. 최대 0xFFFFFFFF (4294967295)까지 표현할 수 있으며, 32비트라 한다. 32비트 레지스터는 EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP가 있다.

11111111 11111111 11111111 11111111 = 4294967295

KILOBYTE:

킬로바이트(KILOBYTE)부터는 32비트의 제곱으로 값을 표현한다. 1024 (32*32) 바이트.

MEGABYTE:

킬로바이트의 제곱으로 값을 표현한다. 1,048,578(1024*1024) 바이트.

이러한 데이터 표현 중 레지스터는 비트 단위로 표현되며, 32비트 시스템에서는 32비트의 레지스터 공간을 가지게 된다. 아래 그림은 각 레지스터 별 비트의 크기이다. 아래 크기는 어셈블리 언어에서 레지스터 저장이나 로드에 사용되므로, 알아두기 바란다(어셈블리 명령 중 MOV AH, AL은 AL(8비트)을 AH로 복사하라는 어셈블리 명령 등이 사용된다)..

[그림] 레지스터 공간 크기 구분

기본적으로 레지스터들은 아래 표와 같이 용도가 구분되어 있지만 개발자 이용 방식에 따라 다르게 사용 가능하며, 반드시 지킬 필요는 없다.

용도

레지스터

용도

범용

EAX

산술/논리 연산, 처리 결과 리턴값 저장

EBX

간접 주소 연산

ECX

카운터, 반복문 횟수

EDX

산술/논리 연산 보조
인덱스

ESI

문자열 출발지 주소

EDI

문자열 목적지 주소
포인터

ESP

현재 스택 주소

EBP

스택 복귀 주소

EIP

현재 명령 실행 주소
세그먼트

(Segment)

CS

코드(Code) 세그먼트, 코드 영역 시작 주소

DS

데이터(Data) 세그먼트, 데이터 영역 시작 주소

SS

스택(Stack) 세그먼트, 스택 영역 시작 주소

ES

데이터(Data) 세그먼트, 데이터 영역 시작 주소

FS

데이터 세그먼트, 데이터 영역 시작 주소

GS

데이터 세그먼트, 데이터 영역 시작 주소
상태

EFLAGS

CPU 동작 제어, 연산 결과를 1/0으로 반영
FPU 수치연산

ST0~7

부동 소수점 처리

[표] 레지스터 별 역할

-박스 시작-

64비트 레지스터

32비트 프로세서 구조에 대한 선점은 인텔이 하였으나 64비트는 AMD에서 선점하면서 AMD64가 표준이 되었다.

64비트 레지스터는 EAX는 RAX, EBX는 RBX로 R로서 구분하여 표시되며, 기존 4개로 사용되던 범용 레지스터에 R8~R15까지 8개의 새로운 레지스터를 추가해 더 많은 레지스터를 사용할 수 있다.

상태 레지스터인 EFLAGS 역시 RFLAGS로 되었으나, 하위 32비트는 기존 EFLAGS와 동일하다.

그리고 64비트에서는 레지스터에 파라미터를 직접 넣어줄 수 있다.

이외에도 몇몇 변경점에 대해서는 아래 링크를 통해 확인해 보기 바란다.

http://msdn.microsoft.com/en-us/library/windows/hardware/ff561499(v=vs.85).aspx

-박스 종료-

위 언급한 레지스터 외에도 디버깅 레지스터(DR0, DR1, DR2, DR3, DR6, DR7)와 제어 레지스터(CR0, CR2, CR3, CR4, CR8)가 있다. 이 레지스터들은 일반적인 역분석 상황에서는 사용되지 않고, 몇몇 레지스터가 커널 제어와 관련되어 이 책 후반부에 나오는 안티 디버깅과 같은 특수한 상황에서 사용된다. EFLAGS 레지스터(다른 레지스터와 마찬가지로 플래그(FLAGS)의 Extend라는 의미)의 시스템 제어용 플래그와 연동되어 설정되거나, 운영체제의 특정 기능 활성화(PAE와 같은), 이외에 CR0, CR4 레지스터는 차후 커널 후킹과 디버깅 확인 등에 사용되며, 이를 제외하고는 사용할 일이 많지 않다.

레지스터에서 CPU에 가장 많이 이용되는 것은 범용, 인덱스, 포인터 레지스터이다.

이는 CPU에서 자주 사용되는 레지스터로 곧 배울 어셈블리를 통해 이해할 수 있다. 따라서 여기서는 이러한 레지스터가 있다는 정도로 확인하고 지금은 데이터 위치를 표시하는 세그먼트 레지스터와 연산 결과를 상태로 구분하여 저장하는 EFLAGS 레지스터에 대해 자세히 알아보자.

EFLAGS 레지스터의 32비트의 각 비트 공간별로 의미를 가지는데 약어와 함께 설명하겠다(역분석에 자주 사용되는 플래그에 굵게 표시하였다).

구분

플래그

설명

연산 결과 CF(Carry flag) 덧셈과 뺄셈에서 빌림수(Borrow) 발생시 1로 설정
PF(Parity flag) 연산 결과가 짝수이면 1, 홀수면 0을 설정
AF(Auxiliary carry flag) 16(8)비트 연산시 빌림수(Borrow) 발생시 1로 설정
ZF(Zeor flag) 연산 결과가 0이면 1로 설정
SF(Sign flag) 연산 결과 최상위 비트가 1인 경우 1로 설정
OF(Overflow flag) 연산 결과가 용량을 초과하였을 경우 1로 설정
시스템 제어 TF(Trap flag) 프로그램 추적(Trace)시 1로 설정, 명령을 한 행씩 실행한다
IF(Interrupt enable flag) 외부 인터럽트 요구를 받아들일 때 1로 설정
AC(Alignment check) CR0 레지스터의 AM 비트와 함께 1로 설정하면 메모리 참조 시 정렬 체크를 활성화한다.
IOPL(I/O privilege level) 현재 특권 수준이 IOPL보다 높은 경우에 I/O 주소 접근, 11이 가장 낮음
NT(Nested task) 연결 작업 제어, 1로 설정 시 현재 작업이 기존 실행 작업과 연결됨을 의미함
RF(Resume flag) 디버깅시 프로세서에 일시 중지 예외 발생 제어
VM(Virtual-8086 mode) Virtual-8086 mode 활성화 시 1로 설정
VIF(Virtual interrupt flag) 가상 이미지의 인터럽트 요구를 받아들일 때 1로 설정됨, VIP와 함께 사용된다.
VIP(Virtual interrupt pending) 가상 모드 인터럽트가 지연 시 1로 설정
ID(ID flag) CPUID 명령어 지원 여부로 1로 설정 시 지원
문자열 제어 DF(Direction flag) 문자열 복사 명령과 관련된 제어 플래그, 1로 설정되어 있으면 문자열 복사 시 주소값이 감소

[표] EFLAGS 레지스터의 각 비트별 플래그의 의미

EFLAGS 레지스터는 CPU 연산 결과에 따라 해당 비트에 0혹은 1로 표시하여 처리 결과를 저장하는 곳이다. 즉 이 플래그들은 CPU가 하는 더하기/빼기/나누기/곱하기가 아닌 비교와 같은 조건문 처리시 사용된다. 그 중 조건 문으로 제일 많이 등장하는 플래그는 ZF인데 이 플레그는 참, 거짓의 결과를 보여준다(프로그램 코드에서 IF문의 ==, != 입력시 실제적인 CPU 처리는 어셈블리의 비교 명령을 이용하는데, 이때 ZF 플래그를 참고하여 참, 거짓을 구분하는 것이다. 이 실습은 잠시 후 어셈블리에서 확인할 수 있다).

[그림] 프로그램 코드 if 명령은 어셈블리 JNE 명령으로 CPU의 ZF 플래그 값을 참고해 분기

레지스터는 어셈블리가 이용하기 때문에 어셈블리와 함께 배워야 이해가 빠르다. 따라서 잠시후 나오는 어셈블리 예제에서 비교 문을 이해하면 쉽게 저 말들이 조금씩 와 닫게 될 것이다.

2.3 스택

스택은 뭘까요? 스택은 컴퓨터 중앙 처리 장치인 CPU에서 기본적으로 사용되는 데이터 구조로, 호출된 함수에서 필요한 문자열 값이나 연산을 위한 값을 저장하는데 사용되는 공간입니다. 그리고 보통 스택은 아래와 같은 상황에서 사용됩니다.

  • 함수 호출의 리턴(복귀)값을 저장할 때
  • 파라미터 값을 저장할 때
  • 로컬 변수를 저장할 때
  • 컨텍스트 전환(스레드 스위칭)와 같은 상황으로 인해 레지스터 내용을 저장할 때

[그림] 스택의 구조

스택에서 중요한 부분은 “스택은 레지스터를 사용한다“는 것인데요. 이유는 CPU가 레지스터를 접근한다고 앞서 애기 드렸습니다. 그럼 레지스터의 값들은 어디서 가져오는 것이냐 하면 바로 스택에서 가져오게 되는 것입니다.

하지만 어디서부터 스택을 공부해야 할 지 막막할 것인데, 저자 생각하기에 가장 먼저 해야 할 일은 스택의 전체적인 구조를 파악 것이 우선입니다. 그럼 다음은 실행파일의 어셈블리 내용입니다. 여러분은 어떤 내용인지 알 수 있을까요?

 

자 스택의 구조인 “선입후출”의 의미로만 알 수 있을까요?

이 의미만으로는 아무것도 알 수가 없습니다. 따라서 먼저 큰 단위로 나눠서 볼 필요가 있습니다.

스택의 내용은 크게 아래와 같이 구분할 수 있습니다.

함수 도입부

함수 코드

함수 종결부

그러다 면 함수 도입부를 확인해 보도록 하겠습니다.

 

양이 많죠? 위 부분이 함수 도입부 인데, 함수 도입부에서 하는 일은 함수를 저장할 공간을 할당하고, 기존 포인터 위치를 기억하는 게 주된 목적이라고 보시면 됩니다. 이 부분에서 ebp에 대해 알아 보도록 하죠. ebp는 알아두어야 할 내용이 항상 주어진 프레임의 베이스 포인터(Base Pointer)를 포함한다고 생각하시면 됩니다.

그럼 아래내용은 무엇을 뜻하는 것일까요?

Push 명령을 통해 ebp를 집어넣으라는 명령으로, Stack에 자료를 집어넣을 때 사용하는 것입니다.

쉽게 말해 ebp를 집어넣으라는 뜻이지요

그래서 mov 명령으로 esp 값(Stack Pointer)을 ebp에 저장하라는 명령이 나오게됩니다.

esp 는 현재 스택 포인터 값으로, 이 포인터 위치는 항상 변하게 되므로, 항상 호출 이전의 원래 상태로 복귀됨을 보장하여야하는 메모리 구조상 esp의 복귀 주소를 사용할수 없기 때문에 ebp를 통해 복귀할 수 있는 주소를 저장하게 됩니다. (이 행동의 거희 모든 스택의 기본 동작으로 들어가 있습니다.)

mov edi,edi는 긴급 패치를 위해 사용하는데 대부분 nop(no operation)으로 상태가 됩니다. 가용성 측면으로 비사용시간을 줄이기위해 필수적으로 사용하게 됩니다. 이는 나중에 jmp 코드를 이용하기 위해 넣어 둔것이라고 생각하시면 됩니다.

자 그럼 도입부분으로 프레임의 베이스 포인터 위치를 ebp에 저장하여 프레임 복귀위치를 저장하였습니다.

이제 다음 스택을 보도록 하겠습니다.

esp, 14h가 의미하는건 14h를 10진수로 변환해 보면, 20바이트가 되며, sub는 빼는것으로, 20바이트 만큼 공간을 생성하도록 되어 있습니다.

왜 이렇게 공간을 생성하는게 빼기를 하였을까요?

메모리의 저장 위치를 잘 보시면 저장 위치의 값이 감소하도록 되어 있습니다.

즉 밑으로 갈수록 값이 감소하고 있는것이지요

그래서 아래 공간은 빼기를 통해 확보하게 됩니다.

그 렇게 확보한 공간에 ebp(베이스 포인터)의 위치 값을 이용하여 (베이스 포인터는 절대 위치 값처럼 그 프레임 내에서는 변화하지 않기 때문에 변수 저장과 같은 절대 위치가 필요한 값 저장에 사용하게 됩니다.)스택기반 지역 변수를 초기화 하게 됩니다.

그 내용이

위 부분이 되겠습니다.

그리고 포인터 전달 명령인 lea를 통해 인자를 스택에 저장 합니다.

이렇게 지역 변수를 다 저장하고 나면, Call 명령을 통해 해당 변수를 사용하게 됩니다.

그리고 수행을 마치고 마지막에 esp에 처음 저장한 ebp를 대입하여, 복귀하고, 해당 프레임을 빠져 나오게 됩니다.

이렇게 하나하나의 프레임의 복귀 주소를 저장하고 변수를 저장 후 Call을 통해 해당 명령을 수행하는 구조가 메모리의 기본 구조가 되겠습니다.

레지스터에 대해서는 인텔에서 제공하는 Intel® 64 and IA-32 Architectures Software Developer’s Manual을 확인하는 것도 필수 입니다. CPU와 밀접한 만큼 여기에서 얘기한 것보다 많은 내용을 담고 있거든요.

레지스터에 대한 내용은 짧지만, 역분석 진행에서 레지스터는 가장 많이 보는, 중요한 부분이라 할 수 있다. 각 레지스터의 의미를 이해하고, 익숙하게 읽어낼 수 있어야 역분석을 원활하게 진행할 수 있기 때문이다. 따라서 반복학습을 통해서 레지스터를 익히기 바라며, 앞으로 레지스터가 부분적으로 계속 나오므로 이 책의 보는 것만으로도 어느 정도는 익힐 수 있을 것이다. 또 2.3장 어셈블리는 CPU 처리 분석에 있어 레지스터와 함께 맞물러 사용되니 이를 통해서도 공부할 수 있으니 계속 알아보도록 하자.

2.4 어셈블리

먼저 어셈블리 명령이 어떻게 진행되는지 명령 구조를 확인해 보자.

어셈블리는 오피코드(Opcode)와 오퍼랜드(Operland)로 구분되는데, 오피코드는 연산자, 지시자라고 하며, 오퍼랜드는 연산자에 이용되는 값들을 말한다.

[그림] 어셈블리 구조

위 명령을 해석해 본다면, EBX 값을 EAX에 복사하라는 명령인 것이다.

어떻게 이렇게 해석이 가능한지 각 지시자들에 대해 오피코드인 명령, 즉 연산자부터 차근차근 알아보자.

먼저 예제 프로그램을 만들어 분석해 본 후, 각 어셈블리 명령어에 대해서 알아볼 것이다. 예제 프로그램이 어렵지 않고 간단하여 큰 어려움은 없을 것이다.

그럼 이제 Masm32를 이용하여 프로그래밍 시작을 알리는 Hello World를 만들어 이를 분석해 봄으로써 어떻게 분석을 진행하는 익혀보도록 하겠다.

실습 1 Hello MASM 어셈블리 분석 (시작)

[예제] MASM로 작성한 Hello world

이제 설치한 Masm32를 이용하여 컴파일을 진행하도록 하자. 아래와 같은 명령을 이용하여 컴파일을 진행할 수 있다.


 

[실습] 어셈블리 컴파일 진행

[실습 9-1]에서 사용한 옵션들 이외에 보다 자세한 정보는 ml.exe /? 명령이나 함께 설치되는
C:masm32helpmasm32.chm를 통해 확인해 보기 바란다.

프로그램을 컴파일이 완료되면 실행해 보자. 다음과 같은 메시지 창이 나타난다.

[실습그림] 예제 실행 결과

이제 우리가 알고자 하는 운영체제에서 이 Hellomasm를 어떻게 처리하는지 확인하기 위하여, 이 실행 파일을 분석하여야 한다. 실행파일은 운영체제가 실행할 수 있도록 기계어로 구성된 파일로써, 이를 분석하기 위해서는 컴퓨터가 이용하는 Opcode(기계어)에 대한 이해가 필요하다. 우리가 기계어를 바로 본다는 것은 참 어려운 일이다. 그래서 인간이 보다 쉽게 이해할 수 있도록 만든 어셈블리를 이용하여 분석을 진행하게 한다. 이 내용이 조금은 어려울 수 도 있지만 간단한 Hellomasm을 분석하다 보면 운영체제 내부에서 처리되는 기계어는 물론 어셈블리와 스택, 그리고 레지스터에 대해서도 조금은 쉽게 이해할 수 있을 것이다. 그럼 본격적으로 내부 처리 과정을 살펴보기 위해 조금 전 설치한 Ollydbg를 이용하여 분석해 보자. Windbg는 커널 디버거로는 좋지만, 유저 모드 프로그램의 분석을 진행하기에는 번거로운 부분이 많다. 그래서 여기서는 Ollydbg를 이용하여 분석을 진행하도록 하겠다(Ollydbg는 실제 악성코드 분석에도 많이 사용되며, 3부에서 다룬다).

[실습그림] Ollydbg 메인 화면

Ollydbg를 실행하고 분석하고자 하는 프로그램인 Hellomasm.exe를 File
à
Open 을 통해 열면 위와 같은 화면이 나타난다. Ollydbg의 분석 원리는 파일의 Opcode를 사람이 이해하기 쉬운 어셈블리 언어로 변환해주는 도구로써, 화면은 어셈블리, 레지스터, 스택, 메모리, 이렇게 4개의 윈도우으로 구성되어 있다. 이 4개의 윈도우를 통해 우리는 프로그램이 처리하는 과정을 다각도로 분석할 수 있다.

어셈블리를 표시하는 윈도우을 통해 프로그램 처리 흐름을 파악하고 해당 변경 내용들을 레지스터, 스택, 메모리 창에서 확인할 수 있다.

Ollydbg에 대해서는 3부에서 자세히 다룰 예정이니 여기서는 기본적인 분석 방법만 다루고 어셈블리에 집중하자. 그럼 Hellomasm.exe를 분석하면서, 기본이 되는 어셈블리 진행 상황을 확인하고 제어할 수 있는 어셈블리 창에 대해 알아보자.

어셈블리 윈도우는 우리가 생성한 프로그램의 동작 현황을 확인할 수 있다.

디버깅과 마찬가지로 명령을 한 줄씩 실행하다, 프로시저 호출(CALL)을 만나면 프로시저 안으로 들어가는 Step into(F7키), 혹은 프로시저를 실행하고 다음 라인으로 이동하는 Step over(F8키)와 프로그램 실행상태로 변경하는 Run(F9키), Run thread(F11키) 그리고 일시 중지하는 Pause(F12키)가 있다. 그 외 브레이크 포인트(F2키)를 이 어셈블리 창을 통해 지정할 수 있다(코드 영역 이외 메모리 영역에도 브레이크 포인트를 설정할 수 있다. 실제 사용법에 대해서는 4부 패킹에서 자세히 다룬다).

그리고 현재 명령 줄 정보 창은 선택한 어셈블리의 명령이 참조하는 주소지의 값을 표시해 주어, 처리 내용을 쉽게 확인할 수 있도록 도와준다.

아래 그림의 네모의 아이콘을 순서대로, 실행(프로그램) | 실행(스레드) | 일시중지 | Stepinto | Stepover 순이다.

[실습그림] Ollydbg 어셈블리 창

그럼 0x0040100E까지 프로그램을 Step into(F7키)로 진행해보자. 아이콘을 클릭하여 진행해도 무방하다. 진행하면서 스택 창과 레지스터 창을 함께 확인하도록 하자.


 

[실습] 0x0040100E까지의 스택에 값을 입력하고 있다

우선 ESP 레지스터와 EBP 레지스터의 변화에 대해서는 <4장. 스택>에서 집중적으로 다룰 것이다(스택에 있어 ESP, EBP 레지스터는 뜨거운 감자이다). 이 장에서는 어셈블리에 주목하자.

위 0x0040100E까지 실행하면 PUSH 명령에 의해 스택 창에 4개의 값이 추가되었음을 확인할 수 있다. 이는 우리가 작성한 코드에 MessageBox 호출시 입력한 4개의 파라미터들이다. 즉 함수 호출에 이용될 값을 스택에 저장해두고, 2부 8장 Ntdll.dll에서 애기한 Sysenter 명령를 통해 커널에 요청하기 위한 준비 작업을 진행한 것이다.

[그림] 스택 창, 입력한 4개의 값이 쌓여있다

이제 Ollydbg에서 실행할 부분이 CALL 명령 구문인데 이는 Step into(F7키)냐 Step over(F8키)냐에 따라 처리 흐름을 따라 갈 수도 있고 해당 프로시저를 실행 이후 바로 다음 명령 라인으로 이동할 수 있다.

Step into(F7키)를 누르면 CALL 명령이 호출하는 0x00401020으로 실행라인을 이동하여 실제 MessageBoxA의 처리 내용을 어셈블링하게 되고, Step over(F8키)를 누르면 0x00401020을 실행한 후, 실행라인을 0x00401013으로 이동하게 된다. 즉 CALL 명령으로 호출하는 프로시저를 실행한 후 다음 라인을 가리키게 된다.

[실습그림] Step over는 현 프로시저 단계를 유지하는 데 유용하다

프로그램은 프로시저를 통한 계층적 구조로 이루어진다. 그럼 먼저 그림을 확인하자.

[실습그림] Step into와 Step over의 진행 차이점

위 그림과 같이 디버거에서 Step over는 현재 처리중인 프로시저에서 하위 프로시저를 만나게 되면, 해당 프로시저 실행을 완료한 뒤에 현재 프로시저의 다음 명령라인으로 진행하게 된다. Step into의 경우 하위 프로시저로 명령 라인을 넘기게 되는데, 이는 하위 프로시저를 분석하고자 할 때 이용하게 된다.

처음에는 자세히 분석을 진행하는 것이 도움이 되므로, MessageBoxA 호출을 Step into(F7키)로 진행해 보자(앞서 1부 5장 Ntdll.dll에서 Sysenter 명령을 설명하였으므로, 실제 MessageBoxA를 호출하는 지점까지만 진행한다).


 

[실습] MessageBoxA 처리 내용

여러분은 프로그램이 운영체제 내부에서 처리되는 과정을 지켜보았다. 프로그램 역분석의 리버스 엔지니어링의 가장 기본이 될 것이며, 앞으로 진행할 여러 프로그램들을 분석해 나가면서 점점 실력이 늘어나게 될 것이다. 다시 처음부터 분석하고자 한다면, Restart(Ctrl F2키)로 다시 처음부터 0x0040100E까지 실행한 후에 Step over(F8키)를 누르거나, [실습 9-3]에서 계속 진행하고자 한다면, Step over(F8키)를 눌러 0x76F3E9C9를 실행하면 다음과 같이 우리가 만들었던 메시지 박스가 열리고 프로그램은 실행 상태가 된다.

[그림] CALL 0x0040100E을 실행한 결과

이후 확인 버튼을 누르면 CALL 명령을 빠져 나오게 되며, 프로그램 종료 코드를 실행하게 된다. 그리고 프로그램 종료가 진행된다. 프로그램을 종료하는 코드는 크게 어렵지 않으므로, 해당 부분에 대한 분석을 생략한다. 이렇게 어셈블리에 대한 Ollydbg를 이용하여 분석을 해보았다. 이러한 방식으로 앞으로 배울 각 어셈블러에 대해 분석과 이해을 진행할 것이다.

실습 Hello MASM 어셈블리 분석 (끝)

 

리틀 인디안, 빅 인디안

컴퓨터에서 데이터 저장 방식으로 리틀 인디안과 빅 인디안을 사용하는데, 리틀 인디안은 하위 주소부터 상위 주소로 저장하는 방식을 말하며 인텔 계열 프로세서인 윈도우 시스템에서 사용되며, 빅 인디안은 상위 주소부터 하위 주소로 저장하고 유닉스 계열에서 사용하는 방식이다.

윈도우 시스템의 메모리 주소를 보면 초기 커널 로드시 0x0000000부터 이용하고 0xFFFFFFF까지 리틀 인디언 방식으로 써 내려가게 된다.

위 Ollydbg에서 데이터 영역을 보면, 0x00403000 부터 큰 주소로 써 내려가는 것을 알 수 있다

이는 데이터를 조작할 때 주의해야 하는데, 데이터 쓰기의 기본 단위인 4바이트 단위로 역순으로 변경하여야 한다. 만약 아래 메모리 주소 4a890070 메모리 주소에 mode라는 값이 저장되어 있다면, mode라는 아스키 코드의 문자 데이터는 6d6f6465 이나, 데이터를 읽어 들이는 순서가 역순으로 되므로, 65646f6d로 입력하여야 정상적으로 출력 된다.

4a890070 65646f6d 0a0d0d2e 00000024 00000000 mode….$…….

이를 분석가들이 편하도록 자동적으로 빅 인디안 구조로 변경해서 보여주는 프로그램도 많다.

[그림] 같은 메모리 주소이지만 표시 방식이 다르다

Ollydbg 자동 분석 내용 제거

Ollydbg를 이용하여 분석을 진행하는 경우, Ollydbg는 해당 파일을 자체적으로 분석하여, 같은 함수로 사용되거나, IAT테이블, 스트링과 같은 메모리 참조 위치를 대조하여, 사용자가 판단하기 편한 값으로 재해석하게 된다.

[그림] 프로그램을 열면 메인 모듈에 대해 자동으로 분석을 진행한다

하지만 종종 이렇게 해석하여 변환된 분석 값들이 분석자의 가독성을 떨어트려, 원래의 코드 내용을 확인해야 하는 경우가 종종 발생한다(이를 이용한 안티 디버깅도 존재하는데, Ollydbg의 경우 재귀 순회을 통해 프로그램의 흐름을 자동 분석으로 분석하는데, 재귀 순회란 프로그램 코드의 흐름을 따라가면서 명령을 분석하는 방식으로, Ollydbg의 분석 능력을 방해하기 위해 의미가 없는 조건 명령을 넣어 자동 분석을 원활히 할 수 없도록 방해할 수 있다).

[그림] Ollydbg가 분석을 통해 변환한 코드 LOCAL.._

이렇게 자동적으로 진행한 분석 내용을 필요에 따라 제거 메뉴를 통해서 제거하여, 원래 제작한 코드대로 확인할 수 있다([그림 2-10]의 Analysis 옵션을 비활성화 하면 항상 자동 분석을 하지 않고 수동으로 하게 된다).

↓ 분석 결과를 제거하면 아래와 같이 원래의 코드를 확인할 수 있다.

[그림] 원래 코드 내용을 확인하여 직관적으로 분석을 진행할 수 있다

 

 

Facebook Comments

Leave A Reply

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.