본문 바로가기
시스템해킹

Syetem Hacking - [02] Computer Science - (3-2) x86 Assembly

by minpiter 2022. 12. 2.
반응형

CONTENTS

 

1. x86-64 어셈블리 명령어 pt.2
 1-1. Opcode : 스택
 1-2. Opcode : 프로시저
 1-3. Opcode : 시스템 콜 

2. 코스 요약

 

 

 

 


 

 

 

 

1. x86-64 어셈블리 명령어 pt.2

 

 1-1. Opcode : 스택

  - x64 아키텍쳐에서는 다음의 명령어로 스택 조작

 

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

 

  [연산]

rsp -= 8
[rsp] = val

 

  [예제]

[Register]
rsp = 0x7fffffffc400

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

[Code]
push 0x1337

 

  [풀이결과]

[Register]
rsp = 0x7fffffffc3f8

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

 

 

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

 

  [연산]

rsp += 8
reg = [rsp-8]

 

  [예제]

[Register]
rax = 0
rsp = 0x7fffffffc3f8

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

[Code]
poprax

 

   [풀이결과]

[Register]
rax = 0x31337
rsp = 0x7fffffff400

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

 


 

 1-2. Opcode : 프로시저

  - 특정 기능을 수행하는 코드 조각

 

  ① 프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체할 수 있어서 전체 코드의 크기를 줄일 수 있다

  ② 기능별로 코드 조각에 이름을 붙일 수 있게 되어 코드의 가독성을 높일 수 있다

  ③ 프로시저를 부르는 행위를 호출(Call)이라 부르며, 프로시저에서 돌아오는 것을 반환(Return)이라고 부른다

  ④ 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call다음의 명령어 주소(return address, 반환 주소)를 스택에 저장하고 프로시저로 rip을 이동시킨다

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

 

  1) 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

 


  2) 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 = 0x7ffffffc500

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

 


 

 

※ 스택프레임

▷ 스택은 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역

   만약 이 스택 영역을 구분없이 사용한다면, 서로 다른 두 함수가 같은 메모리 영역을 사용할 수 있게 된다
▷ 예들 들어 A라는 함수가 B라는 함수를 호출하는데, 이 둘이 같은 스택 영역을 사용한다면, B에서 A의 지역변수를 모두 오염시킬 수 있다. 
▷ 따라서 함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 스택프레임이 사용된다.

 


 

  3) ret : return address로 반환

 

   [연산]

pop rip

 

   [예제]

[Register]
rip = 0x401000
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005 <= rsp

[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-3. Opcode : 시스템 콜

 

  - 운영체제는 해킹으로부터 권한을 보호하기 위해 커널 모드와 유저 모드로 권한을 나눈다.

  

  1) 커널 모드

   - 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한

   - 파일시스템, 입력/출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행

   - 시스템의 모든 부분을 제어할 수 있기 때문에, 해커가 커널 모드까지 진입하게 되면 시스템은 무방비로 노출

 

  2) 유저 모드

   - 운영체제가 사용자에게 부여하는 권한

   - 브라우저 이용, 동영상 시청, 게임, 프로그래밍, 리눅스 루트 권한으로 사용자 추가, 패키지 다운 등 모두 유저 모드에서 수행

   - 유저모드에서는 해킹이 되더라도, 해커가 유저 모드이 권한까지 밖에 획득하지 못해 커널의 권한을 보호 가능

 

   [예시]

   - 유저 모드에서 커널 모드의 시스템 소프트웨에게 어떤 동작을 요청하기 위해 사용

   - 사용자가 cat flag를 실행하면, flag는 파일 시스템에 존재하므로 읽으려면 파일시스템에 접근해야한다

   - 유저 모드에서는 이를 직접 할 수 없으므로 커널이 도움을 줘야 한다

   - 여기서, 도움이 필요하다는 요청을 시스템 콜이라고 한다

   - 유저 모드의 소프트웨어가 필요한 도움을 요청하면, 커널이 요청한 동작을 수행하여 유저에게 결과를 반환한다

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

 

 


 

  3) 시스템 콜(sysem call, 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 table

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

 

 

 


 

 

 

3. 코스 요약

 

 1) 스택

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

 

 2) 프로시저

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

 

 3) 시스템 콜

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

 

 

 

 

 

 

 

 

 

 

※ 이 게시글은 DREAMHACK 강의를 참조하여 작성되었습니다.

반응형