Fuzzing IoT binaries with AFL++ - Part II

Fuzzing IoT binaries with AFL++ - Part II

. 11 min read

In the previous part, we looked at fuzzing simple IoT binaries with AFL++. These programs accepted input from a file and were straightforward to fuzz.

In this post, we will be looking at socket'ed binaries. Fuzzing binaries that communicate over the network using sockets are different from fuzzing binaries that use file-based I/O. Vanilla AFL and AFL++ don’t support fuzzing socket'ed binaries although there have been projects such as AFLNet and AFLNW which use modified versions of AFL for the same. Here however, we will see how to use plain AFL++ to fuzz network programs. The httpd binary at /usr/sbin/httpd is the web server for the firmware and can be used as a candidate for fuzzing.

We can launch httpd with sudo as shown. Sudo is needed to bind on port 80.

Note that qemu is started from within the www/ directory as this is where the web resources (html, css, js files) are. Although it shows a bind error, running netstat confirms that httpd is indeed listening on port 80.

We can open http://127.0.0.1 to cross-check that the web interface is accessible.

The web interface can also be accessed using curl.

Using an intercepting proxy such as Burp Suite, we can view the actual HTTP requests that are being sent. Trying to login to the dashboard with the credentials admin:123456 results in a POST request as shown.

In the image above we are running the webserver over port 8080 (rather than 80)  by appending -p 8080 to the qemu command line.

From here on, the idea is to modify this base request using the fuzzer in subtle ways such that it crashes the web server.

The naive way is to send actual requests over the network. However, this would be slow. The smarter and recommended way is to make the webserver read the HTTP request data from a file. We will look at both ways.

Naive fuzzing using Radamsa

Radamsa is not a fuzzer. It's a test case generator that reads in a file and modifies it in subtle ways. How to use the modified output is up to us. Here we will send the output from the file to the running web server.

# fuzz-radamsa.py
import socket
import pyradamsa
 
base_login_request = open("base-login-request.txt", "rb").read()
 
rad = pyradamsa.Radamsa()
i = j = 0
 
while True:
    # Create a modified request based on the base request
    fuzzed_request = rad.fuzz(base_login_request)
 
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
    # 1 second timeout
    sock.settimeout(1)
 
    sock.connect(("127.0.0.1", 8080))
 
    j += 1
    print(f"[+] Request {j} - ", end="")
 
    sock.sendall(fuzzed_request)
    try:
            sock.recv(50000)
        print("OK")
    except Exception as ex:
            i += 1
            open(f"interesting/{i}.txt", "wb").write(fuzzed_request)
            print(f" {ex} -> saved to {i}.txt")
       sock.close()

The code above uses Radamsa to generate modified request data using the base login request. This data is then sent over the socket to the webserver running at port 8080. If the server doesn’t respond within 1 second, the input is saved to a file in the interesting directory.

We can run the fuzzer as shown.

Request 3 timed out while responding and the corresponding input was saved to 1.txt. Note that a timeout is not the same as a crash. Had the server crashed on request 3, further requests wouldn’t be successful. Fuzzing this way is highly inefficient, slow, and error-prone and would often lead to false positives.

Fuzzing with AFL++

As discussed before, to fuzz with AFL, the program must accept input from a file. We do not have the source code of httpd which we can modify for our purpose. Hence we have to resort to binary level modifications, such as patching the assembly instructions and LD_PRELOAD tricks. Using the latter we can override network functions in libc to make them accept input from a file instead. The desockmulti project on GitHub can be used for this purpose.

Before showing how to use desockmulti, we need to make a few modifications of our own. The httpd binary currently forks to the background using the daemon function. We do not want this forking behavior during fuzzing.

We need to override daemon such that it returns 0 without forking actually. This can be done both with LD_PRELOAD or patching the assembly instructions.

The other change that we need to make is to have httpd process exactly 1 request (unlike a typical web server that processes requests indefinitely) before quitting. This way we can know which request, if any, crashes the web server.

To close a socket, httpd calls the close function. There are three locations from where close is called.

Among them, we need to modify the one at 231c0 to call exit(0) instead of close.

To patch the instructions we will use Cutter which is a GUI for radare2. Ghidra also supports patching binaries but Cutter is better suited for this use case.

Navigating to 0x231c0 in Cutter, we come across the following disassembly.

Double-clicking on close takes us to 0x106b4.

