본문 바로가기
문제 풀이/[DreamHack]

[DreamHack] SystemHacking Stage2 - x86 Assembly: Essential Part 1

by 조랩 2022. 12. 20.

Lecture

 

해커의 언어: 어셈블리

- 컴퓨터 속에는 복잡하고 논리적인 인과관계가 존재하고, 여러 개체가 상호작용한다.

- 그 속에서 통용되는 기계어(Machine Code)라는 언어가 있다.

- 해커는 컴퓨터의 언어로 작성된 소프트웨어에서 취약점을 발견해야 하기 때문에 컴퓨터 언어에 관한 지식을 습득해야한다.

- 그런데, 기계어는 0과 1로만 이루어져있어서 이해하기 어렵다.

- 그래서 David Wheeler는 EDSAC를 개발하면서 어셈블리 언어(Assembly Language)와 어셈블러(Assembler)를 고안했다.

- 어셈블러? 일종의 통역사이다. 어셈블리어로 코드 작성 -> 기계어로 변환 시켜준다.

- 소프트웨어를 역분석 하는 사람들은 기계어를 어셈블리 언어로 번역하는 역어셈블러(Disassembler)를 개발했다.

- 이로인해 소프트웨어 분석가들은 기계어를 읽을 필요가 없어졌다.

- x86-64아키텍처를 비롯하여 대중적으로 많이 사용되는 아키텍처들은 인터넷에서 역어셈블러를 구하기 매우 쉽다.

 

어셈블리 언어

- 컴퓨터의 기계어와 치환되는 언어이다.

- x64에는 x64의 어셈블리어가 존재하고, ARM에는 ARM의 어셈블리어가 존재한다.

- 기계어가 여러종류이기 대문에 어셈블리어도 여러 종류가 존재한다.

- 이건 진짜 많이 알 수록 좋다.

 

x64 어셈블리 언어

기본 구조

- x64 어셈블리 언어는 명령어(Operation Code, Opcode)피연산자(Operand)로 이루어져있다.

mov eax, 3
// mov: 대입해라 (opcode)
// eax: eax에 (operand1)
// 3: 3을 (operand2)

명령어

- x64에는 매우 많은 명령어가 존재한다.

- 다음과 같은 명령어들이 있다.

