Close

Lenovo Thinkpad Supervisor Password - Gotta have a goal if you want to learn something...

A project log for Hacking a Supervisor Password - With a Beagle Bone

I found myself being intrigued with the low level safety features of my X201. I experimented with the I2C bus and it got interesting...

timo-birnscheinTimo Birnschein 02/21/2021 at 19:580 Comments

So far, I have only understood where some Lenovo Thinkpads store their passwords. They are located at 0x380 and 0x400. I interpret this as password and confirmation password. These are seven bytes plus one checksum byte. This is not a CRC. It's literally the 8-bit sum of the previous seven bytes. That's it.

Unfortunately, they use an "encryption" method to hide the password and make it non-human readable.

Brute Force

The supervisor password uses some form of encoding. Calling it encryption might push it a little because - at least on my personal X201 Tablet - the encoding is always the same no matter when I set a password. Maybe it will be different on a different Thinkpad? I'll find out when my "new" W530 arrives in the mail.

To figure out how the encoding works, I bruteforced my way through all possible characters the Thinkpad accepts for the supervisor password and checked how it is represented in the 24RF08. Thankfully, a complete power cycle wasn't required and the EEPROM was written each time I entered a new password in the Bios' security settings. It looks like this:

With that in place, I was able to create a map in Python to automatically give me the decrypted password straight from the binary file I read from the EEPROM.

# (Hopefully) Complete mapping between Lenovo encoded characters and the ASCII table
# Technically, the characters aren't needed because chr(hexcode) will give you the same but this is more debuggable and human readable
pwdMap = [[' ', 0x00, 0x00], ['0', 0x30, 0x0b], ['1', 0x31, 0x02], ['2', 0x32, 0x03], ['3', 0x33, 0x04], ['4', 0x34, 0x05], ['5', 0x35, 0x06], ['6', 0x36, 0x07], ['7', 0x37, 0x08], ['8', 0x38, 0x09], ['9', 0x39, 0x0a], [';', 0x3b, 0x27], ['a', 0x61, 0x1e], ['b', 0x62, 0x30], ['c', 0x63, 0x2e], ['d', 0x64, 0x20], ['e', 0x65, 0x12], ['f', 0x66, 0x21], ['g', 0x67, 0x22], ['h', 0x68, 0x23], ['i', 0x69, 0x17], ['j', 0x6a, 0x24], ['k', 0x6b, 0x25], ['l', 0x6c, 0x26], ['m', 0x6d, 0x32], ['n', 0x6e, 0x31], ['o', 0x6f, 0x18], ['p', 0x70, 0x19], ['q', 0x71, 0x10], ['r', 0x72, 0x13], ['s', 0x73, 0x1f], ['t', 0x74, 0x14], ['u', 0x75, 0x16], ['v', 0x76, 0x2f], ['w', 0x77, 0x11], ['x', 0x78, 0x2d], ['y', 0x79, 0x15], ['z', 0x7a, 0x2c]]
# takes an input value and encodes it into Lenovo speak if encodeDecode == 0
# and decodes it into ascii if encodeDecode == 1
# Granted, this is extremely inefficient but I'm not in a hurry. You?
def convert_value(input, encodeDecode = 0):
    if encodeDecode == 0:
        for r in pwdMap: # look at every entry in our map to check for a match
            if input == r[1]:
                return r[2]
        print(hex(input), " not found while encoding.")
        return -1 # something went wrong, symbol not found in map
    elif encodeDecode == 1:
        for r in pwdMap: # look at every entry in our map to check for a match
            if input == r[2]:
                return r[1]
        print(hex(input), " not found while decoding.")
        return -1 # something went wrong, symbol not found in map
    else:
        print("convert_value(input, encodeDecode = 0) needs either 0 or 1 as input for the encoding direction. You provided: ", encodeDecode)
        return -1 # something went wrong, function called incorrectly
def read_pwd_from_binary(data):
    print("\nExtracting and translating password:", end = " ")
    for i in range(7):
        print(chr(convert_value(data[0x38 + i], 1)), end = "")
    print("\nChecksum of password as read from eeprom: ", hex(data[0x38 + 7]))
    
    print("Confirmation passcode (should be the same):", end = " ")
    for i in range(7):
        print(chr(convert_value(data[0x40 + i], 1)), end = "")
    print("\nChecksum of re-entered password as read from eeprom: ", hex(data[0x40 + 7]))
    
    print("Calculating own checksum:", end = " ")
    print(hex(calculate_checksum(data[0x38:(0x38 + 7)])))

All of this should be self-explanatory due to all my comments and prints in the code.

All I need to do to read the actual password from the last eeprom dump is this:

eeprom.read_pwd_from_binary(eepromDump)

It was never easier. No reading miles long forum entries or sniffing around dubious websites to gain access to the password. However, it appears the story isn't always that simple. While doing my research, I found comments about TCPA encryption. So I don't know if my above encoding scheme is that. But I did read about the password also showing up unencoded sometimes. So be aware of that.

If it does, or if the encoding is different from mine for whatever reason, Lenovo invites you to use their backdoor: Simply remove the password altogether and overwriting it with 0x00s.

I implemented and tested both. Create a new password like "test123" or just replace it with seven 0x00s and the corresponding and very complex checksum: 0x00. To do that, I modify the binary we read from 0x57, where the password is stored, and rewrite those said locations 0x38 (8 bytes) and 0x40(8 bytes):

