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

가장 먼저 공부해야할 부분을 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와 다소 차이가 있다).

[그림 2-1] Masm32 SDK 설치 화면

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

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

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

2.2 레지스터

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

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

[그림 2-1] 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로 복사하라는 어셈블리 명령 등이 사용된다)..

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

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

용도

레지스터

용도

범용

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

부동 소수점 처리

[표 2-1] 레지스터 별 역할

-박스 시작-

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로 설정되어 있으면 문자열 복사 시 주소값이 감소

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

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

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

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

2.3 스택

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

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

[그림 3-1] 스택의 구조

스택에서 중요한 부분은 “스택은 레지스터를 사용한다“는 것인데요. 이유는 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)로 구분되는데, 오피코드는 연산자, 지시자라고 하며, 오퍼랜드는 연산자에 이용되는 값들을 말한다.

[그림 2-5] 어셈블리 구조

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

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

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

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

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

[예제 2-1] MASM로 작성한 Hello world

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


 

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

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

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

[실습그림 2-1-1] 예제 실행 결과

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

[실습그림 2-1-2] 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 순이다.

[실습그림 2-1-3] Ollydbg 어셈블리 창

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


 

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

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

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

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

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

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

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

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

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

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

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


 

[실습 2-1-3] MessageBoxA 처리 내용

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

[그림 2-1-7] CALL 0x0040100E을 실행한 결과

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

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

-박스시작-

리틀 인디안, 빅 인디안

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

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

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

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

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

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

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

Ollydbg 자동 분석 내용 제거

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

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

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

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

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

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

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

-박스종료-

2.4 MOV 명령

이 명령은 어셈블리 중 제일 자주 접하게 되는 명령으로써, 데이터를 복사(Move가 아니다)하는 역할을 한다.

MOV 명령은 몇 가지 제한 사항이 있는데, 레지스터에서는 같은 사이즈의 데이터끼리만 복사가 가능하며, 메모리에서 메모리로 복사하는 것은 불가능하다. 따라서 복사하고자 하는 데이터는 레지스터를 이용하여 복사해 한다. 그리고 세그먼트 레지스터의 내용을 범용 레지스터가 아닌 곳으로 이동은 가능하지만, 그 반대는 불가능하다.

그럼 실습을 통해 MOV 명령을 확인해 보자(여기서부터 레지스터라는 말이 자주 사용되어, 예제에서 레지스터라는 단어는 제외한다. 예) EAX 레지스터 = EAX.

실습 2 MOV 명령 (시작)


 

[예제 2-2] Masm를 이용한 어셈블리 프로그래밍

어셈블리 프로그램을 작성할 때는 자료 저장에 스택을 사용하는 만큼 스택 공간에 주의해야 한다. 위 프로그램을 통해 아래 내용을 이해하기 바란다.

PUSH 명령은 스택의 기본 크기가 4바이트이므로, 4바이트가 넘으면 여러 번 나누어 사용해야 한다. 그리고 ESP 레지스터의 포인터 값을 SUB 명령으로 자신이 할당 받고 싶은 바이트만큼 뺌으로써 스택 공간을 할당 받을 수 있다.

EBP 레지스터는 지역변수나 매개변수 접근의 기준점으로 사용된다(항상 메모리 값에 접근하기 위해서는 PTR로 사이즈를 정의해줘야 한다).

생성된 어셈블리 파일을 Ollydbg를 이용하여 파일을 열어보도록 하자. Ollydbg를 실행하고, File
à
Open(F3키)를 이용하여 Masm1.exe를 열면 우리가 코딩한 어셈블리 내용이 존재하는 것을 확인할 수 있다. 우리가 주의 깊게 확인해야 할 내용들은 아래 그림과 같다.

[실습그림 2-2-1] 어셈블리, 레지스터, 스택의 변화를 주의 깊게 살펴보자

분석을 진행하는 방법은 앞서 조금씩 진행해 보았으니, 어렵지 않을 것이다.

Step into(F7키)는 어셈블리 명령을 한 줄씩 실행하는데, 프로시저를 만나면 프로시저 안으로 진입하여 계속 한 줄씩 실행한다.

