[모각코] 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