Reverse Engineering
x86 Assembly: Essential Part(1)
박연준
2023. 7. 2. 15:03
- 컴퓨터 과학자 중 한 명인 David Wheeler는 EDSAC을 개발하면서 어셈블리 언어와 어셈블러라는 것을 고안했다.
- 소프트웨어를 역분석하는 사람들은 역발상을 더해, 기계어를 어셈블리 언어로 번역하는 역어셈블러를 개발했다.
x86-64 어셈블리 언어
기본 구조
- x64 어셈블리 언어는 우리가 사용하는 한국어보다는 훨씬 단순한 문법 구조를 지닌다.
- 이들의 문장은 동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성된다.
명령어
- 인텔의 x64에는 매우 많은 명령어가 존재한다.
- 이 중 중요한 21개의 명령어는 다음과 같다.
명령 코드 | |
---|---|
데이터 이동(Data Transfer) | mov , lea |
산술 연산(Arithmetic) | inc , dec , add , sub |
논리 연산(Logical) | and , or , xor , not |
비교(Comparison) | cmp , test |
분기(Branch) | jmp , je , jg |
스택(Stack) | push , pop |
프로시져(Procedure) | call , ret , leave |
시스템 콜(System call) | syscall |
피연산자
- 피연산자에는 총 3가지 종류가 올 수 있다.
- 상수
- 레지스터
- 메모리
- 메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다.
- 여기서 타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다.
메모리 피연산자 | |
---|---|
QWORD PTR [0x8048000] | 0x8048000의 데이터를 8바이트만큼 참조 |
DWORD PTR [0x8048000] | 0x8048000의 데이터를 4바이트만큼 참조 |
WORD PTR [rax] | rax가 가르키는 주소에서 데이터를 2바이트 만큼 참조 |
x86-64 어셈블리 명령어
데이터 이동
- 데이터 이동 명령어는 어떤 값을 레지스터나 메모리에 옮기도록 지시한다.
mov dst, src : src에 들어있는 값을 dst에 대입 | |
---|---|
mov rdi, rsi | rsi의 값을 rdi에 대입 |
mov QWORD PTR[rdi], rsi | rsi의 값을 rdi가 가리키는 주소에 대입 |
mov QWORD PTR[rdi+8*rcx], rsi | rsi의 값을 rdi+8*rcx가 가리키는 주소에 대입 |
lea dst, src : src의 유효 주소(Effective Address, EA)를 dst에 저장합니다. | |
---|---|
lea rsi, [rbx+8*rcx] | rbx+8*rcx 를 rsi에 대입 |
산술 연산
- 산술 연산 명령어는 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다.
add dst, src : dst에 src의 값을 더합니다. | |
---|---|
add eax, 3 | eax += 3 |
add ax, WORD PTR[rdi] | ax += *(WORD *)rdi |
sub dst, src: dst에서 src의 값을 뺍니다. | |
---|---|
sub eax, 3 | eax -= 3 |
sub ax, WORD PTR[rdi] | ax -= *(WORD *)rdi |
inc op: op의 값을 1 증가시킴 | |
---|---|
inc eax | eax += 1 |
dec op: op의 값을 1 감소 시킴 | |
---|---|
dec eax | eax -= 1 |
논리 연산
- 논리 연산 명령어는 and, or, xor, neg 등의 비트 연산을 지시한다.
- 이 연산은 비트 단위로 이루어 진다.
and dst, src: dst와 src의 비트가 모두 1이면 1, 아니면 0
[Register] eax = 0xffff0000 ebx = 0xcafebabe [Code] and eax, ebx [Result] eax = 0xcafe0000
or dst, src: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
[Register] eax = 0xffff0000 ebx = 0xcafebabe [Code] or eax, ebx [Result] eax = 0xffffbabe
xor dst, src: dst와 src의 비트가 서로 다르면 1, 같으면 0
[Register] eax = 0xffffffff ebx = 0xcafebabe [Code] xor eax, ebx [Result] eax = 0x35014541
not op: op의 비트 전부 반전
[Register] eax = 0xffffffff [Code] not eax [Result] eax = 0x00000000
비교
- 비교 명령어는 두 피연산자의 값을 비교하고, 플래그를 설정한다.
cmp op1, op2: op1과 op2를 비교
[Code] 1: mov rax, 0xA 2: mov rbx, 0xA 3: cmp rax, rbx ; ZF=1
test op1, op2: op1과 op2를 비교
[Code] 1: xor rax, rax 2: test rax, rax ; ZF=1
분기
- 분기 명령어는
rip
를 이동시켜 실행 흐름을 바꾼다.jmp addr: addr로 rip를 이동시킵니다.
[Code] 1: xor rax, rax 2: jmp 1 ; jump to 1
je addr: 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
[Code] 1: mov rax, 0xcafebabe 2: mov rbx, 0xcafebabe 3: cmp rax, rbx ; rax == rbx 4: je 1 ; jump to 1
jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)
[Code] 1: mov rax, 0x31337 2: mov rbx, 0x13337 3: cmp rax, rbx ; rax > rbx 4: jg 1 ; jump to 1