Step over(F8키)는 한 줄씩 실행하는데 프로시저를 만나면, 프로시저를 안으로 진입하지 않고 실행 결과를 받아서 다음 줄을 실행하게 된다.

그럼 위 Masm1.exe를 분석을 Step into(F7키)를 이용하여 한 줄씩 실행하면서, 주소 0x0040100D까지는 스택창을 주의깊게 보자.


 

[실습 2-2-1] 어셈블리 0x0040100D까지 분석 내용

0x0040100D까지 Step into(F7키)로 진행해보면 아래와 같이 스택에 값이 저장됨을 확인할 수 있다.

[그림 2-2-2] 스택에 우리가 지시한 값이 저장된다

그림 이제 0x0040101A까지 어셈블리를 Step into(F7키)로 실행해 보도록 하자.


 

[실습 2-2-2] 어셈블리 0040101D까지 분석 내용

0x0040101A까지 진행하였다면 레지스터 창에서 ESI와 EDI 레지스터가 변경되어 있음을 확인할 수 있을 것이다.

[그림 2-2-3] ESI, EDI 레지스터가 지시한 값으로 저장된다

실습 2 MOV 명령 (끝)

-박스시작-

주소위치

AX와 [AX]의 차이는 바로 AX는 AX의 값을 복사하는 곳이고, [AX]는 AX에 저장된 값이 주소가 되어 AX가 가리키는 메모리의 값이 복사되는 것이다.

만약 EBX레지스터에 아래 값이 저장되어 있다고 하자.

ebx=12345678

1) mov eax, ebx

EBX 레지스터에 저장된 값 12345678을 EAX레지스터로 복사

2) mov eax, [ebx]

EBX 레지스터에 저장된 값인 메모리 주소 0x12345678에 위치한 값을 EAX 레지스터로 복사

-박스종료-

MOVZX 명령

이 명령은 MOV의 확장 명령 중 하나로서 Operland2보다 크기가 큰 Operland1으로 이동할 수 있다. 단 서로 크기가 다르기에 데이터를 이동할 때 크기가 커서 남는 공간은 0으로 채워진다.

실습 3 MOVZX 명령 (시작)

[예제 2-3] movzx 명령 예제

위 파일을 앞서 설명한 방법을 이용하여 MASM32를 통해 컴파일한 후 Ollydbg를 이용하여 분석을 진행해보자. 0x00401011처리시 내용을 주의 깊게 확인하도록 하자.


 

[그림 2-17] EAX 레지스터에 0xFFFF 값이 업데이트 된다.

 

[실습 2-3-1] MOVZX 명령 분석

실습 3 MOVZX 명령 (끝)

LEA 명령

지정한 주소값을 가져오는 명령으로, 일반적으로 값을 가져와야 하는 경우 사용된다. MOV 명령과는 다른 결과를 나타내는데, 다음 예제를 통해 차이를 확인해 보자.

실습 4 LEA 명령 (시작)

[예제 2-4] LEA 명령 사용 예제

그럼 Masm3도 분석을 진행해 보자. 주의 깊게 관찰할 부분은 스택에 저장된 값을 그대로 가져온다는 점을 상기하자.


 

[실습그림 2-4-1] LEA 명령으로 다르게 나타나는 EBX, EDX 레지스터의 결과

 

[실습 2-4-1] LEA 명령 예제 분석

실습 4 LEA 명령 (끝)

2.4 CMP 명령

CMP 명령은 비교 명령으로, 그전에 플래그라는 개념을 이해할 필요가 있다. 앞서 EFLAGS 레지스터로 설명하였지만 다시 한번 살펴보자.

플래그는 비교적 간단하다. 플래그는 CPU의 (E)FLAGS 레지스터에 저장되는 처리 데이터로, (E)FLAGS의 한 비트가 한 플래그가 된다. 명령어들을 실행하면서 각 실행 결과에 따라서 플래그가 변경된다(XOR EAX, EAX 명령을 실행하면 Z(Zero) 플래그가 설정된다). 이렇게 CMP 명령을 이용하여 비교 결과를 플래그에 남기고 J로 시작하는 조건 분기 명령들을 이용하여, 각 플래그의 상태에 따라서 점프를 할 수 있는 분기점을 만드는 것이다.

