티스토리 뷰

System Hacking

x86 Assembly (1)

SeYuNi 2024. 1. 28. 17:08

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

 

어셈블러

: 개발자들이 어셈블리어로 코드를 작성하면 컴퓨터가 이해할 수 있는 기계어로 코드를 치환해줌.

역어셈블러

: 기계어를 어셈블리 언어로 번역해줌.

 

1. 어셈블리 언어 (Assembly Language)와 x86-64

어셈블리 언어 (Assembly Language)

: 컴퓨터의 기계어와 치환되는 언어. 기계어도 여러 종류가 있기 때문에 어셈블리어도 여러 종류가 있다.

x64 어셈블리 언어

x64 어셈블리 언어는 동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성된다.

x86-64 어셈블리어 문법 구조

명령어

명령 코드  
데이터 이동(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, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다.

 

메모리 피연산자의 예

QWORD PTR [0x8048000] 0x8048000의 데이터를 8바이트만큼 참조
DWORD PTR [0x8048000] 0x8048000의 데이터를 4바이트만큼 참조
WORD PTR [rax] rax가 가르키는 주소에서 데이터를 2바이트 만큼 참조

 

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

 

 초기에 인텔은 WORD의 크기가 16비트인 IA-16 아키텍처를 개발했다.  CPU의 WORD가 16비트였기 때문에, 어셈블리어에서도 WORD를 16비트 자료형으로 정의하는 것이 자연스러웠다.

 이후에 개발된 IA-32, x86-64 아키텍처는 CPU의 WORD가 32비트, 64비트로 확장됐다. 그러므로 이 둘의 아키텍처에서는 WORD 자료형이 32비트, 64비트의 크기를 지정하는 것이 당연할 것 같았다.

 그러나 인텔은 WORD 자료형의 크기를 16비트로 유지했다. 왜냐하면, WORD 자료형의 크기를 변경하면 기존의 프로그램들이 새로운 아키텍처와 호환되지 않을 수 있기 때문이다. 그래서 인텔은 기존에 사용하던 WORD의 크기를 그대로 유지하고, DWORD(Double Word, 32bit)와 QWORD(Quad Word, 64bit)자료형을 추가로 만들었다.

 

2. 어셈블리 명령어

데이터 이동

: 어떤 값을 레지스터나 메모리에 옮기도록 지시.

 

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

 

산술 연산

: 덧셈, 뺄셈, 곱셈, 나눗셈 연산 지시.

 

·  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

 

논리 연산 - and & or

·  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

 

논리 연산 - 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

 

비교

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

 

·  cmp op1, op2: op1과 op2를 비교

cmp는 두 피연산자를 빼서 대소를 비교한다. 연산의 결과는 op1에 대입하지 않는다.

예를 들어, 서로 같은 두 수를 빼면 결과가 0이 되어 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

 

분기

: 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, 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

 

Opcode: 스택

·  push val : val을 스택 최상단에 쌓음

[연산]

rsp -= 8
[rsp] = val

 

[예제]

[Register]
rsp = 0x7fffffffc400

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

[Code]
push 0x31337

 

[결과]

[Register]
rsp = 0x7fffffffc3f8

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

 

 

·  pop reg : 스택 최상단의 값을 꺼내서 reg에 대입

[연산]

rsp += 8
reg = [rsp-8]

 

[예제]

[Register]
rax = 0
rsp = 0x7fffffffc3f8

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

[Code]
pop rax

 

[결과]

[Register]
rax = 0x31337
rsp = 0x7fffffffc400

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

 

Opcode: 프로시저(Procedure)

: 특정 기능을 수행하는 코드 조각.  프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체할 수 있어서 전체 코드의 크기를 줄일 수 있으며, 기능별로 코드 조각에 이름을 붙일 수 있게 되어 코드의 가독성을 크게 높일 수 있다.

프로시저를 부르는 행위를 호출(Call)이라 부르며, 프로시저에 돌아오는 것을 반환(Return)이라고 부른다. 프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(Return Address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동시킨다.

x64 어셈블리 언어에는 프로시저의 호출과 반환을 위한 call, leave, ret 명령어가 있다.

 

· call addr : addr에 위치한 프로시져 호출

[연산]

push return_address
jmp addr

 

[예제]

[Register]
rip = 0x400000
rsp = 0x7fffffffc400 

[Stack]
0x7fffffffc3f8 | 0x0
0x7fffffffc400 | 0x0 <= rsp

[Code]
0x400000 | call 0x401000  <= rip
0x400005 | mov esi, eax
...
0x401000 | push rbp

 

[결과]

[Register]
rip = 0x401000
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005  <= rsp
0x7fffffffc400 | 0x0

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | push rbp  <= rip

 

·  leave: 스택프레임* 정리

[연산]

mov rsp, rbp
pop rbp

 

[예제]

[Register]
rsp = 0x7fffffffc400
rbp = 0x7fffffffc480

[Stack]
0x7fffffffc400 | 0x0 <= rsp
...
0x7fffffffc480 | 0x7fffffffc500 <= rbp
0x7fffffffc488 | 0x31337 

[Code]
leave

 

[결과]

[Register]
rsp = 0x7fffffffc488
rbp = 0x7fffffffc500

[Stack]
0x7fffffffc400 | 0x0
...
0x7fffffffc480 | 0x7fffffffc500
0x7fffffffc488 | 0x31337 <= rsp
...
0x7fffffffc500 | 0x7fffffffc550 <= rbp

 

* 스택프레임이란?

스택은 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역이다. 만약 이 스택 영역을 아무런 구분 없이 사용하게 된다면, 서로 다른 두 함수가 같은 메모리 영역을 사용할 수 있게 된다.

예를 들어 A라는 함수가 B라는 함수를 호출하는데, 이 둘이 같은 스택 영역을 사용한다면, B에서 A의 지역변수를 모두 오염시킬 수 있다. 이 경우, B에서 반환한 뒤 A는 정상적인 연산을 수행할 수 없다.

따라서 함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 스택프레임이 사용된다. 대부분의 Application binary interface (ABI)에서는 함수는 호출될 때 자신의 스택프레임을 만들고, 반환할 때 이를 정리합니다.

 

·  ret : return address로 반환

[연산]

pop rip

 

[예제]

[Register]
rip = 0x401008
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005    <= rsp
0x7fffffffc400 | 0

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | mov rbp, rsp  
...
0x401007 | leave
0x401008 | ret  <= rip

 

[결과]

[Register]
rip = 0x400005
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc3f8 | 0x400005
0x7fffffffc400 | 0x0    <= rsp

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax   <= rip
...
0x401000 | mov rbp, rsp  
...
0x401007 | leave
0x401008 | ret

 

※ 스택 프레임의 할당과 해제

1. func 함수를 호출한다. 이때 다음 명령어의 주소인 0x400005는 스택에 push된다.

 

2. 기존의 스택 프레임을 저장하기 위해 rbp를 스택에 push한다.

 

3. 새로운 스택 프레임을 만들기 위해 rbp를 rsp로 옮긴다.

 

4. 새로 만든 스택 프레임의 공간을 확장하기 위해 rsp를 0x30만큼 뺀다.

 

5. 할당한 스택 프레임에 지역변수를 할당한다.

 

6. 스택 프레임 위에서 여러 연산을 수행한다.

 

7. 저장해뒀던 rbp를 꺼내서 원래의 스택 프레임으로 돌아간다.

 

8. 저장해뒀던 반환 주소를 꺼내 원래의 실행 흐름으로 돌아간다.

 

9. 기존의 스택프레임과 함께 원래의 실행 흐름을 이어간다.

 

Opcode: 시스템 콜

 커널 모드는 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한이다. 파일시스템, 입력/출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행된다. 커널 모드에서는 시스템의 모든 부분을 제어할 수 있기 때문에, 해커가 커널 모드까지 진입하게 되면 시스템은 거의 무방비 상태가 된다. 

 

 유저 모드는 운영체제가 사용자에게 부여하는 권한이다. 브라우저를 이용하여 드림핵을 보거나, 유튜브를 시청하는 것, 게임을 하고 프로그래밍을 하는 것 등은 모두 유저 모드에서 이루어진다. 리눅스에서 루트 권한으로 사용자를 추가하고, 패키지를 내려 받는 행위 등도 마찬가지이다. 유저 모드에서는 해킹이 발생해도, 해커가 유저 모드의 권한까지 밖에 획득하지 못하기 때문에 해커로 부터 커널의 막강한 권한을 보호할 수 있다.

 

 시스템 콜(system call, syscall)은 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용된다. 소프트웨어 대부분은 커널의 도움이 필요하다. 예를 들어, 사용자가 cat flag를 실행하면, cat은 flag라는 파일을 읽어서 사용자의 화면에 출력해 줘야 한다. 그런데 flag는 파일 시스템에 존재하므로 이를 읽으려면 파일시스템에 접근할 수 있어야 한다. 유저 모드에서는 이를 직접 할 수 없으므로 커널이 도움을 줘야 한다. 여기서, 도움이 필요하다는 요청을 시스템 콜이라고 한다. 유저 모드의 소프트웨어가 필요한 도움을 요청하면, 커널이 요청한 동작을 수행하여 유저에게 결과를 반환한다.

 

 x64 아키텍처에서는 시스템콜을 위해 syscall 명령어가 있다.

리눅스 계층

 

 

시스템 콜은 함수이다. 필요한 기능과 인자에 대한 정보를 레지스터로 전달하면, 커널이 이를 읽어서 요청을 처리한다. 리눅스에서는 x64 아키텍처에서 rax로 무슨 요청인지 나타내고, 아래의 순서로 필요한 인자를 전달한다.

 

·  syscall

요청: rax

인자 순서: rdi → rsi → rdx → rcx → r8 → r9 → stack

 

[예제]

[Register]
rax = 0x1   
rdi = 0x1   
rsi = 0x401000  
rdx = 0xb   

[Memory]
0x401000 | "Hello Wo"   
0x401008 | "rld"    

[Code]  
syscall

 

[결과]

Hello World

 

[해석]

오른쪽의 syscall table을 보면, rax가 0x1일 때, 커널에 write 시스템콜을 요청한다. 이때 rdi, rsi, rdx가 0x1, 0x401000, 0xb 이므로 커널은 write(0x1, 0x401000, 0xb)를 수행하게 된된다.

write함수의 각 인자는 출력 스트림, 출력 버퍼, 출력 길이를 나타낸다. 여기서 0x1은 stdout이며, 이는 일반적으로 화면을 의미한다. 0x401000에는 Hello World가 저장되어 있고, 길이는 0xb로 지정되어 있으므로, 화면에 Hello World가 출력된다.

 

※ x64 syscall 테이블

syscall rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
read 0x00 unsigned int fd char *buf size_t count
write 0x01 unsigned int fd const char *buf size_t count
open 0x02 const char *filename int flags umode_t mode
close 0x03 unsigned int fd    
mprotect 0x0a unsigned long start size_t len unsigned long prot
connect 0x2a int sockfd struct sockaddr * addr int addrlen
execve 0x3b const char *filename const char *const *argv const char *const *envp

 


※ 요약

1. 데이터 이동 연산자

  • mov dst, src: src의 값을 dst에 대입
  • lea dst, src: src의 유효 주소를 dst에 대입

2. 산술 연산

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

3. 논리 연산

  • 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의 비트를 모두 반전

4. 비교

  • cmp op1, op2: op1에서 op2를 빼고 플래그를 설정
  • test op1, op2: op1과 op2에 AND 연산을 하고, 플래그를 설정

5. 분기

  • jmp addr: addr로 rip 이동
  • je addr: 직전 비교에서 두 피연산자의 값이 같을 경우 addr로 rip 이동
  • jg addr: 직전 비교에서 두 피연산자 중 전자의 값이 더 클 경우 addr로 rip 이동

6. 스택

  • push val: rsp를 8만큼 빼고, 스택의 최상단에 val을 쌓는다.
  • pop reg: 스택 최상단의 값을 reg에 넣고, rsp를 8만큼 더한다.

7. 프로시저

  • call addr: addr의 프로시저를 호출한다.
  • leave: 스택 프레임을 정리한다.
  • ret: 호출자의 실행 흐름으로 돌아간다.

8. 시스템 콜

  • syscall: 커널에게 필요한 동작을 요청한다.

 

 

 

 

 

'System Hacking' 카테고리의 다른 글

[프로젝트] 악성코드 - 웜  (0) 2024.02.24
x86 Assembly (2)  (1) 2024.02.17
기본 명령어 및 vi 에디터  (1) 2024.01.28
리눅스 메모리 구조 (Linux Memory Layout)  (0) 2024.01.28
컴퓨터 구조 (Computer Architecture)  (1) 2024.01.28
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함