NetGear Series 1.4 — Comparing different analysis environment for analysing Netgear R6700V3 circled binary (CVE-2022–27644, CVE-2022–27646)
Introduction
In the fourth and last part of the series, we will be documenting the differences in analysing the Netgear circled binary in different emulation environment. During this journey, we had successfully emulated the binary using the Emux framework and QEMU User-Mode emulation method. We will first highlight the challenges faced in QEMU User-Mode emulation followed by presenting our opinion on the pros and cons of each method.
Emulating circled in User-Mode emulation
As mentioned in the first part of the series, QEMU User-Mode emulation allowed us to execute the binary directly on a x86/x64 architecture as though it was running on its native architecture. Through multiple trial and error attempts, we have managed to emulate it using QEMU User-Mode emulation method. The source code of the hook to perform User-Mode emulation is provided below:
Hooks for qemu-arm-static
// LD_PRELOAD=/hooks.so /bin/circled start
// ~/buildroot-2022.02.6/output/host/bin/arm-linux-gcc hooks.c -o hooks.so --shared
#define _GNU_SOURCE
#include "stdio.h"
#include "dlfcn.h"
#include "stdlib.h"
#include "string.h"
#include "stdarg.h"
#include <unistd.h>
#include <sched.h>
int daemon(int nochdir, int noclose) {
return 0; // daemon will have no effect now
}
typedef FILE *(*fopen_t)(const char *pathname, const char *mode);
fopen_t real_fopen;
FILE* tracked_proc_mounts;
FILE *fopen(const char *filename, const char *mode) {
if (!real_fopen) {
real_fopen = dlsym(RTLD_NEXT, "fopen");
}
if (strstr("/proc/mounts", filename)) {
system("touch /proc/mounts");
tracked_proc_mounts = real_fopen(filename, mode);
return tracked_proc_mounts;
}
if (strstr(filename, "/proc/") && (strstr(filename, "/cmdline") || strstr(filename, "/status" ))) {
int size_of_command = strlen(filename)*4 + strlen("mkdir -p ; rmdir ; touch ; echo /bin/sh > ") + 1;
char* command = malloc(size_of_command);
snprintf(command, size_of_command, "mkdir -p %s; rmdir %s; touch %s; echo /bin/sh > %s", filename, filename, filename, filename);
unsetenv("LD_PRELOAD");
system(command);
setenv("LD_PRELOAD", "/hooks.so", 1);
free(command);
}
return real_fopen(filename, mode);
}
typedef pid_t (*fork_t)();
fork_t real_fork;
int fork() {
return 0; // We become the child process
}
typedef FILE *(*popen_t)(const char *command, const char *type);
popen_t real_popen;
FILE* popen(const char *command, const char *type) {
if (!real_popen) {
real_popen = dlsym(RTLD_NEXT, "popen");
}
if (strstr(command, "nvram get") || strstr(command, "nvram commit") || strstr(command, "nvram set")) {
// Perform hooking for all nvram
const char* preload_str = "LD_PRELOAD=/nvram_hooks.so;";
char* newcommand = malloc(strlen(command) + strlen(preload_str));
strcpy(newcommand, preload_str);
strcat(newcommand, command);
return real_popen(newcommand, type);
}
return real_popen(command, type);
}
typedef int (*fclose_t)(FILE *stream);
fclose_t real_fclose;
int fclose(FILE *stream) {
if (!real_fclose) {
real_fclose = dlsym(RTLD_NEXT, "fclose");
}
if (stream == tracked_proc_mounts) {
tracked_proc_mounts = NULL;
}
return real_fclose(stream);
}
typedef char *(*fgets_t)(const char *str, int n, FILE *stream);
fgets_t real_fgets;
char *fgets(char *str, int n, FILE *stream) {
if (!real_fgets) {
real_fgets = dlsym(RTLD_NEXT, "fgets");
}
if (stream == tracked_proc_mounts) {
strcpy(str, "/tmp/media/nand\0");
return str;
}
return real_fgets(str, n, stream);
}
typedef int (*system_t)(const char* command);
system_t real_system;
int system(const char* command) {
if (!real_system) {
real_system = dlsym(RTLD_NEXT, "system");
}
if (strstr(command, "tar zxf")) {
unsetenv("LD_PRELOAD");
int rc = real_system(command);
setenv("LD_PRELOAD", "/hooks.so", 1);
return rc;
}
if (strstr(command, "sploit")) {
unsetenv("LD_PRELOAD");
int rc = real_system(command);
setenv("LD_PRELOAD", "/hooks.so", 1);
return rc;
}
const char* preload_str = "LD_PRELOAD=/hooks.so;";
char* new_command = malloc(strlen(command) + strlen(preload_str));
strcpy(new_command, preload_str);
strcat(new_command, command);
int result = real_system(new_command);
free(new_command);
return result;
}
typedef char* (*getenv_t)(const char *name);
getenv_t real_getenv;
char *getenv(const char *name) {
if (!real_getenv) {
real_getenv = dlsym(RTLD_NEXT, "getenv");
}
if (strstr(name, "LD_PRELOAD")) {
return NULL;
}
return real_getenv(name);
}
typedef int (*clone_t)(int (*fn)(void *), void *stack, int flags, void *arg, ...);
clone_t real_clone;
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...) {
if (!real_clone) {
real_clone = dlsym(RTLD_NEXT, "clone");
}
va_list args;
va_start(args, arg);
int rc;
if (flags == (CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND)) {
rc = real_clone(fn, stack, CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, arg, args);
va_end(args);
return rc;
}
if (flags < CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID) {
rc = real_clone(fn, stack, CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID|flags, arg, args);
va_end(args);
return rc;
}
rc = real_clone(fn, stack, flags, arg, args);
va_end(args);
return rc;
}
With the source code of the hook used to emulate circled under User-Mode emulation, we could observe that the amount of functions we have to hook was much more than the hook that we wrote for the Emux framework. One notable hook to point out the difference was the hooking of clone library call which was used in Netgear libpthread library implementation. The failure of the clone system call caused the binary to crash. In order for the circled binary to emulate properly, we had to set the flags which was originally (CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND) to (CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID). The actual reason to why this modification was required remained unknown to us but we hypothesised that it could probably be the firmware’s libpthread library was not compatible with User-Mode emulation. Since the circled binary made use of threads to spawn the status server and the watchdog, an alternative way to overcome this was to return 0 for pthread_create function call as the two threads were not actually needed for exploitation purposes.
Comparison between User-Mode emulation and Emux
Based on our experience in using qemu-arm-static and Emux, we realised that each methodology has its pros and cons. For User-Mode emulation, the qemu-arm-static has a built in debugger and strace which allows us to catch any crashes instantly and visualise the system calls easily. The ability to start the binary with the debugger instead of attaching helped in developing the hooks required for the proper functionality of the circled binary. On the other hand, strace assisted to pinpoint roughly where the fault lies in by correlating the strace results with static analysis. However, User-Mode emulation had its own pitfalls as well forcing us to deal with workarounds for system calls that failed. In the case of circled binary, the libpthread library used certain combination of clone flags that would cause clone system call to fail and eventually crash the whole binary. Such problems are difficult to troubleshoot as it is usually not intuitive to pinpoint the exact cause of failures or crashes. Another disadvantage of using User-Mode emulation is that we were unable to debug child processes as the debugger in qemu-arm-static did not follow child processes as seen in the source code [1] which required us to hook the fork function to influence which “process” we are actually debugging in.
On the other hand, Emux emulates an ARM environment for the binary to run “natively” which means that system calls are less likely to have issues. However, the process of developing hooks was slightly more complicated. Due to the requirement that the circled binary had to run in the chroot environment and the fact that we were unable to get gdbserver to run in the same chroot environment, attaching the debugger to the process was the only way to debug the process. This defeated the purpose of debugging as the process would have terminated before we could debug to find out why it crashed in order to write the hook. We overcame this by writing a binary wrapper to perform an execve function call to execute the binary of interest so that we have ample time to debug he process and follow the new process after the excve function call.
Ending notes
This ends our series with emulating and exploiting circled binary for Netgear R6700V3. We hope that you have learnt something along with us in our journey to familiarise in this domain of work. You may follow us on twitter, medium and linkedin for more of such posts to come.