CMP 명령의 경우 Operland1가 Operland2보다 작을 경우(즉 첫번째 연산자 – 두번째 연산자가 마이너스일 경우) CF(캐리 플래그)가 설정(1)된다.

Operland1에서 Operland2를 뺀 결과가 0일 경우 ZF(제로 플래그)가 설정(1)된다(프로그래밍에서도 1은 TRUE와 같은 의미를 가진다. FALSE의 경우 0과 혼용하여 설정할 수 있다).

조건 분기 명령은 아래와 같다.

연산자

설명

JE 두 피연산자의 내용이 같을 경우 지정 주소로 점프 (ZF=1)
JA 부호가 없는 비교에서 첫번째 피연산자가 더 큰 경우 지정 주소로 점프 (CF=0 and ZF=0)
JB 부호가 없는 비교에서 두번째 피연산자가 더 큰 경우 지정 주소로 점프 (CF=1)
JAE 부호가 없는 비교에서 첫번째 피연산자가 더 크거나 같은 경우 지정 주소로 점프 (CF=0)
JBE 부호가 없는 비교에서 두번째 피연산자가 더 크거나 같은 경우 지정 주소로 점프 (CF=1 and ZF=1)
JG 부호가 있는 비교에서 첫번째 피연산자가 더 큰경우 지정 주소로 점프 (ZF=0 and SF=OF)
JL 부호가 있는 비교에서 두번째 피연산자가 더 큰경우 지정 주소로 점프 (SF!=OF)
JGE 부호가 있는 비교에서 첫번째 피연산자가 더 크거나 같은 경우 지정 주소로 점프 (SF=OF)
JLE 부호가 있는 비교에서 두번째 피연산자가 더 크거나 같은 경우 지정 주소로 점프 (ZF=1 and SF!=OF)

* N이 들어가면 조건이 반대가 된다. 예) JNG : 크지 않으면 지정 주소로 점프

JC CARRY 플래그 값이 설정(1)되어 있으면 지정 주소로 점프 (CF=1)
JNC CARRY 플래그 값이 설정되어 있지 않으면 지정 주소로 점프 (CF=0)
JO OVERFLOW 플래그 값이 설정(1)되어 있으면 지정 주소로 점프 (OF=1)
JNO OVERFLOW 플래그 값이 설정되어 있지 않으면 지정 주소로 점프 (OF=0)
JP(JPE) PARITY 플래그 값이 설정(1)되어 있으면 지정 주소로 점프 (PF=1)
JNP(JPO) PARITY 플래그 값이 설정되어 있지 않으면 지정 주소로 점프 (PF=0)
JS SIGNAL 플래그 값이 설정(1)되어 있으면 지정 주소로 점프 (SF=1)
JNS SIGNAL 플래그 값이 설정되어 있지 않으면 지정 주소로 점프 (SF=0)
JCXZ CX레지스터 값이 설정되어 있지 않으면 지정 주소로 점프 (CX=0)

[내용] 조건 분기 명령

위 실행 결과는 Ollydbg의 플래그 레지스터를 통해 결과를 확인 할 수 있다.

어셈블리는 조건 분기를 이 플래그 레지스터를 이용하여 변경하게 된다.

[그림 2-10] Ollydbg에서 확인할 수 있는 플래그 레지스터

O D T Z C가 플래그 레지스터이며,
모든 산술연산 및 논리연산이 끝난 후에 결과값에 대해 상태 플래그를 설정(1)하거나 해제(0)하게 된다.

D T는 제어 플래그 용도로 사용되며, A는 상태 플래그 용도로 사용된다.

이중 많이 사용되는 플래그인 상태 플래그는 연산결과가 아래와 같을 때 설정(1)된다.

S : Sign Flag: 연산 결과가 1일 때 설정(1) 됨

