excel / generate sheet password collision
Yesterday, I demonstrated how to brute force the Excel protected sheet/cells password. (Write protection! Not read protection a.k.a. encryption!) Today, I figured there must be a faster way, as the hash is not at all complicated.
After fiddling around a little, I hacked together this bit of Python:
def reverse_f(wanted):
"Calculate Excel protected sheet password"
# https://wjd.nu/notes/2020#excel-generate-sheet-password-collision
# https://wjd.nu/notes/2020#libreoffice-asking-for-cell-password-brute-force
def reverse_rotate(v):
"Right shift by one, rotating the right most bit to bit 15"
if v & 0x1:
return (v >> 1) | 0x4000
return v >> 1
chars = []
valid_tokens = tuple([ord(i) for i in (
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'abcdefghijklmnopqrstuvwxyz'
'0123456789')])
# Length 9 should be enough to go down from 16 to 7 bits:
# we skip some shorter solutions, but there are only a few hashes
# that benefit from that.
length = 9
h = wanted ^ 0xce4b ^ length # step 7 (xor CE4BH xor len)
for i in range(length - 2): # (step 1 and 2 and 6)
r = reverse_rotate(h) # step 5
# Find a char that blanks out as much from the right hand side.
# Ensuring that no bit is left over on the right side either,
# which would propagate to the left. This way, we trim the
# number down as fast as posible.
ch = r & 0x7f
while ch not in valid_tokens and ch < 0x70:
ch += 0x10
while ch not in valid_tokens and ch > 0x30:
ch -= 0x10
assert ch in valid_tokens, ch
h = r ^ ch # step 4 and 3
chars.append(ch)
r = reverse_rotate(h)
# There is 1 rotation left, and we have to get below 0x7f.
assert r < 0x100, (hex(wanted), hex(h), hex(r))
# Lastly, brute force our way to the last two characters.
for ch1 in valid_tokens:
for ch2 in valid_tokens:
if not (reverse_rotate(r ^ ch1) ^ ch2): # step 5, 4, 3, and 1
chars.extend([ch1, ch2])
pwd = ''.join(chr(ch) for ch in chars)
print('found password for 0x{:x}: {} [{!r}]'.format(
wanted, pwd, pwd))
return pwd
assert False, 'no solution found for 0x{:x}'.format(wanted)
If you recall the f
function from yesterday’s post:
def f(s): # takes a string
h = 0 # step 1
for idx in range(len(s) - 1, -1, -1): # step 1 and 2 and 6
h ^= ord(s[idx]) # step 3 and 4
h <<= 1 # step 5 (left shift)
if h & 0x8000: # step 5 (check high bit)
h = (h & 0x7fff) | 1 # step 5 (truncate + rotate)
return (h ^ len(s)) ^ 0xce4b # step 7
You can run it like this:
>>> hex(f('abcdefghij'))
'0xfef1'
>>> reverse_f(0xfef1)
found password for 0xfef1: Y0800PPBe ['Y0800PPBe']
'Y0800PPBe'
>>> hex(f('Y0800PPBe'))
'0xfef1'
Or you can enumerate all at once:
>>> for x in range(0x0000, 0x10000):
... reverse_f(x)
...
So, next time you encounter a
<sheetProtection sheet="true" password="ca5b" formatColumns="false" formatRows="false" insertRows="false"/>
you can do this:
>>> reverse_f(0xca5b)
found password for 0xca5b: L08X080Bi ['L08X080Bi']
'L08X080Bi'