AmateursCTF
Participated under the banner of HCS, 144
the CTF was held at the same time as a significant event in our country & culture so we didn't have much time to play. but it was really fun learning experience !
Challenge | Category | Points | Solves |
---|---|---|---|
bearsay | Binary Exploitation | 266 pts | 134 |
heaps-of-fun | Binary Exploitation | 352 pts | 56 |
baby-sandbox | Binary Exploitation | 392 pts | 34 |
bearsay
Description
bearsay - configurable speaking/thinking bear (and a bit more)
Binary Analysis
given a binary, glibc and dockerfile, lets do some footprinting
running the binary, its just print back our input indefinitely, my first thought of this is it ought to be format string vulnerability
upon analyzing in its decompiled form, will handle the input differently based upon a strcmp()
of our input as follows:
"flag"
"leave"
"moo"
_ (default)
leave and moo both basically exits the program and none of our interest, flag however seems to be the goal however has a check which is a global variable that is never modified anywhere within the code.
eventually if none of the input matches the strcmp()
it will resort to the default handler below
and will eventually jump to below here as well, note earlier we saw that it prints back our input in a box, and there's a box function, so that's where our controlled input goes
following the routine calls, we can see below the format string vulnerability where printf()
is called without any hard coded format as its first argument
Exploitation
exploitation is quite straight forward and trivial, we wanna use the format string vuln to write into is_mother_bear
to pass the check to flag.
even though the binary has PIE enabled, this won't be a problem since we have indefinite format string vuln so we can easily leak the binary base address and perform the write afterwards.
We found an elf address leak at offset 15, using pwndbg, we can use the vmmap
or pie
command to get the current base address of the process and calculate the offset as follow:
to get the format string offset to our input that is needed to perform an write attack, I just screamed at it and manually count the $p
's needed to reach our input
to perform the write, use pwntools built-in to build payload.
Below is the full exploit script:
Flag: amateursCTF{bearsay_mooooooooooooooooooo?}
heaps-of-fun
the discussion below assumes you have some knowledge about the heap structure and dynamic memory allocator algorithm. If you're not familiar with it check out the external links and reading here !
Description
We decided to make our own custom super secure database with absolutely no bugs!
Binary Analysis
given a binary, glibc and dockerfile, lets do some footprinting
running the binary, its a typical CRUD heap type of challenge
below is the main function that routes our input/choice to its handler
[ 1 ] Create
the first option does what it says, it creates a new instance of key-value pair
db_index()
is a wrapper that returns an index from our input after performing some checks and validation. From this we know the global variable has a maximum capacity of 32 pairs of key-value.
there's a vulnerability here that it doesn't check for index for negative values, so we can possibly do OOB, though this won't be relevant to the exploit I perform later on.
next it calls db_line()
which also a wrapper that handles our input and allocates the key-value pair in the heap. The function will only calls malloc only if the create flag is enabled, otherwise it simply performs update on the existing data.
some things to note here:
We are in control of the size of the chunks made by malloc
In one create request, we are creating 2 chunks, one for the key, one for the value
[ 2 ] Update
update uses the same wrappers like in create, however we can only update each instance values and unable for its keys. Also notice it calls db_line()
with the create flag set to 0.
[ 3 ] Read
read prints both the key and value of the specified index
it prints the chunk's data in a quite odd format I would say
[ 4 ] Delete
delete is where the main vulnerability lies, which in turn will enable update and read respectively to our advantage
as you can see, unlike the other handlers, it doesn't even have a wrapper. It simply frees the chunks and doesn't nullify the pointer to it. This allows us to do UAF attack which how we'll get memory leaks and get Code Execution
[ 5 ] Exit
it exits 👍
Exploitation
in our exploit scripts lets define some function to make interacting with the program more intuitive.
Heap and LIBC leaks
first we'll do is to get some libc
leaks, one trivial way to do this is to read the fd
pointer of an free unsorted bin
chunk.
to do this first we need to fill in the tcache
to its maximum capacity with a relatively big sized chunks such that the next time free is called, it would go to unsorted bin
. The reason why small chunks won't work is because it go to the free bin instead.
to understand more in depth in this behaviour I would, read this azeria-labs which explain in highly detailed manner on the algorithm of free, bins and recycling of chunks works !
and as we are create two chunks per request, we only need to make 4 request to fill the tcache
and perfectly left 1 that'll go the unsorted bin
inspecting in pwndbg can confirm that we had just done that, and the pointer in unsorted bin
points to an address in libc
and since we have UAF on dangling pointers, we can perform read that unsorted bin
chunk to leak libc
, while we're on the way, might as well read the other chunks to leak heap address.
before moving on, I like to clear the bins just to refresh the heap state to start fresh as if it was never touched.
next we're going to do a tcache poisoning
attack, I've covered the basic idea of this attack more in-depth in this writeup, so if you're unfamiliar with it go give a read.
However there's some caveats or difference that we will need to tackle here compared to the previous writeup.
glibc >= 2.32 introduces safe linking, in short it encrypts the metadata of a free chunk with a known key, and when it the time to use it comes (e.g. recycling of chunks) it will decrypt it using said key.
read more detail of safe linking here
to defeat this we can reverse the safe linking with these functions:
the key it uses in this case can be the heap base address.
with safe linking defeated, we can then do tcache
poisoning, but where?
Stack leak
libc
has a symbol called environ
which stores a stack address, meaning if we can allocate a chunk to environ
and read its content we get a stack address leak.
however instead of directly creating a chunk in environ
and possibly corrupting the existing data which we want the integrity of, we will create a chunk just a little bit before it to ensure that no bytes are lost.
notice the bytes before environ
is a bunch of null bytes, this is also good to ensure the exact amount of tcache
free chunks that exist within the heap.
recall that tcache
is a singly linked list, so that if we directly created a chunk on environ
, it means tcache
will take its content (i.e. the stack address) as an fd
pointer store it as the next free chunk to allocate to.
this will break malloc's allignment because, initially tcache
only has 2 free chunks but now suddenly there's an additional more.
now let's prepare a chunk and poison it
and as we see below, we managed to create a chunk just before environ, read its content and leak a stack address.
ret2libc
next to do code execution we're going to overwrite the saved return address of a stack frame into a system("/bin/sh")
.
just to be safe, we're going to overwrite main()
's stack frame since it's the data within the stack frame won't be relatively small and we can control when to return. If we choose the other function's stack frame, it would possibly cause some complications because of the abundance of local variables and we can't control when to actually return.
to do that, up to this state, we're going to set a breakpoint right before main()
returns to to calculate the offset between our stack leak and the saved RIP which occurs when choose the option 5.
next we will have to do the exact same tcache poisoning
with leaking the stack address and create a chunk just before the saved RIP.
just 8 byte before the saved RIP seems to be a great target, even though it's not null, it is small enough that when it heap manager does demangle it from the safe linking, it would result in a null which in turns means the end of the tcache
list (or at least that's what I thought what is happening here)
running against remote, gained shell !
Below is the full exploit script:
Flag: amateursCTF{did_you_have_fun?}
baby-sandbox
didn't solve during the CTF, after reading some post-competition discussion I think my approach and overall idea was on the right path.
so I think it will be a good learning process to document what I'm missing and the pitfalls I encountered.
Description
How many different ways are there to make a syscall?
Binary Analysis
given a binary, glibc and dockerfile, lets do some footprinting
let's just jump straight to ghidra
the program has only one function main()
. first it mmap
a new page at a hard coded address, it will then prompt an input to us asking for input at said mmap
page as we can see below:
also notice the program blacklist certain opcode, notably syscall
and int 0x80
.
it will then run mprotect()
to the page right after our input has been given, changing the page's permission to be read and execute (removed write) as we can see below:
it will then fill the registers with bunch of 0x1337133713371337
and execute our shellcode
Skill Issue Exploit
Self modifying shellcode
typically in challenges where syscall
and int 0x80
are blacklisted, the trivial answer to this is to make a self-modifying shellcode.
however that won't work here because the mmap
'ed region where our shellcode resides doesn't have any write access.
sysenter?
before I got my hand on this challenge, one of my senior teammate had already informed me of what he suspect might be the solution to this problem.
this is my first encounter to sysenter
, and so I had to do some research and read what it is. below is some of the references I find very helpful in explaining it, I would highly recommend you to give it a read if you're also a newbie like me :^)
to summarise, apparently int 0x80
is the legacy way to do system calls in 32 bit architecture. the new and more efficient way to do it is using what's called a Fast System Calls which is sysenter
in 32 bit context and syscall
in 64 bit context.
write? where?
anyway, sysenter
is not blacklisted is available to use. but we still need to spawn a shell, which also includes writing a /bin/sh
to the memory. the problem is all addresses is randomised and I really don't wanna do any bruteforce.
this leads me up to this awesome writeup by the legend himself: nobodyisnobody
All the registers being cleared before the execution of our shellcode, we need a leak to locate the program in the memory.
There are many ways to do this
Libc functions are using
xmm
registers for many SIMD optimized functions, and you can find many useful addresses in them frequently: heap , libc, program, etc..in restricted shellcodes often challenge's authors forget to clean them too, that's good for us, so we can find a heap address in
xmm0
actually which we can copy inrax
register like this
xmm
are 128 bit registers
and this holds true as well here, as we can see below, from those xmm
registers, we're able to retrieve heap addresses to RDI, RSP and RBP
have you find out why this should never work in the first place? I should've realized about this sooner :^(
let's try it, we can put /bin/sh
to one of the heap address, and set RBP and RSP to the other address for our stack frame.
and we managed to write /bin/sh
to the memory, next let's set up the registers accordingly to call execve
.
now let's run it !
uh oh... SIGILL ??? Illegal Instruction ???
sysenter 32 bit vs 64 bit address
this is where I realized that, sysenter
will switch the execution context to 32 bit. in the other hand our writeable address is in the size of 64 bit. which sysenter
will take as an argument and obviously will cause the type mismatch and errors.
another approach
remember, although our shellcode in the end can only be read and executed, at some point the program need to write the shellcode to it. So at the start of the program, the mmap
page is still writeable.
using this we can directly write /bin/sh
to the mmap
page at a certain offset, and because the address is hardcoded, we can always infer where the string will be located. Also since the address is 32 bit in size, this should cause no type mismatch.
but alas our effort was for nothing, at this point I don't know what I'm doing wrong and unable to progress further...
Enlightenment
after the CTF was over, I had to look up the official writeup
# Issues
sysenter
only works in 64 bit mode on intel processors, which caused some debugging issues for some players.
and I'm using an AMD CPU which clearly explains why I got Illegal Instruction error ...
m3rrow on discord also confirms this with further explanation and also confirming why my first approach will never work
trying it again againts remote now turns out working perfectly fine
Below is the full exploit script:
Flag: amateursCTF{surely_there_arent_any_more_ways_to_make_syscalls_right}
Last updated