Z : Zero Flag: 연산 결과가 0일 때 설정(1) 됨

C : Carry Flag: 연산 결과가 저장공간을 벗어날 경우 값이 1일때 설정(1) 됨 (부호가 없는 경우)

O : Overflow Flag: 연산된 결과값이 1비트를 넘어셨을때 설정(1) 됨(부호가 있을 경우)

이제 CMP명령과 조건 분기 명령을 이용하여, EAX 레지스터 값이 EBX의 레지스터 값과 같을 시 루프(Loop)를 빠져 나오는 예제이다.

실습 5 CMP 명령 (시작)

[예제 2-5] 비교문과 조건문 사용 예제

위 프로그램을 컴파일한 후 분석을 진행해 보자.

앞서 분석에 설명한 내용들은 생략하고, 비교문과 조건문에 대해서 분석을 해보도록 하겠다.


 

[실습그림 2-5-1] CMP 명령을 실행하면 플래그 레지스터가 설정된다

 

[실습 2-5-1] CMP 명령 예제

실습 5 CMP 명령 (끝)

-박스시작

TEST 명령

내부적으로 AND 연산을 수행하고, Operland1과 Operland2를 AND한 후 결과값의 상태 정보만 플래그 레지스터에 저장하게 된다(연산자들은 변하지 않는다). 연산은 반영하지 않고, 자신의 값에 대한 상태만을 확인하고 싶을 때 주로 이용되며, MOV 명령으로는 플래그 레지스터 정보를 갱신하지 않으므로, 이 명령을 통해 플래그 레지스터를 갱신하여 조건을 설정하게 된다. 즉 플래그 값을 이용하기 위해 사용하는 명령어이다. TEST 명령을 통해 플래그 레지스터의 OF, CF는 초기화되고, ZF는 설정될 수 있다.

-박스종료-

이제부터 분석 내용은 생략할 것이다. 앞서 소개한 3가지 예제를 통해 분석 진행 방식은 충분히 익혔을 것이라 생각된다. 중요 지점만 화면으로 보여줄 것이다.

2.4 ADD/SUB 명령

이 명령은 이름에서도 볼 수 있듯이 더하기/빼기 명령이다.

실행 결과가 0일 경우 제로 플래그를 설정(1)한다(만약 EAX가 1일 때에 SUB EAX, 1을 하게 될 경우 제로 플래그가 설정된다).

실습 6 ADD, SUB 명령 (시작)

[예제 2-6] add, sub 명령 예제

실행 결과는 분석해 보면 다음과 같다.

ADD 명령으로 EAX 주소는 데이터가 저장된 0x00402000을 가리키고, 해당 주소에 저장된 값” 31 32 33 34″를 DEC 명령을 이용해 뺄셈 연산을 진행하게 된다.

[실습그림 2-6-1] ADD, SUB 명령을 이용한 연산

실습 6 ADD, SUB 명령 (끝)

2.4 PUSH/POP 명령

이 명령은 스택에 데이터를 PUSH(저장) 하거나 POP(꺼내기)라는 명령으로 스택과 밀접한 관계가 있다(이후 진행되는 10장 스택에서 자세히 다룬다). 이 명령은 자료구조 스택과 동일하다. PUSH/POP 명령은 한 개의 피연산자를 받는데, 이 피연산자는 값/메모리/레지스터(세그먼트 포함)가 될 수 있다. PUSH가 실행되면, ESP 레지스터는 해당 바이트만큼 감소하고, POP 명령이 실행되면 ESP 레지스터는 해당 바이트만큼 증가하게 된다.

실습 7 PUSH, POP 명령 (시작)

[예제 2-7] push, pop 명령 예제

POP과 PUSH 명령은 스택을 진행할 때 다시 나온다. PUSH 명령을 이용해 4, 3, 2, 1을 스택에 넣으면 스택에 순차적으로 쌓이게 되며, POP 명령을 통해 입력한 스택부터 꺼내어 쓰게 된다.

[실습그림 2-7-1] POP과 PUSH는 스택을 넣거나 뺄 수 있다

