본문 바로가기
Reversing

x64

by Anatis 2021. 1. 29.

명령 주기(Instruction Cycle)

CPU는 아주 복잡해보이지만, 기본적으로는  다음 실행할 명령어를 읽어오고(Fetch) -> 읽어온 명령어를 해석한 다음(Decode) -> 해석한 결과를 실행하는(Excute) 과정을 반복하는 장치이다. 기계 코드가 실행되는 한 번의 과정을 Instruction Cycle이라고 한다.

 

이러한 과정이 매우 고도화 되어있지만, CPU를 구성하는 요소들은 이 역할을 효율적으로 수행하기 위해 필요한 것들이라고 볼 수 있다.

 

레지스터(Register)

CPU는 Instruction Cycle을 수행하기 위해 기계 코드에 해당하는 각종 명령어를 해석하기 위한 구성 요소 외에도 읽어온 명령어가 저장된 공간을 임시로 기억해 둘 구성 요소나, 명령어를 실행한 결과를 저장해 둘 구성 요소가 필요하다. 이러한 CPU의 동작에 필수적인 저장 공간의 역할을 하는 CPU의 구성 요소를 레지스터(Register) 라고 한다.

범용 레지스터(General-Purpose Register, GPR)

범용 레지스터는 특별히 정해두지 않고 다양하게 쓸 수 있는 레지스터다. x64의 범용 레지스터는 총 16개가 있고 원칙적으로는 용도가 정해져 있진 않지만, 관행적으로는 그 쓰임새가 정해져 있는 경우도 있다.

 

rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp, rip, r8, r9, r10, r11, r12, r13, r14, r15의 16개의 범용 레지스터가 있다.

 

RAX는 함수가  실행된 후 리턴값을 저장 하기 위해 쓰인다. 즉, 어떤 함수의 실행이 종료되고 나면 해당 함수의 결과값이 반환될 때 이 rax레지스터에 담겨 반환된다. 하지만 rax는 리턴값을 위해서만 쓰이는것이 아니고 함수가 반환 되기 전까지 범용 레지스터로 자유롭게 사용되다가 종료 후 리턴값을 반환하기 위한 레지스터로 rax가 사용된다.

 

RCX, RDX, R8, R9는 Windows 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장한다.

첫번째 인자는 rcx, 두번째 인자는 rdx에 인자를 레지스터에 담아 함수를 호출한다. rax와 마찬가지로 함수 호출 규약에서 쓰이는 레지스터들 역시 함수를 호출할 때 인자를 전달하는 용도로 이 레지스터들이 정해진다.

 

RSP는 16개의 범용 레지스터 중 하나로 분류되지만 다른 레지스터들과 달리 용도가 정해져 있다.

rsp는 스택 포인터(Stack Pointer)로, 스택의 가장 위쪽 주소를 가리킨다. 스택은 함수가 사용할 지역 변수들(local variables)을 저장하기 위한 공간이다.

 

명령어 포인터(Instruction Pointer)RIP는 다음에 실행될 명령어가 위치한 주소를 가리키고 있다. 프로그램의 실행 흐름과 관련된 중요한 레지스터이므로, 범용으로는 사용되지 않는다.

 

Data Size

CPU가 사용하는 값의 크기 단위를 WORD(16bit)라고 한다. WORD 단위를 처리할 수 있는 범용 레지스터는 ax, cx, dx, bx 등이 있다. 32bit 단위의 레즈스터의 크기는 eax, ebx, ecx, edx가 있고 64bit 단위의 레지스터 크기는 rax, rbx, rcx, rdx가 있다.

 

64bit CPU의 값의 크기는 QWORD(8byte, 64bit)다. 무조건적으로 8byte 단위로만 값을 저장해야 하는 것은 아니다. 위 그림과 같이 rcx 레지스터로 예를 들자면, rcx레지스터에 저장된 값 중 하위 32bit(4byte, DWORD)만 연산에 사용할 수도 있고, 하위 16bit(2byte, WORD)나 하위 8bit(1byte, BYTE)만 사용하는 것도 가능하다.  레지스터의 하위 비트만 접근하려면 어셈블리 코드에서 ecx, cx를 사용하면 된다.

 

r8~r15까지 64bit에서 새로 추가된 범용 레지스터들도 하위 일부 비트만 접근하여 사용하기 위해 다른 레지스터 이름을 사용할 수 있다. r8의 경우 r8d, r8w, r8b를 통해 각각 32bit, 16bit, 8bit에 접근할 수 있다. d, w, b와 같은 접미사를 붙이는 방식은 r8~r15레지스터들이 동일하게 사용된다.

 

Flags

Flags는 상태 레지스터다. 현재 상태나 조건을 0과 1로 나타내고 앞서 본 레지스터들과는 달리 FLAGS 레지스터를 구성하는 64개의 비트들 각각이 서로 다른 의미를 지닌다. 

 

 

