[모각코] Reversing(2~3강)
08-04 수요일
x64 기초
Intro
어셈블리 코드는 기계 코드와 대응되기 때문에, CPU가 어떤 역할을 하고
어떻게 동작하는지를 알아보는 것이 어셈블리 코드를 이해하는데 도움이 됨
CPU는 기본적으로 다음 실행할 명령어를 읽어오고(Fetch)->읽어온 명령어를 해석한 다음(Decode)
->해석한 결과를 실행하는(Excute) 과정을 반복하는 장치
Instruction Cycle - 기계 코드가 실행되는 한 번의 과정
CPU는 Instruction Cycle을 수행하기 위해 각종 명렁어를 저장할 임시 공간
레지스터(Register) 를 사용함
레지스터
레지스터는 특별히 쓰임새가 정해지지 않았지만, 관행적으로 용도를 정해놓고 쓰는 레지스터도 있음
범용 레지스터
CPU는 범용 레지스터를 연습장처럼 사용하며, x64의 볌용 레지스터는 총 16개
rax - 리턴값을 저장하기 위해 쓰임
rcx, rdx, r8, r9 .. - Windows 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장
rsp - 용도가 정해져 있어 스택 포인터로 스택의 가장 위쪽 주소를 가리킴
rip - 다음에 실행될 명령어가 위치한 주소를 가리킴
Data Size
16bit - ax, cx, dx, bx
32bit - eax, ecx, edx, ebx
64bit CPU에서 꼭 8byte 단위로만 값을 저장하는 것이 아니라 어셈블리 코드를 통해 하위 16bit,
하위 8bit만 사용하는 것도 가능하며 이는 어셈블리 코드에서 레지스터 이름으로 ecx, cx를 이용하면 됨
FLAGS
FLAGS는 상태 레지스터로 현재 상태나 조건을 0과 1로 나타내는 레지스터 64개의 비트들 각각이 서로 다른 의미를 지니며, 즉 0번째, 1번째, … 서로 다른 상태를 나타냄
| Flag Abbreviation | 설명 |
|---|---|
| CF(Carry Flag) | 산술 연산 혹은 bit shift/rotate 등의 연산이 일어났을 때, 자리 올림이 생기는 경우 CF의 값이 1 |
| ZF(Zero Flag) | 연산의 결과가 0일때 ZF는 1 |
| SF(Sign Flag) | 부호가 있는 값의 연산에 쓰이며, 최상위 비트가 0이면 SF=0, 음수가 되면 최상위 비트가 1이고 똑같이 SF=1 |
| OF(Overflow Flag) | 부호가 있는 값의 연산에서 CF의 역할 |
Instruction Format
Opcode (Operation Code)
명령 코드(Opcode) - 명령어에서 실제로 어떤 동작을 할지를 나타내는 부분
-
기계 코드(Machine Code) 또는 명령 코드(Opcode)
컴파일러가 만드는 결과물인 바이너리를 구성, CPU가 실제로 수행할 작업을 나타내는 숫자 명령 코드에 따라 피연산자가 필요하기도 함
-
어셈블리 코드(Assembly Code)
숫자로 이뤄진 명령 코드를 사람이 이해할 수 있도록 작성된 코드 명령 코드와 1:1로 대응되며, 연산할때 사용할 피연산자도 알아보기 쉬움
명령코드 어셈블리 코드 55 push rbp 48 89 e5 mov rbp,rsp 48 8d 3d 9f 00 00 00 lea rdi,[rip+0x9f] e8 c6 fe ff ff call 510 <puts@plt> b8 00 00 00 00 mov eax,0x0 5d pop rbp c3 ret-
Operand
명령 코드가 연산할 대상, 명령 코드에 따라 조금씩 다르지만, Intel 방식의 어셈블리를
읽을 때에는 명령 코드에 따라 연산한 결과를 왼쪽 피연산자에 저장된다 고 이해하는 것이 일반적
mov rbp,rsp ; rbp = rsp mov eax,0x0 ; eax = 0x0 add rcx,0x8 ; rcx = rcx+0x8 dec rcx ; rcx = rcx-1-
Operand Types
상수, 레지스터, 혹은 레지스터가 가리키고 있는 메모리의 어떤 주소
-
Addressing Modes
레지스터에 있는 값이 피연산자가 되는 것이 아니라 레지스터에 저장된 메모리 주소를
참조하는 값 이 피연산자가 되는 경우즉, 레지스터에 들어있는 값은 메모리 주소로 실제로는 해당 메모리 주소를 참조한 값이
피연산자로 사용됨
-
-
Instructions
-
Data Movement
-
mov
mov는src에 들어있는 값을dst로 옮김 -
lea
lea는 Load Effective Address로,dst에 주소를 저장
-
-
Arithmetic Operations
-
Unary Instructions
Instructions 설명 inc, dec dst의 값을 1증감neg dst에 들어있는 값의 부호를 바꿈not dst에 들어있는 값의 비트를 반전 -
Binary Instructions
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
Instructions 설명 shl, shr dst의 값을k만큼 왼쪽이나 오른쪽으로 shiftshr의 경우 오른쪽으로 shift 할때 빈 bit에 0이 채워짐sal, sar dst의 값을k만큼 shift하는 것 같지만, 부호가 보전됨.
-
-
Conditional Operations
조건문과 같이 코드의 실행 흐름을 제어하는 것과 연관
-
test
논리연산을 하지만, 결과값을 피연산자에 저장하지 않음 FLAGS 레지스터에 영향을 미침
-
cmp
마찬가지로 FLAGS 레지스터의 ZF와 CF 플래그에만 영향을 미침
-
jmp, jcc
jmp- 피연산자가 가리키는 곳으로 점프jcc- 조건에 따라 점프
-
-
Stack Operations
-
rsp revisit
rsp- 스택의 가장 위쪽을 가리키므로, 마지막으로 데이터가 추가된 위치를 저장하는 레지스터
-
-
Function Prologue/Epilogue
함수가 시작할 때 에는
rsp레지스터에 들어있는 주소에서 충분한 값을 뺌(pop)함수가 끝날 떄 에는 프롤로그에서 빼준 값만큼 다시
rsp에 더해줌(push) -
Procedure Call Instructions
-
call
함수를 실행할 때 쓰임, 피연산자로 실행할 함수의 주소를 받음
call로 호출한 함수고 종료되고 나면 다음 명령어를 실행할 장소로 돌아와야함 -
ret
호출된 함수가 마지막으로 사용하는 명령어
함수를 종료한 뒤 Return Address로 돌아가는 역할
-
puts(“hello world!\n”); -> x86_64 asm
#include <stdio.h>
int main(){
puts("hello world!\n");
return 0;
}
x64dbg를 통한 main 함수 일부분
- 주소
해당 어셈블리 코드의 시작 주소가 표시
- 기계 코드
: 앞에 있는 값은 prefix이고 띄어쓰기 다음에 있는 부분은 어셈블리 코드의 인자 부분
-
어셈블리어
사람이 읽기 쉬운 형태로 표시해 줌
-
코멘트
프로그램을 분석하여 알게된 추가적인 정보 표시
2번째 줄에 7FF6ED802220에 있는 문자열에 대한 정보가 표시되는 것을 확인할 수 있음
7FF6ED801000 | 48:83EC 28 | sub rsp,28 |
7FF6ED801004 | 48:8D0D 15120000 | lea rcx,qword ptr ds:[7FF6ED802220] | 00007FF6ED802220:"hello world!\n"
7FF6ED80100B | FF15 5F110000 | call qword ptr ds:[<&puts>] |
7FF6ED801011 | 33C0 | xor eax,eax |
7FF6ED801013 | 48:83C4 28 | add rsp,28 |
7FF6ED801017 | C3 | ret |
sub rsp, 28
rsp에서 0x28 만큼 빼 함수 내부에서 사용할 스택의 용량을 확보하는 명령어
lea rcx,qword ptr ds:[7FF6ED802220]
rcx에 0x7FF6ED80220 값을 저장, 이는 주소값이며 x64dbg가 생성한
디스어셈블 결과의 코멘트를 확인하면 puts의 첫째 인자 hello world!\n가 위치한 주소임을 알 수 있음
call qword ptr ds:[<&puts>]
puts를 호출하는 명렁어
xor eax,eax
eax를 0으로 만들어주는 명령어, main함수의 리턴값을 0으로 설정해놨기 때문에
함수의 리턴값을 의미하는 eax 레지스터를 0으로 설정하는 것
add rsp,28
함수 시작시 확보해두었던 스택을 정리하는 명령어
ret
함수의 실행을 마치고 리턴하기 위해 사용하는 명령어
64비트 windows의 함수 호출 규약
windows의 함수 호출 규약은 다음과 같은 순서로 첫 4개의 인자를 받음
- rcx(ecx, cx, …)
- rdx(edx, dx, …)
- r8(r8d, r8w, …)
- r9(r9d, r9w, …)
이후 5번째 인자부터는 스택에 넣게 되어 함수의 리턴값은 rax(eax, ax, …)에 저장
8개의 인자를 받는 함수의 디스어셈블 결과
7FF611801040 | sub rsp,48 |
7FF611801044 | mov dword ptr ss:[rsp+38],8 |
7FF61180104C | mov dword ptr ss:[rsp+30],7 |
7FF611801054 | mov dword ptr ss:[rsp+28],6 |
7FF61180105C | mov dword ptr ss:[rsp+20],5 |
7FF611801064 | mov r9d,4 |
7FF61180106A | mov r8d,3 |
7FF611801070 | mov edx,2 |
7FF611801075 | mov ecx,1 |
7FF61180107A | call consoleapplication1.7FF611801000 |
7FF61180107F | xor eax,eax |
7FF611801081 | add rsp,48 |
7FF611801085 | ret |
참고자료
dreamHack Reverse Engineering