실습 7 PUSH, POP 명령 (끝)

2.4 INC/DEC 명령

이 명령도 이름에서 알 수 있듯이 1을 증가/감소 시키는 명령이다(앞서 CMP 명령 예제에서 진행했었다).

피연산자는 1개이며, 메모리/범용 레지스터를 둘 다 지정 가능하다.

[내용] INC, DEC 명령어 이용 방식

2.4 NOT/AND/OR/XOR 명령

인텔 CPU에서는 위 4가지의 논리 연산을 지원한다(NOR나 NAND 등은 지원하지 않는다). NOT 명령의 경우 한 개의 피연산자를 받으며, 대상 피연산자의 각 비트를 반대로 바꾼다.

AND/OR/XOR 명령은 2개의 피연산자를 받으며, 제약 조건은 MOV 와 비슷하다. 이 명령 역시 각 비트에 대하여 연산을 한다.

XOR 명령은 Operland1과 Operland2 각 비트가 같으면 0이 되고, 다르면 1이 된다.

XOR 연산을 2회 진행하면 원래 값으로 되돌아오는 성격을 이용하여, XOR는 데이터를 암호화 할 때 사용되며, Operland1과 Operland2에 같은 값을 넣어, 함수를 0으로 초기화 할 때에도 사용된다.

그럼 XOR 명령 예제를 통해 확인해 보자.

실습 8 XOR 명령 (시작)

[예제 2-8] XOR 명령 예제

XOR 명령은 값 변환이기 때문에 계산기를 함께 이용하는 게 편할 것이다. 그럼 위 예제의 분석 내용을 살펴보자.


 

[실습그림 2-8-1] XOR 명령으로 같은 값을 연산하면 초기화된다

[실습 2-8-1] XOR 명령 예제

실습 8 XOR 명령 (끝)

OR 명령은 두 개를 비교한 값 중 하나라도 1이 있다면 1을, 아니면 0으로 처리한다. XOR 명령과 차이가 크므로 아래 예제를 통해 확인해 보자.

실습 9 OR 명령 (시작)


 

[예제 2-9] XOR, OR 명령 사용 예제

OR 예제의 주요 분석 내용을 확인해 보면 다음과 같다.

[실습그림 2-9-1] 같은 값 연산시 값이 변화되지 않는다

[실습 2-9-1] OR 명령 예제 분석

실습 9 OR 명령 (끝)

AND 명령의 경우 비교하는 두 값이 모두 1인 경우에만 1을, 그 외의 경우는 0으로 처리한다. NOT 명령은 두 값의 비교가 아닌 하나의 값에 대해 반대로 바꾸는 연산자이다. 그럼 예제를 통해 확인해 보자.

실습 10 AND, NOT 명령 (시작)

[예제 2-10] NOT, AND 연산 예제

분석 방식은 XOR와 비슷하다. 변환 과정에 대해 확인해보도록 하자.

[실습그림 2-10-1] AND 연산은 1인 값이 없는 경우 0이 설정된다

[실습 2-10-1] AND, NOT 명령 예제 분석

실습 10 AND, NOT 명령 (끝)

이러한 연산자 중 XOR는 암호화/복호화 용도로 많이 사용되므로, 암호 구문이 있을 때에는 이를 역이용하여 복호화도 가능함을 명심하기 바란다. 실제 암호화를 진행하는 어셈블리 코드를 단순히 복사하여 복호화에 사용하는 방법도 있다. XOR 명령의 특성인 동일한 값으로 XOR 명령를 두 번 진행하여 원래 값으로 되돌아오는 특성을 이용한 암/복호화 용도로도 사용되는데 그 간단한 예제를 C 을 이용하여 만들어 입력값을 동일하게 주어 확인해 보기 바란다.

실습 11 XOR 암/복호화 (시작)

[예제 2-11] XOR 연산 예제

실습 11 XOR 암/복호화 (끝)

-박스시작-

인라인 어셈블리

