Hillstone Firewall Internals (1) - Firmware Format Big Picture
Exploring the firmwares from Hillstone.
Intro
I have several firewalls manufactured by Hillstone:
- SG-6000 G2110
- SG-6000 M3108
- SG-6000 NAV20
- SA-2005
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
.
- header checksum: set
crc32(0, $firmware, 0x8C)
as$header_crc32
; - now
$header_crc32
is filled while$image_crc32
is still0x0000
; - image checksum: set
crc32(0, $firmware, $length)
as$image_crc32
;
#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.