명령 코드  
데이터 이동 (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가지 종류가 있다.

- 상수(Immediate Value), 레지스터(Register), 메모리(Memory)

- 메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다.

- 여기서 타입에는 BYTE(1바이트), WORD(2바이트), DWORD(4바이트), QWORD(8바이트)가 올 수 있다.

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

 

자료형 WORD의 크기가 2바이트인 이유?

- 초기에 인텔은 WORD의 크기가 16비트인 IA-16을 개발했기 때문에 WORD가 16비트였다.

- 따라서 어셈블리 언어 에서도 WORD를 16비트로 정의하는것이 자연스러웠다.

- 이후 32비트, 그리고 64비트로 확장되면서 DWORD(Double Word, 32bit), QWORD(Quad Word, 64bit)이 추가로 만들어졌다.

 

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에 대입

- 예제: 데이터 이동

[Register]
rbx = 0x401A40
=================================
[Memory]
0x401a40 | 0x0000000012345678
0x401a48 | 0x0000000000C0FFEE
0x401a50 | 0x00000000DEADBEEF
0x401a58 | 0x00000000CAFEBABE
0x401a60 | 0x0000000087654321
=================================
[Code]
1: mov rax, [rbx+8]
2: lea rax, [rbx+8]

문제 1. Code를 1까지 실행했을 때 rax에 저장된 값은?

답: 0xCOFFEE

 

문제 2. Code를 2까지 실행했을 때 rax에 들어있는 값은?

답: 0x401A48

 

산술 연산

- 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다.

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

- 예제: 덧셈과 뺄셈

[Register]
rax = 0x31337
rbx = 0x555555554000
rcx = 0x2
=================================
[Memory]
0x555555554000| 0x0000000000000000
0x555555554008| 0x0000000000000001
0x555555554010| 0x0000000000000003
0x555555554018| 0x0000000000000005
0x555555554020| 0x000000000003133A
==================================
[Code]
1: add rax, [rbx+rcx*8]
2: add rcx, 2
3: sub rax, [rbx+rcx*8]
4: inc rax

문제 1. Code를 1까지 실행했을 때, rax에 저장된 값은?

답: 0x3133A

 

문제 2. Code를 3까지 실행했을 때, rax에 저장된 값은?

답: 0

 

문제 3. Code를 4까지 실행했을 때, rax에 저장된 값은?

답: 1

 

논리 연산 - and & or

- 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

- 예제: 논리 연산 - and, or

[Register]
rax = 0xffffffff00000000
rbx = 0x00000000ffffffff
rcx = 0x123456789abcdef0
==================================
[Code]
1: and rax, rcx
2: and rbx, rcx
3: or rax, rbx

문제 1. Code를 1까지 실행했을 때 rax에 저장된 값은?

답: 0x1234567800000000

 

문제 2. Code를 2까지 실행했을 때 rbx에 저장된 값은?

답: 0x000000009abcdef0

 

문제 3. Code를 3까지 실행했을 때 rax에 저장된 값은?

답: 0x123456789abcdef0

 

논리 연산 - xor & not

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

- 예제: 논리 연산 - xor, not

[Register]
rax = 0x35014541
rbx = 0xdeadbeef
==================================
[Code]
1: xor rax, rbx
2: xor rax, rbx
3: not eax

문제 1. Code를 1까지 실행했을 때 rax에 저장되는 값은?

답: 0xebacfbae

 

문제 2. Code를 2까지 실행했을 때 rax에 저장되는 값은?

답: 0x35014541

- xor연산을 동일한 값으로 두 번 실행할 경우, 원래 값으로 돌아간다.

- 이런 성격을 이용하여 XOR cipher라는 단순 암호가 개발되었다. (링크)

 

문제 3. Code를 3까지 실행했을 때 rax에 저장되는 값은?

답: 0xcafebabe

- eax를 not해도 괜찮은 이유는 eax가 rax의 하위 32비트를 가리키기 때문이다.

 

비교

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

cmp op1, op2: op1과 op2를 비교한다.
cmp는 두 피연산자를 빼서 대소를 비교한다. 연산의 결과는 op1에 대입하지 않는다.
예를 들어, 서로 같은 두 수를 빼면 ZF플래그가 설정되는데, 이를 이용하여 CPU는 같았는지 판단할 수 있다.
[Code]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
test op1, op2: op1과 op2를 비교한다.
test는 두 피연산자에 AND 비트연산을 취한다. 연산의 결과는 op1에 대입하지 않는다.
예를 들어, 아래 코드에서 처럼 0이 된 rax를 op1과 op2로 삼아 test를 하면 결과가 0이므로 ZF플래그가 설정된다.
이를 보고 CPU는 rax가 0이었는지 판단할 수 있다.
[Code]
1: xor rax, rax
2: test rax, rax ; ZF=1

 

분기

- 실행 흐름을 바꾼다.

- 여기에 나오는 것 외에도 수많은 경우가 존재한다.

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

Note

 

요약  
데이터 이동 연산자 - mov dst, src: src의 값을 dst에 대입
- lea dst, src: src의 유효 주소를 dst에 대입
산술 연산 - add dst, src: src의 값을 dst에 더함
- sub dst, src: src의 값을 dst에서 뺌
- inc op: op의 값을 1 더함
- dec op: op의 값을 1 뺌
논리 연산 - and dst, src: dst와 src가 모두 1이면 1, 아니면 0
- or dst, src: dst와 src중 한 쪽이라도 1이면 1, 아니면 0
- xor dst, src: dst와 src가 다르면 1 같으면 0
- not op: op의 비트를 모두 반전
비교 - cmp op1, op2: op1에서 op2를 빼고 플래그를 설정
- test op1, op2: op1과 op2에 AND연산을 하고, 플래그를 설정
분기 - jmp addr: addr로 rip이동
- je addr: 직전 비교에서 두 피연산자의 값이 같을 경우 addr로 rip이동
- jg addr: 직전 비교에서 두 피연산자 중 전자의 값이 더 클 경우 addr로 rip 이동

 

728x90