Opcode(Operation Code)

명령 코드(Operation Code)는 명령어에서 실제로 어떤 동작을 할지 나타낸다. 자료를 옮기거나, 산술 연산, 제어 등 다양한 명령 코드가 있다.

기계코드(Machine Code) 또는 명령 코드(Opcode)

바이너리로 구성되어 있으며, CPU가 실제로 수행할 작업을 나타내는 숫자이다. 위 그림과 같이 왼쪽 숫자들처럼 생긴 명령 코드를 확인할 수 있다. 이 코드는 CPU의 종류별로 다른 값일 수 있고, 명령 코드에 따라 피연산자(Operand)가 필요하기도 한다.

 

어셈블리 코드(Assembly Code)

명령 코드는 사람이 구분하고 이해하기 쉽지 않다. 그래서 이것이 어떤 의미를 갖는지 쉽도록 문자로 작성된 코드다.

명령 코드와 1:1로 대응하고 연산할때 사용할 피연산자도 알아보기 쉽다. 명령 코드와 피연산자를 묶어 하나의 명령어(Instruction)이 된다.

 

어셈블리 코드는 CPU의 동작을 그대로 옮겨놓은 것에 가깝기 때문에 실제 소스코드와 달리 고차원적인 전체 흐름을 파악하기는 어렵다.

 

Operand

명령 코드가 연산할 대상을 피연산자(Operand)라고 한다. 명령 코드를 함수라고 생각하면 피연산자는 인자라고 생각하면 된다. 명령 코드에 따라 다를 수 잇지만, Intel 방식의 어셈블리를 읽을 때에는 명령 코드에 따라 연산한 결과를 왼쪽 피연산자에 저장된다.

 

명령 코드가 작업을 수행할 대상인 피연산자는 상수일 수도 있고, 레지스터에 들어 있는 값일 수도 있으며, 어떤 주소에 들어있는 값일 수도 있다.

상수값(Immediate)

피연산자로 사용되는 값이 상수인 경우다. 그림을 보면 mov 명령어의 피연산자 중 하나로 0xbeef가 사용되었다. rcx에 0xbeef가 저장될 것이고 add 명령어로 인해 rcx에 담긴 0xbeef와 0x1337을 더하게 된다면 rcx의 값은 0xd226이 될 것이다.

레지스터

레지스터도 피연산자로 사용될 수 있다. 하지만 이 경우에는 레지스터에 들어있는 값이 피연산자로 사용된다.

예를 들어 rbx = 0xdead, rax = 0xc0de라고 가정해보자 그림과 같이 rcx에는 0xdead가 들어갈것이고 sub 명령어로 인해 rcx의 값 0xdead와 rax의 값 0xc0de를 빼면 rcx는 0x1dcf가 된다.

 

Addreaaing Modes

레지스터에 있는 값이 피연산자가 되는 것이 아니라 레지스터에 저장된 메모리 주소를 참조한 값이 피연산자가 되는 경우다.

레지스터에 들어있는 값은 메모리 주소로, 실제로는 해당 메모리 주소를 참조한 값이 피연산자로 사용된다. c언어의 포인터 개념과 유사하다.

 

이제 그림의 코드를 보자. 

[reg]

첫번째 코드는 rcx가 참조하는 메모리 주소에 rax의 값을 저장하게 된다. 

두번째 사용된 byte ptrPointer Directive라고 하며 실제로 어셈블리 코드에서 사용된다. 즉 rcx가 참조하는 메모리 주소에 rax의 하위 8bit(1byte)만 저장하게 된다.

 

[reg + d]

reg + d는 레지스터에 들어있는 값을 주소의 기준으로 하여 d만큼 떨어진 오프셋을 실제로 참조한 다음 피연산자로 쓴다.

세번째 코드는 rax 레지스터에 들어있는 값을 저장할때 rbp의 값을 참조한 메모리 주소에 바로 넣지 않고 rbp 메모리 주소로부터 -0x1C만큼 떨어진 곳을 계산하여 넣는다. Pointer Directive를 고려하여 DWORD에 해당하는 하위 4byte만 넣는걸 알 수 있다.

 

[reg1 + reg2]

한 레지스터에 들어있는 값과 다른 레지스터에 들어있는 값을 더한 결과를 참조할 메모리 주소로 사용하는 경우.

 

[reg1 + reg2 * i + d]

이 방식은 구조체가 사용된 경우 등에서 자주 보이는 방식이다.

네번째 코드는 rdi 레지스터에 담긴 주소를 기준으로, rcx레지스터의 값을 단위로하여 4단위로 떨어진 곳에 다시 offset 3만큼 더한 주소를 실제로 참조한다. 1byte값인 0xFF가 저장된다.

reg2에 해당하는 레지스터에 담긴 값은 대개 자료형이나 구조체의 크기의 경우가 많다.

 

Data Movement

