We can in fact identify 4 kinds of files :
An AOS file is structured with chunks, which can be :
| string | hexadecimal | decimal |
|---|---|---|
| RIFF | 5249 4646 | 21065 17990 |
| CHOS | 4348 4f53 | 17224 20307 |
| SWID | 5357 4944 | 21335 18756 |
| HWID | 4857 4944 | 18519 18756 |
| FLSH | 464C 5348 | 17996 21320 |
| CCOD | 4343 4F44 | 17219 20292 |
| CODE | 434F 4445 | 17231 17477 |
| SIGN | 5349 474E | 21321 18254 |
| FILE | 4649 4C45 | 17993 19525 |
| LIST |
NB: hexadecimal and decimal are usefull for the reverse.
One AOS file can contain one or more files (MAX_AOS_FILES = 32). The first chunk must be a RIFF chunk, and {RIFF, FLSH, SIGN, CCODE} are mandatory chunks.
The RIFF Chunk is a container.
| offset | length | description |
|---|---|---|
| 0 | 4 | chunk type |
| 4 | 4 | total chunk length (with its content) |
| offset | length | description |
|---|---|---|
| 0 | 4 | chunk type |
| 4 | 4 | total chunk length |
| 8 | 4 | version id (eg 1000) |
| 12 | … | version string |
| offset | length | description |
|---|---|---|
| 0 | 4 | chunk type |
| 4 | 4 | total chunk length |
| 8 | … | ??? (eg. 0000) |
| offset | length | description |
|---|---|---|
| 0 | 4 | chunk type |
| 4 | 4 | total chunk length |
| 8 | 4 | base address (eg. 00 00 60 00) |
| 12 | 4 | ucsize (eg. 00 08 4d c2) : used in CCOD |
| offset | length | description |
|---|---|---|
| 0 | 4 | chunk type |
| 4 | 4 | total chunk length |
| 8 | 4 | csize = AJZ size |
| 12 | … | AJZ content |
with AJZ being
| offset | length | description |
|---|---|---|
| 0 | 4 | header (ZcZc) |
| 4 | 4 | uncompressed size |
| 8 | 4 | compressed size |
| 12 | 4 | checksum |
| 16 | csize-16 | image |
| offset | length | description |
|---|---|---|
| 0 | 4 | chunk type |
| 4 | 4 | total chunk length = 0×6C = 108 (96 + 12) |
| 8 | 2 | ?? |
| 10 | 2 | 0 or other : determine base values for descrambling |
| Content | ||
| 12 | 10 | Cyphered HD serial number |
0000050: 0000 0010 0000 6000 0008 4dc2 5349 474e SIGN
0000060: 0000 006c 0000 0000 c4db f207 4fc9 9bc8 ...l........O...
0000070: 1adb bffb f5d7 9a23 9571 597b 3e07 d8e2 .......#.qY{>...
0000080: 600d 6324 bfff 7bfe ea24 d186 056d d733 `.c$..{..$...m.3
0000090: c6a1 4711 6bab 793b 78b2 7756 c524 61fd ..G.k.y;x.wV.$a.
00000a0: fc42 9bd4 d448 b17b 7443 b398 1239 e014 .B...H.{tC...9..
00000b0: eed5 06cd 1e98 3c9a 7c1c e1c7 5074 be0c ......<.|...Pt..
00000c0: 081e 6fcf 9e9d 5e13 4343 4f44 0004 2ab8 ..o...^.
| offset | offset {110/?} | description | ||
|---|---|---|---|---|
| 0×05F9D6 | 0×064500 | pDecryptKey / 0×7c9ch copied here when R2 = 0 / 0×7d08 otherwise | ||
| 0×05F9DA | 0×064504 | pDECIPHERED_STUFF (computed by scramble_B) | ||
| offset | size | description | ||
| 0 | 4 | 768 – this is the bitlength of the RSA message | ||
| 2 | 10 | deciphered HD serial number | ||
| 12 | 16 | md5 digest of the code chunk | ||
| 28 | 32 | deciphered zaza key | ||
| 60 | 44? | random padding ? | ||
| 0×05F9DE | 0×064508 | pFILE[32] | ||
| 0×05FA5E | 0×064588 | pCCODE | ||
| 0×05FA62 | 0×06458C | pSIGN | ||
| 0×05FA66 | 0×064590 | pFLSH | ||
| 0×05FA6A | 0×064594 | pHWID | ||
| 0×05FA6E | 0×064598 | pSWID | ||
| 0×05FA72 | 0×06459C | pRIFF | ||
A variable size buffer is used to represent a long number b=b0b1…bn-2bn-1
+---------------------+----------+----------+- - - - - -+----------+----------+
| | | | | | |
| size in | b | b | | b | b |
| bits | n-1 | n-2 | | 1 | 0 |
| | | | | | |
| N = 8n | LSB | | | | MSB |
| | | | | | |
+---------------------+----------+----------+- - - - - -+----------+----------+
<----------------------------------------------------------------------------->
n+2 bytes
___________________________
fig.1: the bignum structure
We have chosen to use the gmp library for basic operations on bignums.
Therefore, the following code snippets must be linked with -lgmp -lgmpxx.
This is a draft of a reverse-engineering of the descrambling code. There are errors but it is enough to give a general feel. Basically, the important function is scrambleB. I bet my ass it does the exponent of two bignums (modulo another). In turn, this feels very much like RSA public key decryption algorithm. Thus, I expect the sign chunk data to be scrambled with Archos’ private key.
#include <gmpxx.h> // requires libgmp
typedef mpz_class bignum;
typedef unsigned char u8;
typedef unsigned short u16;
bignum table[200];
int table_length;
bignum one = 1;
void scrambleA(bignum k) {
table[0] = k*k >> 8;
table[1] = 8; // ?
table[2] = k;
int r6 = 2;
while (table[0] > table[r6]) {
table[r6+1] = table[1];
table[r6+1] *= table[r6];
r6++;
}
table_length = r6;
}
bignum table_multimodulo(bignum p1, bignum p2) {
int r6 = table_length;
while (r6 >= 2) {
bignum q = table[r6];
if (p1 <= q)
r6--;
else
p1 -= q;
}
// length p1 is set to length p2. Why for? Ouch!
return p1;
}
void bignum_doSomething1(bignum &p1, bignum p2, bignum p3) {
p1 = table_multimodulo(p1 * p2, p3);
}
void scrambleB(bignum p1, u8* data) {
bignum tmp3, tmp4;
int length = *(u16*)data; // A12 in bits most probably
data += 2;
tmp3 = p1;
int r1 = 0;
p1 = 1;
r1 = 0;
u8 curdata; // r6
int curbit; // r2
while (r1 < length) {
//r2 = (*(u16*)0x05F5C2) % 2;
// computation result ignored!
//lcdStuff(0x28, (*(u16*)0x04CB64) + ((17 * r1) % length));
//clearWatchdog();
// this has probably nothing to do with descrambling
if ((r1 % 8) == 0) { //load next byte.
curdata = *data;
data++;
}
curbit = curdata & 1; // load next bit.
curdata >>= 1;
if (curbit != 0) {
bignum_doSomething1(p1, tmp3, p1);
p1 = table_multimodulo(p1, p1);
}
tmp4 = tmp3;
bignum_doSomething1(tmp3, tmp4, p1);
p1 = table_multimodulo(p1, p1);
r1++;
}
}
Public keys can be retrieved from the gmini memory via the following algorithm.
#include <gmpxx.h> // requires libgmp
#include <iostream.h>
unsigned char keydata1[] = {0xad,0x77,0x47,0xa2,0xb0,0x4c,0x11,0x6f,0xdc,0x8a,0x8b,0x9d,0xd6,0x3e,0xa,0xdc,0xe6,0xa5,0x21,0x4a,0x3b,0x75,0xd,0xd2,0x84,0x1b,0xa7,0xc,0x6b,0x4a,0xca,0x2a,0x41,0x56,0xc9,0x99,0xf2,0x3f,0xa,0xfa,0xac,0xe,0xd5,0x38,0x6,0x3d,0x52,0xa6,0x8e,0xb7,0xc8,0x9d,0xd5,0xb2,0x98,0x11,0x2e,0x59,0xd2,0x1f,0x64,0xbf,0x18,0x7d,0x3e,0xb8,0x4c,0x73,0xa7,0x11,0xff,0x36,0x7d,0xe6,0x60,0xbd,0x4a,0xbe,0x77,0xdc,0x2b,0x6c,0x42,0x74,0xfe,0x4d,0x25,0x1d,0x20,0xe0,0x2a,0xcd,0x38,0x12,0xfa,0x3b};
unsigned char keydata1b[] = {0x18,0xe5,0x37,0x3b,0x45,0x59,0xc6,0x14};
unsigned char keydata2[] = {0x39,0x41,0x44,0x6e,0xbc,0x83,0xc3,0xee,0x63,0x86,0x98,0xb7,0xd4,0xff,0x79,0x4b,0xd1,0xcb,0x91,0x92,0x78,0xe0,0xb9,0x8c,0x3a,0x73,0x77,0x64,0x91,0xdf,0xda,0xb5,0xa0,0xc0,0x44,0xd7,0xdd,0xd9,0x3b,0xd9,0x21,0x38,0x12,0x93,0xfd,0xf8,0xfe,0x6c,0x4f,0x90,0x51,0x51,0xd6,0xd9,0x59,0x58,0xc3,0xa1,0xda,0x20,0xe4,0xe3,0xea,0xae,0xe,0x9b,0xf8,0x47,0x29,0x1d,0xa9,0x9,0x8e,0x9b,0x13,0xa9,0x9c,0x2a,0xf8,0xe1,0xc9,0xe5,0x25,0xee,0xbc,0x48,0x90,0xa1,0x9,0x35,0xf5,0xa0,0xbf,0x2a,0xe8,0xfa};
unsigned char keydata2b[] = {0xf4,0x8b,0x58,0x64,0xcd,0xaa,0xf8,0x10};
bignum loadDecryptKey(unsigned char* key, int len) {
bignum tmp = 0;
for (int i = len-2; i >= 0; i-=2) {
tmp = tmp << 8;
tmp += key[i];
tmp = tmp << 8;
tmp += key[i+1];
}
return tmp;
}
int main() {
cout << loadDecryptKey(keydata1, 96) << ',' << loadDecryptKey(keydata1b, 8) << endl;
cout << loadDecryptKey(keydata2, 96) << ',' << loadDecryptKey(keydata2b, 8) << endl;
}
Data in the previous code is found at the following addresses:
| 0×0031FE | key1aLen |
| 0×003260 | key1bLen |
| 0×003200 | key1aData |
| 0×003262 | key1bData |
| 0×00326A | key2aLen |
| 0×00326C | key2bLen |
| 0×0032CC | hey2aData |
| 0×0032CE | key2bData |
The result is:
| n | e |
|---|---|
| 15175338213866630… | 14273109368524970213 |
| 14129095978200489… | 17875013052544644235 |
Where m’ = (m^e) mod n
Interestingly, those two keys are the same across the whole gmini line. (I’ve checked 100_v1100, SP_v130, 220_v1100).
The above code can be complemented with the actual decryption algorithm in order to retrieve the decrypted sign chunk.
unsigned char f100_1100sign[] = {0x93,0xf9,0x58,0x2e,0x57,0x98,0x6b,0x94,0xf4,0xda,0x6f,0x62,0xf5,0xe5,0xde,0x61,0xb,0xbb,0x84,0x71,0x64,0xd,0xd3,0x3a,0x39,0x5a,0x95,0xc9,0x43,0xc5,0x9,0x10,0x11,0x98,0x40,0x63,0xf0,0xbd,0xc4,0x7b,0x10,0xf4,0x88,0x1f,0x7,0x1b,0xfc,0x40,0x24,0xbf,0xd6,0x9c,0x7a,0x44,0xf1,0x99,0x6b,0x54,0x1,0xe8,0x3b,0x18,0x5c,0x60,0x80,0x3c,0xe8,0xe3,0x21,0x87,0x71,0x6e,0xce,0x74,0x74,0xf2,0xf,0x5f,0xdb,0xe9,0xfb,0xc4,0x8c,0x55,0x11,0x58,0xf0,0x80,0x98,0x55,0xf8,0xa4,0x8e,0x5a,0x1d,0x5d};
bignum loadMessage(unsigned char* msg, int len) {
bignum tmp = 0;
for (int i = len-1; i >= 0; i--) {
tmp = tmp << 8;
tmp += msg[i];
}
return tmp;
}
void storeMessage(unsigned char* msg, int len, bignum n) {
for (int i = 0; i < len; i++) {
bignum m = n % 256;
unsigned long l = m.get_ui();
msg[i] = l;
n = n >> 8;
}
}
void printMessage(unsigned char* msg, int len) {
for (int i = 0; i<96; i++) {
unsigned char c = msg[i];
if (c >= 32 && c < 127) {
cout << c;
} else {
cout << '.';
}
}
cout << endl;
}
int main() {
bignum sign = loadMessage(f100_1100sign, 96);
bignum n = loadDecryptKey(keydata1, 96);
bignum d = loadDecryptKey(keydata1b, 8);
bignum result;
mpz_powm (result.get_mpz_t(), sign.get_mpz_t(),
d.get_mpz_t(), n.get_mpz_t());
printMessage(f100_1100sign, 96);
storeMessage(f100_1100sign, 96, result);
printMessage(f100_1100sign, 96);
}
Giving the output:
..X.W.k...ob...a...qd..:9Z..C.....@c...{.......@$...zD..kT..;.\`.<..!.qn.tt.._.....U.X...U...Z.]
e...8.l..jp.)...3../...3RDNALORDNAXELARIMIDALVXRBJXSOHCRA}.l.tS..Njq=....X../=...W..$"..9......
Which proves the RSA theory. Notice we find the XOR key in there (you were right gromit ;)
Come on, Vladimir, Alex, Roland… Lend us your private key ;)
Once the SIGN chunk operations are performed, the HD serial number is the firmware file is checked (LOS files only in fact). There is a comparison between the deciphered HD SN (which we suppose is in the SIGN chunk) and the Gmini HD SN. It is kinda license system, probably used at the beginning to sell plugins to the customer.
The debug messages inform us about ‘md5 digests’… weird…
#define PRODUCT_KEY_FILE_ORIGIN 05D208h /* for the 220 */
int flag = 0;
for (i = 10; i < 20; i++) {
if (*(PRODUCT_KEY_FILE_ORIGIN + i) != 32) {
flag = 1;
}
if (flag == 1) {
*(param + 1) = *(PRODUCT_KEY_FILE_ORIGIN + i)
}
}
*param+i = 0
s = strlen(param);
if (s >= 10) then return;
ss = 10 - s;
for (i = 9; i => ss; i--)
{
R5R4 = i - ss
*(param+i) = *(param+i) Sign extension....
}
*A13 + 10 = 0
for (i = 0; i < ss; i++)
{
*(param+i) = 48
}
for (i = 0; i < 10; i++) {
memory[A15 + 13 + i] = memory[*064504h + 2 + i] & FFh
}
028C88:( 120) 9FC3 FC40: JSR something_like_md5_digest_of_the_CCODE_chunk /* TODO */
#define pDECIPHERED_STUFF 0x064504 #define DECIPHERED_CCODE_DIGEST 0x0515F8 memcpy(DECIPHERED_CCODE_DIGEST, *pDECIPHERED_STUFF + 12, 16);
#define NB_AOS_FILES 0x007C88
int nb_err = 0;
int i;
for (i = 0; i < NB_AOS_FILES; i++) {
set_R1R0=R11R10*2^R12_bool(R11R10>=R3R2)();
nb_err += CreateFile(FILE[i]);
}
Il we already have a CODE section, and not CCOD, this step is ignored.
#define DECIPHERED_ZAZA_KEY 0x071F38 #define RELATED_TO_ZAZA_KEY 0x071F58 memcpy(DECIPHERED_ZAZA_KEY, *pDECIPHERED_STUFF+28, 32); *RELATED_TO_ZAZA_KEY = 0;