Dreamhack

리버싱 05 - x86 Assembly

kchabin 2022. 7. 12. 03:51

*모든 내용과 이미지의 출처는 x86 Assembly🤖: Essential Part(1) | Dreamhack

 

로그인 | Dreamhack

 

dreamhack.io

 

어셈블리 언어

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

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 : 호출자의 실행 흐름으로 돌아감.