I have to warn you: this is not a standalone article. How I ended up writing about MIPS disassembly with zero previous experience is documented in another article that lists the adventures I had with my ZTE H108NS modem router.
Nevertheless, here I am standing against an unknown architecture, trying to reverse engineer the mighty XOR checksum algorithm used by the modem to distinguish correct from incorrect firmware. Note that this wasn’t a linear process with a start, a main part and an end. It was mostly a tree of attempts with extensive backtracking which is just a cool way to say “trial and error”. However, I do care to make sense to my readers so I will try to present the events in a linear manner.
TrendChip firmware employs a first stage bootloader in partition 0 that has the capability to upload new firmware in order to unbrick your modem router. Since this a minimal bootloader it should be easy to navigate around and find the XOR checksum algorithm.
In addition while in normal mode the TrendChip firmware employs the cfg_manager binary to perform the firmware upload. It is a safe bet that cfg_manager has to implement the XOR checksum algorithm also. cfg_manager is a much bigger binary with all kinds of functionality. It should be significantly more difficult to find the algorithm we are searching in its assembly listings.
There is a reason that the saying
Assumption is the mother of all fuckups.
is a fact of life. In this case the assumptions that cost me several days was
- small binary – easy to understand and manipulate
- big binary – difficult to navigate and understand
BOOTROM and Primary Bootloader
We will see how common sense rules can fail in a short while. In the meantime how do we disassemble a MIPS binary in a Debian GNU/Linux workstation? I never claimed to be google shy so after some searches and non educated guesses I found:
I had problems with the first three. In retrospect the biggest problem was that I was trying to disassemble the primary bootloader of partition 0 without knowing the base address. One very important free variable to the disassembly problem is the base address. If you don’t get the base address right the disassembly listings don’t make sense when the program tries to perform absolute jumps or to read data. So more google…
Looks like that MIPS boards initialize the BOOTROM base address at 0xbfc00000 most of the times. If you get that correctly the later steps are a breeze. Let’s see how it is done.
The first thing to do is install cross MIPS binutils in our system. Debian has excellent support for cross architectures and libraries. Cross architecture support is admittedly of the most difficult problems to get right so let’s use it:
apt-get install binutils-mips-linux-gnu
And now let’s disassemble
mips-linux-gnu-objcopy -I binary -O elf32-tradbigmips -B mips --rename-section .data=.text --change-address 0xbfc00000 p0.head p0.head.elf
mips-linux-gnu-objdump -D p0.head.elf > p0.head.asm
Here is how it looks like
bfc00000 :
bfc00000: 0bf0000a j bfc00028
bfc00004: 00000000 nop
...
bfc00028: 3c02bfbf lui v0,0xbfbf
bfc0002c: 34420200 ori v0,v0,0x200
bfc00030: 24040000 li a0,0
bfc00034: 24050000 li a1,0
bfc00038: ac440000 sw a0,0(v0)
bfc0003c: ac450014 sw a1,20(v0)
bfc00040: 00000000 nop
bfc00044: 3c03bfb0 lui v1,0xbfb0
bfc00048: 3c048000 lui a0,0x8000
bfc0004c: 3484ffff ori a0,a0,0xffff
bfc00050: 24050000 li a1,0
bfc00054: ac640000 sw a0,0(v1)
bfc00058: ac650004 sw a1,4(v1)
bfc0005c: 8c6400b0 lw a0,176(v1)
bfc00060: 00000000 nop
bfc00064: 00042302 srl a0,a0,0xc
bfc00068: 308400ff andi a0,a0,0xff
bfc0006c: 10800016 beqz a0,bfc000c8
bfc00070: 00000000 nop
bfc00074: 3c090463 lui t1,0x463
bfc00078: 35291000 ori t1,t1,0x1000
bfc0007c: 240a018f li t2,399
bfc00080: 240b01f3 li t3,499
bfc00084: 240c01c1 li t4,449
bfc00088: 012a2025 or a0,t1,t2
bfc0008c: 018a082a slt at,t4,t2
bfc00090: 10200002 beqz at,bfc0009c
bfc00094: 00000000 nop
bfc00098: 34848000 ori a0,a0,0x8000
bfc0009c: ac6400b0 sw a0,176(v1)
bfc000a0: 00000000 nop
bfc000a4: 24081388 li t0,5000
bfc000a8: 2508ffff addiu t0,t0,-1
bfc000ac: 1500fffe bnez t0,bfc000a8
bfc000b0: 00000000 nop
bfc000b4: 114b0004 beq t2,t3,bfc000c8
bfc000b8: 00000000 nop
bfc000bc: 214a0005 addi t2,t2,5
bfc000c0: 0bf00022 j bfc00088
bfc000c4: 00000000 nop
bfc000c8: 3c048000 lui a0,0x8000
bfc000cc: 34840300 ori a0,a0,0x300
bfc000d0: 3c058000 lui a1,0x8000
bfc000d4: 34a50504 ori a1,a1,0x504
bfc000d8: 3c068000 lui a2,0x8000
bfc000dc: 34c60706 ori a2,a2,0x706
We obviously need some education on the MIPS architecture, instruction set and register layout. One thing I didn’t know and I couldn’t foresee is that due to the pipeline architecture of MIPS the instruction after a jump will be executed. That’s why it is typical to suffix a jump with a nop instruction like at offset 0xbfc000c4 or 0xbfc000b8.
So what the above code is doing exactly? Well it is the BOOTROM, it sets things up and then copies some code at 0xb8000000 and jumps there. Can we disassemble that part?
dd if=p0 of=p0.1 seek=960 bs=1
mips-linux-gnu-objcopy -I binary -O elf32-tradbigmips -B mips --rename-section .data=.text --change-address 0xb80000000 p0.1 p0.1.elf
mips-linux-gnu-objdump -D p0.1.elf > p0.1.asm
and here is where the control is relinquished to
Disassembly of section .text:
80000000 :
...
80000280: 3c1a8000 lui k0,0x8000
80000284: 275a0290 addiu k0,k0,656
80000288: 03400008 jr k0
8000028c: 00000000 nop
80000290: 3c1c8000 lui gp,0x8000
80000294: 279c1890 addiu gp,gp,6288
80000298: 27881fe0 addiu t0,gp,8160
8000029c: 251dfff0 addiu sp,t0,-16
800002a0: 3c088000 lui t0,0x8000
800002a4: 25081870 addiu t0,t0,6256
800002a8: ad000000 sw zero,0(t0)
800002ac: 3c098000 lui t1,0x8000
800002b0: 2529388c addiu t1,t1,14476
800002b4: 3c1a8000 lui k0,0x8000
800002b8: 275a3890 addiu k0,k0,14480
800002bc: 25080004 addiu t0,t0,4
800002c0: ad000000 sw zero,0(t0)
800002c4: 1509fffd bne t0,t1,800002bc
800002c8: 00000000 nop
800002cc: 0c0000b8 jal 800002e0
800002d0: 00000000 nop
At that point I was lost. I had no idea where the primary bootloader was and what this thing was doing. Back to the drawing board.
QEMU
I have to say at this point that my assembly skills are not that good to figure all these out just from the assembly listings without some help. The help was the qemu emulator program. Although qemu would barf in board specific initialization attempts it was still valuable to figure out how the BOOTROM boots the primary bootloader. Here are some tips:
Connect to gdb
qemu-system-mips -M mips -pflash flash -monitor null -nographic -S -s
#from another terminal
gdb-multiarch p0.head.elf
gdb>
target remote localhost:1234
load p0.head.elf32
x/14i $pc
stepi
info registers
Run and keep instruction set log
qemu-system-mips -L . -option-rom 0xbfc00000.bin -pflash flash -monitor null -nographic -d in_asm 2>stderr
where flash is the part of the flash memory you want to run.
In a perfect world it should be possible to run the primary bootloader by running the flash contents. I believe everyone should have collect enough hints by now about which world we are living in so the fact that qemu doesn’t reach that point doesn’t come as a total surprise. Still qemu was very helpful if you treat it as one additional weapon in your arsenal and not as a silver bullet.
cfg_manager
Trying the alternate plan since I got stuck previously. Let’s see what’s going on with the cfg_manager. After playing with too much open variables, disassembler, base address, unknown platform, I suddenly realized a very simple and very obvious (in retrospect) fact.
The cfg_manager binary was not stripped
At first I realized that with the help of the recstudio running under WINE, through it was obvious also with objdump . I couldn’t figure how to load a binary in the Linux version but I did managed it with the windows executable and WINE. At the right corner was a small almost invisible panel (you had to resize it to view it) with the program’s symbols. I am a visual guy but usually not very good to optical grepping. In this case though even I was not able not to see a function named upgrade_firmware which calls image_check_tc which calls calculate_crc32. Bingo!!!
And here is the C code for the CRC calculation as decompiled by recstudio.Let’s see the assembly code from objdump in case it may be helpful.
0045a8a0 :
...
45a93c: 02251821 addu v1,s1,a1
45a940: 90620000 lbu v0,0(v1)
45a944: 24a50001 addiu a1,a1,1
45a948: 00501026 xor v0,v0,s0
45a94c: 304200ff andi v0,v0,0xff
45a950: 00021080 sll v0,v0,0x2
45a954: 00461021 addu v0,v0,a2
45a958: 8c440000 lw a0,0(v0)
45a95c: 00101a02 srl v1,s0,0x8
45a960: 00b2102a slt v0,a1,s2
45a964: 1440fff5 bnez v0,45a93c
45a968: 00838026 xor s0,a0,v1
45a96c: 1280ffe5 beqz s4,45a904
The interesting bit is that the CRC algorithm uses a lookup table. If you can figure that out and where that table is located you are golden. In cfg_manager the lookup table is located at 0x4769b0 and the C code of the CRC calculation (taken from tcrevenge) looks like this:
const char crc32_c[] = "0000000077073096ee0e612c...8ea15a05df1b2d02ef8d";
#define crc32_c_size ((sizeof(crc32_c) - 1) / sizeof(char))
unsigned char crc32_m[crc32_c_size >> 1];
#define crc32_size sizeof(crc32_m) / sizeof(char)
static int be2int(unsigned char *c) {
return c[3] | (c[2] << 8) | (c[1] << 16) | c[0] << 24);
}
unsigned int calc_crc32(unsigned int sum, const char *filename, int offset) {
unsigned char buffer[4096];
int fd = open(filename, O_RDONLY);
lseek(fd, offset, SEEK_SET); /* skip header */
int rc;
while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
int i;
for(i=0; i< rc; i++) {
sum = be2int(crc32_m + (((buffer[i] ^ sum) & 0xFF) << 2)) ^ sum >> 8;
//printf("byte %02x, sum %08x\n", buffer[i], sum);
}
}
close(fd);
return sum;
}
...
/* Initialize crc32_m from crc32_c */
unsigned int j;
char cnv[] = {0, 0};
for (i = 0; i < crc32_size; i++) {
j = i << 1;
cnv[0] = crc32_c[j];
const int high = (int) strtol(cnv, NULL, 16);
cnv[0] = crc32_c[j + 1];
const int low = (int) strtol(cnv, NULL, 16);
const int val = 16 * high + low;
//printf("%d %d %d %02x\n", low, high, val, val);
crc32_m[i] = val;
}
Back to the primary bootloader
So after all it was easier to reverse engineer the cfg_manager (due to non-stripping) than the primary bootloader. However it is not like me to leave unfinished business. The primary bootloader needs to have the lookup table also in partition 0 in order to be able to clone the behavior. If we do a hex search with the first bytes of the lookup table then we can find the offset of the lookup table in the partition 0.
Nope it isn’t there. Now what?
Let’s try binwalk in the partition 0. We have never tried binwalk in that partition before because it isn’t part of the firmware image.
$ binwalk -e p0
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
7216 0x1C30 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 120416 bytes
$ls
p0 _p0.extracted
$ls _p0.extracted/
1C30 1C30.7z
cd _p0.extracted/
$mips-linux-gnu-objcopy -I binary -O elf32-tradbigmips -B mips --rename-section .data=.text --change-address 0x81fb0000 1C30 1C30.elf
$mips-linux-gnu-objdump -D 1C30.elf > 1C30.asm
That was it. The primary bootloader was actually LZMA compressed inside the partition 0. Here is how it looks like:
Disassembly of section .text:
81fb0000 :
...
81fb0280: 3c1a81fb lui k0,0x81fb
81fb0284: 275a0290 addiu k0,k0,656
81fb0288: 03400008 jr k0
81fb028c: 00000000 nop
81fb0290: 3c1c81fe lui gp,0x81fe
81fb0294: 279c6864 addiu gp,gp,26724
81fb0298: 27881fe0 addiu t0,gp,8160
81fb029c: 251dfff0 addiu sp,t0,-16
81fb02a0: 3c0881fd lui t0,0x81fd
81fb02a4: 2508d660 addiu t0,t0,-10656
81fb02a8: ad000000 sw zero,0(t0)
81fb02ac: 3c0981ff lui t1,0x81ff
81fb02b0: 25298bac addiu t1,t1,-29780
81fb02b4: 3c1a81ff lui k0,0x81ff
81fb02b8: 275a8bb0 addiu k0,k0,-29776
81fb02bc: 25080004 addiu t0,t0,4
81fb02c0: ad000000 sw zero,0(t0)
81fb02c4: 1509fffd bne t0,t1,81fb02bc
81fb02c8: 00000000 nop
Searching reveals that the lookup table is at 0x81fcaad0. Searching for aad0 in the instruction set reveals the position where the lookup table is accessed and so where the CRC calculation leaves. Here is the relevant part:
81fb49c8: 3c0281fd lui v0,0x81fd <--- The lookup table has a different base address than the code
81fb49cc: 2453aad0 addiu s3,v0,-21808 <--- Access Lookup table
81fb49d0: 8e820000 lw v0,0(s4) <--- Start of the main loop
81fb49d4: 00000000 nop
81fb49d8: 30420001 andi v0,v0,0x1
81fb49dc: 10400006 beqz v0,81fb49f8
81fb49e0: 00000000 nop
81fb49e4: 0c7f24f4 jal 81fc93d0
81fb49e8: 02202021 move a0,s1
81fb49ec: 00021600 sll v0,v0,0x18
81fb49f0: 087ed280 j 81fb4a00
81fb49f4: 00021603 sra v0,v0,0x18
81fb49f8: 82220000 lb v0,0(s1)
81fb49fc: 00000000 nop
81fb4a00: 00521026 xor v0,v0,s2
81fb4a04: 304200ff andi v0,v0,0xff
81fb4a08: 00021080 sll v0,v0,0x2
81fb4a0c: 00531021 addu v0,v0,s3
81fb4a10: 00121a02 srl v1,s2,0x8
81fb4a14: 8c420000 lw v0,0(v0)
81fb4a18: 00000000 nop
81fb4a1c: 00629026 xor s2,v1,v0
81fb4a20: 2610ffff addiu s0,s0,-1
81fb4a24: 1600ffea bnez s0,81fb49d0 <--- End of main loop
As you can see the algorithm is equivalent with the one found in the cfg_manager.
Cool project verified and finished. Hope you had fun (for weird values of fun).