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

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