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 이동 |
'문제 풀이 > [DreamHack]' 카테고리의 다른 글
[DreamHack] System Hacking Stage2 - Quiz: x86 Assembly 1 (0) | 2022.12.20 |
---|---|
[DreamHack] SystemHacking Stage2 - x86 Assembly: Essential Part 2 (0) | 2022.12.20 |
[DreamHack] SystemHacking Stage2 - Quiz: Computer Architecture (0) | 2022.12.19 |
[DreamHack] SystemHacking Stage2 - Background: Computer Architecture (0) | 2022.12.19 |
[DreamHack] System Hacking Stage2 - Quiz: Linux Memory Layout (0) | 2022.12.19 |