/* CyberLockPick * Steve Pordon * 2014.07.08 * https://neutronstar.org/CyberLockPick/ * All versions of this program are released to the PUBLIC DOMAIN upon publication. * Copy that floppy. * * genhash function courtesy of Ross Williams (http://www.ross.net) * * CyberLock and CyberAudit were trademarks of Videx, Inc. * (https://www.videx.com/), now CyberLock, Inc. (https://www.cyberlock.com) * * This program consolidates my previous CyberLock work from separate * programs into a comprehensive toolkit of password and file utilities. * The program allows end users to transform passwords into the final * hash used by Videx CyberLock (CRC16/ARC hash with rotates); convert * a wordlist or stdin stream to these hashes line by line; recover password * hashes from database values for use with rainbow tables; generate database * values for password injection; generate or validate comm_pw database values * from passwords and password files; scan dumped EEPROM files from keys and * locks; and brute-force collisions for a given hash. * * Requires crcmodel.c and crcmodel.h to handle CRC16 hashing * (available in this paper): * https://web.archive.org/web/20141222152245/http://www.ross.net/crc/download/crc_v3.txt * * Compile with: * gcc -o clpick clpick.c crcmodel.c -lpthread -std=c11 -D_POSIX_C_SOURCE=199309L -O3 * (add -D_DARWIN_C_SOURCE to compiler flags on OSX) * * Tested build and functionality on x86/x64 Linux, x64 Windows 11 (MinGW), OSX Monterey, * Android, Steam Deck (Arch Linux) * * CHANGELOG: * v1.9 (2024.02.24) * Implemented a lookup table for CRC-16/ARC (somehow slower in most of my * testing, but you can switch between it and the Williams algorithm now). * Added a check for pair patterns in hashes for cracking. Additional code * cleanup and more comments. I've also modified crcmodel.c and .h slightly * to add support for stdbool.h, which avoids compilation errors on some * platforms. Fixed a longstanding Windows bug in the EEPROM scanner which * returned bad values on very specific EEPROM dumps. * * v1.8.1 (2024.02.22) * BUGFIX - v1.8 didn't generate password candidates quite right (boundary * condition on changing password candidate length). This version fixes that. * Also cleaned up some code, made minor optimizations, added a benchmark * switch to tune various threaded settings, and added the second of several * planned hash pattern recognition routines that will make finding collisions * slightly easier in some cases. * * v1.8 (2024.02.20) * Implemented multithreading for much better cracking performance (I hit * 67.5 MH/s on the i7 11800H with 45 (len^4 buffers) / 2000 (len^3 buffers) * threads, which translates to around 15 minutes to get through the full * alphanumeric keyspace of 6-character passwords). This is ~40 times * faster than the (single-threaded) older algorithm. Other improvements * include some code cleanup, readability work, and memory leak testing * with valgrind. NOTE: due to random OS thread scheduling, collisions may * not be deterministic. Multiple runs of the brute-force function may give * different collision results, stopping after the first one is found. As * the goal of cracking these hashes is to find any collision, this turns * out to be a feature rather than a bug. The first collision found is the * fastest. The default number of threads is 8, which can be overridden with * e.g. --threads 16 or whatever you want. High threadcounts will likely peg * your CPU at close to 100%, so monitor that. I also added a straight CRC-16 * mode, because eh why not. v1.8 has been renamed from "CyberLock Suite." * * The new permutation algorithm precomputes a given number of "last-letter" * endings and then generates the rest from there. I modified it slightly to * let the end user choose between generating len^3-word buffers / thread * (three precomputed last letters, or --3pre on the commandline, this is the * default) or generating len^4-word buffers / thread (4 precomputed, --4pre * on the commandline). Smaller buffers allow you to run more threads, and * larger buffers allow fewer threads but more memory. If you have plenty of * memory to spare, try the larger buffers. This is essentially a CPU/RAM * tradeoff; loose benchmarking shows large buffers work well with smaller * alphabets, but smaller buffers were slightly faster over a 6-char * alphanumeric keyspace (my standard benchmark) because the higher * threadcount pushed the CPU harder. More optimizations are coming. * * Known issues in this release (and for the foreseeable future): * incorporating pthread.h was a major undertaking; as a result, the * codebase is no longer as portable as it could be. Your system should * support the C11 standard at a minimum. * * v1.7.2 (2024.02.09) * BUGFIX. I didn't test the last version as much as I should have, * and wound up introducing a serious logic error that segfaulted on * attempts to hash a password (the most basic function here!) * * v1.7.1 (2024.02.08) * Added support for commandline switches to change the brute-force * cracking character set. * * v1.7 (2024.02.07) * Completely overhauled brute-forcing code using a faster algorithm * from https://github.com/Jsdemonsim/Stackoverflow/tree/master/alphabet * (found on stackoverflow from user JS1). The hash cracker also now * attempts to auto-crack passwords with repeating characters and will * move into brute-force methods if a repeating hash doesn't correspond * to a repeating character password. Note that this may be mathematically * impossible for any hash other than all zeros. Other minor cracking * improvements. Brute-forcing is now about an order of magnitude faster * than it was in v1.5.1. * * v1.6.1 (2024.02.04) * Modified brute-forcing very slightly to allow users to check for non- * repeating passwords that have a repeating hash (unpublished, for my own * mathematical amusement mostly). * * v1.6 (2024.02.04) * Implemented a much faster brute-forcing method after threatening * to do so for years. Now, if the first hash of a candidate password * doesn't match the first chunk of the hash to be cracked, the * program will skip to the next candidate rather than finish * generating the other hashes. Very informal testing shows a 3-4x * speed increase and something like 5.2 million comparisons per * second on a middling Intel i7-11800H CPU. While this is still * pathetic compared to hashcat, it still means I can theoretically * check every 6-character alphanumeric password in about 3 hours on * my system. * * v1.5.1 (2024.02.03) * BUGFIX release. Added additional sanity checking on switches * to avoid segfaulting on --stdin switch in some scenarios, * fixed repeated print from brute_impl when the password was * a repeating character * * v1.5 (2024.02.02) * Implemented stdin mode for external program redirecting, * a very slightly faster password rotating method, and the * ability to print spaces in passwords as-is without the visual * glyph, for direct output to files or additional commandline * manipulation * * v1.4.1 (2023.10.19) * The code is identical but I've changed a friend's deadname to * their new preferred one in comments <3 * * v1.4 (2020.11.04) * Implementing basic key/lock EEPROM dump scanning and slight * cracking optimizations * * v1.3 (2020.11.03) * Being able to generate comm_pw with the password hash (but not * the original password) is useful too, so I added that ability. * This only works well with single hashes. Go ahead and throw a * wordlist at the -cph switch, it won't be helpful * * v1.2 (2020.10.31) * comm_pw is a checksum in the key's EEPROM. It's stored as the * unsigned lower 4 bytes, so I've added the hex output to the * function for direct EEPROM manipulation * * v1.1 (2020.10.23) * Updating functionality for hashes pulled out of lock memory, * removing collision function, and improving iv_val I/O handling * to allow direct copy/paste from database to commandline and * vice-versa * * v1.01 (2014.12.05) * slight optimizations * * v1.0 (2014.09.20) * Initial public release (LockCon 2014, Sneek, Netherlands) * * v0.15 - added shitty, slow hash cracking functionality (2014.08.30) - * public release candidate 3 * v0.14 - code cleanup & version consistency rename (2014.08.21) - public * release candidate 2 * v0.13 - very minor code cleanup (2014.08.16) - public release candidate * v0.12 - bug fixes and optimization (2014.07.31) * v0.11 - further code cleanup (2014.07.27) * v0.1 - code cleanup and optimization (2014.07.19) * v0.01 - initial limited release (2014.07.08) * * Technical information on the CyberLock password hashing algorithm: * I'm not going into detail on what I've presented (see the video at * https://archive.org/details/LockCon2014 and the related .pptx * at https://neutronstar.org/tmp/BSD2015.pptx), but I will explain the * extremely bad algorithm mathematically here, which I haven't really * done anywhere else yet: * * The core of the CyberLock algorithm is a very simple CRC-16 hash. Since * CyberLock system passwords can be between 6 and 32 characters long * using any printable character, the total password keyspace is * 95^32 - 95^5 - 95^4 - 95^3 - 95^2 - 95. However, since 95^32 is so * absolutely enormous, we can say the keyspace is ~95^32 and call it a * day. CRC-16 hashes comprise exactly two bytes, each byte being 0x00-0xFF, * for a total hashspace of 256^2 (65,536). You can see why this is a problem. * Videx clearly wanted manageable values they could store in a database * without storing the actual passwords, so hashes make sense. Collapsing * 95^32 down to 256^2 is a bad idea, because now the hashspace is your * actual keyspace when it comes to brute-forcing passwords, due to the * extreme number of hash collisions. * * So Videx came up with a somewhat clever way to transform the passwords * and increase the hashspace. Clever in the sense that it increased their * keyspace in a fixed-size database form, terrible in the sense that it's * not real crypto and their various obfuscation tactics didn't help at all. * * To increase the hashspace, Videx calculates a total of four CRC-16 * hashes, all based on the system password. The final 8-byte hash results * in a total hashspace of 256^8 (about 18.4 quintillion). Still a huge * reduction of the initial keyspace, but brute-forcing every possibility * is still unlikely. The way each hash is calculated is by rotating * the system password left one character (moving the first char to the end), * finding the CRC-16 of the new word, and repeating. So if the system password * is "password" then the other three hashes are those of "asswordp", * "sswordpa", and "swordpas". Mathematically, the final hash is * generated by: * hash_f = ((crc16(pass_0) << 48) + (crc16(pass_1) << 32) + (crc16(pass_3) << 16) + (crc16(pass_3)) * (where "<<" is a bitwise shift operator). This hash is what opens locks. * Bytes in the hash are XOR-ed with bytes in an internal lookup table * according to a simple algorithm and this modified hash is what's stored * in the database, to keep the real hash from prying eyes. I recreated the * table and algorithm in this tool. * * Videx accidentally (I hope) reduced their hashspace by about 38%, though. * In their implementation of the CRC-16/ARC algorithm, any byte beginning with * an initial 0x0 becomes a byte of 0x00 in the final hash (note that when * I refer to "final hash," I mean the key/lock hash and not the mangled one * stored in the database). Discovering this bug may have been the hardest * part of my research, as it resulted from pure data manipulation and * comparisons to actual hashes stored in the keys. The bug drops the available * keyspace down to 241^8, so they burned about 7 quintillion possible * hashes with this one weird trick (of fucking up the algorithm). You can * see the bug in play with the password "password" (and in fact, that's how * I discovered it): Plugging values into the algorithm should give you * hash_f = crc16(password)<<48+crc16(asswordp)<<32+crc16(sswordpa)<<16+crc16(swordpas) * = 0xC877000000000000 + 0xEC00000000 + 0xF7650000 + FC07 * = 0xC87700ECF765FC07 * You can verify this with any CRC-16 generator, just remember not to pass the * newlines for each part. The last two bytes are where the Videx bug come * into play. The proper "CRC-16/rot/VidexBug" hash of "password", stored on * the key EEPROM, is 0xC87700ECF765FC00. * * At any rate, hash collisions are generally easy to find. The keys and locks * don't care about the system password, only the hash. Collisions come into * play if you want to log into a system and you don't have the password, * only the hash (dumped from any system key or the database file). The software * also doesn't care about the password, as it happens. It only cares that * the password you enter has a final hash that collides with the original * system password's final hash. * * I'll leave you with this. Every single CyberLock installation on the original * CyberAudit software has the same first database hash. Operating on the assumption * that a neighboring field is indicating a "creation date epoch" of 0, this hash * is 0xC5BEE641CFC7B700 after transforming it back into the final hash used * by keys and locks. My challenge for you: find a password or passphrase that * collides with this hash. What might it unlock? * * Acknowledgements: * Rifleman (major rewriting help and programming best practices) * Delta Regeer (helping with logic issues and code cleanup) * Ross N. Williams (CRC-16 whitepaper and code to facilitate hashing) * Jason Scott and the archive.org team (that CRC-16 paper's original site is gone) Jsdemonsim (JS1 on stackoverflow) (faster new password-generating algorithm) * Vines on stackoverflow (code logic for original brute-forcing algorithm) * * Known issues: * Windows chokes on unicode and refuses to read the rest of the file. * Suggested workaround: strip unicode from your wordlists. I'm pretty * sure CA and CA-Web can't take unicode passwords anyway. * * Linux gcc throws compiler warnings regarding the fread lines in * eepromscan(). It looks like I need to explicitly set those as vars * (e.g., char magic[2] = fread(&magic [...]), but I don't feel like * debugging all that right now and the function works regardless. (TODO) * * The default threadcount of 8 might be too many in some resource-limited * environments. My linux testing box is my webserver, with 4GB RAM (further * limited by userspace restrictions), which segfaults on exit without * --threads 2 on the commandline. This can likely be fixed with better error * and limit checking in the program, but the vast majority of users should be * able to run the default 8 threads without issue, so this isn't a pressing * concern. (TODO) * * OSX displays the "spaces" visual glyph as a regular question mark. I can either * replace it with a poop emoji or just force --printspaces as default in the future * (TODO) */ #include #include #include #include #include #include #include #include #include #include "crcmodel.h" #define MAXLEN 32 // 32 characters is the maximum password length in a CyberLock system #define NBBY 8 // Number of Bits per BYte (for readability later when I start bitshifting) //#define DEBUG // uncomment beginning of line to print debug information at runtime /* functions: */ void* compare_hash(void *arg); int hashpart(char *pass); int genhash(char *pass); int workaround(int crc16); int getseconds(char *mdy, char *hms, char *ampm, int64_t *recstamp); uint64_t calc(uint64_t input); int pwhash(int byte, int *y, int *x); int rol(int byte, int n, int bits); int ror(int byte, int n, int bits); int rcl(int byte, int *cf, int n); int rcr(int byte, int *cf, int n); int parity(int byte); int nofile(char *fname); int pwlen(); int timeformat(); int usage(char *fname); /* global vars: */ atomic_int buflen, passlen, crc0, crc1, crc2, crc3, found=0; atomic_uintmax_t ccount=0; // used for debugging, the printout of this can show whether the full keyspace was generated uint64_t hash, rhash, comm_pw; char inputpass[33], mdyarray[11], hmsarray[9], ampm[3]; int pf; // parity flag for commpw calculations int sw_cp=0,sw_cph=0,sw_h=0,sw_p=0,sw_r=0,sw_ph=0,sw_cr=0,sw_crc16=0,minlen=6,sw_nobrute=0, sw_table=0; // command-line switch flags int sw_stream=0,sw_spaces=0,sw_e, sw_nrp=0,charsetflag=0,sw_4pre=0,sw_bench=0,threadcount=8; // more command-line switch flags pthread_mutex_t threadlock = PTHREAD_MUTEX_INITIALIZER; int64_t hashpass(char *word) { size_t passlen; int i, rotator, filemode=0; uint64_t hash0, hash1, hash2, hash3; char line[35], pass[35], pass1[35], pass2[35], pass3[35]; FILE *fp; if(!sw_cp && !sw_cph && !sw_cr) { if(!sw_stream && !sw_crc16) { fp = fopen(word, "r"); if(fp != NULL) filemode = 1; } else fp = stdin; } if((filemode || sw_stream) && (!sw_cp && !sw_cph && !sw_cr && !sw_crc16)) { // file or stdin mode (multiple passwords) while(fgets(line, sizeof(line), fp) != NULL) { passlen = strlen(line); if(line[passlen-1] == '\n') // ignore newlines for length calculation passlen -= 1; if(passlen < minlen) continue; // if word isn't valid, skip to next line if(passlen > MAXLEN) { while((fgetc(fp) != '\n') && (fgetc(fp) != 0x0a)) {} continue; // super inefficient way to discard long lines } // (otherwise 32-char blocks are discarded and // the remainder of the line is processed as if // it were a new line) strncpy(pass, line, sizeof(pass)); pass[passlen] = '\0'; // overwrites \n to ensure accurate CRC-16. strncpy(inputpass, pass, sizeof(inputpass)); inputpass[passlen] = '\0'; /* This rotator code is reused below and in the hash cracking function slowcrack(), which * definitely means I should move this block into its own function (TODO) */ for(rotator=0;rotator<=passlen;rotator++) // password chars rotate left 3x for hash values { pass1[rotator] = pass[(rotator+1) % (passlen)]; pass2[rotator] = pass[(rotator+2) % (passlen)]; pass3[rotator] = pass[(rotator+3) % (passlen)]; } pass1[passlen] = '\0'; // if pass="password", pass1="asswordp" pass2[passlen] = '\0'; // similarly, pass2="sswordpa" pass3[passlen] = '\0'; // similarly, pass3="swordpas" hash0 = hashpart(pass); hash1 = hashpart(pass1); hash2 = hashpart(pass2); hash3 = hashpart(pass3); // doing it this way speeds up hash comparisons in slowcrack() hash = (hash0 << (NBBY*6)) + (hash1 << (NBBY*4)) + (hash2 << (NBBY*2))+ hash3; if(!sw_spaces) { for(i=0; i MAXLEN) { pwlen(); return -1; } strncpy(pass, currentpass, sizeof(pass)); pass[sizeof(pass)-1] = '\0'; for(rotator=0;rotator<=passlen;rotator++) { pass1[rotator] = pass[(rotator+1) % (passlen)]; pass2[rotator] = pass[(rotator+2) % (passlen)]; pass3[rotator] = pass[(rotator+3) % (passlen)]; } pass1[passlen] = '\0'; pass2[passlen] = '\0'; pass3[passlen] = '\0'; hash0 = hashpart(pass); hash1 = hashpart(pass1); hash2 = hashpart(pass2); hash3 = hashpart(pass3); hash = (hash0 << (NBBY*6)) + (hash1 << (NBBY*4)) + (hash2 << (NBBY*2))+ hash3; if(!sw_spaces) { for(i=0; i MAXLEN) { pwlen(); return -1; } } else if((sw_r) && (argc == 5)) wflag = 1; // CA-Web recovery mode (argc>5 indicates a legacy CA timestamp) if(!wflag) // legacy CA mode--ls_id_byte calculation sucks { strncpy(mdyarray, arg2, sizeof(mdyarray)); // set up timestamp arrays for ls_id_byte mdyarray[sizeof(mdyarray)-1] = '\0'; strncpy(hmsarray, arg3, sizeof(hmsarray)); hmsarray[sizeof(hmsarray)-1] = '\0'; strncpy(ampm, arg4, sizeof(ampm)); ampm[sizeof(ampm)-1] = '\0'; ls_id_byte = getseconds(mdyarray, hmsarray, ampm, &recstamp); if(ls_id_byte == -1) // getseconds() returns -1 on some bad formats { timeformat(); return -1; } if(sw_r) // Recover password hash from DB values { /* This is seriously stupid. Converting the scientific notation used by the DB should have been a really simple single line atof command. But I was getting inconsistent results and suspect it's because there were too many decimal places. I tried casting to various things and multiplying at various stages (because Videx's VB6 databases don't actually support the entire number, so they fudged the decimal location), but ultimately the best way to ensure that the value is correctly interpreted is to read the numbers as a string and ignore the decimal and exponent information. Believe me, I hate myself for writing this as much as you're about to hate me for reading it. */ char convertivval[26]; // Raw copypasta from the DB iv_val field strncpy(convertivval, arg1, sizeof(convertivval)); // Notice the lack of error handling. convertivval[sizeof(convertivval)-1] = '\0'; // YOU handle it. char ivstring[21]; // This is going to be the unsigned number without the "." or "E+14" int negflag = 1; // (actually it's just a string. We'll worry about the sign later) int j = 1; int loopstart; int expflag = 0; // Set when the loop sees "E" in the input string if(convertivval[0] == '-') // This is a negative value { ivstring[0] = convertivval[1]; loopstart = 3; // Store the whole part of the number and set up to read the mantissa negflag = -1; } else // Not a negative number { ivstring[0] = convertivval[0]; loopstart = 2; } for(i=loopstart; i0; i-=2) // this is where the XOR matrix is generated { xorarray[i] = (lookuptable[256 * j + ls_id_byte])+48; // +48 to store ASCII char values xorarray[i-1] = '0'; j++; if(((sw_p) || (sw_ph)) && (inarray[i-1] == '0')) // let's emulate the Videx CRC16 bug! xorarray[i] = '0'; } xorarray[sizeof(xorarray)-1] = '\0'; if(sw_r) // recover or Web-recover mode { for(i=0; i<16; i=i+2) if(inarray[i] == '0') xorarray[i+1] = inarray[i+1]; /* When a byte starts with 0, Videx's CRC16 implementation flattens the * entire byte to 0x00. This ensures more collisions, so I'm assuming * it's a bug and that they didn't do this on purpose. */ printf("CRC16-rot hash: "); printf("%016" PRIX64, \ (uint64_t)((strtoull(inarray, NULL, 16))^(strtoull(xorarray, NULL, 16)))); } else // password injection mode { long double iv_val; int iv_1, iv_2; convert = (int64_t) (hash ^ strtoull(xorarray, NULL, 16)); iv_val = (long double)convert/1E18; iv_1 = convert & 0xFFFFFFFF; // LSB iv_2 = convert >> (NBBY*4); // MSB printf("\niv_val to paste into database: %.18LfE+14", iv_val); printf("\n\nor for CA-Web:\nHIV_VAL1:\t%d\nHIV_VAL2:\t%d\n", iv_1, iv_2); printf("HIV_RECSTAMP:\t%" PRId64 "\n", recstamp); } printf("\n"); return 0; } int commpw(int argc, char *word) // Key updates generate this DB value; it's not really injectable. { // but this function will let you validate what's in the DB. size_t passlen; int i, filemode=0; FILE *fp; if(!sw_stream) { fp = fopen(word, "r"); if(fp != NULL) filemode = 1; } else fp = stdin; // stdin mode for working with redirected output from other programs if((filemode) || (sw_stream)) // file or stdin mode { char line[35]; while(fgets(line, sizeof(line), fp) != NULL) { passlen = strlen(line); if(line[passlen-1] == '\n') // kill newlines { line[passlen-1] = '\0'; passlen -= 1; } if(!sw_cph) // working with passwords, not password hashes { if(passlen < minlen) continue; //skip line on invalid input if(passlen > MAXLEN) { while((fgetc(fp) != '\n') && (fgetc(fp) != 0x0a)) {} continue; // super inefficient way to discard long lines } hash = hashpass(line); } else // working with password hashes { int invalidbyte=0; if(passlen != 16) continue; for(i=0;i MAXLEN) { pwlen(); return -1; } hash = hashpass(word); } else hash = (int64_t)strtoull(word, NULL, 16); calc(hash); printf("comm_pw: %" PRId64 " (0x%" PRIX64 ")\n", comm_pw, comm_pw&0xFFFFFFFF); } return 0; } int eepromscan(char *fname) { char stash[9]; char magic[2]; // The dump's magic number indicates the type. I'll arbitrarily int type = 0; // define 0 as CyberKey classic, 1 as lock, 2 as Cellular CyberKey. int i; FILE *fp = fopen(fname, "rb"); if(!fp) { printf("Error: file %s not found\n\n", fname); return -1; } fread(&magic, 1, 2, fp); printf("\nType:\t\t "); if((magic[0] == 0x43) && (magic[1] == 0x06)) { type = 1; printf("Lock "); } else if((magic[0] == 0x69) && (magic[1] == 0x18)) { type = 2; printf(" Cellular CyberKey "); } else if((magic[0] != 0x66) || (magic[1] != 0x08)) printf(" Unknown - using CyberKey Classic offsets "); else printf(" CyberKey Classic "); printf("(Magic number: %02X %02X)\n", magic[0], magic[1]); fseek(fp, 3, SEEK_SET); fread(&stash, 1, 4, fp); if((type == 0) || (type == 2)) printf("Key ID:\t\t "); else printf("Lock ID:\t "); for(i=0; i<4; i++) printf("%02X", stash[i] & 0xFF); printf("\n"); if(type != 1) // this section needs more debugging with more data when I get it { fseek(fp, 41, SEEK_SET); // byte 41 is a flag for silencing the key's speaker fread(&stash, 1, 1, fp); printf("Speaker silenced: "); if((stash[0]&0xff) == 1) printf("yes"); else printf("no"); fseek(fp, 312, SEEK_SET); fread(&stash, 1, 4, fp); printf("\nComm_pw:\t "); for(i=0; i<4; i++) printf("%02X", stash[i] & 0xFF); printf(" (Comm_pw for the first password hash shown below)"); if(((stash[0]&0xFF) == 0x1A) && ((stash[1]&0xFF) == 0x78) && ((stash[2]&0xFF) == 0xD2) && ((stash[3]&0xFF) == 0x97)) { printf("\n (this is a passwordless evaluation system. The hash on the next line is "); printf("whatever was last written to the key)"); } printf("\nPassword hash(es): "); stash[8] = 0x00; // EEPROM may have multiple hashes. This makes sure we read at least one while((stash[8]&0xFF) != 0xFF) { fread(&stash, 1, 9, fp); for(i=0; i<8; i++) printf("%02X", stash[i] & 0xFF); printf(" "); fseek(fp, -1, SEEK_CUR); // hashes are 8 bytes but we read in 9, so correct for the next hash } printf("\nStored lock IDs: "); fseek(fp, 2973, SEEK_SET); fread(&stash, 1, 4, fp); for(i=0; i<4; i++) printf("%02X", stash[i] & 0xFF); } printf("\n\n"); return 0; } int slowcrack(char *hashstr, char *word) { int i=0, wlmode=0; uint64_t hash = (uint64_t)strtoull(hashstr, NULL, 16); FILE *fp; if((word != NULL)) { if((fp = fopen(word, "r")) != NULL) wlmode = 1; } if(wlmode) // wordlist mode, extremely recommended { uint64_t comp; char line[256]; size_t passlen; while(fgets(line, sizeof(line), fp) != NULL) { passlen = strlen(line); if(line[passlen-1] == '\n') // kill newlines { line[passlen-1] = '\0'; passlen -= 1; } if(passlen < minlen) continue; //skip line on invalid input if(passlen > MAXLEN) { while((fgetc(fp) != '\n') && (fgetc(fp) != 0x0a)) {} continue; // super inefficient way to discard long lines } comp = hashpass(line); if(comp == hash) { printf("Password/collision found: \"%s\"\n", line); return 0; // This may not be the original password, but it sure does collide with it } } } else // Brute-force mode. There are 56.8 billion permutations of 6 alphanumeric characters. Good luck! { struct timespec start, end; uint64_t comp; double timer, totaltime=0; bool cycled = FALSE; char alphabet[96], candidate[33]; int *letters=malloc(MAXLEN * sizeof(int)); int let0, let1, let2, let3, alphalen, i_pos, j_pos; crc0 = (hash >> (NBBY*6)); // these vars break the given hash into discrete CRC-16 blocks crc1 = ((hash & 0xFFFF00000000) >> (NBBY*4)); crc2 = ((hash & 0xFFFF0000) >> (NBBY*2)); crc3 = (hash & 0xFFFF); static const char fullcharset[] = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "~!@#$%^&*()_+`-=[]{}|;:\\'\",./<>? "; #ifdef DEBUG printf("DEBUG: crc0=%04X crc1=%04X crc2=%04X crc3=%04X\n", crc0, crc1, crc2, crc3); #endif if((crc0 == crc1) && (crc0 == crc2) && (crc0 == crc3) && !sw_nrp) { /* same hash after every rotation implies a single, repeating character for the password * sw_nrp indicates user doesn't want to look for repeating char passwords. Because * single-char brute-forcing only takes a few ms, we'll use the entire char set as-is: */ memset(candidate, '\0', sizeof(candidate)); // init candidate buffer for(passlen = 6; passlen<= 32; passlen++) // allowable password lengths { printf("Trying %d-character repeating passwords...\n", passlen); for(i = 0; i < 95; i++) { memset(candidate, fullcharset[i], passlen); comp = hashpass(candidate); if((comp == hash) && (found == 0)) { found = 1; printf("Password/collision found: %016" PRIX64 " \"%s\"\n", comp, candidate); return found; } } } printf("This is a repeating hash but not a repeating-char password. Checking known patterns...\n"); } if((crc0 == crc2) && (crc1 == crc3)) // heavily implies a repeating pair pattern (e.g., 121212) { printf("Potential hash pattern found. Testing repeating pairs...\n"); alphalen=95; // I keep re-using this chunk of code, so it really needs its own function (TODO) char *pairs = malloc((MAXLEN + 1) * alphalen * alphalen * alphalen); if(pairs == NULL) { fprintf(stderr, "Not enough memory.\n"); exit(1); } buflen = 2 * alphalen * alphalen * alphalen; // holds every permutation of 2 chars memset(pairs, fullcharset[0],buflen); // init to first char memset(candidate, '\0', 33); let0 = 0; let1 = 0; for (i_pos=0;i_pos= threadcount) { for(threadloop=0; threadloop= alphalen) // Handle wraparound. letters[i_pos] = 0; c = alphabet[letters[i_pos]]; // Set this letter in the proper places in the buffer. for (j_pos=i_pos;j_pos= threadcount) { for(threadloop=0; threadloop> 8) ^ crctable[(crc ^ *pass++) & 0xFF]; return crc; } else // Ross Williams CRC-16/ARC algorithm { int i; cm_t cm; p_cm_t p_cm = &cm; p_cm->cm_width = 16; // These are the right settings for CRC16/ARC p_cm->cm_poly = 0x8005L; // according to Ross Williams p_cm->cm_init = 0L; p_cm->cm_refin = TRUE; p_cm->cm_refot = TRUE; p_cm->cm_xorot = 0L; cm_ini(p_cm); for(i=0; pass[i] != '\0'; i++) cm_nxt(p_cm, pass[i]); return cm_crc(p_cm); } } int workaround(int crc16) // called from hashpart() { int mask = 0xFFFF; if((crc16 & 0xF000) == 0) // workaround for the Videx bug that mask &= 0xF0FF; // turns CRC16 bytes starting with 0x0 if((crc16 & 0xF0) == 0) // into nulls. i.e., proper CRC16 of mask &= 0xFFF0; // "FB04" becomes "FB00" in Videx's // implementation. This emulates that. return mask; } /* Apparently there are built-in functions that check standard datestamps * already (in time.h, which I wound up using anyway), but since I spent * so much miserable time reinventing that wheel, here it is: */ int getseconds(char *mdy, char *hms, char *ampm, int64_t *recstamp) // called from recover() { int leap = 0; // for counting the extra day in leap years int y, mo, d, h, mi, i; int64_t s; char month[3], day[3], year[5], hour[3], minute[3], second[3]; if(mdyarray[3] == '/') // m/d/yyyy format { month[0] = mdyarray[0]; month[sizeof(month)-1] = '\0'; day[0] = mdyarray[2]; day[sizeof(day)-1] = '\0'; for(i=0; i<4; i++) year[i] = mdyarray[i+4]; year[sizeof(year)-1] = '\0'; } else if(mdyarray[4] == '/') { if(mdyarray[1] == '/') // m/dd/yyyy format { month[0] = mdyarray[0]; month[sizeof(month)-1] = '\0'; day[0] = mdyarray[2]; day[1] = mdyarray[3]; day[sizeof(day)-1] = '\0'; for(i=0; i<4; i++) year[i] = mdyarray[i+5]; year[sizeof(year)-1] = '\0'; } else // mm/d/yyyy format { month[0] = mdyarray[0]; month[1] = mdyarray[1]; month[sizeof(month)-1] = '\0'; day[0] = mdyarray[3]; day[sizeof(day)-1] = '\0'; for(i=0; i<4; i++) year[i] = mdyarray[i+5]; year[sizeof(year)-1] = '\0'; } } else if(mdyarray[5] == '/') // mm/dd/yyyy format { month[0] = mdyarray[0]; month[1] = mdyarray[1]; month[sizeof(month)-1] = '\0'; day[0] = mdyarray[3]; day[1] = mdyarray[4]; day[sizeof(day)-1] = '\0'; for(i=0; i<4; i++) year[i] = mdyarray[i+6]; year[sizeof(year)-1] = '\0'; } else // bad format return -1; if(hmsarray[1] == ':') // h:mm:ss format { for(i=0; i<2; i++) { hour[i] = hmsarray[i]; minute[i] = hmsarray[i+2]; second[i] = hmsarray[i+5]; } hour[sizeof(hour)-1] = '\0'; } else if(hmsarray[2] == ':') // hh:mm:ss format { for(i=0; i<2; i++) { hour[i] = hmsarray[i]; minute[i] = hmsarray[i+3]; second[i] = hmsarray[i+6]; } hour[sizeof(hour)-1] = '\0'; } else // bad format return -1; minute[sizeof(minute)-1] = '\0'; second[sizeof(second)-1] = '\0'; y = atoi(year); mo = atoi(month); d = atoi(day); h = atoi(hour); mi = atoi(minute); s = atoi(second); if(s < 0 || s > 59 || mi < 0 || mi > 59 || h < 0 || h > 12) return -1; if((h == 12) && (!strcmp(ampm, "AM"))) h = 0; if((!strcmp(ampm, "PM")) && (h != 12)) // 24-hour clock conversion h += 12; if(y > 1996) leap++; else if(y == 1996) if((mo > 2) || (mo == 2 && d > 28)) leap++; if((y % 100 !=0) && (y > 2000)) leap = leap + ((y - 1996) / 4) - 1; // -1 because we already counted 1996 if((y > 2003) && ((y - 1996) % 4 == 0) && (mo < 3)) if((mo == 2 && d < 29) || (mo == 1)) leap--; if(d < 1 || d > 31 || mo < 1 || mo > 12 || y < 1996 || y > 2064) return -1; if(((strcmp(ampm, "AM"))) && ((strcmp(ampm, "PM")))) // i.e. strcmp(...) != 0 return -1; int err = 0; // check bound errors in switch: switch (mo) // add all days in previous months to current days...very { // tedious, but it works. Expanded notation for debugging and readability. case 1 : break; // when mo = 1, days = d case 2 : if(d > 29) err = -1; if((y % 4 != 0) && (d > 28)) err = -1; else if((y % 100 == 0) && (d > 28)) err = -1; d += 31; break; case 3 : d += 31+28; // "28" because leap days are added later break; case 4 : if(d > 30) err = -1; d += 31+28+31; break; case 5 : d += 31+28+31+30; break; case 6 : if(d > 30) err = -1; d += 31+28+31+30+31; break; case 7 : d += 31+28+31+30+31+30; break; case 8 : d += 31+28+31+30+31+30+31; break; case 9 : if(d > 30) err = -1; d += 31+28+31+30+31+30+31+31; break; case 10 : d += 31+28+31+30+31+30+31+31+30; break; case 11 : if(d > 30) err = -1; d += 31+28+31+30+31+30+31+31+30+31; break; case 12 : d += 31+28+31+30+31+30+31+31+30+31+30; } if(err == -1) return -1; d = d + (y - 1996) * 365 + leap; s = s + (mi * 60) + (h * 3600) + (d * 86400); *recstamp = s; // need full recstamp for CA-Web tables s &= 0xFFFF; s &= 0xFF; // we only care about the LSB for CA legacy return s; } uint64_t calc(uint64_t input) // called from commpw() { const int cybr[] = {0x43,0x59,0x42,0x45,0x52,0x4C,0x4F,0x43,0x4B}; // "CYBERLOCK" int x=0, y=0, s=0, i, byte; int cf = 0; // carry flag int feed = 0; // feedback for loops for(i=0; i<8; i++) // chop up input into individual hex bytes and run them through first transform { byte = (input >> ((NBBY-1)-i)*NBBY) & 0xFF; hash = pwhash(byte, &y, &x); } for(i=0; i<8; i++) // beginning of "intermediate byte" function { // that links user password to "CYBERLOCK" byte = (input >> ((NBBY-1)-i)*NBBY) & 0xFF; x = byte ^ feed; s = ror(x, 4, NBBY); y = ror(s, 1, NBBY); s ^= y; rcl(y, &cf, 1); // only setting carry flag on/off s ^= rcr(y, &cf, 5) & 0xF; y = (x & 0xF0) ^ s; x = rcr(x, &cf, 1); if(cf) y ^= 0x46; rcr(x, &cf, 1); // only setting carry flag on/off if(cf) y ^= 0x8C; feed = y; // these transformations make more sense after vodka } comm_pw = hash; y = feed; x = 0; for(i=0; i<9; i++) // run "CYBERLOCK" bytes through hash = pwhash(cybr[i], &y, &x); // transform for first half of comm_pw comm_pw = (hash << (NBBY*2)) + comm_pw; if(!pf) // these values will be negative comm_pw += 0xFFFFFFFF00000000; return comm_pw; } int pwhash(int byte, int *y, int *x) // called from calc() { int temp1 = byte ^ *y; int temp2 = *x; pf = parity(temp1); // ...and these transformations make more sense after bourbon: if(!pf) temp2 ^= 0xC001; temp1 = rol(temp1, 6, NBBY*2); temp2 ^= temp1; temp1 = rol(temp1, 1, NBBY*2) ^ temp2; *y = temp1 & 0xFF; // LSB *x = (temp1 & 0xFF00) >> NBBY; // MSB return temp1; } int rol(int byte, int n, int bits) // rotateleft isn't built into C { byte = (byte << n) | (byte >> (bits - n)); return byte; } int ror(int byte, int n, int bits) // neither is rotateright { unsigned int shift = byte & ((1 << n) - 1); byte = (byte >> n) | (shift << (bits - n)); return byte; } int rcl(int byte, int *cf, int n) // ...or rotatewithcarryleft { int tempcf, i; for(i=0; i> (NBBY-1); byte = (byte << 1) | *cf; *cf = tempcf; } return byte; } int rcr(int byte, int *cf, int n) // guess what? { int tempcf, i; for(i=0; i> 1) | (*cf << (NBBY-1)); *cf = tempcf; } return byte; } // called from pwhash: int parity(int byte) // I wrote a function to count bits and { // return odd/even parity, but this lookup // table is a lot quicker. const int compare[] = {1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, // 0x00-0x0F 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1}; // 0xF0-0xFF if((byte < 0) || (byte > 0xFF)) // this should never happen { fprintf(stderr, "Something went entirely wrong here.\n"); // You broke the universe return -1; } return compare[byte]; // 1 = even parity, 0 = odd. It seems backward to me too but I didn't } // write the spec. // "YOU DONE GOOFED" functions: int nofile(char *fname) { char errstr[64]; snprintf(errstr, sizeof(errstr), "Open of %s failed", fname); perror(errstr); return -1; } int pwlen() { fprintf(stderr, "Valid CyberAudit passwords are 6-32 characters long\n"); return -1; } int timeformat() { fprintf(stderr, "Error: date should be m/d/yyyy, m/dd/yyyy, mm/d/yyyy,"); fprintf(stderr, " or mm/dd/yyyy.\n"); fprintf(stderr, " time should be h:mm:ss or hh:mm:ss on a "); fprintf(stderr, "12-hour clock. AM and\n PM should be capitalized.\n"); return -1; } int usage(char *fname) { fprintf(stderr, "\n\tCyberLockPick v1.9\n\n"); fprintf(stderr, "\tUsage: %s [options or additional switches]\n", fname); fprintf(stderr, "\t or %s etc.\n", fname); fprintf(stderr, "\t (spaces in output will be replaced with the"); fprintf(stderr, " extended ASCII\n\t character \"%c\" for readability, unless the --printspaces switch is used)\n\n", 0xFE); fprintf(stderr, "\tSwitches:\n\n"); fprintf(stderr, "\t -h [--printspaces]\n\t\tto generate "); fprintf(stderr, "CRC16-rot hashes (equivalent to --hash)\n"); fprintf(stderr, "\t -p \n\t\tto generate iv_val for "); fprintf(stderr, "a specific password (equivalent to --injectpw)\n"); fprintf(stderr, "\t -ph \n\t\tto generate iv_val for "); fprintf(stderr, "a password hash (useful if you've pulled one out\n"); fprintf(stderr, "\t\tof an EEPROM dump) (equivalent to --injectpwhash)\n"); fprintf(stderr, "\t -r \n"); fprintf(stderr, "\t\tto recover CRC16-rot hash (pre-Web versions) (equivalent to --recover) or:\n"); fprintf(stderr, "\t -r \n\t\tto "); fprintf(stderr, "recover CRC16-rot hash "); fprintf(stderr, "(CyberAudit-Web versions) (equivalent to --recover)\n"); fprintf(stderr, "\t -cp \n"); fprintf(stderr, "\t\tto generate comm_pw values from password or filename (equivalent to --commpw)\n"); fprintf(stderr, "\t -cph \n"); fprintf(stderr, "\t\tto generate comm_pw values from a recovered hash (equivalent to --commpwhash)\n"); fprintf(stderr, "\t -e \n"); fprintf(stderr, "\t\tto scan a dumped key or lock EEPROM for information (equivalent to --eeprom)\n"); fprintf(stderr, "\t -cr [[wordlist] | [--nonrepeating] [(one of the following switches)]]\n"); fprintf(stderr, "\t\tto crack hashes by brute-force (SLOW) or with a"); fprintf(stderr, " wordlist (equivalent to --crack)\n"); fprintf(stderr, "\t\tyou can use one of these switches to change the default cracking character\n"); fprintf(stderr, "\t\tset: --lower --upper --alpha --numbers --symbols --lowerupperspaces --allchars\n"); fprintf(stderr, "\t\t(otherwise, the default character set is alphanumeric)\n"); fprintf(stderr, "\t -crc16 \n"); fprintf(stderr, "\t\tto generate just a plain CRC-16/ARC hash of a given word\n\n"); fprintf(stderr, "\tAdditional settings for hash cracking function:\n\n"); fprintf(stderr, "\t--threads \n"); fprintf(stderr, "\t\tto set the number of running threads (default: 8)\n"); fprintf(stderr, "\t--minlen \n"); fprintf(stderr, "\t\tto set the minimum password length, mainly useful for cracking hashes (default: 6)\n"); fprintf(stderr, "\t--3pre\n"); fprintf(stderr, "\t\tto configure (alphabetlength^3)-word buffers per thread for cracking (this is the\n\t\tdefault and purely cosmetic)\n"); fprintf(stderr, "\t--4pre\n"); fprintf(stderr, "\t\tto configure (alphabetlength^4)-word buffers per thread for cracking (uses more memory)\n"); fprintf(stderr, "\t--bench\n"); fprintf(stderr, "\t\tto run through a single keyspace and report speed benchmarks (ProTip: use the hash\n"); fprintf(stderr, "\t\tof a word 1-char longer than your benchmark goal and use --minlen of your goal length).\n"); fprintf(stderr, "\t--table\n"); fprintf(stderr, "\t\tto use a CRC-16/ARC lookup table instead of the generator (may be faster in some cases)\n"); fprintf(stderr, "\t--nobrute\n"); fprintf(stderr, "\t\tto skip brute-forcing if you're just checking hash patterns (passwords with repeating\n"); fprintf(stderr, "\t\tgroups have corresponding hashes with repeating blocks)\n\n"); fprintf(stderr, "\tExamples:\n"); fprintf(stderr, "\t cat wordlist.txt | %s -h --stdin\n", fname); fprintf(stderr, "\t (equivalent to %s -h wordlist.txt)\n", fname); fprintf(stderr, "\t %s -r -3.786963327609602816E+14 3/22/2014 11:22:34 PM\n", fname); fprintf(stderr, "\t %s --recover -261751552 -881721110 575076154\n", fname); fprintf(stderr, "\t %s -p password 3/22/2014 11:22:34 PM\n", fname); fprintf(stderr, "\t %s -ph C87700ECF765FC00 10/23/2020 12:02:36 PM\n", fname); fprintf(stderr, "\t %s -cr 0000000000000000 --allchars --nonrepeating\n", fname); fprintf(stderr, "\t %s -cr BB3D3FEA6F9E4D5E --minlen 8 --threads 100 --numbers --bench\n\n", fname); return -1; } int main(int argc, char **argv) { if(argc < 3) { usage(argv[0]); return -1; } int i; if((!strcmp(argv[1], "-cp")) || (!strcmp(argv[1], "--commpw"))) sw_cp = 1; // generate comm_pw else if((!strcmp(argv[1], "-cph")) || (!strcmp(argv[1], "--commpwhash"))) sw_cph = 1; // generate comm_pw from known hash else if((!strcmp(argv[1], "-h")) || (!strcmp(argv[1], "--hash"))) sw_h = 1; // generate hash else if((!strcmp(argv[1], "-p")) || (!strcmp(argv[1], "--injectpw"))) sw_p = 1; // generate DB values for passwd injection else if((!strcmp(argv[1], "-r")) || (!strcmp(argv[1], "--recover"))) sw_r = 1; // recover password hash from DB vals else if((!strcmp(argv[1], "-ph")) || (!strcmp(argv[1], "--injectpwhash"))) sw_ph = 1; // recover iv_val from original hash else if((!strcmp(argv[1], "-e")) || (!strcmp(argv[1], "--eeprom"))) sw_e = 1; // EEPROM dump scan else if((!strcmp(argv[1], "-cr")) || (!strcmp(argv[1], "--crack"))) sw_cr = 1; // crack arbitrary hash(es) else if(!strcmp(argv[1], "-crc16")) sw_crc16 = 1; else // missing or invalid switch { usage(argv[0]); return -1; } if((sw_p || sw_r || sw_ph) && argc < 5) { usage(argv[0]); return -1; } for(i=2;i 0)) threadcount = atoi(argv[i+1]); if((!strcmp(argv[i], "--minlen")) && (atoi(argv[i+1]) > 5) && (atoi(argv[i+1]) < 33)) minlen = atoi(argv[i+1]); if(!strcmp(argv[i], "--bench")) // Show benchmark stats for a single, arbitrary keyspace (give it a hash like sw_bench = 1; // FFFFFFFFFFFFFFFF and set --minlen and alphabet to what you want to benchmark) if(!strcmp(argv[i], "--nobrute")) // skip brute-forcing, just check for patterns in hash blocks sw_nobrute = 1; if(!strcmp(argv[i], "--table")) // use lookup table for CRC-16/ARC XOR values (faster in some cases) sw_table = 1; if((!strcmp(argv[i], "--4pre"))) // larger buffers that use more memory sw_4pre = 1; else if((!strcmp(argv[i], "--3pre"))) // smaller buffers for less memory (DEFAULT) sw_4pre = 0; if(!strcmp(argv[i], "--lower")) // character sets (mutually exclusive from each other) charsetflag = 1; else if(!strcmp(argv[i], "--upper")) charsetflag = 2; else if(!strcmp(argv[i], "--alpha")) charsetflag = 3; else if(!strcmp(argv[i], "--numbers")) charsetflag = 4; else if(!strcmp(argv[i], "--symbols")) charsetflag = 5; else if(!strcmp(argv[i], "--lowerupperspaces")) charsetflag = 6; else if(!strcmp(argv[i], "--allchars")) charsetflag = 7; else if(!strcmp(argv[i], "--custom")) // the exact alphabet for this one can be set in the slowcrack() switch table charsetflag = 8; } // end of for loop, check next arg if(sw_cp || sw_cph) // comm_pw generator commpw(argc, argv[2]); if(sw_h || sw_crc16) // hash/pass generator hashpass(argv[2]); if(sw_p || sw_r || sw_ph) // database recovery and injection functions recover(argc, argv[2], argv[3], argv[4], argv[5]); if(sw_e) // EEPROM dump scan eepromscan(argv[2]); if(sw_cr) // hash cracking implementation slowcrack(argv[2], argv[3]); return 0; }