The exit function is located at 0x10b64.

We can thus change bl close to bl 0x10b64 to call the exit function instead.

The instruction immediately before can be changed from mov r0, sl to eor r0, r0 which sets register r0 to 0 to give us the following disassembly.

The net effect is that it calls exit(0).  The other change we need to do is patch out the daemon call at 0x22CB4.

We can change the instruction to eor r0, r0 to make the application believe the call succeeded.

Finally, with the changes in place go to File -> Commit changes to save the modifications. Let's rename the file to httpd_patched.

Testing patched httpd

Running httpd_patched we can see that it doesn’t fork to the background.

Additionally, it quits after processing a single request as shown below.

Setting up desockmulti

We need to use an ARM cross compiler to compile desockmulti. The armv7-eabihf-uclibc toolchain from bootlin works great for this purpose. We need to use a uclibc based toolchain as the firmware binaries also use the same. Running the file command on /usr/bin/httpd points out the binary is dynamically linked to ld-uClibc.

$ file usr/sbin/httpd
usr/sbin/httpd: ELF 32-bit LSB executable, ARM, EABI4 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

Before compiling desockmulti, we have to make a tiny change to its source.

$ git diff
diff --git a/desockmulti.c b/desockmulti.c
index 719e6ac..6bcc223 100644
--- a/desockmulti.c
+++ b/desockmulti.c
@@ -450,7 +450,7 @@ int socket(int domain, int type, int protocol)
                pthread_mutex_unlock(&mutex);
        }
 
