SECCON CTF 2023 Quals - selfcet
I wrote Software CET because Intel CET is not yet widely available.
nc selfcet.seccon.games 9999
Team: HK Guesser
Attachment: selfcet.tar.gz xpl.py
selfcet
let you read into key
and buf
of the ctx
object. Since the size for the read
was sizeof(ctx)
, we’re able to also overwrite the data behind it.
The first read let’s you overwrite everything in the ctx
object (since offset
is 0
and size
is sizeof(ctx)
), while the second one also lets you overwrite data behind the ctx
object (since offset
points to buf
object, while size
is still sizeof(ctx)
).
If we’d been able to leak or control the canary
, that could have been used to do a rop chain after the second read_member
.
Didn’t find a way to leak the canary, so I went on with a small bruteforce approach (1 nibble had to be bruteforced for this in the first read).
When we overwrite status
in read_member
, it will call
Initially ctx->throw
will point to err
in libc
, so this would call err(ctx->status, ctx->error)
, but will pass the function first to the CFI
macro.
This will basically just check, that the function, that is called here, begins with an endbr64
, so we cannot use arbitrary rop gadgets here, but have to call matching functions.
To get a leak, I checked which functions are near err
, so that we could call them with a partial overwrite.
So, doing a partial overwrite, only overwriting the last 2 bytes of ctx->throw
would allow us to call warn
instead of err
, which will also do output but not exit
after printing the data.
So, our first payload looks like this
When the partial overwrite succeeds, this will call warn(e.got["read], 0x404000)
, which will just print a warning containing a leak to read.got
.
With this leak, we can now call everything from libc
in the second payload (and even overwrite the canary and put a rop chain behind the ctx
object). At that point, I already thought that we’d need to find a way to leak the canary
and do a ropchain, to get around the endbr64
restriction, but didn’t find anything useful for that.
One of the main issues, that we couldn’t do something useful with the remaining payload was, that status
was an int32
, meaning we wouldn’t be able to pass a 64-bit address as first argument (which would be needed, if we would want to call something like system("/bin/sh")
, while using /bin/sh
from libc). Also going for execveat
wasn’t helpful, since we didn’t control rdx
(properly).
If we’d just have one additional call to read /bin/sh
to bss
(which would be accessable via a 32 bit address) before calling system
, this would be easy.
So, let’s just use the second payload to jump back into main!
This will now call __libc_start_main(main)
, which will just jump back into main
, giving us the possibility to do 2 payloads again.
This will call gets(0x404500)
and the next send will write /bin/sh
to 0x405000
. Since we now have our path at an address, that can be referenced via a 32-bit address, we can now just call system(0x404500)
with the second payload.
which will now trigger a shell.
As the flag hints, the intended solution was using prctl
, but well :)