mov - src에 들어있는 값을 dst로 옮긴다.

lea - Load Effective Address로, dst에 주소를 저장한다.

 

첫번째 예문 mov rax, [rbx + 8]의 결과로 rax에 rbx+8이 가리키는 주소에 담긴 값을 rax에 옮기는 것이므로 0x00COFFEE가 들어간다.

두번째 lea rax, [rbx + 8]의 결과로 rax에 rbx에 담긴 주소에 +8한 값이 들어가므로 rax에 0x401A48이 들어간다.

Arithmetic Operations

산술 연산은 FLAGS 레지스터의 CF, OF, ZF 등과 관련이 있다.

 

Unary Instructions

inc, dec - dst의 값을 1 증가시키거나 감소한다.

neg - dst에 들어있는 값의 부호를 바꾼다(2의 보수).

not - dst에 들어있는 값의 비트를 반전한다.

 

Binary Instructions

add - dst에 들어있는 값에 src를 더한다.

sub - dst에 들어있는 값에 src를 뺀다.

imul - dst에 들어있는 값에 src를 곱한다.

and - dst에 들어있는 값과 src간에 AND 논리연산을 한 결과를 dst에 저장한다.

or - dst에 들어있는 값과 src간에 OR 논리연산을 한 결과를 dst에 저장한다.

xor - dst에 들어있는 값과 src간에 XOR 논리연산을 한 결과는 dst에 저장한다.

 

Shift Instructions

shl, shr - dst의 값을 k만큼 왼쪽이나 오른쪽으로 shift한다. 이 때 shift는 logical shift이므로, shr의 경우 오른쪽으로 shift할 때 빈 bit 자리에는 0이 채워진다.

 

sal, sar - dst의 값을 k만큼 왼쪽이나 오른쪽으로 shift하는 것은 같지만, arithmetic shift이기 때문에 부호가 보전된다. sar은 최상위비트(MSB, Most Significant Bit)가 shift 이후에도 보전된다.

Conditional Operations

이번에 볼 명령어들은 분기문이나 조건문과 같이 코드의 실행 흐름을 제어하는 것과 밀접한 연관이 있다. 특히 분기에서 어떻게 코드의 실행 흐름을 정할지는 FLAGS 레지스터의 각종 플래그와 밀접한 관련이 있다.

 

test

test dst, src는 and와 마찬가지로 AND 논리연산을하지만 결과값을 피연산자에 저장하지 않는다.

and dst, src의 결과는 dst에 저장되지만, test 명령어에서는 그렇지 않는다.

test의 연산 결과는 FLAGS 레지스터에 영향을 미친다. 두 피연산자에 대해 AND 연산을 할 경우 음수이면(최상위비트가 1이면) SF=1이 되고, 연산의 결과가 0이면 ZF=1이 된다.

 

cmp

cmp dst, src는 sub와 마찬가지로 dst에서 src를 빼지만 그 결과값이 피연산자인 dst에 저장되지 않고 FLAGS 레지스터의 ZF와 CF 플래그에만 영향을 미친다는 점에서 test와 유사하다.

dst=src일 때 ZF=1, CF=0이되고 dst<src일 때 ZF=0, CF=1 반대로 dst>src일 때 ZF=0, CF=0이 된다.

 

jmp, jcc

jmp, jcc는 피연산자가 가리키는 곳으로 점프한다는 점은 같지만,  무조건 점프하는 것과(jmp) 조건에 따라 점프의 수행여부가 달라진다는 점에서 차이가 있다.

 

jcc가 사용하는 조건은 FLAGS 레지스터의 플래그와 관련이 있다. 이 경우 점프 명령어를 수행하기 전에 어떤 산술 연산을 하거나, test, cmp 등의 연산을 수행한 결과로 바뀐 플래그를 바탕으로 점프의 수행 여부를 결정한다.

jcc는 명령어의 이름이 아니라, 조건부 jmp를 묶어서 이르는 이름이다(Jump if condition is met).

 

위 그림의 밑에코드로 예를 들어보자.

cmp dword ptr [rbp-0x2c], 0x47은 cmp 명령어의 결과를 바탕으로 jle 명령어를 수행한다.

rbp 레지스터 주소에서 -0x2c만큼 떨어진 곳에 있는 값과 0x47을 비교하여 작거나 같으면 0x400a31로 점프한다.

 

test rax, rax는 rax가 0이면 test 명령어를 수행한뒤 ZF=1이 되므로, je명령어로 인해 0x4006c5로 점프한다.

주어진 조건을 만족하지 않으면 jcc를 수행하지않고 다음으로 넘어간다.

'Reversing' 카테고리의 다른 글

Base Relocation Table  (0) 2021.12.30
PE File Format  (0) 2021.12.15
윈도우 기초  (0) 2021.04.22
easy-crackme1  (0) 2021.02.20
Reverse Engineering  (0) 2021.01.28

댓글