-       setup_timer();
+       //setup_timer();
 
        if ((fd = original_socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
                perror("socket error");

In desockmulti.c there’s a call to a setup_timer function that needs to be commented out as shown in the diff above.

We can then run make specifying the bath to the arm-linux-gcc compiler in the CC environment variable.

$ make CC=~/armv7-eabihf--uclibc--stable-2020.08-1/bin/arm-linux-gcc      

The generated file desockmulti.so can be copied to the squashfs-root directory.

Testing desockmulti

To test that desockmulti is indeed working as expected we can debug httpd with gdb-multiarch. First, we need to add a dependency to the library libpthread.so.0 using patchelf. Patchelf can be installed using apt. This is necessary as desockmulti uses threads while httpd doesn’t link to libpthread by default.

$ patchelf --add-needed ./lib/libpthread.so.0 ./usr/sbin/httpd_patched

In terminal 1, run the binary in qemu specifying the -g parameter.

ubuntu@binwalk:~/cisco/_RV130X_FW_1.0.3.55.bin.extracted/squashfs-root/www$ sudo qemu-arm-static -g 5555 -L .. -E USE_RAW_FORMAT=1 -E LD_PRELOAD=../desockmulti.so ../usr/sbin/httpd_patched
-p 8080 < ../../base-login-request.txt

The path to desockmulti.so is specified in the LD_PRELOAD environment variable. The other variable USE_RAW_FORMAT is specific to desockmulti.

In another terminal, we can start gdb-multiarch, set a breakpoint on fprintf and attach to port 5555.

$ gdb-multiarch -q ./usr/sbin/httpd
GEF for linux ready, type `gef' to start, `gef config' to configure
95 commands loaded for GDB 9.2 using Python engine 3.8
[*] 1 command could not be loaded, run `gef missing` to know why.
Reading symbols from ./usr/sbin/httpd...
(No debugging symbols found in ./usr/sbin/httpd)
gef➤  b fprintf
Breakpoint 1 at 0x10a38
gef➤  target remote :5555
…
gef➤  c

When the breakpoint on fprintf hits we can press c and continue for a couple of times to finally inspect the contents of the register r2.

0xfffe5fa8│+0x0018: 0x30303220  →  0x30303220
0xfffe5fac│+0x001c: 0x0d6b4f20  →  0x0d6b4f20
─────────────────────────────────────── code:arm:ARM ────
   0xff4eb7b8 <fprintf+4>      push   {lr}              ; (str lr,  [sp,  #-4]!)
   0xff4eb7bc <fprintf+8>      add    r2,  sp,  #8
   0xff4eb7c0 <fprintf+12>     ldr    r1,  [sp,  #4]
 → 0xff4eb7c4 <fprintf+16>     bl     0xff4ee024 <vfprintf>
   ↳  0xff4ee024 <vfprintf+0>     push   {r4,  r5,  r6,  r7,  r8,  lr}
      0xff4ee028 <vfprintf+4>     mov    r5,  r0
      0xff4ee02c <vfprintf+8>     ldr    r6,  [r0,  #76]        ; 0x4c
      0xff4ee030 <vfprintf+12>    ldr    r12,  [pc,  #144]      ; 0xff4ee0c8 <vfprintf+164>
      0xff4ee034 <vfprintf+16>    cmp    r6,  #0
      0xff4ee038 <vfprintf+20>    add    r12,  pc,  r12
──────────────────────────────────── arguments (guessed) ────
vfprintf (
   $r0 = 0x000be3c0 → 0xff006085 → 0xff006085,
   $r1 = 0x00093f5c → 0x00007325 → 0x00007325,
   $r2 = 0xfffe5f98 → 0xfffe5fa0 → 0x50545448 → 0x50545448,
   $r3 = 0x000006c8 → 0x000006c8
)
────────────────────────────────────────────── threads ────
[#0] Id 1, stopped 0xff4eb7c4 in fprintf (), reason: BREAKPOINT
───────────────────────────────────────────── trace ────
[#0] 0xff4eb7c4 → fprintf()
[#1] 0x1dd5c → add sp,  sp,  #1004      ; 0x3ec
───────────────────────────────────────────────────────────
gef➤  x/s *$r2
0xfffe5fa0:     "HTTP/1.1 200 Ok\r\n"

R2 points to a readable string "HTTP/1.1 200 Ok\r\n" which is the first line of a typical HTTP response. This indicates that desockmulti is working. We are not able to see the HTTP response on-screen but nevertheless it's working as intended.

At this point we can start fuzzing httpd_patched however, we can further make quality of life improvements. For example, the binary requires root to run. It prints the following error message if started without root.

ubuntu@binwalk:~/cisco/_RV130X_FW_1.0.3.55.bin.extracted/squashfs-root/www$ qemu-arm-static -L .. -E USE_RAW_FORMAT=1 -E LD_PRELOAD=../desockmulti.so ../usr/sbin/httpd_patched -p 8080 < ../../base-login-request.txt
===>HTTPD : scheduler set RR with proirity = 99 FAILED
--- [1640588459:322474] accept_num=1, connect_num=0
--- [1640588459:323006] Get pkt, sockindex=0, length=943, pkt[0]=80
+++ [1640588459:323333] Intercepted socket()! original type=AF_INET6 fd=4
--- [1640588459:323785] preeny socket bound, Emulating bind on port 8080
--- [1640588459:324011] preeny listen called, accepting connections ...
--- [1640588459:324223] preeny connect_write for serverfd=4 started
--- [1640588459:324466] preeny connect succeeds, write for serverfd=4, client sock index=0
--- [1640588459:324778] preeny write a 943 bytes packet, client socket index = 0, client sockfd=5
--- [1640588459:325074] preeny connection for serverfd=4 client sockfd=5 shutdown
--- [1640588459:325151] pthread_created or directly called for preeny_connect_write, accept_done_num 1, selected_fd_index 0  
+++ [1640588459:325246] Intercepted socket()! original type=AF_INET6 fd=6
--- [1640588459:325334] preeny socket bound, Emulating bind on port 8080
--- [1640588459:325393] preeny listen called, accepting connections ...
+++ [1640588459:325488] Intercepted socket()! original type=AF_INET fd=7
--- [1640588459:325725] preeny socket bound, Emulating bind on port 8080
--- [1640588459:325747] preeny listen called, accepting connections ...
+++ [1640588459:325976] Intercepted socket()! original type=AF_INET fd=8
--- [1640588459:326095] preeny socket bound, Emulating bind on port 81       
--- [1640588459:326118] preeny listen called, accepting connections ...      
+++ [1640588459:326480] Intercepted socket()! original type=AF_INET6 fd=9    
--- [1640588459:329767] preeny socket bound, Emulating bind on port 81       
--- [1640588459:329820] preeny listen called, accepting connections ...      
/var/run/httpd.pid: Permission denied
+++ [1640588459:330676] shutting down desockmulti...
+++ [1640588459:330844] ... shutdown complete!

It fails on trying to access /var/run/httpd.pid. We can patch the binary and change the path to something which doesn’t require root privilege to access. This can be done using a hex editor and also with Cutter.

We can change /var/run/httpd.pid to /home/ubuntu/h.pid and save. The new path is located under the home directory and can be accessed without root. It’s also important to note the length of the replacement string must be less than or equal to the original.

Rerunning httpd_patched we can see it doesn’t show the permission denied error anymore.

ubuntu@binwalk:~/cisco/_RV130X_FW_1.0.3.55.bin.extracted/squashfs-root/www$ qemu-arm-static -L .. -E USE_RAW_FORMAT=1 -E LD_PRELOAD=../desockmulti.so ../usr/sbin/httpd_patched -p 8080 < ../../base-login-request.txt
===>HTTPD : scheduler set RR with proirity = 99 FAILED
--- [1640594090:533269] accept_num=1, connect_num=0
--- [1640594090:533738] Get pkt, sockindex=0, length=943, pkt[0]=80
+++ [1640594090:533930] Intercepted socket()! original type=AF_INET6 fd=4
--- [1640594090:534277] preeny socket bound, Emulating bind on port 8080
--- [1640594090:534400] preeny listen called, accepting connections ...
--- [1640594090:534562] preeny connect_write for serverfd=4 started
--- [1640594090:534704] preeny connect succeeds, write for serverfd=4, client sock index=0
--- [1640594090:534880] preeny write a 943 bytes packet, client socket index = 0, client sockfd=5
--- [1640594090:535045] preeny connection for serverfd=4 client sockfd=5 shutdown
--- [1640594090:535144] pthread_created or directly called for preeny_connect_write, accept_done_num 1, selected_fd_index 0
+++ [1640594090:535228] Intercepted socket()! original type=AF_INET6 fd=6
--- [1640594090:535283] preeny socket bound, Emulating bind on port 8080
--- [1640594090:535316] preeny listen called, accepting connections ...
+++ [1640594090:535359] Intercepted socket()! original type=AF_INET fd=7
--- [1640594090:535389] preeny socket bound, Emulating bind on port 8080
--- [1640594090:535404] preeny listen called, accepting connections ...
+++ [1640594090:535432] Intercepted socket()! original type=AF_INET fd=8
--- [1640594090:535478] preeny socket bound, Emulating bind on port 81
--- [1640594090:535511] preeny listen called, accepting connections ...
+++ [1640594090:535559] Intercepted socket()! original type=AF_INET6 fd=9
--- [1640594090:535601] preeny socket bound, Emulating bind on port 81
--- [1640594090:535632] preeny listen called, accepting connections ...
--- [1640594090:537111] Accept socket at serverfd=4, got fd=10, accept_sock_num=1.
+++ [1640594090:550073] shutting down desockmulti...
+++ [1640594090:550229] ... shutdown complete!

Additionally, the file h.pid is created within the user's home directory.

$ ls -la /home/ubuntu/h.pid
-rw-rw-r-- 1 ubuntu ubuntu 4 Dec 27 08:34 /home/ubuntu/h.pid

Fuzzing httpd

We can now finally proceed to fuzz the patched httpd binary. We need to create two directories: input-httpd and output-httpd. The former will contain the file base-login-request.txt which AFL++ will use to generate further test cases.

ubuntu@fuzz:~/_RV130X_FW_1.0.3.55.bin.extracted/squashfs-root/www$ QEMU_LD_PREFIX=.. QEMU_SET_ENV=USE_RAW_FORMAT=1,LD_PRELOAD=../desockmulti.so ../../../AFLplusplus/afl-fuzz -Q -i ../../input-httpd/ -o ../../output-httpd/ -- ../usr/sbin/httpd_patched -p 8080

We can leave the fuzzer as is to continue fuzzing. To quit press Ctrl+C anytime. In our brief test, AFL++ wasn’t able to crash the application.

With this, we come to the end of the two-part AFL fuzzing series. In the first part, we saw how to fuzz simple binaries which accepted input from a file. They required no modifications and were straightforward to fuzz. In this part, we learned how to convert a socketed binary to accept input from a file instead. This required patching the binary on an assembly level and using LD_PRELOAD further to override libc functions. We also saw how to use radamsa to generate test cases as a crude way to fuzz. There is no universal technique that can be applied as-is to fuzz any given closed IoT firmware binary. It will vary on a case-by-case basis but the idea is similar. For any comments, questions or suggestions feel free to leave a comment below.