[리버스 엔지니어링] x86 Assembly(1)
1. Disassembler
Disassembler를 이해하기 위해서는 먼저 assembler를 이해해야한다. 이는 밑에 사진으로 설명을 대체한다.
리버스 엔지니어링에서는 어셈블러에 역발상을 더해 역어셈블러(Disassembler)를 개발했다. 이는 어셈블러와 반대로 기계어를 어셈블리 언어로 번역한다.
대표적인 CPU 아키텍처의 명령어 집합 구조(Instruction Set Architecture, ISA)인 x86-64를 비롯하여 대중적으로 많이 사용되는 아키텍처들은 인터넷에서 역어셈블러를 구하기 매우 쉽다. 여러 ISA가 있지만 우리는 x86-64을 다룰 예정이다.
2. x86-64
x86-64 == x64 라고 생각해도 괜찮다.
우선 명령어 구조부터 살펴보겠다. 이 구조는 기본적으로 명령어(operation code), 피연산자(operand)로 구성되어 있다.
위 코드에서는 이와 같다.
- mov → opcode (명령어)
- eax, 3 → operand (피연산자)
1.1 명령어
<명령어> src, dst
→ src는 source, dst는 destination
명령 코드 | |
데이터 이동(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 |
각 명령어를 예시를 들어서 설명하겠다.
1) 데이터 이동
오른쪽에 있는 피연산자를 왼쪽에 대입하는 형식
※ mov는 데이터를 복사하고, lea는 주소를 계산한다.
mov rdi, rsi
→ rsi의 값을 rdi에 대입
mov QWORD PTR[rdi], rsi
→ rsi에 들어있는 8바이트 값을 rdi가 가리키는 메모리 주소에 저장한다
→ 즉, 메모리에 8바이트 크기의 값이 복사됨
메모리 주소인 [rdi]에 몇 바이트를 저장할 건지를 명시해야 하기 때문에 QWORD PTR을 쓰는 것
lea rsi, [rbx+8*rcx]
→ rsi에 주소 rbx + 8 * rcx를 계산해서 저장 (계산된 값이 주소 자체)
2) 산술 연산
sub, add는 너무 쉬우니 넘어가려고 했으나 컴퓨터구조 시간에 배운 ARM, LEG와 다른 점을 하나 설명해보겠다.
ARM 같은 경우는 add 연산자를 사용할 때 피연산자가 세개 필요(3피연산자 방식)하지만 x64는 두개면 충분(2피연산자 방식)하다.
이제 inc, dec의 예시를 들어보겠다.
inc eax // eax += 1
dec eax // eax -= 1
3) 논리 연산
이 연산은 비트 단위로 이루어진다.
and: 비트가 모두 1이면 1, 아니면 0
or: 비트 중 하나라도 1이면 1
xor: 비트가 서로 다르면 1
not: 비트 전부 반전
[Register]
eax = 0xffff0000
ebx = 0xcafebabe
[Code]
and eax, ebx
[Result]
eax = 0xcafe0000
(참고)
레지스터값 (16진수)이진수
eax | 0xffff0000 | 11111111 11111111 00000000 00000000 |
ebx | 0xcafebabe | 11001010 11111110 10111010 10111110 |
[Register]
eax = 0xffffffff
[Code]
not eax
[Result]
eax = 0x00000000
4) 비교
두 연산자의 값을 비교하고 플래그 설정
cmp op1, op2: 두 피연산자를 빼서 대소 비교
test op1, op2: 두 연산자에 AND 연산을 취한다. 연산의 결과는 op1에 대입하지 않는다.
[Code]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
[Code]
1: xor rax, rax
2: test rax, rax ; ZF=1
→ 이 코드처럼 0이된 rax를 op1, op2 삼아 test를 수행하면 결과가 0으로 ZF 플래그가 설정된다. 이후에 CPU가 이 플래그를 보고 rax가 0이었는지 판단할 수 있다.
5) 분기
jmp: 무조건 점프
je: 같으면 점프 (ZF = 1)
jg: 크면 점프 (ZF = 0 && SF = OF)
[Code]
1: xor rax, rax
2: jmp 1 ; jump to 1
[Code]
1: mov rax, 0xcafebabe
2: mov rbx, 0xcafebabe
3: cmp rax, rbx ; rax == rbx
4: je 1 ; jump to 1
[Code]
1: mov rax, 0x31337
2: mov rbx, 0x13337
3: cmp rax, rbx ; rax > rbx
4: jg 1 ; jump to 1
→ 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)
1.2 피연산자
피연산자 자리에는 상수(immediate value), 레지스터(register), 메모리(Memory)가 올 수 있다.
메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다.
여기서 TYPE에는 BYTE,WORD, DWORD, QWORD가 올 수 있으며 각각 1, 2, 4, 8바이트의 크기를 지정한다.
ex)
QWORD PTR [0x8048000] -> 0x8048000의 데이터를 8바이트만큼 참조
1.3 플래그
주요 플래그는 다음과 같다.
Carry Flag | CF | 덧셈/뺄셈 시 자릿수가 넘어갈 때(캐리/버로우 발생) 설정 |
Zero Flag | ZF | 연산 결과가 0이면 설정됨 |
Sign Flag | SF | 연산 결과가 음수이면 설정됨 (MSB=1일 때) |
Overflow Flag | OF | 부호 있는 연산에서 오버플로우가 발생하면 설정 |
Parity Flag | PF | 연산 결과의 하위 8비트에서 1의 개수가 짝수면 설정 |
Auxiliary Carry Flag | AF | BCD 연산에서 사용 (4비트 자리에서 캐리 발생 시) |
Direction Flag | DF | 문자열 처리 시, 방향 결정 (0: 증가, 1: 감소) |
Interrupt Flag | IF | 인터럽트 허용 여부 (1: 허용, 0: 금지) |