보안/드림핵 강의

[Reverse Engineering]x86 Assembly 정리

haerim9.9 2025. 4. 1. 00:49

 
이 글은 워게임(rev-basic-0)을 풀고 정리하면 추가로 공부하면 좋을 내용을 정리한 것이다.
 
어셈블리어 못 읽는게 이번 워게임 풀 때는 크게 문제가 되지 않았지만, 어느 정도 알아두는 편이 좋을 것 같았다. 그래서 드림핵의 리버싱 트랙의 Assembly Essential Part를 읽으며 공부했다. 아래 내용은 해당 부분의 요약본이다.


x86 Assembly🤖 : Essential Part(1), Essential Part(2)
어셈블리 언어(Assembly Language) : 컴퓨터의 기계와 치환되는 언어. 어셈블리어의 종류도 다양하다.

  • 어셈블러(Assembler) : 일종의 통역사로, 개발자가 어셈블리어로 코드를 작성하면 기계어로 코드를 치환해준다.
  • 역어셈블러(Disassembler) : 기계어를 어셈블리 언어로 번역해준다.

 
x64 어셈블리 언어
기본 구조 : 명령어(Operation Code, Opcode) + 피연산자(Operand)
 
명령어

데이터 이동(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

 
피연산자

  • 상수(Immediate Value)
  • 레지스터(Register)
  • 메모리(Memory) : []으로 둘러싸인 것으로 표현된다. 앞에 크기 지정자(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 어셈블리 명령어
1. 데이터 이동

  • mob dst, src : src에 들어있는 값을 dst에 대입
  • lea dst, src : src의 유효 주소(Effective Adress, EA)를 dst에 저장

2. 산술 연산

  • add dst, src : dst에 src의 값을 더함
  • sub dst, src : dst에서 src의 값을 뺌
  • inc op : op의 값을 1 증가
  • dec op : op의 값을 1 감소

3. 논리 연산 - AND, OR

  • and dst, src : dst와 src의 비트가 모두 1이면 1, 아니면 0
  • or dst, src : dst와 src의 비트 중 하나라도 1이면 1, 아니면 0

*논리 연산 예시

#AND 연산자
[Register]
eax = 0xffff0000
ebx = 0xcafebabe

[Code]
and eax, ebx

[Result]
eax = 0xcafe0000


#OR 연산자
[Register]
eax = 0xffff0000
ebx = 0xcafebabe

[Code]
or eax, ebx

[Result]
eax = 0xffffbabe

 
4. 논리 연산 - XOR, NOT

  • xor dst, src : dst와 src의 비트가 서로 다르면 1, 같으면 0
  • not op : op의 비트 전부 반전

*논리 연산 예시

#XOR 연산자
[Register]
eax = 0xffffffff
ebx = 0xcafebabe

[Code]
xor eax, ebx

[Result]
eax = 0x35014541


#NOT 연산자
[Register]
eax = 0xffffffff
ebx = 0xcafebabe

[Code]
xor eax, ebx

[Result]
eax = 0x35014541

 
5. 비교 : 두 피연산자의 값 비교, 플래그 설정

  • cmp op1, op2 : op1과 op2 비교, 두 피연산자를 빼서 대소를 비교한다. 연산 결과를 op1에 대입하지 않는다.
  • test op1, op2 : op1과 op2 비교, 두 피연산자에 AND 비트연산을 취한다. 연산 결과를 op1에 대입하지 않는다.

*cmp에서 같은 두 수를 뺀 결과 = 0, ZF 플래그 설정. -> CPU는 이 플래그를 보고 두 값이 같았는지 판단.
 
6. 분기 : rip를 이동시켜 실행 흐름 변경

  • jmp addr : addr로 *rip를 이동시킴
  • je addr : 직전에 비교한 두 피연산자가 같으면 점프(jump if equal)
  • jg addr : 직전에 비교한 두 연산자 중 전자가 더 크면 점프(jump if greater)

*RIP(Instruction Pointer, 프로그램 카운터) : 현재 실행 중인 명령어의 메모리 주소를 저장하는 레지스터, 쉽게 말해 다음에 실행할 명령어가 어딨는지 가리키는 레지스터
 
7. 스택

  • push val : val을 스택 최상단에 쌓음
  • pop reg : 스택 최상단의 값을 꺼내서 reg에 대입

*스택 예시

#push 예제
[Register]
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc400 | 0x0  <- rsp
0x7fffffffc408 | 0x0

[Code]
push 0x31337

#push 결과
[Register]
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp 
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0


#pop 예제
[Register]
rax = 0
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp 
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0

[Code]
pop rax

#pop 결과
[Register]
rax = 0x31337
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc400 | 0x0 <- rsp 
0x7fffffffc408 | 0x0

*RSP(Stack Pointer, 스택 포인터) : 스택의 최상단을 가리키는 레지스터 (변동됨)
 
8. 프로시저(Procedure) : 특정 기능을 수행하는 코드 조각
호출(Call) : 프로시저를 부르는 행위
반환(Return) : 프로시저에서 돌아오는 것
 
프로시저 호출 시, 실행 후 원래의 실행 흐름으로 돌아와야 하므로 call 다음의 명령어 주소(Return Address, 반환 주소)를 스택에 저장. 후에 프로시저로 rip를 이동시킴.
 

  • call addr : addr에 위치한 프로시저 호출
  • leave : 스택프레임 정리
  • ret : return address로 반환

*스택프레임 : 함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 사용, 대부분의 ABI(Application binary interface)에서 함수는 호출될 때 자신의 스택프레임을 만들고 반환할 때 이를 정리한다.
*RBP(Base Pointer) : 스택 프레임의 기준점이 되는 레지스터, 함수가 시작될 때 현재 rsp 값을 rbp에 저장(고정)