def write_new_password_to_binary(data, password = [0, 0, 0, 0, 0, 0, 0]):
    print("\nWriting and encoding new password:", end = " ")
    convertedPassword = [0] * 7
    
    for i in range(7):
        data[0x38 + i] = convert_value(password[i], 0)
        convertedPassword[i] = data[0x38 + i]
        print(chr(password[i]), end = "")
        
    checksum = calculate_checksum(convertedPassword)
    data[0x38 + 7] = checksum
    
    print("\nChecksum added to eeprom: ", hex(data[0x38 + 7]))
    
    print("Writing confirmation password (must be the same):", end = " ")
    for i in range(7):
        data[0x40 + i] = convert_value(password[i], 0)
        print(chr(password[i]), end = "")
    checksum = calculate_checksum(convertedPassword)
    print("\nAdding checksum to confirmation password: ", hex(checksum))
    data[0x40 + 7] = checksum
    
    return data

In execution this would look like this:

# Create a new password and put it back into the binary
newPassword = "test123"
# eepromDump = write_new_password_to_binary(eepromDump, convert_password_to_byte_array(newPassword))
# Alternatively, we simply delete the password from the EEPROM
eepromDump = eeprom.write_new_password_to_binary(eepromDump)
eeprom.write_binary_to_file(eepromDump, "eeprom_mod.bin", 0)

Now, we have a modified binary file and also stored it to disk to be able to double check everything went fine, if desired. All we need to do now is write this new modified binary file to the 24RF08 and restart the laptop. The supervisor password is now gone or set to the value you defined. In the example above it would be either gone or "test123".

Writing to your laptop eeprom is extremely dangerous! Do No Do It unless you know exactly what you do! ALWAYS have a backup of the original file to be able to recover when something goes wrong!

I found some example code to write 8 byte blocks to an eeprom and modified it like so (I would love to give the credits here but I honestly forgot where I found this):

# Write data to eeprom. Again, extremely danger!
################################################################################
# EXTREMELY DANGEROUS! DO NOT USE ON LIVE HARDWARE AND ESPECIALLY LAPTOPS!!!   #
################################################################################
def write_to_eeprom_8(bus, address, data, bs=8):
    print("\nWriting binay file back into EEPROM, length:", len(data))
    # Check number of bytes in the data field
    b_l = len(data)
    # split data into blocks of 8 bytes
    b_c = int(ceil(b_l / float(bs))) 
    
    # Create a list or something from the data to make 8 byte chunks
    blocks = [data[bs * x:][:bs] for x in range(b_c)]
    
    # Run through the blocks and send each block individually until all blocks sent
    for i, block in enumerate(blocks):
        # Calculate start address for each block
        start = i * bs
        
        # Send 9 bytes total. 1 byte row address and 8 bytes of data
        # DO NOT USE write_block_data() - very unreliable! Bit errors en mass!
        bus.write_i2c_block_data(address, start, block)
        print(bytes(block).hex(), end = " ")
        print(round((b_c/10*i), 1), end = "%\n")
        
        # Wait a bit to not overload the eeprom.
        sleep(0.01)
    print("100% - Writing to EEPROM completed!")

And then to do the actual execution I simply do

response = input("\n\nDo you really want to write to the EEPROM of your computer?\n************** THIS MIGHT BRICK YOUR LAPTOP!!! **************\nType: <Yes I want to> and hit enter (case sensitive, leave out the brackets!)...\n")
# # Pretending to write to the eeprom
if response == "Yes I want to":
    eeprom.write_to_eeprom_8(bus, 0x57, eepromDump)
else:
    print("Okey, I won't write anything...\n")

Note that simply pressing Enter doesn't write! You have to enter a phrase to write to the EEPROM to ensure you know what you're doing.

Please note two important things here: I am only writing THE LAST dump that I got back to the EEPROM and not the entire EEPROM. Only device address 0x57, nothing else is being touched! That works here because of how I read the EEPROM. As a reminder:

input("\nPress Enter to read EEPROM contents...\n")
print("Reading from EEPROM...")
eepromDump = eeprom.read_from_eeprom_8(bus, 0x54, 256)
eeprom.write_binary_to_file(eepromDump, "eeprom.bin", 0)
eepromDump = eeprom.read_from_eeprom_8(bus, 0x55, 256)
eeprom.write_binary_to_file(eepromDump, "eeprom.bin", 1)
eepromDump = eeprom.read_from_eeprom_8(bus, 0x56, 256)
eeprom.write_binary_to_file(eepromDump, "eeprom.bin", 1)
eepromDump = eeprom.read_from_eeprom_8(bus, 0x57, 256)
eeprom.write_binary_to_file(eepromDump, "eeprom.bin", 1) 

As you can see, I dump a part of the EEPROM into the variable eepromDump, write the 256 byte to a file and then overwrite the same byte array! That's why I can modify the only one section and then write that one 0x57 section back to the EEPROM.

And to be clear: I tested this before I started writing back to my laptop. I detached SDA and SCL from my laptop and plugged in my 24C02 with the address set to 0x57! That way I could write to my own EEPROM and double check my work before I unplugged the 24C02 and plugged my laptop back in.

IMPORTANT ITEM 2:

My laptop accesses this I2C bus periodically every four seconds! IT IS EXTREMELY DESTRUCTIVE AND WILL CAUSE DATA CORRUPTION if you access the I2C bus while the laptop itself is accessing the bus at the same time! I have an oscilloscope attached to the I2C bus to see what's going on and only accessing the bus when the laptop isn't! Please note, I didn't know this during my very first experiments four years ago and destroyed parts of the EEPROM. My RFID area is corrupted and the laptop complains loudly about it at every boot! No Good!

So be careful! Everything at your own risk!

Discussions