From 704f7309afcbb9eb7ab443db9610d675045f1f3a Mon Sep 17 00:00:00 2001 From: Joel Beckmeyer Date: Sat, 29 Jun 2024 13:06:23 -0400 Subject: [PATCH] revise code examples --- content/posts/cracking_att_vvm_cipher.md | 126 +++++++++++++++-------- 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/content/posts/cracking_att_vvm_cipher.md b/content/posts/cracking_att_vvm_cipher.md index aa307b4..0700533 100644 --- a/content/posts/cracking_att_vvm_cipher.md +++ b/content/posts/cracking_att_vvm_cipher.md @@ -55,15 +55,8 @@ Our dictionary is self-contained in an upper 4-bit prefix. Let's focus on the bottom four bits (or [nibble](https://en.wikipedia.org/wiki/Nibble)) by removing the upper bits: ``` -def strip_prefix(char): - return ord(char)&0x0f -``` - -Now we can create a bytearray containing the stripped versions of the passcode: -``` def get_stripped(text): - stripped = [strip_prefix(c) for c in text] - return bytearray(stripped) + return [ord(c) & 0x0f for c in text] ``` Now, how do we actually figure out the transform? There are a number of ciphers @@ -78,19 +71,20 @@ ciphering](https://en.m.wikipedia.org/wiki/XOR_cipher). Let's try it: ``` -def decode(cipher, secret): - cipher = get_stripped(cipher) - secret = get_stripped(secret) - - # remember that the cipher "passes through" digits past the 10th, so we - # just overwrite the first 10 +def xor_cipher(cipher, secret): + if isinstance(cipher[0], str): + cipher = get_stripped(cipher) + if isinstance(secret[0], str): + secret = get_stripped(secret) + # remember that the cipher "passes through" digits past the length of the + # secret, so we just take the rest unciphered text = [i^j for i, j in zip(cipher, secret)] - if len(cipher) > 10: - text += cipher[10:] + if len(cipher) > len(secret): + text += cipher[len(secret):] return text -decode("[VW^QW\\W_X0", "7345839476") +xor_cipher("[VW^QW\\W_X0", "7345839476") [12, 5, 3, 11, 9, 4, 5, 3, 8, 14, 0] ``` @@ -99,16 +93,15 @@ it doesn't output the plaintext password that I am expecting. Not to worry, because we can also use the plaintext instead of the actual secret to gain some insight. Below is the one with my throwaway number: ``` -decode("[VW^QW\\W_X0", "00000000000") -[11, 6, 7, 14, 1, 7, 12, 7, 15, 8, 0, 0] +xor_cipher("[VW^QW\\W_X0", "00000000000") +[11, 6, 7, 14, 1, 7, 12, 7, 15, 8, 0] ``` -Ignore the slight bug due to an assumption about the length of the secret :| The output of this is different for my throwaway and my actual phone number. So there is a unique 10-digit secret. Let's try a two-step decode: ``` -first_pass = ''.join(chr(i) for i in decode("[VW^QW\\W_X0", "7345839476")) -decode(first_pass, "00000000000") -[12, 5, 3, 11, 9, 4, 5, 3, 8, 14, 0, 0] +first_pass = ''.join(chr(i) for i in xor_cipher("[VW^QW\\W_X0", "7345839476")) +xor_cipher(first_pass, "00000000000") +[12, 5, 3, 11, 9, 4, 5, 3, 8, 14, 0] ``` Now this is interesting! The output of this is the same for both of my phone @@ -119,25 +112,19 @@ secret XOR phonenumber XOR plaintext = ciphertext ``` Let's verify: ``` -def decode(cipher, phonenumber, secret): - cipher = get_stripped(cipher) - phonenumber = get_stripped(phonenumber) +def decode(cipher, phonenumber): secret = [12, 5, 3, 11, 9, 4, 5, 3, 8, 14] - # remember that the cipher "passes through" digits past the 10th, so we - # just overwrite the first 10 - text = [i^j^k for i, j, k in zip(cipher, phonenumber, secret)] - if len(cipher) > 10: - text += cipher[10:] - - return text + first_pass = ''.join(chr(i) for i in xor_cipher(cipher, phonenumber)) + return xor_cipher(first_pass, secret) decode("[VW^QW\\W_X0", "7345839476") [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] decode("[WU]URZPWQ", "7345839476") [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ``` -Beatiful! This yields the same result for both phone numbers. +Beatiful! This yields the same result for both phone numbers. Our secret is +[12, 5, 3, 11, 9, 4, 5, 3, 8, 14]. Let's validate this more completely: ``` @@ -153,17 +140,17 @@ lookup_table = [ ['S', '^', '_', 'V', 'Y', '_', 'T', '_', 'W', 'P'], ['R', '_', '^', 'W', 'X', '^', 'U', '^', 'V', 'Q'], ] -def validate_decode(table): +def validate_decode(table, phone): for plaintext_char in range(10): expected_plaintext = str(plaintext_char) * 10 - ciphertext = ''.join([table[plaintext_char][i] for i in range(10)]) - plaintext = ''.join([str(i) for i in decode(ciphertext, "7345839476")]) + ciphertext = "".join([table[plaintext_char][i] for i in range(10)]) + plaintext = "".join([str(i) for i in decode(ciphertext, phone)]) if plaintext != expected_plaintext: - print(f"Failed on \"{plaintext}\" != decode(\"{ciphertext}\", ...)") + print(f'Failed on "{plaintext}" != decode("{ciphertext}", ...)') else: - print(f"Success! decode(\"{ciphertext}\", ...) == \"{plaintext}\"") + print(f'Success! decode("{ciphertext}", ...) == "{plaintext}"') -validate_decode(lookup_table) +validate_decode(lookup_table, "7345839476") Success! decode("[VW^QW\W_X", ...) == "0000000000" Success! decode("ZWV_PV]V^Y", ...) == "1111111111" Success! decode("YTU\SU^U]Z", ...) == "2222222222" @@ -176,7 +163,8 @@ Success! decode("S^_VY_T_WP", ...) == "8888888888" Success! decode("R_^WX^U^VQ", ...) == "9999999999" ``` -Addendum: After writing the majority of this, [a user pointed +**Addendum**: +After writing the majority of this, [a user pointed out](https://gitlab.com/LineageOS/issues/android/-/issues/6964#note_1961619585) that there is already some documentation on this ciphering, so I took a look. Unfortunately, it looks like the cipher method has changed as this does not @@ -213,3 +201,59 @@ secret XOR plaintext = ciphertext phonenumber XOR plaintext = ciphertext ``` But neither worked. + +There is a secret that works for decoding this, and we can find it by following +the same method from above, using the fact that +``` +ciphertext XOR plaintext = secret +``` + +Let's try: +``` +xor_cipher("XTQ^ZSUU_Y", "0000000000") +[8, 4, 1, 14, 10, 3, 5, 5, 15, 9] +``` +And then validating it: +``` +kop316_lookup_table = [ + ["X", "T", "Q", "^", "Z", "S", "U", "U", "_", "Y"], + ["Y", "U", "P", "_", "[", "R", "T", "T", "^", "X"], + ["Z", "V", "S", "\\", "X", "Q", "W", "W", "]", "["], + ["[", "W", "R", "]", "Y", "P", "V", "V", "\\", "Z"], + ["\\", "P", "U", "Z", "^", "W", "Q", "Q", "[", "]"], + ["]", "Q", "T", "[", "_", "V", "P", "P", "Z", "\\"], + ["^", "R", "W", "X", "\\", "U", "S", "S", "Y", "_"], + ["_", "S", "V", "Y", "]", "T", "R", "R", "X", "^"], + ["P", "\\", "Y", "V", "R", "[", "]", "]", "W", "Q"], + ["Q", "]", "X", "W", "S", "Z", "\\", "\\", "V", "P"], +] + +def old_decode(cipher): + cipher = get_stripped(cipher) + secret = [8, 4, 1, 14, 10, 3, 5, 5, 15, 9] + + return xor_cipher(cipher, secret) + +def validate_old_decode(table): + for plaintext_char in range(10): + expected_plaintext = str(plaintext_char) * 10 + ciphertext = "".join([table[plaintext_char][i] for i in range(10)]) + plaintext = "".join([str(i) for i in old_decode(ciphertext)]) + if plaintext != expected_plaintext: + print(f'Failed on "{plaintext}" != decode("{ciphertext}", ...)') + else: + print(f'Success! decode("{ciphertext}", ...) == "{plaintext}"') + +validate_old_decode(kop316_lookup_table) +Success! decode("XTQ^ZSUU_Y", ...) == "0000000000" +Success! decode("YUP_[RTT^X", ...) == "1111111111" +Success! decode("ZVS\XQWW][", ...) == "2222222222" +Success! decode("[WR]YPVV\Z", ...) == "3333333333" +Success! decode("\PUZ^WQQ[]", ...) == "4444444444" +Success! decode("]QT[_VPPZ\", ...) == "5555555555" +Success! decode("^RWX\USSY_", ...) == "6666666666" +Success! decode("_SVY]TRRX^", ...) == "7777777777" +Success! decode("P\YVR[]]WQ", ...) == "8888888888" +Success! decode("Q]XWSZ\\VP", ...) == "9999999999" +``` +To reiterate, the secret used here was `[8, 4, 1, 14, 10, 3, 5, 5, 15, 9]`.