*모든 내용과 이미지의 출처는 x86 Assembly🤖: Essential Part(1) | Dreamhack
어셈블리 언어
컴퓨터의 기계어와 치환되는 언어.
CPU에 사용되는 ISA는 IA-32, x86-64 등 종류가 굉장히 다양함 -> 이 종류만큼 많은 수의 어셈블리어가 존재함.
해당 강의는 x64 아키텍처를 대상으로 하기 때문에 x64 어셈블리어만 공부함.
x64 어셈블리 언어
기본 구조
명령어(Operation Code) + 피연산자(Operand)
명령어는 동사 역할, 피연산자는 목적어 역할
mov eax, 3
opecode = 대입해라
operand1 = eax에operand2 = 3을
주요 명령어 21가지
산술 연산(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) -> []으로 둘러싸인 것으로 표현됨.
TYPE PTR [메모리 피연산자]
*크기 지정자
메모리 피연산자의 예
TYPE에는 BYTE, WORD, DWORD, QWORD가 올 수 있음.
각각 1, 2, 4, 8 바이트.
QWORD PTR [0x8048000] | 0x8048000의 데이터를 8바이트만큼 참조 |
DWORD PTR [0x8048000] | 0x8048000의 데이터를 4바이트만큼 참조 |
WORD PTR [rax] | rax가 가리키는 주소에서 데이터를 2바이트 만큼 참조 |
Opcode: 데이터 이동
어떤 값을 레지스터나 메모리에 올리도록 지시함.
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에 대입
Opcode: 산술 연산
add = 더하기, sub = 뺴기, inc = 1 증가, dec = 1 감소
add dst, src : dst에 src의 값을 더함.
add eax, 3 | eax += 3 |
add ax, WORD PTR[rdi] | ax += *(WORD *)rdi |
sub dst, src : dst에서 src의 값을 뺌.
inc op : op += 1
dec op : op -= 1\
Opcode: 논리 연산
and, or, xor, neg 등
and dst, src : dst와 src의 비트가 모두 1이면 1, 아니면 0
or dst, src : dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
계산방식은 이진수로 바꾸어서 and 연산, or 연산 하고 다시 16진수로 바꾼다.
xor = 서로 다르면 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
-> 비트가 이진수로 바꾸면 전부 1이니까 전부 반전 시켜서 0만 나옴
XOR 연산을 동일한 값으로 두 번 실행할 경우, 원래 값으로 돌아감.
-> XOR Cipher : 이런 성격을 이용한 단순 암호.
비교
cmp op1, op2 : op1과 op2를 비교
두 피연산자를 빼서 대소를 비교하고, 연산의 결과는 op1에 대입하지 않음.
예를 들어 같은 두 수끼리 빼면 값이 0이니까 플래그가 ZF(Zero Flag)가 되는데, 이후에 CPU는 이 플래그를 보고 두 값이 같았는지 판단할 수 있음.
[Code]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
test op1, op2 : 두 피연산자에 AND 비트연산을 취함.
역시나 연산의 결과는 op1에 대입하지 않음.
[Code]
1: xor rax, rax
2: test rax, rax ; ZF=1
위 코드처럼 rax로 xor 연산 이후에 test를 하면 결과가 0이므로 ZF 플래그 설정됨.
-> rax끼리 xor 하면 비트가 같으니까 전부 0이 되고, test에서 and 연산을 할 경우에도 0이 나오기 때문에.
분기
rip을 이동시켜 실행 흐름을 바꿈.
jmp addr : addr로 rip을 이동시킴
[Code]
1: xor rax, rax
2: jmp 1 ; jump to 1
je addr : 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
[Code]
1: mov rax, 0xcafrbabe
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
push val : val을 스택 최상단에 쌓음
0x31337을 스택 최상단에 쌓음. rsp는 사용중인 스택의 위치를 가리키는 포인터임.
pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
pop rax 명령어를 통해서 스택 최상단에 있던 0x31337 값이 스택이란 바구니 안에서 꺼내지고 rax로 대입됨.
Opcode : 프로시저
procedure : 특정 기능을 수행하는 코드 조각
- 반복되는 연산을 프로시저 호출로 대체, 전체 코드 크기를 줄일 수 있음.
- 기능별로 코드 조각에 이름을 붙일 수 있어 코드의 가독성 좋아짐.
- 호출(Call) : 프로시저를 부름.
- 반환(Return) : 프로시저에서 돌아오는 것.
프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소를 스택에 저장하고 프로시저로 rip를 이동시킴
call addr : addr에 위치한 프로시저 호출
rip는 어느 부분의 코드를 실행할지 가리키는 역할. 0x401000 주소를 호출해서 그 부분의 코드를 실행하는 것 같음.
esi에 eax를 대입함.
ret : 호출자의 실행 흐름으로 돌아감.
'Dreamhack' 카테고리의 다른 글
리버싱 07 - rev-basic-1 (0) | 2022.07.12 |
---|---|
리버싱 06 - rev-basic-0 (0) | 2022.07.12 |
리버싱 04 - Windows Memory Layout (0) | 2022.07.10 |
리버싱 03 - 컴퓨터 구조, 명령어 집합 구조 (0) | 2022.07.10 |
리버싱 02 - 동적 분석과 정적 분석 (0) | 2022.07.10 |