DreamHack/System Hacking

Background: x86-64 어셈블리 Pt.2

Younngjun 2025. 2. 16. 23:17
Opcode: 스택🧱

 

 

[예제]

[Register]
rsp = 0x7fffffffc400

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

[Code]
push 0x31337

 

[결과]

[Register]
rsp = 0x7fffffffc3f8

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

 

 

 

[예제]

[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 명령어가 있다.

 

 

[예제]

[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

 

 

 

[예제]

[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

 

 

 

[예제]

[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

 

 

Opcode: 시스템 콜📟

 

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

 

유저 모드는 운영체제가 사용자에게 부여하는 권한이다. 리눅스에서 루트 권한으로 사용자를 추가하고, 패키지를 내려 받는 행위 등도 유저 모드에서 동작한다. 유저 모드에서는 해킹이 발생해도, 해커가 유저 모드의 권한까지 밖에 획득하지 못하기 때문에 해커로 부터 커널의 막강한 권한을 보호할 수 있다.

 

시스템 콜(system call, syscall)은 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용된다.

소프트웨어 대부분은 커널의 도움이 필요하다. 예를 들어, 사용자가 cat flag를 실행하면, cat은 flag라는 파일을 읽어서 사용자의 화면에 출력해 줘야한다. 그런데 flag는 파일 시스템에 존재하므로 이를 읽으려면 파일시스템에 접근할 수 있어야 한다. 유저 모드에서는 이를 직접 할 수 없으므로 커널이 도움을 줘야 한다.

 

여기서, 도움이 필요하다는 요청을 시스템 콜이라고 한다. 유저 모드의 소프트웨어가 필요한 도움을 요청하면, 커널이 요청한 동작을 수행하여 유저에게 결과를 반환한다.

 

 

[예제]

[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가 출력됩니다.