diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..198fd17 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +.PHONY: clean + +OBJS = md5.o md_common.c reg.o sb.o snak.o snakext.o +CFLAGS += -std=c11 -D_POSIX_C_SOURCE=2 + +all: sb + +md5.o: md5.c md5.h +md_common.o: md_common.c md5.h +reg.o: h.h md5.h +sb.o: h.h +snak.o: snak.c h.h +snakext.o: snakext.c h.h md5.h + +sb: $(OBJS) + $(CC) -o sb $(OBJS) + +clean: + rm -f sb *.o + +dist: + mkdir sb-1.2 + cp *.c *.h README Makefile sb-1.2 + cp -r doc/ sb-1.2 + groff -Tpdf -mdoc doc/sb.1 > sb-1.2/doc/sb.1.pdf + groff -Tpdf -ms doc/drm.ms > sb-1.2/doc/drm.ms.pdf + tar czf sb-1.2.tar.gz --owner=root --group=root --format=ustar sb-1.2 + rm -rf sb-1.2 + diff --git a/README b/README new file mode 100644 index 0000000..5090d86 --- /dev/null +++ b/README @@ -0,0 +1,46 @@ +Overview +-------- + +sb, the SCO Breaker, generates serial numbers, activation keys and +registration keys for SCO products, such as UnixWare 7.1.4. + +It still works for versions released after SCO was acquired by Xinuos. +Products made by Xinuos themselves (namely OpenServer 10) do not employ any +kind of DRM. + +License +------- + +MIT + +Installation +------------ + + $ make + $ install -s sb /usr/local/bin + $ install -m 0644 doc/sb.1 /usr/local/man/man1 + +The install step is optional; sb does not depend on any files and can thus be +run from any directory. + +Usage and Documentation +----------------------- + +See doc/sb.1 for using sb and doc/drm.ms for a description of the DRM +mechanisms employed by SCO. + +For those with no access to troff implementation, a PDF version of the +documents have been included. + +Motivation +---------- + +UNIX wants to be free. +It's an integral part of the UNIX experience to be able to read and understand +the source code of the system. +If you haven't done so yet, use something from the V6 through 4.4BSD era. +You'll see what I mean. + +So this is a very elaborate "Fuck you" to SCO. +I hope it's to your liking. + diff --git a/README.md b/README.md deleted file mode 100644 index 5eb2a13..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# sco-breaker -Generates keys for an aptly named Operating System diff --git a/doc/drm.ms b/doc/drm.ms new file mode 100644 index 0000000..1ae741d --- /dev/null +++ b/doc/drm.ms @@ -0,0 +1,274 @@ +.de UX +\s-1UNIX\s0\\$1 +.. +.RP +.TL +Digital Rights Management Mechanisms in the SCO Product Line +.AU +Anonymous +.ND +.AB +This document attempts to explain the primary mechanisms with which SCO +has attempted to protect their intellectual property as well as how they fell +short of doing so. +.PP +In particular, the serial number and activation key (SNAK) combination, the +license data and the registration key shall be covered. +.AE +.SH +Overview +.LP +The product line of SCO Group, Inc. (SCO) consists of the OpenServer and +UnixWare operating systems as well as associated add-on software, such as user +licenses or development kits. +After SCO had gone bankrupt, UnXis, Inc. bought their +.UX +business and continued to offer it. +UnXis, Inc. later renamed to Xinuos, Inc. +The new OpenServer 10 operating system offered by Xinuos, Inc. is +.I not +covered by this document; +OpenServer 10 eschews digital rights management entirely. +Because of this, ``SCO products'' is still used, even though the products are +now maintained and sold by a different entity. +.PP +SCO's digital rights management consists of a number of different parts: +.IP 1. +the serial number (henceforth +.B serno ) +and activation key, +.B actkey ), ( +forming the +.B SNAK '', `` +.IP 2. +the license data indicating additional information about a product, and +.IP 3. +the registration procedure. +.LP +The purchase of an SCO product provides the buyer with a SNAK and license data +if required. +For continued use, a product usually also needs to be +.I registered . +When the system prompts for registration, a registration lock +.B reglock ) ( +is displayed. +The reglock is then supposed to be entered together with personally +identifying information on a Xinuos, Inc. web portal. +.SH +Serial Number and Activation Key +.LP +The pair of serial number and activation key, internally also called +``SNAK'', consists of a nine-character serno and an eight-character actkey. +The actkey encodes the product ID, product version and whether license data is +required as well as a checksum over the serno and the data contained in the +actkey. +The first three characters of the actkey denote the product ID. +The second triplet denotes the product version and whether license data is +required. +The final two characters are the checksum. +While the serno is effectively an arbitrary string, the actkey is an +all-lowercase string. +For example, a valid SNAK would be the serno ``SCO524572'' and the actkey +``mnridxjo''. +There are two layers of protection: obscurity and a checksum. +.PP +The +.B actkey +is encrypted. +Presumably because of cryptography export restrictions in the United States of +America, the encryption algorithm is weak. +The ciphertext (i.e. the actkey) is processed in reverse, starting with the +NUL. +Each character is +.I rotated +in the alphabet (a-z) by the previous character's offset in the alphabet. +The first character to be decrypted, which is the last character in the +string, will always be unchanged by this algorithm. +See +.I incfrp () +(decryption) and +.I decfrp () +(encryption) +functions for the implementation. +.PP +The second layer of protection is a simple checksum over the serno and +the first six characters of the decrypted actkey. +See the +.I mnsnc () +function for the implementation.\(dg +.FS +\(dg ``incfrp'' and ``mnsnc'' are the names used by SCO. +It is unclear what they stand for. +``decfrp'' has been named that way because the opposite of increment is +decrement, assuming ``inc'' in ``incfrp'' stands for ``increment''. +.FE +.PP +After decrypting the actkey and validating the checksum, +the decrypted actkey is parsed. +Both of the data triplets in the actkey are encoded in base 26 with alphabet +a-z; the most significant digit is written first. +For the example above, the decrypted actkey is ``ahvneoco''. +``ahv'' decodes to 0xCB (203), which is the product ID. +``neo'' decodes to 0x22CA (8906), which contains the flag if license data is +required and the product version. +The version is contained in the lower three nibbles: +0x22CA & 0xFFF equals 714. +The last decimal digit (4) denotes the minor version, the other ones denote +the major version (71). +``co'' is the checksum value. +.SH +License Data +.LP +License data is required if bits 11\(en15 of the second triplet in the actkey +equal 3. +In the given example, this wasn't the case; the relevant bits equal 2 instead. +Values other than 2 and 3 are invalid. +License data is used to supply additional information about a license and +enforce restrictions. +If license data is required, it is printed alongside the SNAK on Certificates +of License and Authenticity. +For example, license data could look like this: ``c4;k0;mjqs8r7''. +.PP +The core of the algorithm is an MD5 hash over a secret value, the serno, the +actkey and the rest of the license data. +Then, this hash is translated to a custom base 32 alphabet. +It is important to note that the license data is ordered by the flag +characters, except for the m flag always being at the end. +See the +.Pa snakext.c +file for the implementation. +.PP +This algorithm is actually susceptible to length-extension attacks because the +secret is merely prepended and MD5 has no mitigations for length-extension +attacks. +However, because license data is fairly strict in validating the input, it is +not actually practical. +.PP +The following fields are known: +.IP c +number of CPUs; +.IP d +expiration of license in days; +this is used for evaluation licenses; +.IP k +if no argument or a non-zero argument is given, registration is required, but +more than one user can access the system; +if the argument is zero, no registration is required, but only one user can +access the system and possibly the network stack is gimped; +.IP u +number of users that may access the system at the same time; +.IP m +MD5 hash over the license data secret, the serno, the actkey and a +canonicalized version of the string until this flag. +.LP +More flags exist. +At least b, g, q have been observed but their format and effects are unknown. +.SH +Registration Key +.LP +Some products must be registered, else they expire after a set amount of time. +This involves a per-installation identifier, the host ID (also referred to as +node ID). +The host ID and serial number are then combined to generate the registration +key. +Because the host ID is different for each installation of a product, storing +registration keys is meaningless. +Most likely, SCO realized the deficiencies of the simple SNAK scheme and added +another layer of protection. +Customers are meant to use an online portal to register their products, +which also checks for double registrations of the same product, allowing the +identification of serial numbers that have been shared. +.I "/etc/brand -k serno" +or +.I scoadmin +can be used to generate the registration lock. +Some of the data there is superfluous for registration code generation; +it is surmised that they are collected for statistical purposes. +This also implies that SCO has customers, which may be a stretch in the first +place, especially considering the USD 2,500 price tag. +.PP +The core of the algorithm is an MD5 hash over a secret value, the host ID and +the serno. +The secret value differs from the one used for the license data. +After that, the first four bytes of the hash are swapped and then encoded in +base16 with a custom alphabet and a two-character checksum. +Because this scheme is symmetric, no actual interaction with SCO is required. +Furthermore, no measures were taken to obscure the secret \(en it is stored in +the +.I /etc/brand +binary with no obfuscation whatsoever. +The secret value used to sign a registration key is the same as the one used +to create the m flag for the registration lock. +There is something truly, profoundly wrong with this; I believe I need not +spell it out. +See the +.I reg.c +file for the implementation. +.PP +A registration lock may look like this: +``d180120;e380106;i203/71.4;oSCO310807;uorxrrwjwxz;mg7fuxu''. +Like the license data, it is a flag string. +The following flags are known: +.IP i +the product ID and version; +the product ID is separated by a slash, the product major and minor versions +are separated by a dot; +.IP o +the serno; +.IP u +the host ID, encoded in some variation of base16 with a custom alphabet +(kbwtacorhzgsejqx) with an appended checksum; +.IP m +MD5 hash over the registration secret, and a canonicalized version of the +string until this flag; +the canonicalization algorithm is the same as for license data. +.LP +Other flags exist, namely d and e, but their purpose is unknown. +.SH +Conclusions +.LP +SCO has done everything wrong that could possibly be done wrong, while also +making matters much more complicated for themselves than necessary. +There are a total of four checksums: +one in the SNAK, a different one in the flag string for the license data, a +different one in the flag string for the registration lock and a different one +for the registration key. +Furthermore, there are two secrets: +the one for the flag string in the license data and the one used for both +sides of the registration process. +And then there are three different encoding schemes: +the encryption of the activation key, the encoding of the activation key (base +26), the base 32 encoding in the m flag for license data and registration +lock and the base 16 encoding for the registration key. +It would not have been necessary to keep this many separate encodings and +algorithms around. +.PP +Due to poor operational security, a mostly complete code dump of SCO UnixWare +leaked on the Internet. +They realized that keeping the +.I /etc/brand +utility in the main tree would be dangerous, so it was checked in only as a +binary file. +However, the binary was neither optimized nor stripped, making reverse +engineering effectively trivial. +.PP +Because all secrets in this DRM mechanism are known to both SCO and +.I /etc/brand , +reverse engineering is all that is required to break every layer of +protection; +there is no cryptographic layer of protection, such as asymmetric signatures +over the registration key. +Elliptic curve signatures in a base64 encoding would likely have been +tolerable for users. +Alternatively, a truncated RSA signature could have been used \(en a full +signature would be too long for users to type into the terminal. +The short Schnorr signatures would have been another option, used by Microsoft +in the Windows XP era. +.PP +None of this has a real-world impact and the estimated amount of lost sales +tends towards zero. +The only reasons to buy an SCO product are either legacy applications or the +support contract that comes with it. +Legacy applications generally do not generate new sales. +Breaking the DRM mechanisms does not cause a support contract to come into +existence out of thin air. diff --git a/doc/drm.ms.pdf b/doc/drm.ms.pdf new file mode 100644 index 0000000..09278c3 Binary files /dev/null and b/doc/drm.ms.pdf differ diff --git a/doc/sb.1 b/doc/sb.1 new file mode 100644 index 0000000..c53cf02 --- /dev/null +++ b/doc/sb.1 @@ -0,0 +1,160 @@ +.Dd January 27, 2018 +.Dt SB 1 +.Os +. +.Sh NAME +.Nm sb +.Nd generate serial numbers, activation keys and registration keys +. +.Sh SYNOPSIS +.Nm +.Ar product_id +.Ar major_ver +.Ar minor_ver +.Op Ar license_data +.Nm +.Fl r +.Ar serial_number +.Ar host_id +.Nm +.Fl r +.Ar registration_lock +. +.Sh DESCRIPTION +.Nm +generates serial numbers, activation keys and registration keys for SCO +products. +SCO products from 1992 and earlier may not be compatible with the licensing +information generated by +.Nm . +.Pp +During installation of an SCO product, you will be prompted for at least a +serial number and an activation key. +This information can be generated by calling +.Nm +.Em without +.Fl r . +In order to generate a serial number and activation key +.Dq ( SNAK ) , +you need the correct +product ID, major version and minor version. +The product ID is an SCO-internal integer that denotes the licensed product. +The major and minor versions denote the version of the licensed product. +.Sy This may not be the same as the version the product is marketed as . +For example, UnixWare 7.1.4 has major version 71 and minor version 4. +The distinction between 7.1.4, 7.1.4+ and 7.1.4 Definitive is made through the +product ID, rather than the version fields. +.Pp +On an SCO UNIX installation, you can use +.Cm /etc/brand -d +to list all known product IDs for that installation. +Usually, there is also a pre-installation and post-installation script bundled +with the SCO product that shows the specific expectations for product ID and +version values; +search those scripts for +.Dq Cm brand -Q . +.Pp +Optionally, you can supply +.Em license data . +License data is a semicolon-delimited list of fields that specify various +aspects about the license in question. +Each field consists of a character and then a value, e.g. +.Dq c4;u100 +denotes a license for four CPUs and 100 users. +License data may also only consist of a single field. +The following characters are known: +.Bl -tag -width Ds +.It Cm c +Number of CPUs. +.It Cm d +Expiry time of the license in days. +This is used for evaluation licenses. +.It Cm k +If no argument or a non-zero argument is given, registration is required, but +more than one user can access the system. +If the argument is zero, no registration is required, but only one user can +access the system and possibly the network stack is gimped. +.It Cm u +Number of users. +.It Cm m +Checksum over the other fields. +.Sy You must not supply this value . +.El +.Pp +The characters g, q and x have also been observed, but their effect is +unknown. +.Sy This program does not check license data is not checked for validity . +.Sy You must order the license data alphabetically . +.Pp +After installation, you may also be asked for a +.Em registration key +for continued operation. +.Nm +will generate a registration key if the +.Fl r +flag is passed. +You need to supply either the serial number of the product and the host ID, or +a valid registration lock. +The host ID differs for every installation, even for the same product and +serial number. +If you have forgotten the serial number of the product, it can be found with +.Cm /etc/brand -L . +The host ID can be found with +.Pa /etc/brand Fl I . +If you supply a registration lock, please be aware that it contains semicolons +(;). +You will have to quote it to prevent the shell from parsing the semicolons as +command delimiters. +. +.Sh EXIT STATUS +.Ex -std +. +.Sh EXAMPLES +Generate a new serial number and activation key for UnixWare 7.1.4 Definitive +2018 (product ID 203, version major 71, version minor 4) with no license data: +.Bd -literal -offset indent +$ sb 203 71 4 +.Ed +.Pp +Register a product for your host: +.Bd -literal -offset indent +$ /etc/brand -I +orxrrwjwxz +$ sb -r SCO539702 orxrrwjwxz +.Ed +. +.Sh DIAGNOSTICS +.Bl -diag +.It "%s not a number" +The supplied product ID, major version or minor version is not actually a +number. +.It "%s out of range (max %u)" +The supplied product ID, major version or minor version is out of range. +If you are +.Em absolutely certain +that you have the correct parameters, please contact the author to fix the +limit. +The limits are theoretical maximum limits, not ones observed in use by SCO. +.It "malloc" +Memory allocation has failed. +If you had enough memory to display this manual page, you can probably just +try again. +You may need to check +.Pa /etc/malloc.conf +if the issue persists. +.It "internal: limit > UINT16_MAX" +You should never see this. +If you do, your platform is almost certainly clinically insane. +Please stop trying to run +.Nm +on a literal Game Boy. +.El +. +.Sh CAVEATS +While this code appears to work, it may be possible that the product +nonetheless phones home over IP. +No efforts have been made to try and observe such activity. +It may prove advantageous to first strictly firewall an SCO UNIX installation. +If the outgoing connections look okay for approximately 24 hours, it may be +safe to let loose on the Internet. +More cautious users may wish to wait up to 31 days. diff --git a/doc/sb.1.pdf b/doc/sb.1.pdf new file mode 100644 index 0000000..a26078a Binary files /dev/null and b/doc/sb.1.pdf differ diff --git a/h.h b/h.h new file mode 100644 index 0000000..0d9f413 --- /dev/null +++ b/h.h @@ -0,0 +1,24 @@ +#ifndef SB_H_H +#define SB_H_H + +/* sb.c */ +void usage(bool fail); +_Noreturn void die(const char *msgfmt, ...); + +/* md_common.c */ +char *BSCanon(const char *s); +void extmd(char md[static 7], unsigned int nstr, ...); + +/* reg.c */ +int gen_regcode(int argc, char *argv[]); + +/* snak.c */ +int gen_snak(int argc, char *argv[]); + +/* snakext.c */ +void mdsnakext(const char *serno, const char *actkey, const char *snakext, + char snakextmd[static 7]); + + +#endif + diff --git a/md5.c b/md5.c new file mode 100644 index 0000000..caf3c41 --- /dev/null +++ b/md5.c @@ -0,0 +1,246 @@ +/* $OpenBSD: md5.c,v 1.11 2015/09/11 09:18:27 guenther Exp $ */ + +/* + * This code implements the MD5 message-digest algorithm. + * The algorithm is due to Ron Rivest. This code was + * written by Colin Plumb in 1993, no copyright is claimed. + * This code is in the public domain; do with it what you wish. + * + * Equivalent code is available from RSA Data Security, Inc. + * This code has been tested against that, and is equivalent, + * except that you don't need to include two pages of legalese + * with every copy. + * + * To compute the message digest of a chunk of bytes, declare an + * MD5Context structure, pass it to MD5Init, call MD5Update as + * needed on buffers full of bytes, and then call MD5Final, which + * will fill a supplied 16-byte array with the digest. + */ + +#include +#include +#include "md5.h" + +#define PUT_64BIT_LE(cp, value) do { \ + (cp)[7] = (value) >> 56; \ + (cp)[6] = (value) >> 48; \ + (cp)[5] = (value) >> 40; \ + (cp)[4] = (value) >> 32; \ + (cp)[3] = (value) >> 24; \ + (cp)[2] = (value) >> 16; \ + (cp)[1] = (value) >> 8; \ + (cp)[0] = (value); } while (0) + +#define PUT_32BIT_LE(cp, value) do { \ + (cp)[3] = (value) >> 24; \ + (cp)[2] = (value) >> 16; \ + (cp)[1] = (value) >> 8; \ + (cp)[0] = (value); } while (0) + +static uint8_t PADDING[MD5_BLOCK_LENGTH] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/* + * Start MD5 accumulation. Set bit count to 0 and buffer to mysterious + * initialization constants. + */ +void +MD5Init(MD5_CTX *ctx) +{ + ctx->count = 0; + ctx->state[0] = 0x67452301; + ctx->state[1] = 0xefcdab89; + ctx->state[2] = 0x98badcfe; + ctx->state[3] = 0x10325476; +} + +/* + * Update context to reflect the concatenation of another buffer full + * of bytes. + */ +void +MD5Update(MD5_CTX *ctx, const unsigned char *input, size_t len) +{ + size_t have, need; + + /* Check how many bytes we already have and how many more we need. */ + have = (size_t)((ctx->count >> 3) & (MD5_BLOCK_LENGTH - 1)); + need = MD5_BLOCK_LENGTH - have; + + /* Update bitcount */ + ctx->count += (uint64_t)len << 3; + + if (len >= need) { + if (have != 0) { + memcpy(ctx->buffer + have, input, need); + MD5Transform(ctx->state, ctx->buffer); + input += need; + len -= need; + have = 0; + } + + /* Process data in MD5_BLOCK_LENGTH-byte chunks. */ + while (len >= MD5_BLOCK_LENGTH) { + MD5Transform(ctx->state, input); + input += MD5_BLOCK_LENGTH; + len -= MD5_BLOCK_LENGTH; + } + } + + /* Handle any remaining bytes of data. */ + if (len != 0) + memcpy(ctx->buffer + have, input, len); +} + +/* + * Pad pad to 64-byte boundary with the bit pattern + * 1 0* (64-bit count of bits processed, MSB-first) + */ +void +MD5Pad(MD5_CTX *ctx) +{ + uint8_t count[8]; + size_t padlen; + + /* Convert count to 8 bytes in little endian order. */ + PUT_64BIT_LE(count, ctx->count); + + /* Pad out to 56 mod 64. */ + padlen = MD5_BLOCK_LENGTH - + ((ctx->count >> 3) & (MD5_BLOCK_LENGTH - 1)); + if (padlen < 1 + 8) + padlen += MD5_BLOCK_LENGTH; + MD5Update(ctx, PADDING, padlen - 8); /* padlen - 8 <= 64 */ + MD5Update(ctx, count, 8); +} + +/* + * Final wrapup--call MD5Pad, fill in digest and zero out ctx. + */ +void +MD5Final(unsigned char digest[MD5_DIGEST_LENGTH], MD5_CTX *ctx) +{ + int i; + + MD5Pad(ctx); + for (i = 0; i < 4; i++) + PUT_32BIT_LE(digest + i * 4, ctx->state[i]); +} + + +/* The four core functions - F1 is optimized somewhat */ + +/* #define F1(x, y, z) (x & y | ~x & z) */ +#define F1(x, y, z) (z ^ (x & (y ^ z))) +#define F2(x, y, z) F1(z, x, y) +#define F3(x, y, z) (x ^ y ^ z) +#define F4(x, y, z) (y ^ (x | ~z)) + +/* This is the central step in the MD5 algorithm. */ +#define MD5STEP(f, w, x, y, z, data, s) \ + ( w += f(x, y, z) + data, w = w<>(32-s), w += x ) + +/* + * The core of the MD5 algorithm, this alters an existing MD5 hash to + * reflect the addition of 16 longwords of new data. MD5Update blocks + * the data and converts bytes into longwords for this routine. + */ +void +MD5Transform(uint32_t state[4], const uint8_t block[MD5_BLOCK_LENGTH]) +{ + uint32_t a, b, c, d, in[MD5_BLOCK_LENGTH / 4]; + +#if BYTE_ORDER == LITTLE_ENDIAN + memcpy(in, block, sizeof(in)); +#else + for (a = 0; a < MD5_BLOCK_LENGTH / 4; a++) { + in[a] = (uint32_t)( + (uint32_t)(block[a * 4 + 0]) | + (uint32_t)(block[a * 4 + 1]) << 8 | + (uint32_t)(block[a * 4 + 2]) << 16 | + (uint32_t)(block[a * 4 + 3]) << 24); + } +#endif + + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + + MD5STEP(F1, a, b, c, d, in[ 0] + 0xd76aa478, 7); + MD5STEP(F1, d, a, b, c, in[ 1] + 0xe8c7b756, 12); + MD5STEP(F1, c, d, a, b, in[ 2] + 0x242070db, 17); + MD5STEP(F1, b, c, d, a, in[ 3] + 0xc1bdceee, 22); + MD5STEP(F1, a, b, c, d, in[ 4] + 0xf57c0faf, 7); + MD5STEP(F1, d, a, b, c, in[ 5] + 0x4787c62a, 12); + MD5STEP(F1, c, d, a, b, in[ 6] + 0xa8304613, 17); + MD5STEP(F1, b, c, d, a, in[ 7] + 0xfd469501, 22); + MD5STEP(F1, a, b, c, d, in[ 8] + 0x698098d8, 7); + MD5STEP(F1, d, a, b, c, in[ 9] + 0x8b44f7af, 12); + MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); + MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); + MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); + MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); + MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); + MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); + + MD5STEP(F2, a, b, c, d, in[ 1] + 0xf61e2562, 5); + MD5STEP(F2, d, a, b, c, in[ 6] + 0xc040b340, 9); + MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); + MD5STEP(F2, b, c, d, a, in[ 0] + 0xe9b6c7aa, 20); + MD5STEP(F2, a, b, c, d, in[ 5] + 0xd62f105d, 5); + MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); + MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); + MD5STEP(F2, b, c, d, a, in[ 4] + 0xe7d3fbc8, 20); + MD5STEP(F2, a, b, c, d, in[ 9] + 0x21e1cde6, 5); + MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); + MD5STEP(F2, c, d, a, b, in[ 3] + 0xf4d50d87, 14); + MD5STEP(F2, b, c, d, a, in[ 8] + 0x455a14ed, 20); + MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); + MD5STEP(F2, d, a, b, c, in[ 2] + 0xfcefa3f8, 9); + MD5STEP(F2, c, d, a, b, in[ 7] + 0x676f02d9, 14); + MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); + + MD5STEP(F3, a, b, c, d, in[ 5] + 0xfffa3942, 4); + MD5STEP(F3, d, a, b, c, in[ 8] + 0x8771f681, 11); + MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); + MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); + MD5STEP(F3, a, b, c, d, in[ 1] + 0xa4beea44, 4); + MD5STEP(F3, d, a, b, c, in[ 4] + 0x4bdecfa9, 11); + MD5STEP(F3, c, d, a, b, in[ 7] + 0xf6bb4b60, 16); + MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); + MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); + MD5STEP(F3, d, a, b, c, in[ 0] + 0xeaa127fa, 11); + MD5STEP(F3, c, d, a, b, in[ 3] + 0xd4ef3085, 16); + MD5STEP(F3, b, c, d, a, in[ 6] + 0x04881d05, 23); + MD5STEP(F3, a, b, c, d, in[ 9] + 0xd9d4d039, 4); + MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); + MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); + MD5STEP(F3, b, c, d, a, in[2 ] + 0xc4ac5665, 23); + + MD5STEP(F4, a, b, c, d, in[ 0] + 0xf4292244, 6); + MD5STEP(F4, d, a, b, c, in[7 ] + 0x432aff97, 10); + MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); + MD5STEP(F4, b, c, d, a, in[5 ] + 0xfc93a039, 21); + MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); + MD5STEP(F4, d, a, b, c, in[3 ] + 0x8f0ccc92, 10); + MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); + MD5STEP(F4, b, c, d, a, in[1 ] + 0x85845dd1, 21); + MD5STEP(F4, a, b, c, d, in[8 ] + 0x6fa87e4f, 6); + MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); + MD5STEP(F4, c, d, a, b, in[6 ] + 0xa3014314, 15); + MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); + MD5STEP(F4, a, b, c, d, in[4 ] + 0xf7537e82, 6); + MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); + MD5STEP(F4, c, d, a, b, in[2 ] + 0x2ad7d2bb, 15); + MD5STEP(F4, b, c, d, a, in[9 ] + 0xeb86d391, 21); + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; +} + diff --git a/md5.h b/md5.h new file mode 100644 index 0000000..9f6cf37 --- /dev/null +++ b/md5.h @@ -0,0 +1,36 @@ +#include + +/* $OpenBSD: md5.h,v 1.17 2012/12/05 23:19:57 deraadt Exp $ */ + +/* + * This code implements the MD5 message-digest algorithm. + * The algorithm is due to Ron Rivest. This code was + * written by Colin Plumb in 1993, no copyright is claimed. + * This code is in the public domain; do with it what you wish. + * + * Equivalent code is available from RSA Data Security, Inc. + * This code has been tested against that, and is equivalent, + * except that you don't need to include two pages of legalese + * with every copy. + */ + +#ifndef _MD5_H_ +#define _MD5_H_ + +#define MD5_BLOCK_LENGTH 64 +#define MD5_DIGEST_LENGTH 16 +#define MD5_DIGEST_STRING_LENGTH (MD5_DIGEST_LENGTH * 2 + 1) + +typedef struct MD5Context { + uint32_t state[4]; /* state */ + uint64_t count; /* number of bits, mod 2^64 */ + uint8_t buffer[MD5_BLOCK_LENGTH]; /* input buffer */ +} MD5_CTX; + +void MD5Init(MD5_CTX *); +void MD5Update(MD5_CTX *, const uint8_t *, size_t); +void MD5Pad(MD5_CTX *); +void MD5Final(uint8_t [MD5_DIGEST_LENGTH], MD5_CTX *); +void MD5Transform(uint32_t [4], const uint8_t [MD5_BLOCK_LENGTH]); + +#endif /* _MD5_H_ */ diff --git a/md_common.c b/md_common.c new file mode 100644 index 0000000..cab1871 --- /dev/null +++ b/md_common.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include +#include + +#include "md5.h" +#include "h.h" + +const char *fromHextet = "0123456789abcdefghjkmnpqrstuwxyz"; + +static int +asciiCode(int c) +{ + static const int fromAlpha[] = { + 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 1, 0x12, 0x13, 1, + 0x14, 0x15, 0, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f + }; + + if (c >= '0' && c <= '9') + return c - '0'; + if (isupper(c)) + c = tolower(c); + if (islower(c)) + return fromAlpha[c - 'a']; + return -1; +} + +char * +BSCanon(const char *s) +{ + char *ret; + int c; + + ret = malloc(strlen(s) + 1); + if (ret == NULL) + die("malloc"); + strcpy(ret, s); + + for (char *p = ret; *p; ++p) { + c = asciiCode(*p); + if (c != -1) + *p = fromHextet[c]; + } + + return ret; +} + +void +extmd(char md[static 7], unsigned int nstr, ...) +{ + char *str; + MD5_CTX ctx; + char digest[MD5_DIGEST_LENGTH]; + va_list ap; + + MD5Init(&ctx); + va_start(ap, nstr); + + for (unsigned int i = 0; i < nstr; ++i) { + str = va_arg(ap, char *); + MD5Update(&ctx, (const uint8_t *)str, strlen(str)); + } + + MD5Final(digest, &ctx); + for (size_t i = 0; i < 6; ++i) + md[i] = fromHextet[digest[i] & 0x1F]; + md[6] = '\0'; +} + diff --git a/reg.c b/reg.c new file mode 100644 index 0000000..5321447 --- /dev/null +++ b/reg.c @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include + +#include "h.h" +#include "md5.h" + +static const char *regSecret = "\x75\xf8\xe8\x5e\x83\xc4\x5e\x4c\xff\x75\x5e\x48\xe8\x65\x5e\x46\x59\x8b\x45"; + +static char * +toAsciiBase16(uint32_t in, unsigned char ckbase) +{ + static const char *charmap = "\005k\001b\002w\003t\004a\005c\001o\002r\003h\004z\005g\001s\002e\003j\004q\005x\001"; + static char buf[11]; + unsigned int a, i; + unsigned char ck; + + ck = ckbase; + for (i = 8; i > 0; --i) { + a = 15 - (in & 0xF); + buf[i - 1] = charmap[2 * a + 1]; + ck += a * (i); + in >>= 4; + } + + /* checksum */ + for (i = 9; i >= 8; --i) { + buf[i] = charmap[2 * (ck & 0xF) + 1]; + ck >>= 4; + } + + buf[10] = '\0'; + return buf; +} + +static uint32_t +l32be(uint8_t i[4]) +{ + return (uint32_t)i[3] + | ((uint32_t)i[2] << 8) + | ((uint32_t)i[1] << 16) + | ((uint32_t)i[0] << 24); +} + +static char * +generateRegistrationID(const char *serno, const char *szHostid) +{ + static char buf[11]; + uint32_t regkey; + MD5_CTX ctx; + uint8_t digest[MD5_DIGEST_LENGTH]; + + MD5Init(&ctx); + MD5Update(&ctx, (const uint8_t *)regSecret, strlen(regSecret)); + MD5Update(&ctx, (const uint8_t *)szHostid, strlen(szHostid)); + MD5Update(&ctx, (const uint8_t *)serno, strlen(serno)); + MD5Final(digest, &ctx); + + regkey = l32be(digest); + snprintf(buf, sizeof(buf), "%s", toAsciiBase16(regkey, 3)); + return buf; +} + +static bool +valid_reglock(char *reglock) +{ + char *canon, *theirs, *p; + char mine[7]; + + if ((p = strstr(reglock, ";m")) == NULL) + return false; + if (*(p + 2) == '\0') + return false; + + *p = '\0'; + theirs = p + 2; + + canon = BSCanon(reglock); + extmd(mine, 2, regSecret, canon); + free(canon); + + return (strcmp(mine, theirs) == 0); +} + +static void +parse_reglock(char *reglock, char **serno, char **hostid) +{ + char *last, *p; + + if (!valid_reglock(reglock)) + die("registration lock %s invalid; check for typos", reglock); + + /* Assumption: Nobody generates bogus reglocks, so they're well-behaved + * after the MD5 checks out. + */ + for (p = strtok_r(reglock, ";", &last); + p != NULL; + p = strtok_r(NULL, ";", &last)) { + switch (*p) { + case 'o': + *serno = p + 1; + break; + case 'u': + *hostid = p + 1; + break; + default: + break; + } + } +} + +int +gen_regcode(int argc, char *argv[]) +{ + char *serno, *hostid, *regcode; + size_t serno_len, hostid_len; + + if (argc < 1) { + usage(true); + return EXIT_FAILURE; + } + + if (argc == 2) { + serno = argv[0]; + hostid = argv[1]; + serno_len = strlen(serno); + hostid_len = strlen(hostid); + if (serno_len == 10 && hostid_len == 9) { + /* Arguments are probably swapped. */ + serno = argv[1]; + hostid = argv[0]; + } else if (serno_len != 9 || hostid_len != 10) { + die("invalid length for serno or hostid"); + } + } else { + parse_reglock(argv[0], &serno, &hostid); + } + + regcode = generateRegistrationID(serno, hostid); + printf("Registration Key: %s\n", regcode); + + return EXIT_SUCCESS; +} + + diff --git a/sb.c b/sb.c new file mode 100644 index 0000000..b85da7c --- /dev/null +++ b/sb.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include +#include + +#include "h.h" + +static char *progname; + +void +usage(bool fail) +{ + fprintf(fail ? stderr : stdout, + "usage: %s product_id major_ver minor_ver " + "[license_data]\n" + " %s -r serial_number host_id\n" + " %s -r registration_lock\n", + progname, progname, progname); +} + +_Noreturn void +die(const char *msgfmt, ...) +{ + va_list ap; + + fprintf(stderr, "%s: ", progname); + + va_start(ap, msgfmt); + vfprintf(stderr, msgfmt, ap); + va_end(ap); + + putchar('\n'); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[]) +{ + int c; + int ret = EXIT_SUCCESS; + bool want_regcode = false; + + progname = basename((argv[0] != NULL) ? argv[0] : "sb"); + + while ((c = getopt(argc, argv, "hr")) != -1) { + switch (c) { + case 'r': + want_regcode = true; + break; + + default: + ret = EXIT_FAILURE; + case 'h': + usage(ret == EXIT_FAILURE); + return ret; + } + } + + argc -= optind; + argv += optind; + + if (want_regcode) + ret = gen_regcode(argc, argv); + else + ret = gen_snak(argc, argv); + + return ret; +} + diff --git a/snak.c b/snak.c new file mode 100644 index 0000000..57c1a7f --- /dev/null +++ b/snak.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "h.h" + +static bool +overflow15(uint32_t a, uint32_t b) +{ + return (a + b > 0x7FFF); +} + +static char * +mnsnc(const char *s) +{ + static char buf[3]; + uint16_t a; + uint16_t flag; + int c = s[8] % 16; + + for (a = 0; *s != '\0'; ++s) { + if (overflow15(a, *s)) + flag = 1; + else + flag = 0; + a = flag | (2 * (a + *s)); + } + + for (; c > 0; --c) + a = ((a & 0x8000U) >> 15) | (uint16_t)(2 * a); + + buf[2] = 0; + buf[1] = a % 26 + 'a'; + a /= 26; + buf[0] = a % 26 + 'a'; + + return buf; +} + +static void +strbn(char *out, unsigned int in) +{ + static const char *alphabet = "abcdefghijklmnopqrstuvwxyz"; + size_t i; + + for (i = 3; i > 0; --i) { + out[i - 1] = alphabet[in % 26]; + in /= 26; + } +} + +static void +decfrp(char *s) +{ + char *p; + unsigned char a, b; + + a = b = 0; + + for (p = s + strlen(s) - 1; p >= s; --p) { + a = (*p - 'a' + b) % 26; + a = (a + 'a') & 0xFF; + b += a; + b %= 26; + *p = a; + } +} + +/* Implementation of xorshift* without retaining seed state. */ +static uint64_t +rnd(uint64_t seed) +{ + seed ^= seed >> 12; + seed ^= seed << 25; + seed ^= seed >> 27; + return seed * 0x2545F4914F6CDD1D; +} + +static unsigned int +mkver(unsigned int lictype, unsigned int major, unsigned int minor) +{ + return ((lictype << 12) | (major * 10 + minor)); +} + +static void +mksnak(bool has_snakext, uint16_t product_id, uint16_t major, uint16_t minor, + char *serno, char *actkey) +{ + const char *cksum; + uint64_t serial; + unsigned int version = mkver((has_snakext ? 3 : 2), major, minor); + char merged[18]; + + memset(serno, 0, 10); + memset(actkey, 0, 9); +#ifdef DBG + printf("has_snakext: %d, major: %u, minor: %u, version: %u\n", + !!has_snakext, major, minor, version); +#endif + + /* bitmask to ensure at most six digits */ + serial = rnd((uintptr_t)serno * time(NULL)) & 0xEFFFF; + snprintf(serno, 10, "SCO%06" PRIu64, serial); + + strbn(actkey, product_id); + strbn(actkey + 3, version); + snprintf(merged, sizeof(merged), "%s%s", serno, actkey); + + cksum = mnsnc(merged); + actkey[6] = cksum[0]; + actkey[7] = cksum[1]; + + decfrp(actkey); +} + +static uint16_t +strtou16lim(const char *in, unsigned long limit) +{ + char *end; + unsigned long i; + + if (limit > UINT16_MAX) + die("internal: limit > UINT16_MAX"); + + i = strtoul(in, &end, 10); + if (*in == '\0' || *end != '\0') + die("%s not a number", in); + if (i > limit) + die("%s out of range (max %"PRIu16")", in, limit); + + return i; +} + +int +gen_snak(int argc, char *argv[]) +{ + char *snakext = NULL; + uint16_t product_id, major, minor; + char serno[10], actkey[9], snakextmd[7]; + + if (argc < 3) { + usage(true); + return EXIT_FAILURE; + } + + /* "zzz" is the maximum possible product ID encoded value. + * This decodes to a value of 17575. + */ + product_id = strtou16lim(argv[0], 17575); + + /* License type (whether snakext is to be read), version major and + * version minor share an integer, max encoded as "zzz". + * The license type is shifted up by 12, leaving 0xFFF (4095) for the + * version major and minor. + * Of that, the version major is all the upper digits, and the minor is + * the bottom digit (i.e. version/10 => major, version%10 => minor). + * + * Without doing too much checking, the maximum major version is 409 and + * the maximum minor version is 9. + */ + major = strtou16lim(argv[1], 409); + minor = strtou16lim(argv[2], 9); + + if (argc >= 4) + snakext = argv[3]; + + mksnak(snakext != NULL, product_id, major, minor, serno, actkey); + printf("Serial number: %s\n" + "Activation key: %s\n", serno, actkey); + + if (snakext != NULL) { + mdsnakext(serno, actkey, snakext, snakextmd); + printf("License data: %s;m%s\n", snakext, snakextmd); + } + + return EXIT_SUCCESS; +} + diff --git a/snakext.c b/snakext.c new file mode 100644 index 0000000..9ffc353 --- /dev/null +++ b/snakext.c @@ -0,0 +1,23 @@ +#include +#include +#include +#include +#include +#include + +#include "md5.h" +#include "h.h" + +static const char *extSecret = "\x5e\x4f\xbe\x45\x5e\x4c\x8d\x40\x9f\xeb\x26\x5e\x4f\xbe\x45\x5e\x4c\x3d\x30\x7c"; + +void +mdsnakext(const char *serno, const char *actkey, const char *snakext, + char snakextmd[static 7]) +{ + char *canon; + + canon = BSCanon(snakext); + extmd(snakextmd, 4, extSecret, serno, actkey, canon); + free(canon); +} +