|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import argparse |
| 4 | +from collections import namedtuple |
| 5 | +import struct |
| 6 | +import sys |
| 7 | + |
| 8 | +from pcapng import FileScanner, blocks |
| 9 | + |
| 10 | +FORMATS = [ |
| 11 | + # Both binaries stitched right after another |
| 12 | + 'binary', |
| 13 | + # Flashimage, like it's on the flash, with padded regions |
| 14 | + 'flashimage', |
| 15 | + # Two CYACD files, like the CCG3 SDK outputs and FWUPD expects |
| 16 | + 'cyacd' |
| 17 | +] |
| 18 | + |
| 19 | +# CCG3 row size |
| 20 | +ROW_SIZE = 128 |
| 21 | +# CCG3 has a maximum of 1024 rows |
| 22 | +MAX_ROWS = 1024 |
| 23 | + |
| 24 | +DEBUG = False |
| 25 | +VERBOSE = False |
| 26 | + |
| 27 | +# Run the updater on Windows and capture the USB packets with Wireshark and USBPcap |
| 28 | +# Then you can use this script to extract the binary from it |
| 29 | +# |
| 30 | +# Sample files |
| 31 | +# -t dp -V 006 -b 1.7 dp-flash-006.pcapng |
| 32 | +# -t dp -V 008 -b 1.8 flash-100-to-8.pcapng |
| 33 | +# -t dp -V 100 -b 1.6 reflash100.pcapng |
| 34 | +# -t dp -V 101 -b 2.12 reflash101.pcapng |
| 35 | +# -t hdmi -V 005 -b 1.4 --second-first hdmi-flash-005.pcapng |
| 36 | +# -t hdmi -V 006 -b 1.29 hdmi-flash-6.pcapng |
| 37 | +# -t hdmi -V 102 -b 2.8 --second-first hdmi-flash-102.pcapng |
| 38 | +# -t hdmi -V 103 -b 2.6 --second-first hdmi-flash-103.pcapng |
| 39 | +# -t hdmi -V 104 -b 2.4 --second-first hdmi-flash-104.pcapng |
| 40 | +# -t hdmi -V 105 -b 1.26 hdmi-flash-105.pcapng |
| 41 | + |
| 42 | +# From https://github.com/JohnDMcMaster/usbrply/blob/master/usbrply/win_pcap.py#L171 |
| 43 | +# transfer_type=2 is URB_CONTROL |
| 44 | +# irp_info: 0 means from host, 1 means from device |
| 45 | +usb_urb_win_nt = namedtuple( |
| 46 | + 'usb_urb_win', |
| 47 | + ( |
| 48 | + # Length of entire packet entry including htis header and additional pkt_len data |
| 49 | + 'pcap_hdr_len', |
| 50 | + # IRP ID |
| 51 | + # buffer ID or something like that |
| 52 | + # it is not a unique packet ID |
| 53 | + # but can be used to match up submit and response |
| 54 | + 'id', |
| 55 | + # IRP_USBD_STATUS |
| 56 | + 'irp_status', |
| 57 | + # USB Function |
| 58 | + 'usb_func', |
| 59 | + # IRP Information |
| 60 | + # Ex: Direction: PDO => FDO |
| 61 | + 'irp_info', |
| 62 | + # USB port |
| 63 | + # Ex: 3 |
| 64 | + 'bus_id', |
| 65 | + # USB device on that port |
| 66 | + # Ex: 16 |
| 67 | + 'device', |
| 68 | + # Which endpoint on that bus |
| 69 | + # Ex: 0x80 (0 in) |
| 70 | + 'endpoint', |
| 71 | + # Ex: URB_CONTROL |
| 72 | + 'transfer_type', |
| 73 | + # Length of data beyond header |
| 74 | + 'data_length', |
| 75 | + )) |
| 76 | +usb_urb_win_fmt = ( |
| 77 | + '<' |
| 78 | + 'H' # pcap_hdr_len |
| 79 | + 'Q' # irp_id |
| 80 | + 'i' # irp_status |
| 81 | + 'H' # usb_func |
| 82 | + 'B' # irp_info |
| 83 | + 'H' # bus_id |
| 84 | + 'H' # device |
| 85 | + 'B' # endpoint |
| 86 | + 'B' # transfer_type |
| 87 | + 'I' # data_length |
| 88 | +) |
| 89 | +usb_urb_sz = struct.calcsize(usb_urb_win_fmt) |
| 90 | +def usb_urb(s): |
| 91 | + return usb_urb_win_nt(*struct.unpack(usb_urb_win_fmt, bytes(s))) |
| 92 | + |
| 93 | + |
| 94 | +def format_hex(buf): |
| 95 | + return ''.join('{:02x} '.format(x) for x in buf) |
| 96 | + |
| 97 | +def print_image_info(binary, index): |
| 98 | + rows = len(binary) |
| 99 | + size = rows * len(binary[0][1]) |
| 100 | + print("Image {} Size: {} B, {} rows".format(index, size, rows)) |
| 101 | + print(" FW at: 0x{:04X} Metadata at 0x{:04X}".format(binary[0][0], binary[-1][0])) |
| 102 | + |
| 103 | +def twos_comp(val, bits): |
| 104 | + """compute the 2's complement of int value val""" |
| 105 | + if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 |
| 106 | + val = val - (1 << bits) # compute negative value |
| 107 | + return val # return positive value as isdef twos_comp(val, bits): |
| 108 | + """compute the 2's complement of int value val""" |
| 109 | + if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 |
| 110 | + val = val - (1 << bits) # compute negative value |
| 111 | + return val # return positive value as isi |
| 112 | + |
| 113 | +def checksum_calc(s): |
| 114 | + sum = 0 |
| 115 | + for c in s: |
| 116 | + sum = (sum + c) & 0xFF |
| 117 | + sum = -(sum % 256) |
| 118 | + return (sum & 0xFF) |
| 119 | + |
| 120 | +def write_cyacd_row(f, row_no, data): |
| 121 | + # No idea what array ID is but it seems fine to keep it 0. Official builds also have that |
| 122 | + array_id = 0 |
| 123 | + data_len = len(data) |
| 124 | + |
| 125 | + # Sum all bytes and calc two's complement |
| 126 | + cs_bytes = bytes([array_id, row_no&0xFF, (row_no&0xFF00)>>8, data_len&0xFF, (data_len&0xFF00)>>8]) |
| 127 | + checksum = checksum_calc(cs_bytes+data) |
| 128 | + |
| 129 | + if data_len != 0x80: |
| 130 | + print("Len is {} instead of 0x80", data_len) |
| 131 | + sys.exit(1) |
| 132 | + data_hex = ''.join("{:02X}".format(x) for x in data) |
| 133 | + |
| 134 | + f.write(":{:02X}{:04X}{:04X}{}{:02X}\n".format(array_id, row_no, data_len, data_hex, checksum)) |
| 135 | + |
| 136 | +# Write the binary as cyacd file. Can only hold one firmware image per file |
| 137 | +def write_cyacd(path, binary1): |
| 138 | + with open(path, "w") as f: |
| 139 | + # CYACD Header |
| 140 | + # Si ID ######## |
| 141 | + # Si Rev ## |
| 142 | + # Checksum Type ## |
| 143 | + f.write("1D0011AD0000\n") |
| 144 | + |
| 145 | + for (addr, row) in binary1[0:-1]: |
| 146 | + write_cyacd_row(f, addr, row) |
| 147 | + |
| 148 | + write_cyacd_row(f, binary1[-1][0], binary1[-1][1]) |
| 149 | + |
| 150 | +# Just concatenate both firmware binaries |
| 151 | +def write_bin(path, binary1, binary2): |
| 152 | + with open(path, "wb") as f: |
| 153 | + for (_, row) in binary1: |
| 154 | + f.write(row) |
| 155 | + |
| 156 | + for (_, row) in binary2: |
| 157 | + f.write(row) |
| 158 | + |
| 159 | +# Write the binary in the same layout with padding as on flash |
| 160 | +def write_flashimage(path, binary1, binary2): |
| 161 | + with open(path, "wb") as f: |
| 162 | + # Write fist padding |
| 163 | + # Verified |
| 164 | + print("Padding 1: {:04X} to {:04X}".format(0, binary1[0][0])) |
| 165 | + for row in range(0, binary1[0][0]): |
| 166 | + f.write(b'\0' * ROW_SIZE) |
| 167 | + |
| 168 | + # Verified |
| 169 | + print("FW IMG 1: {:04X} to {:04X}".format(binary1[0][0], binary1[-2][0])) |
| 170 | + for (addr, row) in binary1[0:-1]: |
| 171 | + f.write(row) |
| 172 | + |
| 173 | + print("Padding 2: {:04X} to {:04X}".format(binary1[-2][0], binary2[0][0]-1)) |
| 174 | + for row in range(binary1[-2][0], binary2[0][0]-1): |
| 175 | + f.write(b'\0' * ROW_SIZE) |
| 176 | + |
| 177 | + print("FW IMG 2: {:04X} to {:04X}".format(binary2[0][0], binary2[-2][0])) |
| 178 | + for (addr, row) in binary2[0:-1]: |
| 179 | + f.write(row) |
| 180 | + |
| 181 | + print("Padding 3: {:04X} to {:04X}".format(binary2[-2][0], binary2[-1][0]-1)) |
| 182 | + for row in range(binary2[-2][0], binary2[-1][0]-1): |
| 183 | + f.write(b'\0' * ROW_SIZE) |
| 184 | + |
| 185 | + # Notice that these are in reverse order! |
| 186 | + # FW2 metadata is before FW1 metadata |
| 187 | + print("Metadata 2: {:04X} to {:04X}".format(binary2[-1][0], binary2[-1][0])) |
| 188 | + f.write(binary2[-1][1]) |
| 189 | + print("Metadata 1: {:04X} to {:04X}".format(binary1[-1][0], binary1[-1][0])) |
| 190 | + f.write(binary1[-1][1]) |
| 191 | + |
| 192 | + # Pad from end of FW1 metadata |
| 193 | + print("Padding 4: {:04X} to {:04X}".format(binary1[-1][0], MAX_ROWS-1)) |
| 194 | + for row in range(binary1[-1][0], MAX_ROWS-1): |
| 195 | + f.write(b'\0' * ROW_SIZE) |
| 196 | + |
| 197 | + |
| 198 | +def check_assumptions(img1_binary, img2_binary): |
| 199 | + # TODO: Check that addresses are in order |
| 200 | + # TODO: Metadata right after each other |
| 201 | + |
| 202 | + # Check assumptions that the updater relies on |
| 203 | + if len(img1_binary) != len(img2_binary): |
| 204 | + print("VIOLATED Assumption that both images are of the same size!") |
| 205 | + sys.exit(1) |
| 206 | + if len(img1_binary[0][1]) != ROW_SIZE: |
| 207 | + print("VIOLATED Assumption that the row size is {} bytes! Is: {}", ROW_SIZE, img1_binary[0][1]) |
| 208 | + sys.exit(1) |
| 209 | + if img1_binary[0][0] != 0x0030: |
| 210 | + print("VIOLATED Assumption that start row of image 1 is at 0x0030. Is at 0x{:04X}".format(img1_binary[0][0])) |
| 211 | + sys.exit(1) |
| 212 | + if img1_binary[-1][0] != 0x03FF: |
| 213 | + print("VIOLATED Assumption that metadata row of image 1 is at 0x03FF. Is at 0x{:04X}".format(img1_binary[-1][0])) |
| 214 | + sys.exit(1) |
| 215 | + if img2_binary[0][0] != 0x0200: |
| 216 | + print("VIOLATED Assumption that start row of image 2 is at 0x0200. Is at 0x{:04X}".format(img2_binary[0])) |
| 217 | + sys.exit(1) |
| 218 | + if img2_binary[-1][0] != 0x03FE: |
| 219 | + print("VIOLATED Assumption that metadata row of image 2 is at 0x03FE. Is at 0x{:04X}".format(img2_binary[-1][0])) |
| 220 | + sys.exit(1) |
| 221 | + if img1_binary == img2_binary: |
| 222 | + print("VIOLATED Assumption that both images are not the same"); |
| 223 | + sys.exit(1) |
| 224 | + |
| 225 | + |
| 226 | +def decode_pcapng(path, bus_id, dev, second_first): |
| 227 | + img1_binary = [] # [(addr, row)] |
| 228 | + img2_binary = [] # [(addr, row)] |
| 229 | + with open(path, "rb") as f: |
| 230 | + scanner = FileScanner(f) |
| 231 | + block_no = 1 |
| 232 | + for i, block in enumerate(scanner): |
| 233 | + if type(block) is blocks.EnhancedPacket or type(block) is blocks.SimplePacket: |
| 234 | + img1 = False |
| 235 | + img2 = False |
| 236 | + |
| 237 | + #print(block_no, block.packet_len, block.packet_data) |
| 238 | + packet = block.packet_data |
| 239 | + urb = usb_urb(packet[0:usb_urb_sz]) |
| 240 | + |
| 241 | + # Filter device |
| 242 | + if urb.bus_id == bus_id and urb.device == dev: |
| 243 | + img1 = True |
| 244 | + elif urb.bus_id == bus_id and urb.device == dev+1: |
| 245 | + img2 = True |
| 246 | + else: |
| 247 | + #print(f"Other device bus_id: {urb.bus_id}, dev: {urb.device}") |
| 248 | + #print(f"bus_id: {bus_id}, dev_id: {dev}") |
| 249 | + continue |
| 250 | + |
| 251 | + # Only consider outgoing packets |
| 252 | + if urb.irp_info != 0: |
| 253 | + continue |
| 254 | + |
| 255 | + #print(block_no, urb) |
| 256 | + |
| 257 | + # Skip small packets |
| 258 | + if urb.data_length != 140: |
| 259 | + continue |
| 260 | + |
| 261 | + if DEBUG: |
| 262 | + print(block_no, " ", format_hex(packet)) |
| 263 | + |
| 264 | + hid_packet = packet[36:] |
| 265 | + if DEBUG: |
| 266 | + print(block_no, " ", format_hex(hid_packet)) |
| 267 | + |
| 268 | + addr = (hid_packet[3] << 8) + hid_packet[2] |
| 269 | + payload = hid_packet[4:] |
| 270 | + if VERBOSE: |
| 271 | + print("{:4d} 0x{:08X} {}".format(block_no, addr, format_hex(payload))) |
| 272 | + |
| 273 | + if img1: |
| 274 | + if second_first: |
| 275 | + img2_binary.append((addr, payload)) |
| 276 | + else: |
| 277 | + img1_binary.append((addr, payload)) |
| 278 | + elif img2: |
| 279 | + if second_first: |
| 280 | + img1_binary.append((addr, payload)) |
| 281 | + else: |
| 282 | + img2_binary.append((addr, payload)) |
| 283 | + |
| 284 | + block_no += 1 |
| 285 | + else: |
| 286 | + pass |
| 287 | + #print(block) |
| 288 | + return (img1_binary, img2_binary) |
| 289 | + |
| 290 | + |
| 291 | +def main(args): |
| 292 | + [bus_id, dev] = args.bus_dev.split('.') |
| 293 | + (img1_binary, img2_binary) = decode_pcapng(args.pcap, int(bus_id), int(dev), args.second_first) |
| 294 | + |
| 295 | + check_assumptions(img1_binary, img2_binary) |
| 296 | + |
| 297 | + print("Firmware version: {}".format(args.version)) |
| 298 | + |
| 299 | + print_image_info(img1_binary, 1) |
| 300 | + print_image_info(img2_binary, 2) |
| 301 | + |
| 302 | + if args.format == 'binary': |
| 303 | + write_bin("{}-{}.bin".format(args.type, args.version), img1_binary, img2_binary) |
| 304 | + elif args.format == 'flashimage': |
| 305 | + write_flashimage("{}-{}.bin".format(args.type, args.version), img1_binary, img2_binary) |
| 306 | + elif args.format == 'cyacd': |
| 307 | + write_cyacd("{}-{}-1.cyacd".format(args.type, args.version), img1_binary) |
| 308 | + write_cyacd("{}-{}-2.cyacd".format(args.type, args.version), img2_binary) |
| 309 | + else: |
| 310 | + print(f"Invalid Format {args.format}") |
| 311 | + sys.exit(1) |
| 312 | + |
| 313 | +if __name__ == "__main__": |
| 314 | + parser = argparse.ArgumentParser(description='Extract firmware from PCAPNG capture') |
| 315 | + parser.add_argument('-t', '--type', help='Which type of card', required=True, choices=['dp', 'hdmi']) |
| 316 | + parser.add_argument('-V', '--version', help='Which firmware version', required=True) |
| 317 | + parser.add_argument('-f', '--format', help='Which output format', required=True, choices=FORMATS) |
| 318 | + parser.add_argument('-v', '--verbose', help='Verbose', action='store_true') |
| 319 | + parser.add_argument('-b', '--bus-dev', help='Bus ID and Device of first time. Example: 1.23') |
| 320 | + parser.add_argument('--second-first', help='If the second image was update first', default=False, action='store_true') |
| 321 | + parser.add_argument('pcap', help='Path to the pcap file') |
| 322 | + args = parser.parse_args() |
| 323 | + |
| 324 | + if args.verbose: |
| 325 | + VERBOSE = True |
| 326 | + |
| 327 | + main(args) |
0 commit comments