Quick Note: This post assumes some knowledge of UART and U-boot, and touches slightly on eMMC dumping.
Many familiar with hardware hacking know that UART can be a quick and easy way to find yourself with a shell on a target device. Often times, especially in older home routers and the like, you’ll be automatically logged in as root or be able to log in with an easily-guessed or default password.
In other circumstances, you may need to edit some boot arguments in the bootloader to trigger a shell (such as adding a 1 for single-user mode or adding init=/bin/sh). With this initial shell, you can dump and crack passwords or modify the firmware to grant access without the modified bootargs (change password).
Recently, I came head-to-head with a device that had a slightly more complicated boot process with many environment variables setting other environment variables that eventually called a boot script from an eMMC that did more of the same.
Some Background
My target device was being driven by a cl-som-imx6; an off-the-shelf, bolt-on System on Module from Compulab. My target version of the cl-som-imx6 utilized a 16 gig eMMC for firmware storage that had two partitions: a FAT boot partition (in addition to U-boot on an EEPROM) and an EXT4 Linux filesystem.
My first goal for this device was to get an active shell on the device while it was fully booted. Since I had multiple copies of my target device, I went for a quick win and removed eMMC then dumped it’s contents with the hope of recovering and cracking password hashes. While I was able to get the hashes from /etc/shadow, I was disappointed to see they were hashed with sha512crypt ($6$) and have yet been unable to crack them.
Without valid credentials, my next goal was to modify boot args to bypass authentication and drop me directly into a root shell, with the hope of being able to change the password. The classic init=/bin/sh trick.
It’s important to note that when modifying the bootargs with init=/bin/sh, the device will not go through its standard boot process, therefore it will not kick off any scripts or applications that would normally fire on boot. So, while you may have a root shell, you will not be interacting with the device in its normal state. It is also temporary and will not persist after reboot.
The Problem
This is where it started getting a bit tricker. In my experience, U-boot usually has an environment variable called bootargs that passes necessary information to the kernel. In this case, there were several variables that set bootargs under different circumstances. I attempted to modify every instance where bootargs were getting set (to add init=/bin/sh) to no avail.
# binwalk part1.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 28672 0x7000 Linux kernel ARM boot executable zImage (little-endian) 34516 0x86D4 LZO compressed data 34884 0x8844 LZO compressed data 35489 0x8AA1 device tree image (dtb) 1322131 0x142C93 SHA256 hash constants, little endian 3218379 0x311BCB mcrypt 2.5 encrypted data, algorithm: "5o", keysize: 12292 bytes, mode: "A", 3982809 0x3CC5D9 device tree image (dtb) 4273569 0x4135A1 Unix path: /var/run/L 4932888 0x4B4518 xz compressed data 5359334 0x51C6E6 LZ4 compressed data, legacy 5513216 0x542000 uImage header, header size: 64 bytes, header CRC: 0x665C5745, created: 2018-09-26 16:36:26, image size: 2397 bytes, Data Address: 0x0, Entry Point: 0x0, data CRC: 0x9F621F80, OS: Linux, CPU: ARM, image type: Script file, compression type: none, image name: "boot script" 5517312 0x543000 device tree image (dtb) ...
During this time, I also discovered that there appeared to be some sort of watch-dog active that would completely reset the device after about 2 minutes of playing around in U-boot’s menu options.
As a note: I don’t believe this was an intended “Security” function but rather an unintended effect caused by the rest of the device (attached to the cl-som-imx6) after it failed to fully boot after X time.
After an hour or so reading the UART output during boot and attempting to understand the logic flow of the environment variables, I discovered that U-boot was calling a boot script before it touched any of my edited boot args. Luckily for me, this boot script was being called from the eMMC’s boot partition, which I had dumped previously.
Binwalk quickly identified the boot script’s location, but failed to extract it. Using the offset of the script as a starting point and the offset of the following signature as the end point, I used dd to extract the script. As luck would have it, the script was actually a script (plaintext) and not a binary.
# dd if=partition1.bin of=boot.script skip=5513216 count=4096 bs=1 4096+0 records in 4096+0 records out 4096 bytes (4.1 kB, 4.0 KiB) copied, 0.0173901 s, 236 kB/s
The script was exactly 80 lines and contained several if/else statements, but most importantly, it had only one line setting the bootargs. At this point, my theory was that the only environment variables that mattered were being set by this script. I needed to modify this script to add init=/bin/sh.
# cat boot.script setenv loadaddr 0x10800000 setenv fdt_high 0xffffffff setenv fdt_addr 0x15000000 setenv bootm_low 0x15000000 setenv kernel_file zImage setenv vmalloc vmalloc=256M setenv cma cma=384M setenv dmfc dmfc=3 setenv console ttymxc3,115200 setenv env_addr 0x10500000 setenv env_file boot.env setenv ext_env ext_env=empty ... setenv setup_args 'setenv bootargs console=${console} root=${rootdev} rootfstype=ext4 rw rootwait ${ext}' ...
The next hurdle was that I didn’t have a direct way of modifying the contents of the eMMC without removing it and that’s the easy part. Getting it back on the SOM would have been tougher work than I was willing to tackle at the time.
The Solution
Without a simple way to modify the boot script, I decided to try to manually copy and paste each line of the script into the U-boot menu shell and, if necessary, remove all other environment variables.
I ran into two problems with this approach. First, any line over 34 characters that I tried to paste got truncated to 34. This was likely just caused by the ft232h buffer or something else with the serial connection. Second, and more annoying, was the watch-dog reset. There was simply no way I was going to paste in 80 lines (especially as many would require multiple c/p’s due to the 34 char limit). Even after removing as much as possible
My only answer was to automate the process. I had previously been playing around with the idea of bruteforcing simple 4 digit security codes, so I already had the outline of a script ready.
I modified the script to read lines from an input file and write them to the serial device, where any line over 32 (to be safe) characters would be chucked up. To ensure data was sent at the correct time, I put made sure the script waited for the shell prompt to return before sending the next line, with an additional .5 second sleep for good measure. Also, since the script would take over my ft232h, I needed to make sure it stopped autoboot at the correct time to enter the U-Boot shell.
This approach worked perfectly and I was dropped into a /bin/sh shell as root. I then took control of my ft232h again so I could interact manually. With a quick passwd
, I changed the root password and rebooted. As the modified environment variables didn’t persist through reboot, the device booted as normal and presented me with a login prompt. I entered my newly set password and I was in.
I’d post a screenshot of the final successful login after full boot, but I’d have to redact too much stuff that it doesn’t make any sense.
As a note: So I could keep an eye on everything, I used a second ft232h to watch the target’s TX pin and since it echoed everything back, I could also see my script’s input. Also, the watch-dog was still in effect since the device didn’t boot as it should have, therefore I had to be quick on the passwd
.
The Script
Below is the script exactly as I used it. With a touch of modification to the until1
and until2
vars, it should be useable for other targets.
#!/usr/bin/env python # By Mike Kelly # exfil.co # @lixmk import serial import sys import argparse import re from time import sleep # Key words until1 = "Hit any key to stop autoboot:" until2 = "SOM-iMX6 #" # Read from device until prompt identifier # Not using resp in this, but you can def read_until(until): resp = "" while until not in resp: resp += dev.read(1) return resp def serialprint(): # Get to U-Boot Shell read_until(until1) dev.write("\n") # Wait for U-Boot Prompt read_until(until2) sleep(.5) dev.write("\n") with open(infile) as f: lines = f.readlines() for line in lines: # Lines < 32 if len(line) < 32: read_until(until2) sleep(.5) print "Short Line: "+line.rstrip("\n") dev.write(line) # Break up longer lines else: read_until(until2) sleep(.5) for chunk in re.findall('.{1,32}', line): print "Long Line: "+chunk.rstrip("\n") dev.write(chunk) sleep(.5) dev.write("\n") print "" print "Done... Got root?" exit() if __name__ == '__main__': # Argument parsing parser = argparse.ArgumentParser(usage='./setenv.py -d /dev/ttyUSB0 -b 115200 -f infile.txt') parser.add_argument('-d', '--device', required=True, help='Serial Device path ie: /dev/ttyUSB0') parser.add_argument('-b', '--baud', required=True, type=int, help='Serial Baud rate') parser.add_argument('-f', '--infile', type=str, help="Input file") args = parser.parse_args() device = args.device baud = args.baud infile = args.infile # Configuring device dev = serial.Serial(device, baud, timeout=5) # Executing serialprint()