Exploring the firmwares from Hillstone.

Intro

I have several firewalls manufactured by Hillstone:

Latest firmwares are available at https://images.hillstonenet.com/StoneOS (registration required). Some models in the list above are claimed to be “unsupported” officially by Hillstone (of course, they cannot upgrade to the latest).

Hillstone does not publicly offer old firmwares, as well as enough technical documents, which makes it difficult for personal “hackers” to customize some features. Out of curiosity, I began to find those hidden internals by myself.

Firmware Format Big Picture

This article mainly describes the big picture of Hillstone firmware formats.

squashfs

Using binwalk, I found a large gzip data which almost starts at the beginning of the firmware. Sometimes binwalk -e does not handle nested data well, but it should be able to extract gzip data at 0x14004.

First Glance at Hillstone Firmware

Trying to binwalk 14004, and I found the squashfs.

Find a squashfs

Then I got the squashfs through binwalk -e, and mount it as a loop device.

Mount squashfs as a Loop Device

Library Analysis

The library at /usr/local/lib/libplatform.so.1 is partly used to do firmware integrity check.

Analysis of libplatform in Ghidra

Analysis is boring, results are charming.

Hillstone Firmware Format

NOTICE: Since the architecture is MIPS 64 (Big Endian), integers stored in binary file are also big endian.

Size Description
HEADER BEGIN
4B Hillstone Image Magic (0x7F 0x48 0x53 0x00)
4B Unknown
2B $is_release ($is_release & 1 indicates if it is a release version)
2B Unknown
4B Image Length $length (header + body)
4B $platform_id
4B Header CRC32 Checksum $header_crc32
4B Image CRC32 Checksum $image_crc32
32B Image Name (NUL padding)
32B Image Build Time (%Y-%m-%d %H:%i:%s, e.g. 2021-01-27 20:51:37) (NUL padding)
32B Image Build Host (NUL padding)
1B $oem_id
15B Unknown
HEADER END (#HEADER = 140B = 0x8C B)
BODY BEGIN
?B Image Body
BODY END (#BODY = ($length - 140) B = ($length - 0x8C) B)
SIGN BEGIN
256B Image Signature
SIGN END

Checksum

Before checksum was calculated, set both $header_crc32 and $image_crc32 = 0x0000.

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <zlib.h>

#pragma pack(push, 1)
struct hs_header
{
    uint32_t magic;
    uint32_t _;
    uint32_t header_size;
    uint32_t data_size;
    uint32_t __;
    uint32_t checksum1;
    uint32_t checksum2;
    char filename[0x20];
    char build_time[0x20];
    char build_version[0x20];
    uint8_t unknown[0x10];
};
#pragma pack(pop)

#define INIT_CRC32 (crc32(0L, Z_NULL, 0))

uint32_t get_crc32(uint32_t previous, void *data, size_t size)
{
    return crc32(previous, data, size);
}

uint32_t reverse(uint32_t x)
{
    uint8_t *p = (uint8_t *) &x;
    for(size_t i = 0; i < 2; i++)
    {
        p[i] ^= p[3 - i];
        p[3 - i] ^= p[i];
        p[i] ^= p[3 - i];
    }
    return x;
}

#define MIN(x,y) ((x)<(y)?(x):(y))

int main(int argc, char *argv[])
{
    if(argc > 1)
    {
        struct hs_header header;
        FILE *f = fopen(argv[1], "r");
        fread(&header, 1, sizeof(header), f);
        uint32_t old_checksum1 = header.checksum1;
        uint32_t old_checksum2 = header.checksum2;
        uint32_t data_size = reverse(header.data_size);
        header.checksum1 = 0;
        header.checksum2 = 0;
        printf("Old: %x Header CRC32: %x\n", old_checksum1, get_crc32(INIT_CRC32, &header, sizeof(header)));
        
        fseek(f, 0, SEEK_SET);
        uint8_t buffer[0x400];
        uint32_t checksum2 = INIT_CRC32;
        bool first = true;
        while(data_size > 0)
        {
            size_t read = fread(buffer, 1, sizeof(buffer), f);
            if(first)
            {
                struct hs_header *temp_header = (struct hs_header *) buffer;
                temp_header->checksum2 = 0;
                first = false;
            }
            checksum2 = get_crc32(checksum2, buffer, MIN(read, data_size));
            if(data_size < read)
            {
                break;
            }
            data_size -= read;
        }
        printf("Old: %x Header CRC32: %x\n", old_checksum2, checksum2);
    }
    return 0;
}

Signature

The image signature is a signed SHA1 (SHA1 + RSA). Assuming the private key is key.pem, and the file firmware.bin only contains header & body, it can be signed by

openssl dgst -sha1 -binary -sign key.pem firmware.bin > firmware.sign

A signed Hillstone firmware is generated by concatenating firmware.bin and firmware.sign by cat.

Apparently, we do not have the private key from Hillstone. But the public key used to verify the signature can be easily modified, which is stored in a NAND JFFS2 partition.