C/C 을 이용하여 프로그램을 작성하다가 부분적으로 어셈블리 언어를 사용하는데, 이것을 인라인 어셈블리라 한다. 인라인 어셈블리는 보안 쪽 특히 해킹 쪽에서 많이 사용되며, 다름 사람이 암호화 하였거나, 복호화 하는 코드를 어셈블리 단계에서 복사해서 쓰거나, 프로그램 역분석 중 코드를 어셈블리상에서 복사하여 바로 사용하고자 할 때 종종 사용된다. 그 만큼 리버스 엔지니어에게 필요한 기술로, 프로그램 코드상에서 간단히 선언만으로 사용할 수 있도록 편리하게 구성되어 있다. 앞서 진행한 XOR 예제에 대한 인라인 어셈블리를 아래와 같이 작성할 수 있다.

[예제 2-12] 인라인 어셈블리 예제

-박스종료-

그럼 계속해서 나머지 어셈블리 명령을 살펴보자.

2.4 DIV/IDIV 명령

이 명령은 나누기 연산 명령이다. 부호 없는 나눗셈을 진행한다. 단 이 명령의 결과를 저장할 때 몇 가지 규칙을 통해 결과값이 레지스터에 저장된다.

나눠지는 수로는 EAX 레지스터를 사용하고, 연산값이 32비트 이상인 경우 EDX 레지스터를 함께 사용하게 된다. 이유는 간단하다. 32비트 시스템에서 하나의 레지스터 크기에 각 32비트의 나누어야 하는 값, 나누어지는 값, 이 두 가지 값을 넣을 수 없기 때문에 32비트 이상인 경우 값들을 저장하기 위해 하나 이상의 레지스터가 필요한 것이다.

나눠지는 수가 16비트일 경우 나누는 수는 무조건 8비트로 레지스터 혹은 메모리가 될 수 있다. 이 경우 나눠지는 수는 AX에 저장되어야만 하며, 몫은 AL, 나머지는 AH 레지스터에 저장된다.

나눠지는 수가 32비트일 경우 나누는 수는 무조건 16비트로 레지스터 혹은 메모리가 될 수 있다. 이 경우 나눠지는 수는 DX에서 AX에 걸쳐 저장되며, 몫은 AX, 나머지는 DX 레지스터에 저장된다.

나눠지는 수가 64비트일 경우 나누는 수는 무조건 32비트로 레지스터 혹은 메모리가 될 수 있다. 이 경우 나눠지는 수는 EDX에서 EAX에 걸쳐서 저장되며, 몫은 EAX, 나머지는 EDX 레지스터에 저장된다.

실습 12 DIV, IDIV 명령 (시작)


 

[예제 2-13] DIV, IDIV 명령 예제

IDIV 명령은 DIV 명령과 기능은 같으며, 부호 있는 나눗셈을 실시한다.

부호 있는 나눗셈에서는 음수(-)가 되었는데, 만약 앞에 사용하지 않는 비트가 있다면 CBW, CWD, CDQ 명령을 이용하여 값을 변환한다. CBW 명령은 나눠지는 수가 8비트일 경우 AL에 사용되며, 음수로 만들기 위해 AH 레지스터가 부호 비트로 사용되며, CWD 명령은 16비트인 경우 AX 레지스터의 값의 부호비트 표현을 위해 DX 레지스터가 사용된다. CDQ 명령 역시 나눠지는 수의 크기가 32비트인지, 64비트인지에 따라 부호생성을 위해 사용된다. 설명하다 보니 내용이 어려워 보이는데, 프로그램을 역분석 할 때에는 해당 명령이 어떠한 결과값을 만들어 낼 수 있는지 예측만 하면 되므로 크게 걱정하지 말고 실습을 통해 확인해 보자. 그리고 역분석을 하면서 실제 값이 얼마 인지까지 예측을 할 필요는 없고, 해당 명령이 어떤 용도인지만 이해해도 무방하다.

[실습그림 2-12-1] EAX에 몫, EDX 레지스터에 나머지가 저장된다

[실습그림 2-12-2] DX 레지스터가 부호용도로 사용된다

[실습그림 2-12-3] AX, DX 레지스터에 몫과 나머지가 입력된다

