Skip to content

Instantly share code, notes, and snippets.

@Wack0
Created May 23, 2017 11:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Wack0/a0cf2fa943274d39f5c4400a8414891d to your computer and use it in GitHub Desktop.
Save Wack0/a0cf2fa943274d39f5c4400a8414891d to your computer and use it in GitHub Desktop.
Compaq/HP Recovery Media (c. late 1990s-early 2000s) .CEX File Decryptor
/*
Compaq/HP Recovery Media (c. late 1990s-early 2000s) .CEX File Decryptor
another rrrring of lightningggg production by slipstream/RoL!
Yesterday I received in the post some Compaq recovery media I ordered from Yahoo! Auctions Japan to dump.
Having done that, I took a closer look at the disc images.
The recovery media came in two CDs: a boot CD ("COMPAQ Restore CD"), and an OS CD ("Compaq CD for Microsoft Windows
NT Workstation 4.0 Operating System").
The OS CD was normal enough. A few extra files on the root, drvlib/drvlibj/support folders (it's ja-JP NT!), and the
install files for x86 (PC)/x86 (NEC PC-98)/PowerPC architectures.
Wait, normal? The files in the \i386 folder (and only that architecture -- the PC-98/PPC install files were cleartext
and untouched) all had a .CEX extension.
Closer look in a hex editor revealed that the .CEX files were all encrypted. The files had a 0x192 byte header (including
identifier: null-terminated string "CPQRESTORE") followed by crypted data.
Why crypto? Probably some dumbass protection to try and make these OS CDs only useful inside the recovery environment
(which has BIOS-lock protection of course).
The crypto is handled by TOOLS\CPQFSMGT.OCX on the boot CD.
Most surprisingly, the cryptographic key is actually stored in cleartext in the .CEX header, 0x60 bytes at offset 0x1D.
The crypto algorithm is weird: it basically involves XORing non-finalised MD5 states together with parts of the 256-bit
ciphertext block, after MD5Init and MD5Transform of raw blocks, created by concatenating half of the 256-bit ciphertext
block with half of the 768-bit key material in different ways.
If you wonder about the use of MD5, this is historical crypto code from somewhere between late April 1998 and
mid-January 2000.
The same Compaq recovery media style/code has been used by HP until at least 2008; however, I believe they removed the
crypto code in the early 2000s. The OS discs of the 2008 HP recovery media set I have just has a custom boot sector that
requests the first disc of the set.
*/
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.IO;
// This MD5 implementation is from https://dlaa.me/blog/post/9380245 - thanks!
public class MD5Managed {
///////////////////////////////////////////////
// MD5 reference implementation begins here. //
///////////////////////////////////////////////
/* MD5 context. */
public class MD5_CTX
{
public readonly uint[] state; /* state (ABCD) */
public readonly uint[] count; /* number of bits, modulo 2^64 (lsb first) */
public readonly byte[] buffer; /* input buffer */
public MD5_CTX()
{
state = new uint[4];
count = new uint[2];
buffer = new byte[64];
}
public void Clear()
{
Array.Clear(state, 0, state.Length);
Array.Clear(count, 0, count.Length);
Array.Clear(buffer, 0, buffer.Length);
}
}
/* Constants for MD5Transform routine. */
private const int S11 = 7;
private const int S12 = 12;
private const int S13 = 17;
private const int S14 = 22;
private const int S21 = 5;
private const int S22 = 9;
private const int S23 = 14;
private const int S24 = 20;
private const int S31 = 4;
private const int S32 = 11;
private const int S33 = 16;
private const int S34 = 23;
private const int S41 = 6;
private const int S42 = 10;
private const int S43 = 15;
private const int S44 = 21;
private static byte[] PADDING;
[SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "More compact this way")]
static MD5Managed()
{
PADDING = new byte[64];
PADDING[0] = 0x80;
}
/* F, G, H and I are basic MD5 functions. */
private static uint F(uint x, uint y, uint z) { return (((x) & (y)) | ((~x) & (z))); }
private static uint G(uint x, uint y, uint z) { return (((x) & (z)) | ((y) & (~z))); }
private static uint H(uint x, uint y, uint z) { return ((x) ^ (y) ^ (z)); }
private static uint I(uint x, uint y, uint z) { return ((y) ^ ((x) | (~z))); }
/* ROTATE_LEFT rotates x left n bits. */
private static uint ROTATE_LEFT(uint x, int n) { return (((x) << (n)) | ((x) >> (32 - (n)))); }
/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
Rotation is separate from addition to prevent recomputation. */
private static void FF(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
{
(a) += F((b), (c), (d)) + (x) + (uint)(ac);
(a) = ROTATE_LEFT((a), (s));
(a) += (b);
}
private static void GG(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
{
(a) += G((b), (c), (d)) + (x) + (uint)(ac);
(a) = ROTATE_LEFT((a), (s));
(a) += (b);
}
private static void HH(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
{
(a) += H((b), (c), (d)) + (x) + (uint)(ac);
(a) = ROTATE_LEFT((a), (s));
(a) += (b);
}
private static void II(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
{
(a) += I((b), (c), (d)) + (x) + (uint)(ac);
(a) = ROTATE_LEFT((a), (s));
(a) += (b);
}
/* MD5 initialization. Begins an MD5 operation, writing a new context. */
public static void MD5Init(MD5_CTX context) /* context */
{
context.count[0] = context.count[1] = 0;
/* Load magic initialization constants. */
context.state[0] = 0x67452301;
context.state[1] = 0xefcdab89;
context.state[2] = 0x98badcfe;
context.state[3] = 0x10325476;
}
/* MD5 block update operation. Continues an MD5 message-digest
operation, processing another message block, and updating the
context. */
private static void MD5Update(MD5_CTX context, /* context */
byte[] input, /* input block */
uint inputIndex, // Starting index for input block
uint inputLen) /* length of input block */
{
/* Compute number of bytes mod 64 */
uint index = (uint)((context.count[0] >> 3) & 0x3F);
/* Update number of bits */
if ((context.count[0] += ((uint)inputLen << 3)) < ((uint)inputLen << 3))
{
context.count[1]++;
}
context.count[1] += ((uint)inputLen >> 29);
uint partLen = 64 - index;
/* Transform as many times as possible. */
uint i = 0;
if (inputLen >= partLen)
{
Buffer.BlockCopy(input, (int)inputIndex, context.buffer, (int)index, (int)partLen);
MD5Transform(context.state, context.buffer, 0);
for (i = partLen; i + 63 < inputLen; i += 64)
{
MD5Transform(context.state, input, inputIndex + i);
}
index = 0;
}
/* Buffer remaining input */
Buffer.BlockCopy(input, (int)(inputIndex + i), context.buffer, (int)index, (int)(inputLen - i));
}
/* MD5 finalization. Ends an MD5 message-digest operation, writing the
the message digest and zeroizing the context. */
public static void MD5Final(byte[] digest, /* message digest */
MD5_CTX context) /* context */
{
byte[] bits = new byte[8];
/* Save number of bits */
Encode(bits, context.count, 8);
/* Pad out to 56 mod 64. */
uint index = (uint)((context.count[0] >> 3) & 0x3f);
uint padLen = (index < 56) ? (56 - index) : (120 - index);
MD5Update(context, PADDING, 0, padLen);
/* Append length (before padding) */
MD5Update(context, bits, 0, 8);
/* Store state in digest */
Encode(digest, context.state, 16);
/* Zeroize sensitive information. */
context.Clear();
}
/* MD5 basic transformation. Transforms state based on block. */
public static void MD5Transform(uint[] state,
byte[] block,
uint blockIndex)
{
uint a = state[0], b = state[1], c = state[2], d = state[3];
uint[] x = new uint[16];
Decode(x, block, blockIndex, 64);
/* Round 1 */
FF(ref a, b, c, d, x[0], S11, 0xd76aa478); /* 1 */
FF(ref d, a, b, c, x[1], S12, 0xe8c7b756); /* 2 */
FF(ref c, d, a, b, x[2], S13, 0x242070db); /* 3 */
FF(ref b, c, d, a, x[3], S14, 0xc1bdceee); /* 4 */
FF(ref a, b, c, d, x[4], S11, 0xf57c0faf); /* 5 */
FF(ref d, a, b, c, x[5], S12, 0x4787c62a); /* 6 */
FF(ref c, d, a, b, x[6], S13, 0xa8304613); /* 7 */
FF(ref b, c, d, a, x[7], S14, 0xfd469501); /* 8 */
FF(ref a, b, c, d, x[8], S11, 0x698098d8); /* 9 */
FF(ref d, a, b, c, x[9], S12, 0x8b44f7af); /* 10 */
FF(ref c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
FF(ref b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
FF(ref a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
FF(ref d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
FF(ref c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
FF(ref b, c, d, a, x[15], S14, 0x49b40821); /* 16 */
/* Round 2 */
GG(ref a, b, c, d, x[1], S21, 0xf61e2562); /* 17 */
GG(ref d, a, b, c, x[6], S22, 0xc040b340); /* 18 */
GG(ref c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
GG(ref b, c, d, a, x[0], S24, 0xe9b6c7aa); /* 20 */
GG(ref a, b, c, d, x[5], S21, 0xd62f105d); /* 21 */
GG(ref d, a, b, c, x[10], S22, 0x02441453); /* 22 */
GG(ref c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
GG(ref b, c, d, a, x[4], S24, 0xe7d3fbc8); /* 24 */
GG(ref a, b, c, d, x[9], S21, 0x21e1cde6); /* 25 */
GG(ref d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
GG(ref c, d, a, b, x[3], S23, 0xf4d50d87); /* 27 */
GG(ref b, c, d, a, x[8], S24, 0x455a14ed); /* 28 */
GG(ref a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
GG(ref d, a, b, c, x[2], S22, 0xfcefa3f8); /* 30 */
GG(ref c, d, a, b, x[7], S23, 0x676f02d9); /* 31 */
GG(ref b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */
/* Round 3 */
HH(ref a, b, c, d, x[5], S31, 0xfffa3942); /* 33 */
HH(ref d, a, b, c, x[8], S32, 0x8771f681); /* 34 */
HH(ref c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
HH(ref b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
HH(ref a, b, c, d, x[1], S31, 0xa4beea44); /* 37 */
HH(ref d, a, b, c, x[4], S32, 0x4bdecfa9); /* 38 */
HH(ref c, d, a, b, x[7], S33, 0xf6bb4b60); /* 39 */
HH(ref b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
HH(ref a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
HH(ref d, a, b, c, x[0], S32, 0xeaa127fa); /* 42 */
HH(ref c, d, a, b, x[3], S33, 0xd4ef3085); /* 43 */
HH(ref b, c, d, a, x[6], S34, 0x04881d05); /* 44 */
HH(ref a, b, c, d, x[9], S31, 0xd9d4d039); /* 45 */
HH(ref d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
HH(ref c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
HH(ref b, c, d, a, x[2], S34, 0xc4ac5665); /* 48 */
/* Round 4 */
II(ref a, b, c, d, x[0], S41, 0xf4292244); /* 49 */
II(ref d, a, b, c, x[7], S42, 0x432aff97); /* 50 */
II(ref c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
II(ref b, c, d, a, x[5], S44, 0xfc93a039); /* 52 */
II(ref a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
II(ref d, a, b, c, x[3], S42, 0x8f0ccc92); /* 54 */
II(ref c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
II(ref b, c, d, a, x[1], S44, 0x85845dd1); /* 56 */
II(ref a, b, c, d, x[8], S41, 0x6fa87e4f); /* 57 */
II(ref d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
II(ref c, d, a, b, x[6], S43, 0xa3014314); /* 59 */
II(ref b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
II(ref a, b, c, d, x[4], S41, 0xf7537e82); /* 61 */
II(ref d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
II(ref c, d, a, b, x[2], S43, 0x2ad7d2bb); /* 63 */
II(ref b, c, d, a, x[9], S44, 0xeb86d391); /* 64 */
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
/* Zeroize sensitive information. */
Array.Clear(x, 0, x.Length);
}
/* Encodes input (UINT4) into output (unsigned char). Assumes len is
a multiple of 4. */
private static void Encode(byte[] output,
uint[] input,
uint len)
{
for (uint i = 0, j = 0; j < len; i++, j += 4)
{
output[j] = (byte)(input[i] & 0xff);
output[j + 1] = (byte)((input[i] >> 8) & 0xff);
output[j + 2] = (byte)((input[i] >> 16) & 0xff);
output[j + 3] = (byte)((input[i] >> 24) & 0xff);
}
}
/* Decodes input (unsigned char) into output (UINT4). Assumes len is
a multiple of 4. */
private static void Decode(uint[] output,
byte[] input,
uint inputIndex,
uint len)
{
for (uint i = 0, j = 0; j < len; i++, j += 4)
{
output[i] = ((uint)input[inputIndex + j]) |
(((uint)input[inputIndex + j + 1]) << 8) |
(((uint)input[inputIndex + j + 2]) << 16) |
(((uint)input[inputIndex + j + 3]) << 24);
}
}
}
public class CEX {
public static uint[] DoMD5(byte[] block) {
var ctx = new MD5Managed.MD5_CTX();
MD5Managed.MD5Init(ctx);
MD5Managed.MD5Transform(ctx.state,block,0);
return ctx.state;
}
public static uint[] DoMD5(byte[] chunk1,byte[] chunk2) {
var bytesToHash = new byte[chunk1.Length + chunk2.Length];
Buffer.BlockCopy(chunk1,0,bytesToHash,0,chunk1.Length);
Buffer.BlockCopy(chunk2,0,bytesToHash,chunk1.Length,chunk2.Length);
return DoMD5(bytesToHash);
}
public static byte[] chunkHigh = new byte[0x10];
public static byte[] chunkLow = new byte[0x10];
public static byte[] keyHigh = new byte[0x30];
public static byte[] keyLow = new byte[0x30];
public static byte[] DecryptChunk(byte[] chunk,byte[] key) {
var ret = new byte[chunk.Length];
// copy the chunks
Buffer.BlockCopy(chunk,0,chunkHigh,0,chunkHigh.Length);
Buffer.BlockCopy(chunk,chunkHigh.Length,chunkLow,0,chunkLow.Length);
Buffer.BlockCopy(key,0,keyHigh,0,keyHigh.Length);
Buffer.BlockCopy(key,keyHigh.Length,keyLow,0,keyLow.Length);
var md5 = DoMD5(chunkLow,keyHigh);
var lastXor = new uint[4];
var temp_buffer = new byte[16];
for (var md5_offset = 0; md5_offset < 4; md5_offset++) {
lastXor[md5_offset] = md5[md5_offset] ^ BitConverter.ToUInt32(chunkHigh,md5_offset * 4);
Buffer.BlockCopy(BitConverter.GetBytes(lastXor[md5_offset]),0,temp_buffer,md5_offset * 4,4);
}
md5 = DoMD5(temp_buffer,keyLow);
for (var md5_offset = 0; md5_offset < 4; md5_offset++) {
var dword = md5[md5_offset] ^ BitConverter.ToUInt32(chunkLow,md5_offset * 4);
Buffer.BlockCopy(BitConverter.GetBytes(dword),0,temp_buffer,md5_offset * 4,4);
Buffer.BlockCopy(temp_buffer,md5_offset * 4,ret,md5_offset * 4,4);
}
md5 = DoMD5(temp_buffer,keyHigh);
for (var md5_offset = 0; md5_offset < 4; md5_offset++) {
var dword = md5[md5_offset] ^ lastXor[md5_offset];
Buffer.BlockCopy(BitConverter.GetBytes(dword),0,ret,(md5_offset + 4) * 4,4);
}
return ret;
}
public static byte[] Decrypt(byte[] Cex) {
var content_size = BitConverter.ToUInt32(Cex,23);
var header_size = BitConverter.ToUInt32(Cex,0);
var content_len = BitConverter.ToUInt64(Cex,0xf);
var extra_bytes = BitConverter.ToUInt16(Cex,0x1b);
if (extra_bytes > 0) content_size++;
var key = new byte[0x60];
Buffer.BlockCopy(Cex,0x1d,key,0,key.Length);
var ret = new byte[content_len];
var chunk = new byte[0x20];
for (var i = 0; i < content_size; i++) {
Buffer.BlockCopy(Cex,(int) (header_size + (i * chunk.Length)),chunk,0,chunk.Length);
chunk = DecryptChunk(chunk,key);
int copylen = chunk.Length;
if ((ulong)copylen > content_len) copylen = (int)content_len;
Buffer.BlockCopy(chunk,0,ret,(i * chunk.Length),copylen);
content_len -= (ulong)copylen;
}
return ret;
}
public static bool IsCEX(byte[] Cex) {
return (Encoding.ASCII.GetString(Cex,0x4,10) == "CPQRESTORE");
}
public static void DecryptCEX(string CexFile) {
byte[] Cex = File.ReadAllBytes(CexFile);
if (!IsCEX(Cex)) return;
var header_size = BitConverter.ToUInt32(Cex,0);
string out_filename = string.Empty;
if (header_size <= 0x92) {
out_filename = CexFile.Split(new char[] { '.' })[0] + ".";
for (var i = 0x7d; Cex[i] != 0; i++) out_filename += (char)Cex[i];
} else {
if (CexFile.IndexOf(Path.DirectorySeparatorChar) != -1) {
out_filename = CexFile.Split(new char[] { Path.DirectorySeparatorChar })[0] + Path.DirectorySeparatorChar;
}
for (var i = 0x92; Cex[i] != 0; i++) out_filename += (char)Cex[i];
}
File.WriteAllBytes(out_filename,Decrypt(Cex));
}
public static void Main(string[] args) {
foreach (var arg in args) DecryptCEX(arg);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment