revise code examples

This commit is contained in:
Joel Beckmeyer 2024-06-29 13:06:23 -04:00
parent 6afec009c4
commit a42bc2bd42

View File

@ -1,7 +1,8 @@
--- ---
title: "Cracking the AT&T VVM cipher" title: "Cracking the AT&T VVM cipher"
tags: ["Hacking"] tags: ["Hacking"]
date: 2024-06-19T20:16:00-04:00 date: 2024-06-19
lastmod: 2024-06-29
draft: false draft: false
--- ---
I've been wanting to get the AT&T visual voicemail "protocol" (ADVVM) working I've been wanting to get the AT&T visual voicemail "protocol" (ADVVM) working
@ -55,15 +56,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 bottom four bits (or [nibble](https://en.wikipedia.org/wiki/Nibble)) by
removing the upper bits: 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): def get_stripped(text):
stripped = [strip_prefix(c) for c in text] return [ord(c) & 0x0f for c in text]
return bytearray(stripped)
``` ```
Now, how do we actually figure out the transform? There are a number of ciphers Now, how do we actually figure out the transform? There are a number of ciphers
@ -78,19 +72,20 @@ ciphering](https://en.m.wikipedia.org/wiki/XOR_cipher).
Let's try it: Let's try it:
``` ```
def decode(cipher, secret): def xor_cipher(cipher, secret):
cipher = get_stripped(cipher) if isinstance(cipher[0], str):
secret = get_stripped(secret) cipher = get_stripped(cipher)
if isinstance(secret[0], str):
# remember that the cipher "passes through" digits past the 10th, so we secret = get_stripped(secret)
# just overwrite the first 10 # 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)] text = [i^j for i, j in zip(cipher, secret)]
if len(cipher) > 10: if len(cipher) > len(secret):
text += cipher[10:] text += cipher[len(secret):]
return text 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] [12, 5, 3, 11, 9, 4, 5, 3, 8, 14, 0]
``` ```
@ -99,16 +94,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 because we can also use the plaintext instead of the actual secret to gain some
insight. Below is the one with my throwaway number: insight. Below is the one with my throwaway number:
``` ```
decode("[VW^QW\\W_X0", "00000000000") xor_cipher("[VW^QW\\W_X0", "00000000000")
[11, 6, 7, 14, 1, 7, 12, 7, 15, 8, 0, 0] [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 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: 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")) first_pass = ''.join(chr(i) for i in xor_cipher("[VW^QW\\W_X0", "7345839476"))
decode(first_pass, "00000000000") xor_cipher(first_pass, "00000000000")
[12, 5, 3, 11, 9, 4, 5, 3, 8, 14, 0, 0] [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 Now this is interesting! The output of this is the same for both of my phone
@ -119,25 +113,19 @@ secret XOR phonenumber XOR plaintext = ciphertext
``` ```
Let's verify: Let's verify:
``` ```
def decode(cipher, phonenumber, secret): def decode(cipher, phonenumber):
cipher = get_stripped(cipher)
phonenumber = get_stripped(phonenumber)
secret = [12, 5, 3, 11, 9, 4, 5, 3, 8, 14] secret = [12, 5, 3, 11, 9, 4, 5, 3, 8, 14]
# remember that the cipher "passes through" digits past the 10th, so we first_pass = ''.join(chr(i) for i in xor_cipher(cipher, phonenumber))
# just overwrite the first 10 return xor_cipher(first_pass, secret)
text = [i^j^k for i, j, k in zip(cipher, phonenumber, secret)]
if len(cipher) > 10:
text += cipher[10:]
return text
decode("[VW^QW\\W_X0", "7345839476") decode("[VW^QW\\W_X0", "7345839476")
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
decode("[WU]URZPWQ", "7345839476") decode("[WU]URZPWQ", "7345839476")
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [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: Let's validate this more completely:
``` ```
@ -153,17 +141,17 @@ lookup_table = [
['S', '^', '_', 'V', 'Y', '_', 'T', '_', 'W', 'P'], ['S', '^', '_', 'V', 'Y', '_', 'T', '_', 'W', 'P'],
['R', '_', '^', 'W', 'X', '^', 'U', '^', 'V', 'Q'], ['R', '_', '^', 'W', 'X', '^', 'U', '^', 'V', 'Q'],
] ]
def validate_decode(table): def validate_decode(table, phone):
for plaintext_char in range(10): for plaintext_char in range(10):
expected_plaintext = str(plaintext_char) * 10 expected_plaintext = str(plaintext_char) * 10
ciphertext = ''.join([table[plaintext_char][i] for i in range(10)]) ciphertext = "".join([table[plaintext_char][i] for i in range(10)])
plaintext = ''.join([str(i) for i in decode(ciphertext, "7345839476")]) plaintext = "".join([str(i) for i in decode(ciphertext, phone)])
if plaintext != expected_plaintext: if plaintext != expected_plaintext:
print(f"Failed on \"{plaintext}\" != decode(\"{ciphertext}\", ...)") print(f'Failed on "{plaintext}" != decode("{ciphertext}", ...)')
else: 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("[VW^QW\W_X", ...) == "0000000000"
Success! decode("ZWV_PV]V^Y", ...) == "1111111111" Success! decode("ZWV_PV]V^Y", ...) == "1111111111"
Success! decode("YTU\SU^U]Z", ...) == "2222222222" Success! decode("YTU\SU^U]Z", ...) == "2222222222"
@ -176,7 +164,8 @@ Success! decode("S^_VY_T_WP", ...) == "8888888888"
Success! decode("R_^WX^U^VQ", ...) == "9999999999" 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) 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. 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 Unfortunately, it looks like the cipher method has changed as this does not
@ -213,3 +202,59 @@ secret XOR plaintext = ciphertext
phonenumber XOR plaintext = ciphertext phonenumber XOR plaintext = ciphertext
``` ```
But neither worked. 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]`.