[실습 2-12-1] DIV, IDIV 예제 분석

실습 12 DIV, IDIV 명령 (끝)

MUL/IMUL 명령

이 명령 곱하기 연산 명령이다.

곱해지는 수는 무조건 EAX레지스터의 EAX, AX, AL등, A* 계열의 레지스터이어야 하며, 곱하는 수는 레지스터 혹은 메모리로 곱해지는 수와 같은 사이즈이어야 한다. 8비트 곱셈의 경우 결과는 AX에, 16비트의 경우 DX에서 AX에 걸쳐서, 32비트의 경우 EDX에서 EAX에 걸쳐서 저장된다.

IMUL 명령은 부호 있는 곱셈이다. IMUL의 경우 IDIV에서의 경우와 달리 MUL과 부호 생성 명령이 존재하지 않고, MUL와 사용법이 같다.

2.4 CALL/JMP/RET 명령

CALL 명령은 프로시저를 호출하는 명령으로, 프로그램 단위로 볼 때는 함수라 할 수 있다.

CALL, JMP 명령의 큰 차이점은 CALL 명령은 RET 명령을 해야 하고(현재 프로시저에서 상위 프로시저로 복귀), JMP 명령은 하지 않아도 된다(현 프로시저 유지).

CALL 명령을 통해서 호출하면, 스택의 프롤로그부터 생성하게 된다(현ESP 레지스터를 EBP 레지스터에 저장하여 복귀 지점을 기록하는 행위). 그리고 해당 프로시저에서 사용될 매개변수가 있다면 EBP(혹ESP) 레지스터 기준으로 스택 공간을 할당한다.

그에 반해 JMP 명령은 매개변수 전달을 할 수 없다. JMP 명령은 단지 어셈블리상의 실행 경로 를 변경할 때 사용되는 이동 명령이다. RET 명령은 CALL 명령을 통해 호출한 프로시저로부터 복귀하는 명령으로서, 지역변수의 저장 공간으로 사용하였던 스택을 비우고 이전 프로시저로 복귀하라는 명령이다.

CALL, JMP, RET 명령은 이미 앞서 진행하였으므로 별도 예제는 진행하지 않겠다.


 

[내용] CALL, JUMP, RET 명령 형식

이외에도 여러 어셈블리 명령이 있으며, 어셈블리 언어와 더욱더 친숙해지고 싶다면 Masm32에는 기본적인 교육 예제를 포함하고 있으므로, 해당 내용을 통해 어셈블리를 익히기 바란다. Masm32 설치시 함께 제공하는 C:masm32tutorial에서 확인할 수 있다.

-박스시작-

FPU 레지스터

어셈블리를 배운지 얼마 되지 않은 시절, FADD라는 명령을 분석 도중애 FPU 레지스터를 접한 적이 있다. 많이 생소하고, 이해가 되지 않는 부분들이 많았던 기억이 난다. 여러분 역시 이 레지스터에 대한 궁금증이 있을 거라 생각하여 여기서 해당 부분을 조금은 해소해 보고자 한다.

FPU 레지스터는 부동 소수점 연산을 위한 전용 레지스터라고 생각하면 된다(루트, 삼각함수 연산도 지원한다). FPU 명령은 3D 게임과 같은 좌표 구현시 이용하면 일반 어셈블리 명령을 이용했을 때 보다 빠르게 처리할 수 있다.

FPU 레지스터는 별도의 각 10비트로 구성된 8개의 FPU 전용 스택을 가지고 있는데, 이 레지스터를 부동 소수점 연산뿐만이 아니라, 프로그램 개발시 FPU 레지스터를 멀티 미디어 연산인 MMX 레지스터로도 변경해 사용할 수도 있다. MMX 용도로 사용될 때에는 ST(0)의 하위 64비트는 MM(0)으로 사용된다.

해당 스택은 Ollydbg의 레지스터 창에 표시되며, 아래 그림과 같다.

↓(MMX형으로 표시)

[그림 2-11] FPU 전용 스택

