├── LICENSE ├── README.md ├── copilotbrute.cpp └── copilotcollider.cpp /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Steve Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Copilot Hash Collider 2 | 3 | Used to find collisions in the copilot's hash function. This uses a meet in the middle attack. P.S. I was lazy and used `unordered_multimap` so your compiler needs to be able to do C++11. 4 | 5 | 6 | # Usage 7 | 8 | ## Hashing a string 9 | 10 | ``` 11 | ./copilotcollider hashme 12 | ``` 13 | ``` 14 | ./copilotbrute hashme 15 | ``` 16 | 17 | 18 | ## Meet in the middle attack 19 | 20 | `prefix-length` should be at least 6, otherwise use early exit brute force. `suffix-length` should be at least `prefix-length - 1`. 21 | ``` 22 | ./copilotcollider target-hash prefix-length suffix-length 23 | ``` 24 | 25 | 26 | ### Example 27 | 28 | This will take about a minute, use about 620 MiB of memory, and should find 854673 collisions. 29 | ``` 30 | ./copilotcollider 69712246 5 6 > out.txt 31 | ``` 32 | 33 | 34 | ## Early exit brute force 35 | 36 | This should be about as fast or faster than meet in the middle attack when `prefix-length` is less than 12. 37 | ``` 38 | ./copilotbrute target-hash length 39 | ``` 40 | 41 | 42 | ### Example 43 | 44 | This will take about 10 seconds and should find 854673 collisions. 45 | ``` 46 | ./copilotbrute 69712246 11 > out.txt 47 | ``` 48 | 49 | 50 | # How it works 51 | 52 | ## Meet in the middle attack 53 | 54 | The algorithm takes a 32 bit integer and multiplies by 33 then xors a character and keeps the lower 32 bits. If you try to go backwards imagine having the full 38 bit result after a step. You could easily just guess a character that solves `(result38 ^ ch) % 33 == 0`, but we don't have the full 38 bit result. What we can do is make a guess at it and go from there. There are only 33 possible `result38`s. `result38 = result32 + (i << 32)` where `i` is an integer 0 through 32. We take each one and solve `(result38 ^ ch) % 33 == 0`. Note you are on average going to get character set length number of solutions for all of the `result38`s. For this it's just lowercase letters so character set length is 26. There is a 26/33 chance at solving `(result38 ^ ch) % 33 == 0` and there are 33 of these to try so you are expected to get about 26/33\*33 or 26. 55 | 56 | There are some optimizations on solving `(result38 ^ ch) % 33 == 0` because it's just lowercase letters. `result38 ^ 0x60` because the values are 01100001b to 01111010b and 01100000b (0x60) is always set and the values left to xor are 5 bits: 00001b to 11010b. Only one or zero of these 5 bits values can make it work and it's easy to solve. It's either `(result38 ^ 0x60) - (result38 ^ 0x60) % 33` or `(result38 ^ 0x60) - (result38 ^ 0x60) % 33 + 33` and you know which one depending on if `(result38 ^ 0x60) >= (result38 ^ 0x60) % 33`. 57 | 58 | 59 | # License issue 60 | 61 | `obfuscateWord()` was taken from https://gist.github.com/moyix/f78e0b0d5724a1bf02e1a035e8bec136 which has no license. 62 | -------------------------------------------------------------------------------- /copilotbrute.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifdef _WIN32 6 | #pragma warning(disable:4996) 7 | #endif 8 | 9 | #define MAX_LEN 14 10 | 11 | uint32_t g_count = 0; 12 | 13 | uint32_t obfuscateWord(const unsigned char *e, int len) 14 | { 15 | uint32_t t = 0x1509c000; 16 | 17 | for (int i = 0; i < len; i++) 18 | { 19 | t = (t*33) ^ e[i]; 20 | } 21 | return t; 22 | } 23 | 24 | uint32_t earlyExit(uint32_t hash, uint32_t testHash, uint32_t pow33) 25 | { 26 | uint32_t hi = (hash | 0x7f) * pow33 + pow33 - 1; 27 | uint32_t lo = (hash & ~0x7f) * pow33; 28 | 29 | if ((lo < hi && (lo > testHash || hi < testHash)) || 30 | (lo > hi && (lo > testHash && hi < testHash))) 31 | { 32 | return 1; 33 | } 34 | return 0; 35 | } 36 | 37 | void earlyExitBruteForce(uint32_t hash, int len) 38 | { 39 | unsigned char word[MAX_LEN + 1] = {0}; 40 | 41 | // Init word 42 | int i; 43 | for (i = 0; i < len; i++) 44 | { 45 | word[i] = 'a'; 46 | } 47 | 48 | // Brute force 49 | do 50 | { 51 | uint32_t hash0 = 33 * obfuscateWord(word, len - 5); 52 | if (earlyExit(hash0, hash, 33*33*33*33) == 0) 53 | { 54 | for (unsigned char c0 = 'a'; c0 <= 'z'; c0++) 55 | { 56 | if (earlyExit(33 * (hash0 ^ c0), hash, 33*33*33) == 0) 57 | { 58 | uint32_t hash1 = 33 * (hash0 ^ c0); 59 | for (unsigned char c1 = 'a'; c1 <= 'z'; c1++) 60 | { 61 | if (earlyExit(33 * (hash1 ^ c1), hash, 33*33) == 0) 62 | { 63 | uint32_t hash2 = 33 * (hash1 ^ c1); 64 | for (unsigned char c2 = 'a'; c2 <= 'z'; c2++) 65 | { 66 | if (earlyExit(33 * (hash2 ^ c2), hash, 33) == 0) 67 | { 68 | uint32_t hash3 = 33 * (hash2 ^ c2); 69 | for (unsigned char c3 = 'a'; c3 <= 'z'; c3++) 70 | { 71 | uint32_t c4 = (33 * (hash3 ^ c3)) ^ hash; 72 | if (c4 >= 'a' && c4 <= 'z') 73 | { 74 | word[len - 5] = c0; 75 | word[len - 4] = c1; 76 | word[len - 3] = c2; 77 | word[len - 2] = c3; 78 | word[len - 1] = (unsigned char) c4; 79 | printf("%s\n", word); 80 | g_count++; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | // Next 92 | for (i = 0; i < len - 5; i++) 93 | { 94 | word[i]++; 95 | if (word[i] <= 'z') 96 | { 97 | break; 98 | } 99 | word[i] = 'a'; 100 | } 101 | if (i >= len - 6) 102 | { 103 | fprintf(stderr, "*"); 104 | } 105 | } while (i < len - 5); 106 | } 107 | 108 | int main(int argc, char *argv[]) 109 | { 110 | uint32_t hash; 111 | uint32_t len; 112 | 113 | if (argc == 2) 114 | { 115 | printf("\"%s\": %u", argv[1], obfuscateWord((unsigned char*) argv[1], strlen(argv[1]))); 116 | return 0; 117 | } 118 | if (argc != 3 || 119 | sscanf(argv[1], "%u", &hash) != 1 || 120 | sscanf(argv[2], "%u", &len) != 1) 121 | { 122 | fprintf(stderr, "Usage is:\n%s target-hash length\n%s string-to-hash\n", argv[0], argv[0]); 123 | return 1; 124 | } 125 | if (len < 5 || len > MAX_LEN) 126 | { 127 | fprintf(stderr, "Bad length (5 to %u): %u\n", MAX_LEN, len); 128 | return 1; 129 | } 130 | 131 | fprintf(stderr, "Early exit brute forcing...\n|--------------------------|\n|"); 132 | earlyExitBruteForce(hash, len); 133 | fprintf(stderr, "|\nDone\nFound %u\n", g_count); 134 | 135 | return 0; 136 | } 137 | -------------------------------------------------------------------------------- /copilotcollider.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | using namespace std; 6 | 7 | #ifdef _WIN32 8 | #pragma warning(disable:4996) 9 | #endif 10 | 11 | #define MAX_PREFIX_LEN 7 12 | #define MAX_SUFFIX_LEN 7 13 | #define MAX_LEN (MAX_PREFIX_LEN + MAX_SUFFIX_LEN) 14 | 15 | uint32_t g_count = 0; 16 | 17 | struct prefixStruct 18 | { 19 | unsigned char prefix[MAX_PREFIX_LEN + 1]; 20 | }; 21 | 22 | // Function stolen from (no clue what license) https://gist.github.com/moyix/f78e0b0d5724a1bf02e1a035e8bec136 23 | uint32_t obfuscateWord(const unsigned char *e, int len) 24 | { 25 | uint32_t t = 0x1509c000; 26 | 27 | for (int i = 0; i < len; i++) 28 | { 29 | t = (t*33) ^ e[i]; 30 | } 31 | return t; 32 | } 33 | 34 | void fillPrefixes(unordered_multimap &hashes, int prefixLen) 35 | { 36 | uint32_t prefixIndex = 0; 37 | prefixStruct prefix = {0}; 38 | 39 | // Init prefix 40 | int i; 41 | for (i = 0; i < prefixLen; i++) 42 | { 43 | prefix.prefix[i] = 'a'; 44 | } 45 | 46 | // Fill prefixes 47 | do 48 | { 49 | // Insert 50 | hashes.insert({obfuscateWord(prefix.prefix, prefixLen), prefix}); 51 | prefixIndex++; 52 | 53 | // Next 54 | for (i = 0; i < prefixLen; i++) 55 | { 56 | prefix.prefix[i]++; 57 | if (prefix.prefix[i] <= 'z') 58 | { 59 | break; 60 | } 61 | prefix.prefix[i] = 'a'; 62 | } 63 | } while (i < prefixLen); 64 | } 65 | 66 | void reverseSuffixes(unordered_multimap &hashes, uint32_t hash, unsigned char suffix[MAX_SUFFIX_LEN + 1], uint32_t pos, uint32_t suffixLen) 67 | { 68 | const uint32_t MODS[] = { 69 | 0, 4, 8, 12, 16, 20, 24, 28, 32, 3, 7, 11, 15, 19, 23, 27, 70 | 31, 2, 6, 10, 14, 18, 22, 26, 30, 1, 5, 9, 13, 17, 21, 25, 29}; 71 | const uint32_t DIVS[] = { // (i << 32 + 32) / 33 72 | 0, 130150525, 260301049, 390451573, 520602097, 650752621, 780903145, 911053669, 73 | 1041204193, 1171354718, 1301505242, 1431655766, 1561806290, 1691956814, 1822107338, 1952257862, 74 | 2082408386, 2212558911, 2342709435, 2472859959, 2603010483, 2733161007, 2863311531, 2993462055, 75 | 3123612579, 3253763104, 3383913628, 3514064152, 3644214676, 3774365200, 3904515724, 4034666248, 4164816772}; 76 | 77 | if (pos == 0) 78 | { 79 | auto range = hashes.equal_range(hash); 80 | for (auto it = range.first; it != range.second; ++it) 81 | { 82 | printf("%s%s\n", it->second.prefix, suffix); 83 | g_count++; 84 | } 85 | } 86 | else 87 | { 88 | pos--; 89 | uint32_t modHash = (hash ^ 0x60) % 33; 90 | uint32_t lower5Hash = hash & 0x1f; 91 | for (int i = 0; i < 33; i++) 92 | { 93 | uint32_t modHash2 = modHash + MODS[i]; 94 | if (modHash2 >= 33) 95 | { 96 | modHash2 -= 33; 97 | } 98 | if (modHash2 != 0) 99 | { 100 | // 0x61-0x7a 101 | // 011 00001 - 011 11010 102 | uint32_t lower5Char; 103 | 104 | if (lower5Hash >= modHash2) 105 | { 106 | lower5Char = lower5Hash ^ (lower5Hash - modHash2); 107 | } 108 | else 109 | { 110 | lower5Char = lower5Hash ^ (lower5Hash + 33 - modHash2); 111 | } 112 | if (lower5Char != 0 && lower5Char <= 0x1a) 113 | { 114 | uint32_t ch = 0x60 | lower5Char; 115 | suffix[pos] = (unsigned char) ch; 116 | reverseSuffixes(hashes, (hash ^ ch) / 33 + DIVS[i], suffix, pos, suffixLen); 117 | } 118 | } 119 | 120 | if (pos == suffixLen - 1) 121 | { 122 | fprintf(stderr, "*"); 123 | } 124 | } 125 | } 126 | } 127 | 128 | int main(int argc, char *argv[]) 129 | { 130 | uint64_t prefixSize = 26; 131 | uint32_t hash; 132 | uint32_t prefixLen; 133 | uint32_t suffixLen; 134 | unsigned char suffix[MAX_SUFFIX_LEN + 1] = {0}; 135 | 136 | if (argc == 2) 137 | { 138 | printf("\"%s\": %u", argv[1], obfuscateWord((unsigned char*) argv[1], strlen(argv[1]))); 139 | return 0; 140 | } 141 | if (argc != 4 || 142 | sscanf(argv[1], "%u", &hash) != 1 || 143 | sscanf(argv[2], "%u", &prefixLen) != 1 || 144 | sscanf(argv[3], "%u", &suffixLen) != 1) 145 | { 146 | fprintf(stderr, "Usage is:\n%s target-hash prefix-length suffix-length\n%s string-to-hash\n", argv[0], argv[0]); 147 | return 1; 148 | } 149 | if (prefixLen < 1 || prefixLen > 7) 150 | { 151 | fprintf(stderr, "Bad prefix-length (1 to 7): %u\n", prefixLen); 152 | return 1; 153 | } 154 | if (suffixLen < 1 || suffixLen > 7) 155 | { 156 | fprintf(stderr, "Bad suffix-length (1 to 7): %u\n", suffixLen); 157 | return 1; 158 | } 159 | 160 | for (uint32_t i = 1; i < prefixLen; i++) 161 | { 162 | prefixSize *= 26; 163 | } 164 | prefixSize += prefixSize / 2; 165 | if (12 * prefixSize > SIZE_MAX) 166 | { 167 | fprintf(stderr, "Insufficient memory\n"); 168 | return 1; 169 | } 170 | 171 | 172 | unordered_multimap hashes((size_t) prefixSize); 173 | 174 | fprintf(stderr, "Working on hash: %u\n", hash); 175 | 176 | fprintf(stderr, "Filling prefixes..."); 177 | fillPrefixes(hashes, prefixLen); 178 | fprintf(stderr, " Done\nReversing suffixes...\n|---------------------------------|\n|"); 179 | reverseSuffixes(hashes, hash, suffix, suffixLen, suffixLen); 180 | fprintf(stderr, "|\nDone\n"); 181 | fprintf(stderr, "Found %u\n", g_count); 182 | 183 | return 0; 184 | } 185 | --------------------------------------------------------------------------------