발자취

[Reverse Engineering] #05-1. 어셈블리어와 x86-64 - 이론 본문

Dreamhack Study/Reverse Engineering

[Reverse Engineering] #05-1. 어셈블리어와 x86-64 - 이론

해린 2024. 5. 29. 22:08

본 게시물은 Dreamhack의 Reverse Engineering 로드맵 과정을 학습한 기록이다.

 

 

#01. 어셈블리 언어

: 컴퓨터의 기계어와 치환되는 언어. 기계어의 종류가 많은 만큼 많은 수의 어셈블리어가 존재한다.

 

 

#02. x64 어셈블리 언어

1. 기본 구조

  • 명령어(Operation Code, Opcode): 한국어에서의 동사 역할과도 같음
  • 피연산자(Operand): 한국어에서의 목적어 역할과도 같음

 

1) x86-64 어셈블리어 문법 구조의 예

mov eax, 3
  • mov: 명령어(opcode) / 대입하라는 의미.
  • eax: 첫번째 operand / 대입당할 피연산자
  • 3: 두번째 operand / 대입할 피연산자

→ eax에 3을 대입하라

 

 

2. 명령어

명령 코드
데이터 이동(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. 피연산자

1) 종류

  • 상수
  • 레지스터
  • 메모리
    • []로 둘러싸임.
    • 앞에 크기 지정자 TYPE PTR이 추가될 수 있음
      • TYPE 에는 BYTE(1바이트), WORD(2바이트), DWORD(4바이트), QWORD(8바이트)가 올 수 있음.

 

2) 메모리 피연산자의 예

메모리 피연산자
QWORD PTR [0x8048000] 0x8048000의 데이터를 8바이트만큼 참조
DWORD PTR [0x8048000] 0x8048000의 데이터를 4바이트만큼 참조
WORD PTR [rax] rax가 가리키는 주소에서 데이터를 2바이트 만큼 참조

 

 

 

#03. x64 어셈블리 명령어

1. 데이터 이동 명령어

: 어떤 값을 레지스터나 메모리에 옮기도록 지시

 

 

1) mov 명령어

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가 가리키는 주소에 대입

 

2) lea 명령어

lea dst, src → src의 유효 주소(Effective Address, EA)를 dst에 저장합니다.
lea rsi, [rbx+8*rcx] rbx+8*rcx를 rsi에 대입

 

 

2. 산술 연산 명령어

: 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시하는 명령어. 여기서는 덧셈, 뺄셈만 다룸.

 

 

1) add

add dst, src → dst에 src의 값을 더합니다.
add eax, 3 eax += 3
add ax, WORD PTR[rdi] ax += *(WORD *)rdi

 

 

2) sub

sub dst, src → dst에 src의 값을 뺍니다.
sub eax, 3 eax -= 3
sub ax, WORD PTR[rdi] ax -= *(WORD *)rdi

 

 

3) inc

inc op → op의 값을 1 증가시킴
inc eax eax += 1

 

 

4) dec

dec op → op의 값을 1 감소시킴
dec eax eax -= 1

 

2. 논리 연산 명령어

: and, or, xor, neg 등의 비트 연산을 지시하는 명령어. 연산은 비트 단위로 이루어진다.

 

 

1) and dst, src

: dst와 src의 비트가 모두 1이면 1, 아니면 0

[Register]
eax = 0xffff0000
ebx = 0xcafebabe

[Code]
and eax, ebx

[Result]
eax = 0xcafe0000

 

2) or dst, src

: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0

[Register]
eax = 0xffff0000
ebx = 0xcafebabe

[Code]
or eax, ebx

[Result]
eax = 0xffffbabe

 

3) xor dst, src

: dst와 src의 비트가 서로 다르면 1, 같으면 0

[Register]
eax = 0xffffffff
ebx = 0xcafebabe

[Code]
xor eax, ebx

[Result]
eax = 0x35014541
  • eax = 0xffffffff -> 11111111111111111111111111111111
  • ebx = 0xcafebabe -> 11001010111111101011101010111110

xor: 00110101000000010100010101000001

 

 

4) not op

: op의 비트 전부 반전

[Register]
eax = 0xffffffff

[Code]
not eax

[Result]
eax = 0x00000000

 

 

3. 비교 명령어

: 두 피연산자의 값을 비교하고, 플래그를 설정함.

 

 

1) cmp op1, op2

: op1과 op2를 비교

 

cmp는 두 피연산자를 빼서 대소를 비교함.

두 수를 뺐는데 결과가 0이면 ZF 플래그가 설정됨 (ZF=1)

 

 

2) test op1, op2

: op1과 op2를 비교

 

test는 두 피연산자에 AND 비트연산을 취함

역시 결과가 0이면 ZF플래그가 설정됨 (ZF=1)

 

 

4. 분기 명령어

: rip를 이동시켜 실행 흐름을 바꿈

 

1) jmp addr

: addr로 rip를 이동시킴

 

2) je addr

: 직전에 비교한 두 피연산자가 같으면 점프한다 (jump if equal)

(그렇다보니 cmp 연산자와 연달아 오는 경우가 대부분이다.)

 

3) jg addr

: 직전에 비교한 두 피연산자 중 전자가 더 크면 점프 (jump if greater)

 

 

5. 스택 명령어

1) push val

: val을 스택 최상단에 쌓음

 

*참고: rsp는 스택의 최상단을 가리키는 포인터

 

 

2) pop reg

: 스택 최상단 값을 꺼내서 reg에 대입

 

 

6. 프로시저 명령어

6-1. 프로시저란?

: 특정 기능을 수행하는 코드 조각

  • 호출(Call): 프로시저를 부르는 행위
  • 반환(Return): 프로시저에서 돌아오는 것

❣ 프로시저를 호출할 때는 프로시저를 실행하고 난 뒤에 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(Return Address, 반환 주소)를 ㅅ택에 저장하고 프로시저로 rip를 이동시킨다.

 

 

1) call addr

: addr에 위치한 프로시저 호출

 

2) leave

: 스택 프레임 정리

*스택 프레임: 함수별로 사용하는 스택의 영역을 명확히 구분하기 위해 사용하는 것.

 

3) ret

: return address로 반환