ST(0)에서 ST(7)은 스택 구조와 같이 LIFO(List Input First Out) 방식으로 사용된다. 그럼 먼저 FPU 사용을 위해 사용되는 어셈블리 명령을 알아보자.

연산자

설명

FINIT FPU를 사용하기 위해 초기화 하는 명령
FLD FPU 스택으로 값을 PUSH하는 명령
FILD 정수값을 FPU 스택으로 PUSH하는 명령
FLDPI PI를 FPU 스택으로 PUSH하는 명령
FXCH ST(0)과 ST(1)의 값은 바꾼다
FADD ST(0)과 ST(1)ADD 연산을 진행후 결과값을 꺼내(POP)는 명령
FSUB ST(0)과 ST(1)SUB 연산을 진행후 결과값을 꺼내(POP)는 명령
FMUL ST(0)으로 ST(1)을MUL 연산을 진행후 결과값을 꺼내(POP)는 명령
FDIV ST(0)으로 ST(1)을DIV 연산을 진행후 결과값을 꺼내(POP)는 명령
FSIN ST(0)의 값을 SINE 연산을 진행
FCOS ST(0)의 값을 COSINE 연산을 진행
FTAN ST(0)의 값을 TANGENT 연산을 진행
FASIN ST(0)의 값을 ARCSINE 연산을 진행
FACOS ST(0)의 값을 ARCCOSINE 연산을 진행
FATAN ST(0)의 값을 ARCTANGENT 연산을 진행
FST/FIST FPU 스택에서 ST(0)의 값을 복사한다
FSTP/FISTP FPU 스택에서 ST(0)의 값을 복사한 다음 FPU 스택에서 복사한 값을 꺼내(POP)는 명령(마지막에 p가 붙으면 연산후 해당 값을 꺼내(POP)게 된다)
FWAIT FPU 연산이 완료할 때까지 기다린다.

FPU 연산 명령

FPU 관련 명령은 알아본 것과 같이 연산용 명령들이다. 그럼 이 명령들이 어떻게 사용되는지, 어셈블리의 연산 명령인 ADD와 MUL등과 어떻게 다르고, FPU 전용 스택의 동작 방식을 예제를 통해 알아보도록 보자.

실습 13 FPU 명령 (시작)


 

[예제 2-14] FPU 명령 예제

위 예제를 컴파일 하고 FPU 명령을 사용하는 분석 내용을 확인해보자. FPU에 대해 처음 분석하는 만큼 그림도 함께 추가하였다.

[실습그림 2-13-1] ST(0)과 스택(EBP-4)에 위치한 값을 더한다

[실습그림 2-13-2] ST(1), ST(0)의 값을 곱한다

[실습 2-13-1] FPU 예제 분석

실습 13 FPU 명령 (끝)

http://www.ray.masmcode.com 에 방문하면 FPU 관련 정보를 더 습득할 수 있으므로, FPU에 대해 보다 자세히 알고 싶은 분은 방문해 보기 바란다. 많은 도움이 될 것이다.

-박스종료-

이렇게 어셈블리에서 주요하게 사용되는 명령어들에 대해 알아보았다. 어셈블리를 이해하면서 부가적으로 레지스터에 대한 복습과 스택에 대한 이해도 많은 도움이 되었을 것이다. 계속 애기 하였지만 어셈블리는 운영체제 역분석에서 반드시 이해해야 하는 기초과정과 같다. 이를 통해서 어떻게 기계어를 분석할 수 있는지, 기계어를 분석해주는 디버거와 운영체제 내부에서 처리되는 과정을 이해할 수 있었을 것이다. 이에 조금 더 고급으로 가기 위해서는 Opcode와 어셈블리 명령의 관계를 표로 정리한 Opcode Map과 같은 것을 만들거나, 웹에서 구해서 가까이 두고 보면 역분석을 이해하는데 더 큰 도움이 될 것이다(http://www.mlsite.net/8086/, http://goo.gl/mny0He에서 Opcode Map을 확인할 수 있다). 그럼 계속해서 스택에 대해 알아보도록 하자.

Facebook Comments

Leave A Reply

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