Opcode: 스택
스택 : 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역
push val : val을 스택 최상단에 쌓음
|
||
rsp -= 8
[rsp] = val
|
pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
|
||
rsp += 8
reg = [rsp-8]
|
Opcode: 프로시저
프로시저(Procedure) : 특정 기능을 수행하는 코드 조각
프로시저 사용 : 반복 연산 대체 → 전체 코드의 크기↓ , 기능별로 코드 조각에 이름 → 가독성 ↑
호출(Call) : 프로시저를 부르는 행위
반환(Return) : 프로시저에서 돌아오는 것
프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로 call 다음의 명령어 주소(return address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동시킨다.
call addr : addr에 위치한 프로시져 호출
|
||
push return_address
jmp addr
|
leave: 스택프레임 정리
|
||
mov rsp, rbp
pop rbp
|
스택프레임 : 함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 사용
ret : return address로 반환
|
||
pop rip
|
Opcode: 시스템 콜
운영체제 : 연결된 모든 하드웨어 및 소프트웨어에 접근 및 제어 가능
해킹으로부터 이 막강한 권한을 보호하기 위해 커널 모드와 유저 모드로 권한을 나눈다.

커널 모드 : 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한
메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행된다.
시스템의 모든 부분을 제어할 수 있어 해커가 커널 모드까지 진입하게 되면 시스템은 거의 무방비 상태가 된다.
유저 모드 : 운영체제가 사용자에게 부여하는 권한
유저 모드에서 해킹이 발생해도, 해커가 유저 모드의 권한밖에 획득하지 못하기 때문에 해커로 부터 커널의 막강한 권한을 보호할 수 있다.
시스템 콜(system call, syscall) : 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용한다.
유저 모드에서 직접 할 수 없는 기능을 사용할 때 커널의 도움이 필요하다는 요청을 말한다.
유저 모드의 소프트웨어가 필요한 도움을 요청하면, 커널이 요청한 동작을 수행하여 유저에게 결과를 반환한다.
x64아키텍쳐에서는 시스템콜을 위해 syscall 명령어가 있다.
시스템 콜은 함수이므로 필요한 기능과 인자에 대한 정보를 레지스터로 전달하면, 커널이 이를 읽어서 요청을 처리한다.
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
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
|
Q1. 다음 어셈블리 코드를 실행했을 때 출력되는 결과로 올바른 것은?
[Code]
main:
push rbp
mov rbp, rsp
mov esi, 0xf
mov rdi, 0x400500
call 0x400497 <write_n>
mov eax, 0x0
pop rbp
ret
write_n:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-0x8],rdi
mov DWORD PTR [rbp-0xc],esi
xor rdx, rdx
mov edx, DWORD PTR [rbp-0xc]
mov rsi,QWORD PTR [rbp-0x8]
mov rdi, 0x1
mov rax, 0x1
syscall
pop rbp
ret
==================================
[Memory]
0x400500 | 0x3037207964343372
0x400508 | 0x003f367562336420
A ready to debug?
B r34dy 70 d3bu6?
C 4r3 y0u r34dy 70 d3bu6?
D are you ready to debug?
push rbp
mov rbp, rsp
mov esi, 0xf
mov rdi, 0x400500
rbp를 스택 최상단에 쌓는다.
rsp의 값을 rbp에, esi의 값을 0xf로, rdi의 값을 0x400500로 대입한다.
call 0x400497 <write_n>
0x400497에 위치한 <write_n>을 호출한다.
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-0x8],rdi
mov DWORD PTR [rbp-0xc],esi
rbp를 스택 최상단에 쌓는다.
rsp의 값을 rbp에 대입한다.
rdi(0x400500)를 [rbp-0x8]에 대입한다. 이는 [rsp-0x8]과 같다.
esi(0xf)를 [rbp-0xc]에 대입한다. 이는 [rsp-0xc]과 같다.
xor rdx, rdx
같은 값을 xor 연산했기 때문에 0이다.
rdx 레지스터를 0으로 초기화했다.
mov edx, DWORD PTR [rbp-0xc]
mov rsi,QWORD PTR [rbp-0x8]
mov rdi, 0x1
mov rax, 0x1
[rbp-0xc] 값(0xf)을 edx에, [rbp-0x8] 값(0x400500)을 rsi에, rdi에 0x1을, rax에 0x1을 대입한다.
syscall
현재 rax의 값이 0x1이므로 write 기능을 사용한다.
rdi값이 0x1, rsi값이 [rbp-0x8]이므로 0x400500이다.
rdx값이 0인데 edx 값이 0xf이므로 윗부분은 0, 아랫부분은 0xf가 입력되었고 따라서 rdx는 0xf이다.
rsi의 값이 0x400500이기 때문에 0x30372079643433372이 출력된다. 여기서 x86 아키텍처는 리틀엔디앙 방식을 사용하므로 메모리 뒤에서부터 값을 읽는다는 것에 주의하여 읽어보면 72 33 34 64 79 20 37 30 이다. 15바이트만큼 출력하므로 이어서 읽어보면 20 64 33 62 75 36 3f 이다. 이를 아스키 코드로 변환해 보면 r34dy 70 d3bu6?로 해석된다.
pop rbp
ret
mov eax, 0x0
pop rbp
ret
스택 최상단의 값을 꺼내서 rbp에 대입한 뒤 반환한다.
eax에 0을 대입한 뒤 그 값을 rbp에 대입한 뒤 반환한다.