 # excel / generate sheet password collision

## excel / generate sheet password collision

• Written by
Walter Doekes
• Published on

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):
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'
``````

Back to overview Newer post: tls / testing certificate chains / easycert Older post: libreoffice / asking for cell password / brute force