Created
April 29, 2026 22:31
-
-
Save ysfchn/42d196ab4f58bb0a6deccf09cf765be5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # This program is free software: you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published | |
| # by the Free Software Foundation, version 3 of the License. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| # | |
| """ | |
| SD cards has an Card Identification Number (CID) embedded into the chip and set once during | |
| the factory process. The CID is written once and locked as read-only to ensure it never gets | |
| changed. | |
| Most of the time, the CID value of the SD card shouldn't matter, however there are some | |
| devices exist that specifically require a specific SD card model, the most common example is | |
| car GPS systems. | |
| Fortunately, Samsung forgot to lock their own SD cards after writing CID in the factory, | |
| making it possible to change the CID on our own and ending up spoofing the SD card appear | |
| like an another model of another brand. Note that Samsung updated their cards since, so | |
| this exploit won't work anymore unless you happen to find an old Samsung microSD card | |
| produced many years ago (between 2011 and 2017). | |
| The original program "evoplus_cid" is written by Richard Antony Burton and licensed under | |
| "GNU General Public License version 3" (GPLv3): | |
| https://github.com/raburton/evoplus_cid | |
| See also related blog posts: | |
| https://richard.burtons.org/2016/07/01/changing-the-cid-on-an-sd-card/ | |
| https://richard.burtons.org/2016/07/31/cid-change-on-sd-card-update-evoplus_cid/ | |
| https://orestbida.com/blog/cid-changeable-sdcards/ (for a list of compatible cards) | |
| I've ported the original program into a Python script, so it can be run without the need | |
| of compiling. However, as with the original program, this Python script can only work on | |
| Unix platforms due to "ioctl" call, and it can only work in CPython implementation of Python. | |
| To use the script you also need an on-board SD card slot on your host computer, USB SD card | |
| readers won't work! | |
| USE AT YOUR OWN RISK! | |
| """ | |
| from argparse import ArgumentParser | |
| from fcntl import ioctl | |
| import os | |
| from pathlib import Path | |
| import struct | |
| from sys import stderr | |
| from typing import Annotated, NamedTuple, Tuple, Union, cast, get_args | |
| from ctypes import create_string_buffer, addressof | |
| # _IOWR(MMC_BLOCK_MAJOR, 0, struct mmc_ioc_cmd) | |
| MMC_IOC_CMD = 0xc048b300 | |
| SAMSUNG_VENDOR_OPCODE = 62 | |
| PROGRAM_CID_OPCODE = 26 | |
| # https://github.com/torvalds/linux/blob/dca922e019dd758b4c1b4bec8f1d509efddeaab4/include/linux/mmc/core.h#L35 | |
| MMC_RSP_PRESENT = 1 << 0 | |
| MMC_RSP_CRC = 1 << 2 | |
| MMC_RSP_BUSY = 1 << 3 | |
| MMC_RSP_OPCODE = 1 << 4 | |
| MMC_CMD_AC = 0 << 5 | |
| MMC_RSP_SPI_S1 = 1 << 7 | |
| MMC_RSP_SPI_R1 = MMC_RSP_SPI_S1 | |
| MMC_RSP_R1B = MMC_RSP_PRESENT | MMC_RSP_CRC | MMC_RSP_OPCODE | MMC_RSP_BUSY | |
| MMC_RSP_R1 = MMC_RSP_PRESENT | MMC_RSP_CRC | MMC_RSP_OPCODE | |
| MMC_CMD_ADTC = 1 << 5 | |
| # https://github.com/torvalds/linux/blob/dca922e019dd758b4c1b4bec8f1d509efddeaab4/include/uapi/linux/mmc/ioctl.h | |
| class MmcIocCmd(NamedTuple): | |
| write_flag: Annotated[int, "i"] = 0 | |
| is_acmd: Annotated[int, "i"] = 0 | |
| opcode: Annotated[int, "I"] = 0 | |
| arg: Annotated[int, "I"] = 0 | |
| response: Annotated[Tuple[int, int, int, int], "4I"] = (0, 0, 0, 0, ) | |
| flags: Annotated[int, "I"] = 0 | |
| blksz: Annotated[int, "I"] = 0 | |
| blocks: Annotated[int, "I"] = 0 | |
| postsleep_min_us: Annotated[int, "I"] = 0 | |
| postsleep_max_us: Annotated[int, "I"] = 0 | |
| data_timeout_ns: Annotated[int, "I"] = 0 | |
| cmd_timeout_ms: Annotated[int, "I"] = 0 | |
| pad: Annotated[int, "I"] = 0 | |
| data_ptr: Annotated[int, "Q"] = 0 | |
| def pack(self): | |
| struct_format = "" | |
| values = [] | |
| for key, hint in self.__class__.__annotations__.items(): | |
| struct_format += get_args(hint)[1] | |
| current = getattr(self, key) | |
| values.extend(current if isinstance(current, (tuple, list, bytes)) else [current]) | |
| return bytearray(struct.pack("<" + struct_format, *values)) | |
| def calculate_crc7(data: bytes) -> int: | |
| crc = 0 | |
| poly = 0x89 | |
| for byte in data: | |
| for bit in range(8): | |
| bit_val = (byte >> (7 - bit)) & 1 | |
| crc_msb = (crc >> 6) & 1 | |
| crc <<= 1 | |
| if crc_msb ^ bit_val: | |
| crc ^= poly | |
| crc &= 0x7F | |
| return (crc << 1) | 1 | |
| def cid_with_crc(data: bytes): | |
| """ | |
| Takes an existing CID value and returns it with CRC field included (= 16 bytes). | |
| The given CID value may or may not contain a CRC field. If it does, then it will be validated | |
| and returned as-is, if CRC is NULL (0x00) or non-existing (= 15 bytes), instead it will return | |
| a new CID value with its CRC value correctly set. | |
| """ | |
| # Add CRC field if missing | |
| if len(data) == 15: | |
| return data + bytes([calculate_crc7(data)]) | |
| if len(data) != 16: | |
| raise ValueError("Incomplete CID, must be 15 (without CRC) or 16 (with CRC) bytes") | |
| last_byte = data[-1] | |
| # Replace CRC field if it is reported 0x00 in Linux systems | |
| if last_byte == 0: | |
| return cid_with_crc(data[:-1]) | |
| # Otherwise, validate the existing CRC | |
| if last_byte != calculate_crc7(data[:-1]): | |
| raise ValueError("A CID was provided with non-matching CRC, remove or null out last byte to generate") | |
| return data | |
| def send_vendor_command(fd: int, arg: int): | |
| cmd = MmcIocCmd( | |
| write_flag = 1, | |
| data_timeout_ns = 0x10000000, | |
| opcode = SAMSUNG_VENDOR_OPCODE, | |
| arg = arg, | |
| flags = MMC_RSP_R1B | MMC_CMD_AC | |
| ) | |
| print(cmd, file = stderr) | |
| packed = cmd.pack() | |
| assert len(packed) == 72 | |
| print(packed.hex(" "), file = stderr) | |
| returned = ioctl(fd, MMC_IOC_CMD, packed) | |
| print("command return value", returned, file = stderr) | |
| return returned | |
| def write_cid(fd: int, cid_bytes: bytes): | |
| buf = create_string_buffer(cid_bytes) | |
| address = addressof(buf) | |
| cmd = MmcIocCmd( | |
| write_flag = 1, | |
| data_timeout_ns = 0x10000000, | |
| opcode = PROGRAM_CID_OPCODE, | |
| blksz = 16, # CID size = 16 | |
| blocks = 1, | |
| data_ptr = address, | |
| flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC | |
| ) | |
| print("writing cid...", file = stderr) | |
| print(cmd, file = stderr) | |
| packed = cmd.pack() | |
| assert len(packed) == 72 | |
| print(packed.hex(" "), file = stderr) | |
| returned = ioctl(fd, MMC_IOC_CMD, packed) | |
| print("command return value", returned, file = stderr) | |
| return returned | |
| def unlock_card(fd: int): | |
| """ | |
| Unlocks the card for writing the CID | |
| """ | |
| print("stage 1...", file = stderr) | |
| assert not send_vendor_command(fd, 0xEFAC62EC), "failed to enter vendor mode, make sure it is an CID writable card!" | |
| print("stage 2...", file = stderr) | |
| assert not send_vendor_command(fd, 0xEF50), "couldn't unlock the card!" | |
| print("stage 3...", file = stderr) | |
| assert not send_vendor_command(fd, 0x00DECCEE), "failed to exit from vendor mode!" | |
| def read_cid(device_path: Union[str, Path]): | |
| """ | |
| Read the CID value of the card and fix the CRC value if it is wrong. | |
| """ | |
| cid_path = Path(device_path) / "device/cid" | |
| if not cid_path.exists(): | |
| raise FileNotFoundError( | |
| "couldn't detect a CID reporting SD card device in given device path, make sure the SD card is attached " | |
| "to an on-board SD card slot, USB SD adapters won't work!" | |
| ) | |
| current = bytes.fromhex(cid_path.read_text()) | |
| print(f"current cid of {str(device_path)} reported:", file = stderr) | |
| print(current.hex(), file = stderr) | |
| fixed = cid_with_crc(current) | |
| if current != fixed: | |
| print("though the above value is wrongly reported by kernel with a null crc value, correct one is:", file = stderr) # noqa: E501 | |
| print(fixed.hex(), file = stderr) | |
| return fixed | |
| def main(): | |
| if os.geteuid() != 0: | |
| print("script must be run as root!") | |
| exit(1) | |
| parser = ArgumentParser() | |
| parser.add_argument("path", help = "path of the device, like only 'mmcblk0' part", type = str) | |
| parser.add_argument("--write", "-w", help = "new CID to write as a hexadecimal string (15 or 16 bytes)", type = str, required = False) # noqa: E501 | |
| data = parser.parse_args() | |
| dev_path = Path("/sys/block") / cast(str, data.path) | |
| node_path = Path("/dev") / cast(str, data.path) | |
| new_cid = None if not data.write else cid_with_crc(bytes.fromhex(cast(str, data.write))) | |
| print("trying to reading CID value of the card...", file = stderr) | |
| read_cid(dev_path) | |
| print("backup that value just in case!", file = stderr) | |
| if not new_cid: | |
| print("done.", file = stderr) | |
| exit(0) | |
| print("this operation will set the card's CID value to:", new_cid, file = stderr) | |
| prompt = input("type \"yes\" in lowercase and without quotes to confirm and hit enter: ") | |
| if prompt != "yes": | |
| print("cancelled.", file = stderr) | |
| exit(1) | |
| print("trying to access the card...", file = stderr) | |
| fd = os.open(node_path, os.O_RDWR) | |
| print(f"trying to unlock the card @ {fd:x}...", file = stderr) | |
| unlock_card(fd) | |
| print("the card is now seems to be unlocked.", file = stderr) | |
| write_cid(fd, new_cid) | |
| print("set CID to:", new_cid.hex(), file = stderr) | |
| print("done!", file = stderr) | |
| os.close(fd) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment