From c11a1bc5a67088fa97722f45593f9a21d3899082 Mon Sep 17 00:00:00 2001 From: Bjoern Kerler Date: Thu, 5 Aug 2021 09:04:49 +0200 Subject: [PATCH] Restructure --- README.md | 22 +- edl.py | 26 +- edl/Config/__init__.py | 0 edl/Config/qualcomm_config.py | 677 ++++++++++++++ edl/Config/usb_ids.py | 10 + edl/Library/Modules/__init__.py | 0 edl/Library/Modules/generic.py | 113 +++ edl/Library/Modules/init.py | 99 ++ edl/Library/Modules/oneplus.py | 602 ++++++++++++ edl/Library/Modules/xiaomi.py | 45 + edl/Library/TestFiles/gpt_sm8180x.bin | Bin 0 -> 24576 bytes edl/Library/__init__.py | 0 edl/Library/asmtools.py | 247 +++++ edl/Library/cryptutils.py | 500 ++++++++++ edl/Library/firehose.py | 1238 +++++++++++++++++++++++++ edl/Library/firehose_client.py | 920 ++++++++++++++++++ edl/Library/gpt.py | 357 +++++++ edl/Library/hdlc.py | 240 +++++ edl/Library/memparse.py | 78 ++ edl/Library/nand_config.py | 789 ++++++++++++++++ edl/Library/pt.py | 171 ++++ edl/Library/pt64.py | 153 +++ edl/Library/sahara.py | 834 +++++++++++++++++ edl/Library/sparse.py | 236 +++++ edl/Library/streaming.py | 1069 +++++++++++++++++++++ edl/Library/streaming_client.py | 436 +++++++++ edl/Library/usblib.py | 658 +++++++++++++ edl/Library/usbscsi.py | 52 ++ edl/Library/utils.py | 587 ++++++++++++ edl/Library/xmlparser.py | 36 + edl/Windows/libusb-1.0.dll | Bin 0 -> 166912 bytes edl/__init__.py | 0 32 files changed, 10173 insertions(+), 22 deletions(-) create mode 100755 edl/Config/__init__.py create mode 100644 edl/Config/qualcomm_config.py create mode 100644 edl/Config/usb_ids.py create mode 100644 edl/Library/Modules/__init__.py create mode 100644 edl/Library/Modules/generic.py create mode 100644 edl/Library/Modules/init.py create mode 100755 edl/Library/Modules/oneplus.py create mode 100644 edl/Library/Modules/xiaomi.py create mode 100644 edl/Library/TestFiles/gpt_sm8180x.bin create mode 100755 edl/Library/__init__.py create mode 100755 edl/Library/asmtools.py create mode 100755 edl/Library/cryptutils.py create mode 100755 edl/Library/firehose.py create mode 100644 edl/Library/firehose_client.py create mode 100755 edl/Library/gpt.py create mode 100755 edl/Library/hdlc.py create mode 100755 edl/Library/memparse.py create mode 100644 edl/Library/nand_config.py create mode 100755 edl/Library/pt.py create mode 100755 edl/Library/pt64.py create mode 100755 edl/Library/sahara.py create mode 100755 edl/Library/sparse.py create mode 100755 edl/Library/streaming.py create mode 100644 edl/Library/streaming_client.py create mode 100755 edl/Library/usblib.py create mode 100755 edl/Library/usbscsi.py create mode 100755 edl/Library/utils.py create mode 100755 edl/Library/xmlparser.py create mode 100755 edl/Windows/libusb-1.0.dll create mode 100755 edl/__init__.py diff --git a/README.md b/README.md index 4ea9041..0ac962b 100755 --- a/README.md +++ b/README.md @@ -33,10 +33,24 @@ sudo python3 -m pip install -r requirements.txt ``` Windows: -- Boot device into 9008 mode, install Qualcomm_Diag_QD_Loader_2016_driver.exe from Drivers\Windows -- Use Zadig 2.5 or higher, list all devices, select QUSB_BULK device and replace - driver with libusb >= 1.2.6.0 one (will replace original driver) -- Get latest Zadig release [here] (https://zadig.akeo.ie/) +#### Install python + git +- Install python 3.9 and git +- If you install python from microsoft store, "python setup.py install" will fail, but that step isn't required. +- WIN+R ```cmd``` + +#### Grab files and install +``` +git clone https://github.com/bkerler/edl +cd edl +pip3 install -r requirements.txt +``` + +#### Get latest UsbDk 64-Bit +- Install normal QC 9008 Serial Port driver (or use default Windows COM Port one, make sure no exclamation is seen) +- Get usbdk installer (.msi) from [here](https://github.com/daynix/UsbDk/releases/) and install it +- Test on device connect using "UsbDkController -n" if you see a device with pid 0x9008 +- Works fine under Windows 10 and 11 :D + ## Convert EDL loaders for automatic usage diff --git a/edl.py b/edl.py index 19bc7f2..58f4103 100755 --- a/edl.py +++ b/edl.py @@ -119,28 +119,18 @@ import logging import subprocess import re from docopt import docopt -from Library.utils import LogBase -from Library.usblib import UsbClass -from Library.sahara import sahara -from Library.streaming_client import streaming_client -from Library.firehose_client import firehose_client -from Library.streaming import Streaming +from edl.Config.usb_ids import default_ids +from edl.Library.utils import LogBase +from edl.Library.usblib import UsbClass +from edl.Library.sahara import sahara +from edl.Library.streaming_client import streaming_client +from edl.Library.firehose_client import firehose_client +from edl.Library.streaming import Streaming from binascii import hexlify args = docopt(__doc__, version='3') -default_ids = [ - [0x05c6, 0x9008, -1], - [0x05c6, 0x900e, -1], - [0x05c6, 0x9025, -1], - [0x1199, 0x9062, -1], - [0x1199, 0x9070, -1], - [0x1199, 0x9090, -1], - [0x0846, 0x68e0, -1], - [0x19d2, 0x0076, -1] -] - -print("Qualcomm Sahara / Firehose Client V3.4 (c) B.Kerler 2018-2021.") +print("Qualcomm Sahara / Firehose Client V3.5 (c) B.Kerler 2018-2021.") def parse_cmd(rargs): diff --git a/edl/Config/__init__.py b/edl/Config/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/edl/Config/qualcomm_config.py b/edl/Config/qualcomm_config.py new file mode 100644 index 0000000..2ceb949 --- /dev/null +++ b/edl/Config/qualcomm_config.py @@ -0,0 +1,677 @@ +vendor = { + 0x0000: "Qualcomm ", + 0x0001: "Foxconn/Sony ", + 0x0004: "ZTE ", + 0x0011: "Smartisan ", + 0x0015: "Huawei ", + 0x0017: "Lenovo ", + 0x0020: "Samsung ", + 0x0029: "Asus ", + 0x0030: "Haier ", + 0x0031: "LG ", + 0x0035: "Foxconn/Nokia", + 0x0042: "Alcatel ", + 0x0045: "Nokia ", + 0x0048: "YuLong ", + 0x0051: "Oppo/Oneplus ", + 0x0072: "Xiaomi ", + 0x0073: "Vivo ", + 0x0130: "GlocalMe ", + 0x0139: "Lyf ", + 0x0168: "Motorola ", + 0x01B0: "Motorola ", + 0x0208: "Motorola ", + 0x0228: "Motorola ", + 0x2A96: "Micromax ", + 0x02E8: "Lenovo ", + 0x0328: "Motorola ", + 0x0368: "Motorola ", + 0x03C8: "Motorola ", + 0x00C8: "Motorola ", + 0x0348: "Motorola ", + 0x1043: "Asus ", + 0x1111: "Asus ", + 0x143A: "Asus ", + 0x1978: "Blackphone ", + 0x2A70: "Oxygen " +} + +root_cert_hash = { + "secboot_sha2_pss_subca1" : "afca69d4235117e5bfc21467068b20df85e0115d7413d5821883a6d244961581", + "secboot_sha2_pss_subca2" : "d40eee56f3194665574109a39267724ae7944134cd53cb767e293d3c40497955bc8a4519ff992b031fadc6355015ac87", + "old" : "cc3153a80293939b90d02d3bf8b23e0292e452fef662c74998421adad42a380f", + "new" : "7be49b72f9e4337223ccb84d6eccca4e61ce16e3602ac2008cb18b75babe6d09", + "mdm9x60_tel" : "36c886068d9a6634e9c55185044344e9e756dcc3b5960874942c7a1a1550dee0" +} + +msmids = { + # cc3153a80293939b90d02d3bf8b23e0292e452fef662c74998421adad42a380f pkhash/root-cert + # 7be49b72f9e4337223ccb84d6eccca4e61ce16e3602ac2008cb18b75babe6d09 pkhash/root-cert + 0x9440E1: "QDF2432", + 0x9780E1: "IPQ4018", + 0x9790E1: "IPQ4019", + 0x0160E1: "QCA4020", + 0x9680E1: "APQ8009", + 0x7060E1: "APQ8016", + 0x8100E1: "APQ806x", + 0x9D00E1: "APQ8076", + 0x08A0E1: "APQ807x", + 0x9000E1: "APQ8084", + 0x9630E1: "APQ8092", + 0x0940E1: "MSM8905", + 0x9600E1: "MSM8909", # SnapDragon 210 + 0x0510E1: "MSM8909W", + 0x7050E1: "MSM8916", # SnapDragon 410 + 0x0560E1: "MSM8917", + 0x0860E1: "MSM8920", + 0x91B0E1: "MSM8929", # SnapDragon 415 + 0x04F0E1: "MSM8937", + 0x90B0E1: "MSM8939", # SnapDragon 610 + 0x90C0E1: "APQ8036", + 0x90D0E1: "APQ8039", + 0x06B0E1: "MSM8940", + 0x9720E1: "MSM8952", # SnapDragon 652 + 0x0460E1: "MSM8953", # 8053lat + 0x0660E1: "APQ8053", + 0x9900E1: "MSM8976", # SnapDragon 652 + 0x9690E1: "MSM8992", # SnapDragon 82x + 0x9400E1: "MSM8994", # SnapDragon 808 + 0x9470E1: "MSM8996", # SnapDragon 820 + 0x06F0E1: "MSM8996AU", + 0x05E0E1: "MSM8998_SDM835", + 0x94B0E1: "MSM9055", + 0x9730E1: "MDM9206_MDM9607tx", + 0x04A0E1: "MDM9607", + 0x8090E1: "MDM9916", + 0x80B0E1: "MDM9955", + 0x9210E1: "MDM9x35", + 0x9500E1: "MDM9x40", + 0x9540E1: "MDM9x45", + 0x03A0E1: "MDM9x50", + 0x7F50E1: "MDM9x25", + 0x0320E1: "MDM9250", # MDM9x50 + 0x0340E1: "MDM9255", # MDM9x55 + 0x0390E1: "MDM9350", # MDM9x50 + 0x03B0E1: "MDM9x55", + 0x07D0E1: "MDM9x60", # SDX20 + 0x07F0E1: "MDM9x65", + 0x1280E1: "fsm100xx", + 0x1650E1: "FSM10000", + 0x1680E1: "FSM10005", + 0x1690E1: "FSM10010", + 0x16A0E1: "FSM10051", + 0x16B0E1: "FSM10056", + 0x1530E1: "ipq5018", + 0x1610E1: "olympic_manar", + 0x1060E1: "qm215", + 0x0BE0E1: "SDM429", + 0x0BF0E1: "SDM439", + 0x09A0E1: "SDM450", + 0x0AC0E1: "SDM630", #0x30070x00 + 0x0BA0E1: "SDM632", + 0x0BB0E1: "SDA632", + 0x08C0E1: "SDM660", # 0x30060000 soc_hw_version + 0x07B0E1: "SDX50M", # 0x soc_hw_version, + 0x0E50E1: "SDX55:CD90-PG591", # 0x600b0100 soc_hw_version, 0x8fff7000 dbgpolicy 32Bit, 0x8FCFD000 sec.elf 64Bit + 0x0CF0E1: "SDX55M:CD90-PH809", # 0x600b0100 soc_hw_version, 0x8fff7000 dbgpolicy 32Bit, 0x8FCFD000 sec.elf 64Bit, # Netgear MR5100, sdxprairie + 0x1250E1: "SA515M", + + # afca69d4235117e5bfc21467068b20df85e0115d7413d5821883a6d244961581 + 0x0AB0E1: "QCA6290", # 0x40040100 soc_hw_version + 0x0D90E1: "QCA6390", # 0x400A0000 soc_hw_version + 0x1310E1: "QCA6480", + 0x12E0E1: "QCA6481", + 0x12D0E1: "QCA6491", + 0x0D70E1: "QCA6595", # 0x400B0000 soc_hw_version + 0x0D30E1: "QCN7605", # 0x400B0000 soc_hw_version + 0x0D50E1: "QCN7606", # 0x400B0000 soc_hw_version + 0x0910E1: "SDM670", # 0x60040100 soc_hw_version + 0x0DB0E1: "SDM710", + 0x0AA0E1: "QCS605", + 0x0ED0E1: "SXR1120", + 0x0EA0E1: "SXR1130", + 0x08E0E1: "SDA845", + + # d40eee56f3194665574109a39267724ae7944134cd53cb767e293d3c40497955 + # d40eee56f3194665574109a39267724ae7944134cd53cb767e293d3c40497955bc8a4519ff992b031fadc6355015ac87 pk-hash/root-cert + 0x1260E1: "IPQ6018", + 0x1070E1: "MDM9205", # 0x20130100 + 0x1450E1: "agatti", # soc_vers 0x9003 + 0x13F0E1: "bitra_SDM", # soc_vers 0x6012 + 0x1410E1: "bitra_SDA", + 0x1590E1: "cedros", # soc_vers 0x6017 + 0x1360E1: "kamorta", # soc_vers 0x9002 SnapDragon 662/460 SM4250/SM4350, bengal + 0x1350E1: "lahaina", # soc_vers 0x600F sm8350, SDM875 + 0x1420E1: "lahaina_premier", + 0x14A0E1: "makena", # soc_vers 0x6014 + 0x14B0E1: "SA8295P", + 0x14C0E1: "SA8540P", + #0x1610E1: "mannar", # soc_vers 0x9004 + 0x1470E1: "moselle", # soc_vers 0x4014 + 0x10A0E1: "nicobar", # 0x90010100 soc_hw_version, 0x45FFF000 sec.elf 64Bit, 0x101FF000 dbgpolicy, 64Bit + 0x10B0E1: "qcn90xx", # soc_vers 0x400D + 0x10C0E1: "QCN9001", + 0x1150E1: "QCN9002", + 0x10D0E1: "QCN9003", + 0x10E0E1: "QCN9010", + 0x10F0E1: "QCN9011", + 0x1110E1: "QCN9012", + 0x1140E1: "QCN9013", + 0x0AF0E1: "qcs405", # 0x20140000 soc_hw_version, 0x863DB000 sec.elf 64Bit, 0x863DE000 dbgpolicy, 64Bit + 0x0400E1: "rennell", # soc_vers 0x600E7T A11 CB + 0x12A0E1: "rennell", + 0x12B0E1: "rennell_premier", + 0x1490E1: "rennell_v1.1", + 0x1630E1: "sd7250", + 0x11E0E1: "saipan", # 0x600D0100 soc_hw_version, 0x808FF000 sec.elf 64Bit, 0x1C000000 dbgpolicy, 64Bit, SM7250 Snapdragon 765G + 0x0950E1: "SM6150", # 0x60070100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0EC0E1: "SM6150p", # 0x60070100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0E60E1: "SM7150", # 0x600C0100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0A50E1: "SDM855_SM8150", # Hana 0x60030100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0A60E1: "SDM855p_SM8150p", # Hana 0x60030100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0C30E1: "SM8250:CD90-PH805-1A", # Kona, 0x60080100 soc_hw_version, 0x808FF000 sec.elf 64Bit, 0x1C000000 dbgpolicy, 64Bit + 0x0CE0E1: "SM8250:CD90-PH806-1A", # Kona 0x60080100 soc_hw_version, 0x808FF000 sec.elf 64Bit, 0x1C000000 dbgpolicy, 64Bit + 0x0B80E1: "sc8180x", # Snapdragon 8CX + 0x1560E1: "SM8250", # HDK 8250 + + # Unknown root hash + 0x0B70E1: "SDM850", + 0x0960E1: "SDX24", # 0x60020100 soc_hw_version, 0x8fff7000 dbgpolicy 32Bit, 0x8FCFD000 sec.elf 64Bit + 0x0970E1: "SDX24M", # 0x60020100 soc_hw_version, 0x8fff7000 dbgpolicy 32Bit, 0x8FCFD000 sec.elf 64Bit + 0x0E70E1: "SM7150p", # 0x600C0100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0E80E1: "SA8155", # 0x60030100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0E90E1: "SA8155p", # 0x60030100 soc_hw_version, 0x85FFF000 sec.elf 64Bit, 0x1C1FF000 dbgpolicy, 64Bit + 0x0E40E1: "qcs403", # 0x20140000 soc_hw_version, 0x863DB000 sec.elf 64Bit, 0x863DE000 dbgpolicy, 64Bit + 0x1440E1: "chitwan", # soc_vers 0x6013 + 0x1370E1: "kamortap", + 0x6220E1: "MSM7227A", + 0x8040E1: "APQ8026", + 0x0550E1: "APQ8017", + 0x90F0E1: "APQ8037", + 0x9770E1: "APQ8052", + 0x9F00E1: "APQ8056", + 0x7190E1: "APQ8064", + 0x9300E1: "APQ8092", + 0x0620E1: "APQ8098", + 0x8110E1: "MSM8210", + 0x8140E1: "MSM8212", + 0x8120E1: "MSM8610", + 0x8150E1: "MSM8612", + 0x8010E1: "MSM8626", + 0x8050E1: "MSM8926", # SnapDragon 400 + 0x9180E1: "MSM8928", # SnapDragon 400 + 0x7210E1: "MSM8930", + 0x72C0E1: "MSM8960", + 0x9B00E1: "MSM8956", # SnapDragon 652 + 0x9100E1: "MSM8962", + 0x7B00E1: "MSM8974", # Snapdragon 800 + 0x7B30E1: "MSM8974A", + 0x7B40E1: "MSM8974AB", + 0x7B80E1: "MSM8974Pro", + 0x7BC0E1: "MSM8974ABv3", + 0x6B10E1: "MSM8974AC", + 0x05F0E1: "MSM8996Pro", # SnapDragon 821 + 0x0480E1: "MDM9207", + 0x0CC0E1: "SDM636", + 0x0930E1: "SDA670", # 0x60040100 soc_hw_version + #0x0930E1: "SDA835", # 0x30020000 => HW_ID1 3002000000290022 + 0x08B0E1: "SDM845", # Napali 0x60000100 => HW_ID1 6000000000010000 + #SDM840 NapaliQ ? + #SDM640 Talos ? +} + +sochw = { + 0x2013: "MDM9205", + 0x2014: "qcs405", + 0x2017: "IPQ6018", + 0x3002: "MSM8998_SDM835,SDA835", + 0x3006: "SDM660", + 0x3007: "SDM630", + 0x4003: "QCA4020", + 0x4004: "IPQ8074,QCA6290", + 0x400A: "QCA6390", + 0x400B: "QCN7605,QCA6595,QCN7606", + 0x400D: "qcn90xx", + 0x4014: "moselle", + + #: "SDM632", + #: "SDA632", + #: "SDM636", + 0x6000: "SDM845", + 0x6001: "SDA845", + 0x6002: "SDX24,SDX24M", + 0x6003: "SDM855_SM8150,SDM855p_SM8150p", + 0x6004: "SDA670,SDM670,SDM710", + 0x6005: "SDM670", + #: "SDX50M", + 0x6006: "sc8180x", + 0x6007: "SM6150,SM6150p", + 0x6008: "SM8250:CD90-PH805-1A,SM8250:CD90-PH806-1A,SM8250", + 0x6009: "SDM670", + 0x600B: "SDX55:CD90-PG591,SDX55:CD90-PH809", + 0x600C: "SM7150,SM7150p", + 0x600D: "saipan", + 0x600E: "rennell", + 0x600F: "lahaina", + 0x6012: "bitra_SDM", + 0x6013: "chitwan", + 0x6014: "makena", + 0x6016: "olympic", + 0x6017: "cedros", + 0x9001: "nicobar", + 0x9002: "kamorta", + 0x9003: "agatti", + 0x9004: "mannar" +} + +secgen=[ + # BOOT_ROM_BASE_PHYS, SECURITY_CONTROL_BASE_PHYS, MEMORY_MAP + [[], [0x01900000, 0x100000], []], + [[],[0x01e20000,0x1000],[]], + [[0xFC010000, 0x18000], [0xFC4B8000, 0x60F0], [0x200000, 0x24000]], + [[0x100000, 0x1ffb0], [0x70000, 0x6158], [0x200000, 0x24000]], + [[0x100000, 0x1ffb0], [0x00058000, 0x1000], [0x200000, 0x24000]], + [[0x100000, 0x1ffb0], [0x000A0000, 0x6FFF], [0x200000, 0x24000]], + [[0x100000, 0x1ffb0], [0x00700000, 0x6158], [0x200000, 0x24000]], + [[0x300000, 0x3c000], [0x00780000, 0x10000], [0x14009003, 0x24000]], + [[0x300000, 0x3c000], [0x01B40000, 0x10000], []], +] + +infotbl = { + "QDF2432": secgen[0], + "QCA6290": secgen[1], + "QCA6390": secgen[1], + "QCA6480": secgen[1], + "QCA6481": secgen[1], + "QCA6490": secgen[1], + "QCA6491": secgen[1], + + "APQ8084": secgen[2], + "APQ8092": secgen[2], + "MSM8962": secgen[2], + "MSM8974": secgen[2], + "MSM8974Pro": secgen[2], + "MSM8974AB": secgen[2], + "MSM8974ABv3": secgen[2], + "MSM8974AC": secgen[2], + "MSM8992": secgen[2], + "MSM8994": secgen[2], + "MDM9x25": secgen[2], + "MDM9x35": secgen[2], + + "MSM8996": secgen[3], + "MSM8996AU": secgen[3], + "MSM8996Pro": secgen[3], + + "IPQ4018": secgen[4], + "IPQ4019": secgen[4], + "APQ8009": secgen[4], + "APQ8016": secgen[4], + "APQ8036": secgen[4], + "APQ8039": secgen[4], + "MSM8905": secgen[4], + "MSM8909": secgen[4], + "MSM8909W": secgen[4], + "MSM8916": secgen[4], + "MSM8929": secgen[4], + "MSM8939": secgen[4], + "MSM8952": secgen[4], + "MDM9x40": secgen[4], + "MDM9x45": secgen[4], + + "APQ8017": secgen[5], + "APQ8037": secgen[5], + "APQ8053": secgen[5], + "APQ8056": secgen[5], + "APQ8076": secgen[5], + "MSM8917": secgen[5], + "MSM8920": secgen[5], + "MSM8937": secgen[5], + "MSM8940": secgen[5], + "MSM8953": secgen[5], + "MSM8956": secgen[5], + "MSM8976": secgen[5], + "MSM9206": secgen[5], + "MDM9207": secgen[5], + "MDM9607": secgen[5], + "MDM9x50": secgen[5], + "MDM9x55": secgen[5], + "MDM9x60": secgen[5], + "MDM9x65": secgen[5], + "MDM9250": secgen[5], + "MDM9350": secgen[5], + "MDM9650": secgen[5], + "SDM429": secgen[5], + "SDM439": secgen[5], + "SDM450": secgen[5], + "SDM632": secgen[5], + "SDA632": secgen[5], + "SDX50M": secgen[5], + "qcs403": secgen[5], + "qcs405": secgen[5], + "ipq5018": secgen[5], + "ipq6018": secgen[5], + "qm215": secgen[5], + + "APQ806x": secgen[6], + "MSM8930": secgen[6], + "MSM8936": secgen[6], + + "APQ8098": secgen[7], + "MSM8998": secgen[7], + "SDM630": secgen[7], + "SDM636": secgen[7], + "SDM660": secgen[7], + "SDM670": secgen[7], + "SDA670": secgen[7], + "SDM710": secgen[7], + "QCS605": secgen[7], + "SXR1120": secgen[7], + "SXR1130": secgen[7], + "SDM845": secgen[7], + "SDA845": secgen[7], + "SDM850": secgen[7], + "SDX24": secgen[7], + "SDX24M": secgen[7], + "SDX55M": secgen[7], + "SDX55:CD90-PG591": secgen[7], + "SDX55:CD90-PH809": secgen[7], + "SA515M": secgen[7], + "SM6150": secgen[7], + "SM6150p": secgen[7], + "SM7150": secgen[7], + "SM7150p": secgen[7], + "SDM855_SM8150": secgen[7], + "SDM855p_SM8150p": secgen[7], + "SM8250": secgen[7], + "SM8250p": secgen[7], + "SM8250:CD90-PH805-1A": secgen[7], + "SM8250:CD90-PH806-1A": secgen[7], + "saipan": secgen[7], + "sc8180x": secgen[7], + "bitra": secgen[7], + "cedros": secgen[7], + "chitwan": secgen[7], + "lahaina": secgen[7], + "lahaina_premier": secgen[7], + "mannar": secgen[7], + "rennell": secgen[7], + "sd7250": secgen[7], + + "nicobar": secgen[8], + "agatti": secgen[8], + "kamorta": secgen[8], + "kamortap": secgen[8], + + + # "MSM7227A": [[], [], []], + # "MSM8210": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8212": [[], [], []], + # "MSM8610": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8226": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8926": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8928": [[], [], []], +} + +class memory_type: + nand = 0 + emmc = 1 + ufs = 2 + spinor = 3 + + preferred_memory = { + "QDF2432": emmc, + "QCA6290": emmc, + "QCA6390": emmc, + "QCA6480": emmc, + "QCA6481": emmc, + "QCA6490": emmc, + "QCA6491": emmc, + + "APQ8084": emmc, + "APQ8092": emmc, + "MSM8962": emmc, + "MSM8974": emmc, + "MSM8974Pro": emmc, + "MSM8974AB": emmc, + "MSM8974ABv3": emmc, + "MSM8974AC": emmc, + "MSM8992": emmc, + "MSM8994": emmc, + "MDM9x25": emmc, + "MDM9x35": emmc, + + "MSM8996": ufs, + "MSM8996AU": ufs, + "MSM8996Pro": ufs, + + "IPQ4018": emmc, + "IPQ4019": emmc, + "APQ8009": emmc, + "APQ8016": emmc, + "APQ8036": emmc, + "APQ8039": emmc, + "MSM8905": emmc, + "MSM8909": emmc, + "MSM8909W": emmc, + "MSM8916": emmc, + "MSM8929": emmc, + "MSM8939": emmc, + "MSM8952": emmc, + "MDM9x40": emmc, + "MDM9x45": emmc, + + "APQ8017": emmc, + "APQ8037": emmc, + "APQ8053": emmc, + "APQ8056": emmc, + "APQ8076": emmc, + "MSM8917": emmc, + "MSM8920": emmc, + "MSM8937": emmc, + "MSM8940": emmc, + "MSM8953": emmc, + "MSM8956": emmc, + "MSM8976": emmc, + "MSM9206": emmc, + "MDM9207": nand, + "MDM9607": nand, + "MDM9x50": emmc, + "MDM9x55": emmc, + "MDM9x60": emmc, + "MDM9x65": emmc, + "MDM9250": emmc, + "MDM9350": emmc, + "MDM9650": emmc, + "SDM429": emmc, + "SDM439": emmc, + "SDM450": emmc, + "SDM632": emmc, + "SDA632": emmc, + "SDX50M": emmc, + "qcs403": emmc, + "qcs405": emmc, + "ipq5018": emmc, + "ipq6018": emmc, + "qm215": emmc, + + "APQ806x": emmc, + "MSM8930": emmc, + "MSM8936": emmc, + + "APQ8098": emmc, + "MSM8998": ufs, + "SDM630": emmc, + "SDM636": emmc, + "SDM660": emmc, + "SDM670": emmc, + "SDA670": emmc, + "SDM710": emmc, + "QCS605": emmc, + "SXR1120": emmc, + "SXR1130": emmc, + "SDM845": ufs, + "SDA845": ufs, + "SDM850": ufs, + "SDX24": emmc, + "SDX24M": emmc, + "SDX55M": ufs, + "SDX55:CD90-PG591": ufs, + "SDX55:CD90-PH809": ufs, + "SA515M": emmc, + "SM6150": emmc, + "SM6150p": emmc, + "SM7150": emmc, + "SM7150p": emmc, + "SDM855_SM8150": ufs, + "SDM855p_SM8150p": ufs, + "SM8250": ufs, + "SM8250p": ufs, + "SM8250:CD90-PH805-1A": ufs, + "SM8250:CD90-PH806-1A": ufs, + "saipan": ufs, + "sc8180x": ufs, + "bitra": ufs, + "cedros": ufs, + "chitwan": ufs, + "lahaina": ufs, + "lahaina_premier": ufs, + "mannar": ufs, + "rennell": ufs, + "sd7250": ufs, + + "nicobar": ufs, + "agatti": ufs, + "kamorta": ufs, + "kamortap": ufs, + + # "MSM7227A": [[], [], []], + # "MSM8210": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8212": [[], [], []], + # "MSM8610": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8226": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8926": [[], [0xFC4B8000,0x6FFF], []], + # "MSM8928": [[], [], []], + } + + +secureboottbl = { + "QDF2432": 0x019018c8, + "QCA6290": 0x01e20030, + "QCA6390": 0x01e20010, + "IPQ4018": 0x00058098, + "IPQ4019": 0x00058098, + "APQ8009": 0x00058098, + "APQ8016": 0x00058098, + "APQ8036": 0x00058098, + "APQ8039": 0x00058098, + "APQ8037": 0x000a01d0, + "APQ8053": 0x000a01d0, + "APQ8052": 0x00058098, + "APQ8056": 0x000a01d0, + "APQ8076": 0x000a01d0, + "APQ8084": 0xFC4B83F8, + "APQ8092": 0xFC4B83F8, + "APQ8098": 0x00780350, + "MSM8226": 0xFC4B83E8, + "MSM8610": 0xFC4B83E8, + "MSM8905": 0x00058098, + "MSM8909": 0x00058098, + "MSM8909W": 0x00058098, + "MSM8916": 0x00058098, + "MSM8917": 0x000A01D0, + "MSM8920": 0x000A01D0, + "MSM8929": 0x00058098, + "MSM8930": 0x700310, + "MSM8936": 0x700310, + "MSM8937": 0x000A01D0, + "MSM8939": 0x00058098, + "MSM8940": 0x000A01D0, + "MSM8952": 0x00058098, + "MSM8953": 0x000a01d0, + "MSM8956": 0x000a01d0, + "MSM8974": 0xFC4B83F8, + "MSM8974AB": 0xFC4B83F8, + "MSM8974ABv3": 0xFC4B83F8, + "MSM8974AC": 0xFC4B83F8, + "MSM8976": 0x000a01d0, + "MSM8992": 0xFC4B83F8, + "MSM8994": 0xFC4B83F8, + "MSM8996": 0x00070378, + "MSM8996AU": 0x00070378, + "MSM8996Pro": 0x00070378, + "MSM8998_SDM835": 0x00780350, + "MDM9205": 0x000a0320, + "MDM9206_MDM9207tx": 0x000a01d0, + "MDM9250": 0x000a01d0, + "MDM9350": 0x000a01d0, + "MDM9207": 0x000a01d0, + "MDM9607": 0x000a01d0, + "MDM9x25": 0xFC4B6028, + "MDM9x30": 0xFC4B6028, + "MDM9x35": 0xFC4B6028, + "MDM9x40": 0x00058098, + "MDM9x45": 0x00058098, + "MDM9650": 0x000a01d0, + "MDM9x50": 0x000a01d0, + "MDM9x55": 0x000a01d0, + "MDM9x60": 0x000a01d0, + "MDM9x65": 0x000a01d0, + "SDM429": 0x000a01d0, + "SDM439": 0x000a01d0, + "SDM450": 0x000a01d0, + "SDM630": 0x00780350, + "SDM632": 0x000a01d0, + "SDA632": 0x000a01d0, + "SDM636": 0x00780350, + "SDM660": 0x00780350, + "SDM670": 0x00780350, # Warlock + "SDA670": 0x00780350, + "SDM710": 0x00780350, + "QCS605": 0x00780350, + "SXR1120": 0x00780350, + "SXR1130": 0x00780350, + "SDM845": 0x00780350, + "SDA845": 0x00780350, + "SDX24" : 0x00780390, + "SDX24M": 0x00780390, + "SDX50M": 0x000a01e0, + "SDX55:CD90-PG591": 0x007805E8, + "SDX55:CD90-PH809": 0x007805E8, + "SDX55M" : 0x007804D0, + "SA515M" : 0x007804D0, + "SM6150": 0x00780360, + "SM6150p": 0x00780360, + "SM7150": 0x00780460, + "SM7150p": 0x00780460, + "SDM855_SM8150": 0x007804D0, + "SDM855p_SM8150p": 0x007804D0, + "SM8250:CD90-PH805-1A": 0x007805E8, + "SM8250:CD90-PH806-1A": 0x007805E8, + "agatti": 0x01B40458, + "bitra": 0x007804D8, + "bitra_SDM": 0x007804D8, + "bitra_SDA": 0x007804D8, + "cedros": 0x00780728, + "chitwan": 0x00780668, + "ipq5018": 0x000A01D0, + "ipq6018": 0x000A01D0, + "saipan": 0x007805E8, + "sd7250": 0x007805E8, + "sc8180x": 0x007805E8, + "qcs403": 0x000a0310, + "qcs405": 0x000a0310, + "nicobar": 0x01B40458, + "kamorta": 0x01B40458, + "kamorta_p": 0x01B40458, + "lahaina": 0x780668, + "lahaina_premier": 0x780668, + "mannar": 0x01B40458, + "qm215": 0x000a01d0, + "rennell":0x000780498 + # "MSM7227A":[[], [], []], + # "MSM8210": [[], [], []], + # "MSM8212": + # "MSM8926": [[], [], []], + # "MSM8928": [[], [], []], +} diff --git a/edl/Config/usb_ids.py b/edl/Config/usb_ids.py new file mode 100644 index 0000000..8cf4e77 --- /dev/null +++ b/edl/Config/usb_ids.py @@ -0,0 +1,10 @@ +default_ids = [ + [0x05c6, 0x9008, -1], + [0x05c6, 0x900e, -1], + [0x05c6, 0x9025, -1], + [0x1199, 0x9062, -1], + [0x1199, 0x9070, -1], + [0x1199, 0x9090, -1], + [0x0846, 0x68e0, -1], + [0x19d2, 0x0076, -1] +] \ No newline at end of file diff --git a/edl/Library/Modules/__init__.py b/edl/Library/Modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/edl/Library/Modules/generic.py b/edl/Library/Modules/generic.py new file mode 100644 index 0000000..f311d1c --- /dev/null +++ b/edl/Library/Modules/generic.py @@ -0,0 +1,113 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 + +import logging +from edl.Library.utils import LogBase + + +class generic(metaclass=LogBase): + def __init__(self, fh, serial, args, loglevel): + self.fh = fh + self.serial = serial + self.args = args + self.__logger.setLevel(loglevel) + self.error=self.__logger.error + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def oem_unlock(self, enable): + res = self.fh.detect_partition(self.args, "config") + if res[0]: + lun = res[1] + rpartition = res[2] + offsettopatch = 0x7FFFF + sector = rpartition.sector + (offsettopatch // self.fh.cfg.SECTOR_SIZE_IN_BYTES) + offset = offsettopatch % self.fh.cfg.SECTOR_SIZE_IN_BYTES + if enable: + value = 0x1 + else: + value = 0x0 + size_in_bytes = 1 + if self.fh.cmd_patch(lun, sector, offset, value, size_in_bytes, True): + print(f"Patched sector {str(rpartition.sector)}, offset {str(offset)} with value {value}, " + + f"size in bytes {size_in_bytes}.") + else: + print(f"Error on writing sector {str(rpartition.sector)}, offset {str(offset)} with value {value}, " + + f"size in bytes {size_in_bytes}.") + else: + """ + #define DEVICE_MAGIC "ANDROID-BOOT!" + #define DEVICE_MAGIC_SIZE 13 + #define MAX_PANEL_ID_LEN 64 + #define MAX_VERSION_LEN 64 + #if VBOOT_MOTA + struct device_info + { + unsigned char magic[DEVICE_MAGIC_SIZE]; + bool is_unlocked; + bool is_tampered; + bool is_verified; + bool charger_screen_enabled; + char display_panel[MAX_PANEL_ID_LEN]; + char bootloader_version[MAX_VERSION_LEN]; + char radio_version[MAX_VERSION_LEN]; + bool is_unlock_critical; + }; + #else + struct device_info + { + unsigned char magic[DEVICE_MAGIC_SIZE]; + bool is_unlocked; #0x10 + bool is_tampered; #0x14 + bool charger_screen_enabled; #0x18 + char display_panel[MAX_PANEL_ID_LEN]; + char bootloader_version[MAX_VERSION_LEN]; + char radio_version[MAX_VERSION_LEN]; + bool verity_mode; // 1 = enforcing, 0 = logging + bool is_unlock_critical; + }; + #endif + """ + res = self.fh.detect_partition(self.args, "devinfo") + if res[0]: + lun = res[1] + rpartition = res[2] + offsettopatch1 = 0x10 # is_unlocked + offsettopatch2 = 0x18 # is_critical_unlocked + offsettopatch3 = 0x7FFE10 # zte + offsettopatch4 = 0x7FFE18 # zte + sector1, offset1 = self.fh.calc_offset(rpartition.sector, offsettopatch1) + sector2, offset2 = self.fh.calc_offset(rpartition.sector, offsettopatch2) + sector3, offset3 = self.fh.calc_offset(rpartition.sector, offsettopatch3) + sector4, offset4 = self.fh.calc_offset(rpartition.sector, offsettopatch4) + value = 0x1 + size_in_bytes = 1 + if self.fh.cmd_patch(lun, sector1, offset1, 0x1, size_in_bytes, True): + if self.fh.cmd_patch(lun, sector2, offset2, 0x1, size_in_bytes, True): + print( + f"Patched sector {str(rpartition.sector)}, offset {str(offset1)} with value {value}, " + + f"size in bytes {size_in_bytes}.") + data = self.fh.cmd_read_buffer(lun, rpartition.sector, rpartition.sectors) + if (len(data) > 0x7FFE20) and data[0x7FFE00:0x7FFE10] == b"ANDROID-BOOT!\x00\x00\x00": + if self.fh.cmd_patch(lun, sector3, offset3, value, size_in_bytes, True): + if self.fh.cmd_patch(lun, sector4, offset4, value, size_in_bytes, True): + print( + f"Patched sector {str(rpartition.sector)}, offset {str(offset1)} with " + + f"value {value}, size in bytes {size_in_bytes}.") + return True + print( + f"Error on writing sector {str(rpartition.sector)}, offset {str(offset1)} with value {value}, " + + f"size in bytes {size_in_bytes}.") + return False + else: + fpartitions = res[1] + self.error(f"Error: Couldn't detect partition: \"devinfo\"\nAvailable partitions:") + for lun in fpartitions: + for rpartition in fpartitions[lun]: + if self.args["--memory"].lower() == "emmc": + self.error("\t" + rpartition) + else: + self.error(lun + ":\t" + rpartition) diff --git a/edl/Library/Modules/init.py b/edl/Library/Modules/init.py new file mode 100644 index 0000000..55e2517 --- /dev/null +++ b/edl/Library/Modules/init.py @@ -0,0 +1,99 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 + +import logging +from edl.Library.utils import LogBase + +try: + from edl.Library.Modules.generic import generic +except ImportError as e: + generic = None + pass + +try: + from edl.Library.Modules.oneplus import oneplus +except ImportError as e: + oneplus = None + pass + +try: + from edl.Library.Modules.xiaomi import xiaomi +except ImportError as e: + xiaomi = None + pass + +class modules(metaclass=LogBase): + def __init__(self, fh, serial, supported_functions, loglevel, devicemodel, args): + self.fh = fh + self.args = args + self.serial = serial + self.error = self.__logger.error + self.supported_functions = supported_functions + self.__logger.setLevel(loglevel) + if loglevel==logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + self.options = {} + self.devicemodel = devicemodel + self.generic = None + try: + self.generic = generic(fh=self.fh, serial=self.serial, args=self.args, loglevel=loglevel) + except Exception as e: + pass + self.ops = None + try: + self.ops = oneplus(fh=self.fh, projid=self.devicemodel, serial=self.serial, + supported_functions=self.supported_functions, args=self.args,loglevel=loglevel) + except Exception as e: + pass + self.xiaomi=None + try: + self.xiaomi = xiaomi(fh=self.fh) + except Exception as e: + pass + + def addpatch(self): + if self.ops is not None: + return self.ops.addpatch() + return "" + + def addprogram(self): + if self.ops is not None: + return self.ops.addprogram() + return "" + + def edlauth(self): + if self.xiaomi is not None: + return self.xiaomi.edl_auth() + return True + + def writeprepare(self): + if self.ops is not None: + return self.ops.run() + return True + + def run(self, command, args): + args = args.split(",") + options = {} + for i in range(len(args)): + if "=" in args[i]: + option = args[i].split("=") + if len(option) > 1: + options[option[0]] = option[1] + else: + options[args[i]] = True + if command=="": + print("Valid commands are:\noemunlock\n") + return False + if self.generic is not None and command == "oemunlock": + if "enable" in options: + enable = True + elif "disable" in options: + enable = False + else: + self.error("Unknown mode given. Available are: enable, disable.") + return False + return self.generic.oem_unlock(enable) + return False diff --git a/edl/Library/Modules/oneplus.py b/edl/Library/Modules/oneplus.py new file mode 100755 index 0000000..9b3be83 --- /dev/null +++ b/edl/Library/Modules/oneplus.py @@ -0,0 +1,602 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 + +""" +Usage: + oneplus.py rawxml [--projid=value] [--serial=value] + oneplus.py rawnewxml [--projid=value] [--ts=value] [--serial=value] + oneplus.py setprojmodel_verify [--projid=value] + oneplus.py setswprojmodel_verify [--projid=value] [--ts=value] + oneplus.py program_verify [--projid=value] +Options: + --projid=value Set the appropriate projid [default: 18825] + --serial=value Set the appropriate serial [default: 123456] + --ts=value Set the device timestamp [default: 1604949411] +""" + +import time +import random +from struct import pack +import logging +from edl.Library.utils import LogBase + +try: + from edl.Library.cryptutils import cryptutils +except Exception as e: + print(e) + from ..cryptutils import cryptutils +from binascii import unhexlify, hexlify + +deviceconfig = { + # OP5, cheeseburger + "16859": dict(version=1, cm=None, param_mode=0), + # OP5t, dumpling + "17801": dict(version=1, cm=None, param_mode=0), + # OP6, enchilada + "17819": dict(version=1, cm=None, param_mode=0), + # OP6t, fajita + "18801": dict(version=1, cm=None, param_mode=0), + # OP6t T-Mo, fajitat + "18811": dict(version=1, cm=None, param_mode=0), + # Oneplus 7, guacamoleb + "18857": dict(version=1, cm=None, param_mode=0), + # Oneplus 7 Pro, guacamole + "18821": dict(version=1, cm=None, param_mode=0), + # Oneplus 7 Pro 5G Sprint, guacamoles + "18825": dict(version=1, cm=None, param_mode=0), + # Oneplus 7 Pro 5G EE and Elisa, guacamoleg + "18827": dict(version=1, cm=None, param_mode=0), + # Oneplus 7 Pro T-Mo, guacamolet + "18831": dict(version=1, cm=None, param_mode=0), + # Oneplus 7t, hotdogb + "18865": dict(version=1, cm=None, param_mode=0), + # Oneplus 7t T-Mo, hotdogt + "19863": dict(version=1, cm=None, param_mode=0), + # Oneplus 7t Pro, hotdog + "19801": dict(version=1, cm=None, param_mode=0), + # Oneplus 7t Pro 5G T-Mo, hotdogg + "19861": dict(version=1, cm=None, param_mode=0), + + # OP8, instantnoodle + "19821": dict(version=2, cm="0cffee8a", param_mode=0), + # OP8 T-Mo, instantnoodlet + "19855": dict(version=2, cm="6d9215b4", param_mode=0), + # OP8 Verizon, instantnoodlev + "19867": dict(version=2, cm="4107b2d4", param_mode=0), + # OP8 Visible, instantnoodlevis + "19868": dict(version=-1, cm="178d8213", param_mode=0), + # OP8 Pro, instantnoodlep + "19811": dict(version=2, cm="40217c07", param_mode=0), + # OP8t, kebab + "19805": dict(version=2, cm="1a5ec176", param_mode=0), + # OP8t T-Mo, kebabt + "20809": dict(version=2, cm="d6bc8c36", param_mode=0), + + # OP Nord, avicii + "20801": dict(version=2, cm="eacf50e7", param_mode=0), + # OP N10 5G Metro, billie8t + "20885": dict(version=3, cm="3a403a71", param_mode=1), + # OP N10 5G Global, billie8 + "20886": dict(version=3, cm="b8bd9e39", param_mode=1), + # billie8t, OP N10 5G TMO + "20888": dict(version=3, cm="142f1bd7", param_mode=1), + # OP N10 5G Europe, billie8 + "20889": dict(version=3, cm="f2056ae1", param_mode=1), + + # OP N100 Metro, billie2t + "20880": dict(version=3, cm="6ccf5913", param_mode=1), + # OP N100 Global, billie2 + "20881": dict(version=3, cm="fa9ff378", param_mode=1), + # OP N100 TMO, billie2t + "20882": dict(version=3, cm="4ca1e84e", param_mode=1), + # OP N100 Europe, billie2 + "20883": dict(version=3, cm="ad9dba4a", param_mode=1), + + # OP9 Pro, lemonadep + "19815": dict(version=2, cm="9c151c7f", param_mode=0), + "20859": dict(version=2, cm="9c151c7f", param_mode=0), + # OP9, lemonade + "19825": dict(version=2, cm="0898dcd6", param_mode=1), + # OP9R, lemonades + "20828": dict(version=2, cm=None, param_mode=1), + # OP9 TMO, lemonadet + "20854": dict(version=2, cm="16225d4e", param_mode=1), + # OP9 Pro TMO, lemonadept + "2085A": dict(version=2, cm="7f19519a", param_mode=1), + + # dre8t + "20818": dict(version=2, cm=None, param_mode=1), + # dre8m + "2083C": dict(version=2, cm=None, param_mode=1), + # dre9 + "2083D": dict(version=2, cm=None, param_mode=1) +} + + +class oneplus(metaclass=LogBase): + def __init__(self, fh, projid="18825", serial=123456, ATOBuild=0, Flash_Mode=0, cf=0, supported_functions=None, + args=None, loglevel=logging.INFO): + self.fh = fh + self.__logger=self.__logger + self.args = args + self.ATOBuild = ATOBuild + self.Flash_Mode = Flash_Mode + self.cf = cf # CustFlag + self.supported_functions = supported_functions + self.__logger.setLevel(loglevel) + self.info = self.__logger.info + self.debug = self.__logger.debug + self.error = self.__logger.error + if projid == "": + res = self.fh.detect_partition(arguments=args, partitionname="param") + if res[0]: + lun = res[1] + rpartition = res[2] + data = self.fh.cmd_read_buffer(lun, rpartition.sector, 1, False) + value = data[24:24 + 5] + try: + test = int(value.decode('utf-8')) + self.info("Oneplus protection with prjid %d detected" % test) + projid = value.decode('utf-8') + except: + pass + + if loglevel == logging.DEBUG: + logfilename = "log.txt" + filehandler = logging.FileHandler(logfilename) + self.__logger.addHandler(filehandler) + self.ops_parm = None + self.ops = self.convert_projid(fh, projid, serial) + + def getprodkey(self, projid): + if projid in ["18825", "18801"]: # key_guacamoles, fajiita + prodkey = "b2fad511325185e5" + else: # key_op7t/op8/N10 + prodkey = "7016147d58e8c038" + return prodkey + + def convert_projid(self, fh, projid, serial): + prodkey = self.getprodkey(projid) + pk = "" + val = bytearray(b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + for i in range(0, 16): + rand = int(random.randint(0, 0x100)) + nr = (rand & 0xFF) % 0x3E + pk += chr(val[nr]) + + if projid in deviceconfig: + version = deviceconfig[projid]["version"] + cm = deviceconfig[projid]["cm"] + if version == 1: + return oneplus1(fh, projid, serial, pk, prodkey, self.cf) + elif version == 2: + if cm is not None: + return oneplus1(fh, cm, serial, pk, prodkey, self.cf) + else: + assert "Device is not supported" + exit(0) + elif version == 3: + if cm is not None: + oneplus2(fh, cm, serial, pk, prodkey, self.ATOBuild, self.Flash_Mode, self.cf) + else: + assert "Device is not supported" + exit(0) + return None + + def run(self): + if self.ops is not None: + if "demacia" in self.supported_functions: + if not self.ops.run("demacia"): + exit(0) + if "SetNetType" in self.supported_functions: + self.fh.cmd_send(f"SetNetType") + elif "setprojmodel" in self.supported_functions: + if not self.ops.run(""): + exit(0) + if "setprocstart" in self.supported_functions: + if not self.ops.run(""): + exit(0) + return True + + def setprojmodel_verify(self, pk, token): + if self.ops.setprojmodel_verify: + return self.ops.setprojmodel_verify(pk, token) + + def setswprojmodel_verify(self, pk, token, device_timestamp): + if self.ops.setswprojmodel_verify: + return self.ops.setswprojmodel_verify(pk, token, device_timestamp) + + def program_verify(self, pk, token, tokendata): + if self.ops.program_verify: + return self.ops.program_verify(pk, token, tokendata) + + def generatetoken(self, program=False, device_timestamp="123456789"): + return self.ops.generatetoken(program=program, device_timestamp=device_timestamp) + + def demacia(self): + if self.ops.demacia(): + return self.ops.demacia() + + def enable_ops(self, data, enable, projid, serial): + return None + + def addpatch(self): + if "setprojmodel" in self.supported_functions or "setswprojmodel" in self.supported_functions: + pk, token = self.ops.generatetoken(True) + return f"pk=\"{pk}\" token=\"{token}\" " + else: + return "" + + def addprogram(self): + if "setprojmodel" in self.supported_functions or "setswprojmodel" in self.supported_functions: + pk, token = self.ops.generatetoken(True) + return f"pk=\"{pk}\" token=\"{token}\" " + else: + return "" + + +class oneplus1: + def __init__(self, fh, ModelVerifyPrjName="18825", serial=123456, pk="", prodkey="", cf=0): + self.pk = pk + self.prodkey = prodkey + self.ModelVerifyPrjName = ModelVerifyPrjName + self.fh = fh + self.random_postfix = "8MwDdWXZO7sj0PF3" + self.Version = "guacamoles_21_O.22_191107" + self.cf = str(cf) + self.soc_sn = str(serial) + + def crypt_token(self, data, pk, decrypt=False, demacia=False): + aes = cryptutils().aes() + if demacia: + aeskey = b"\x01\x63\xA0\xD1\xFD\xE2\x67\x11" + bytes(pk, 'utf-8') + b"\x48\x27\xC2\x08\xFB\xB0\xE6\xF0" + aesiv = b"\x96\xE0\x79\x0C\xAE\x2B\xB4\xAF\x68\x4C\x36\xCB\x0B\xEC\x49\xCE" + else: + aeskey = b"\x10\x45\x63\x87\xE3\x7E\x23\x71" + bytes(pk, 'utf-8') + b"\xA2\xD4\xA0\x74\x0f\xD3\x28\x96" + aesiv = b"\x9D\x61\x4A\x1E\xAC\x81\xC9\xB2\xD3\x76\xD7\x49\x31\x03\x63\x79" + if decrypt: + cdata = unhexlify(data) + result = aes.aes_cbc(aeskey, aesiv, cdata) + result = result.rstrip(b'\x00') + if result[:16] == b"907heavyworkload": + return result + else: + return result.decode('utf-8').split(',') + else: + if not demacia: + while len(data) < 256: + data += "\x00" + pdata = bytes(data, 'utf-8') + else: + while len(data) < 256: + data += b"\x00" + pdata = data + result = aes.aes_cbc(aeskey, aesiv, pdata, False) + rdata = hexlify(result) + return rdata.upper().decode('utf-8') + + def cmd_setpro(self): + pk, token = self.generatetoken(False) + data = "\n\n\n" + return data + + def cmd_dem(self): + pk, token = self.demacia() + data = "\n\n\n" + return data + + def generatetoken(self, program=False, device_timestamp=None): + timestamp = str(int(time.time())) + ha = cryptutils().hash() + h1 = self.prodkey + self.ModelVerifyPrjName + self.random_postfix + ModelVerifyHashToken = hexlify(ha.sha256(bytes(h1, 'utf-8'))).decode('utf-8').upper() + # ModelVerifyPrjName=0x1C [0] + # random_postfix=0x2D [1] + # verify_hash=0x3E [2] Len:0x41 + # ver=0x90 [3] + # cf=0x4 [4] + # sn=0x14 [5] + # ts=0x7f [6] Len:0x11 + # secret=0xd1 (hash store) [7], len:0x41 + # 0x7, Len:0x11 + # 0x24, Len:0x41 + # 0x1c 0x4 0x14 0x90 0x7f ModelVerifyHashToken + h2 = "c4b95538c57df231" + self.ModelVerifyPrjName + self.cf + self.soc_sn + self.Version + \ + timestamp + ModelVerifyHashToken + "5b0217457e49381b" + secret = hexlify(ha.sha256(bytes(h2, 'utf-8'))).decode('utf-8').upper() # 0xd1 + if program: + items = [timestamp, secret] + else: + items = [self.ModelVerifyPrjName, self.random_postfix, ModelVerifyHashToken, self.Version, self.cf, + self.soc_sn, timestamp, secret] + data = "" + for item in items: + data += item + "," + data = data[:-1] + token = self.crypt_token(data, self.pk) + return self.pk, token + + def setprojmodel_verify(self, pk, token): + self.pk = pk + ha = cryptutils().hash() + items = self.crypt_token(token, pk, True, False) + info = ["Projid", "ModelVerifyHashToken", "Hash1", "FirmwareString", "CustFlag", "SOC_Serial", "Timestamp", + "secret"] + i = 0 + print() + if len(info) == len(items): + for item in items: + print(info[i] + "=" + item) + i += 1 + # Old + # 0=ModelVerifyPrjName [param+0x1C] + # 1=random_postfix [param+0x2D] + # 2=hash(key+0+1) [ModelVerifyHashToken param+0x3E] + # 3=ver [param_1+0x90] + # 4=cf [param_1+4] + # 5=serial? [param_1+0x14] + # 6=timestamp [param_1+0x7F] + + hash1 = self.prodkey + items[0] + items[1] + res1 = hexlify(ha.sha256(bytes(hash1, 'utf-8'))).decode('utf-8').upper() + if items[2] != res1: + print("Hash1 failed !") + return + # ModelVerifyPrjName cf sn ver ts ModelVerifyHashToken + secret = "c4b95538c57df231" + items[0] + items[4] + items[5] + items[3] + items[6] + \ + items[2] + "5b0217457e49381b" + res2 = hexlify(ha.sha256(bytes(secret, 'utf-8'))).decode('utf-8').upper() + if items[7] != res2: + print("secret failed !") + return + print("setprojmodel good") + return items + + def toSigned32(self, n): + n = n & 0xffffffff + return (n ^ 0x80000000) - 0x80000000 + + def demacia(self): + """ + return "\n\n " + \ + "\n" + """ + ha = cryptutils().hash() + serial = self.soc_sn + while len(serial) < 10: + serial = '0' + serial + hash1 = "2e7006834dafe8ad" + serial + "a6674c6b039707ff" + data = b"907heavyworkload" + ha.sha256(bytes(hash1, 'utf-8')) + token = self.crypt_token(data, self.pk, False, True) + return self.pk, token + + def run(self, flag): + if flag == "demacia": + pk, token = self.demacia() + res = self.fh.cmd_send(f"demacia token=\"{token}\" pk=\"{pk}\"") + if b"verify_res=\"0\"" not in res: + print("Demacia failed:") + print(res) + return False + pk, token = self.generatetoken(False) + res = self.fh.cmd_send(f"setprojmodel token=\"{token}\" pk=\"{pk}\"") + if b"model_check=\"0\"" not in res or b"auth_token_verify=\"0\"" not in res: + print("Setprojmodel failed.") + print(res) + return False + return True + + def program_verify(self, pk, token, tokendata): + print() + self.pk = pk + items = self.crypt_token(token, pk, True, False) + if len(items) == 2: + print("Timestamp=" + items[0]) + print("secret=" + items[1]) + if items[0] != tokendata[6] or items[1] != tokendata[7]: + print("Hash failed !") + return + print("program good") + + +class oneplus2(metaclass=LogBase): + def __init__(self, fh, ModelVerifyPrjName="20889", serial=123456, pk="", prodkey="", ATOBuild=0, Flash_Mode=0, + cf=0, loglevel=logging.INFO): + self.ModelVerifyPrjName = ModelVerifyPrjName + self.pk = pk + self.fh = fh + self.prodkey = prodkey + self.random_postfix = "c75oVnz8yUgLZObh" # ModelVerifyRandom + self.Version = "billie8_14_E.01_201028" # Version + self.device_id = str(int(ModelVerifyPrjName, 16)) + self.flash_mode = str(Flash_Mode) + self.ato_build_state = str(ATOBuild) + self.soc_sn = str(serial) + self.__logger.setLevel(loglevel) + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def crypt_token(self, data, pk, device_timestamp, decrypt=False): + aes = cryptutils().aes() + aeskey = b"\x46\xA5\x97\x30\xBB\x0D\x41\xE8" + bytes(pk, 'utf-8') + \ + pack("\"") + pk, token = op.generatetoken(False) + print( + f"./edl.py rawxml \"\" --debugmode") + elif args["rawnewxml"]: + serial = args["--serial"] + device_timestamp = args["--ts"] + op2 = oneplus(None, projid="20889", serial=serial, ATOBuild=0, Flash_Mode=0, cf=0) + # 20889 OP N10 5G Europe + print(f"./edl.py rawxml \"\"") + # Response should be : + pk, token = op2.generatetoken(False, device_timestamp) + print( + f"./edl.py rawxml \"\" --debugmode") + elif args["setprojmodel_verify"]: + projid = args["--projid"][0] + op = oneplus(None, projid=projid, serial=123456) + token = args[""] + pk = args[""] + # setprojmodel_verify 2BA77B345812E4E45DDB5E407CF9B0F20BCD3E4F0C504A86A3DAA7D70643D0D86F4F5DAEE99E21093D26FF8A8A + # 7C2CCFED387FA4C7D3BC6D8B8C2CC2D27D398886FC150C98CDC521699568C4A419D7E2F2C1A33F6B57AA7CCB5F + # 39D69BB87463986B2CADDD55A41F0F9404C3FB08B0325BFDFCFDE05D1D8314D22F39979A289505D5050D854092 + # CFC9FA3C101A267DD3ECA0442BF89066365ABA6607D43743D86B47B228BAAC5538B622644D74FD4049BE37C520 + # 76DE1B4BFE75187A7B0EE88E6C26E106570B8C0541C4693878BE9B23DEB8E4C530CFBFE9F25597FA3A86223711 + # 2CAF77F0D1EA4CC41EB201FFAE31036FC9E405BABAE43DE9C7E56FE1DC8E82 KHaJV1TfN45ofeLW 18865 + # setprojmodel_verify 633B7E2BBE68BAC392B3E10FC8FEAC09F152853805A6D91FAADDE5A631C7B5A6081C6156F7344BDF407ABF7598 + # 0A9E6DA96964D472FE94311FEAADF6A9032C623A1C5D5B9BDD68C5E049F13DF9D893422C1A44047B1AC8E05A0A + # 2A942B15B409A933A06BAB09F41FB0A3A5C8FEB86B98D39739FA4E2ABDF471DE181646F7AA228C6EC81DB3BAF2 + # F2C3B5381FC9A722F9D11B6A101CAAE31ACD873B83B39AC07B7603EAA38B13F5D0B5E8F9236FB94B967AECE278 + # FEA280E9330636F7C6C72C36A6040F6B8BC3C56AEC9CB0C07360E14EA83D2F6DEC4613FA74D79C325A320B88F2 + # BF025CF9CE528E13169BA255E68909D7E902CE494B49514F6F57713D6F46BE Tgu1kbDW3NemNNqn 18831 + op.setprojmodel_verify(pk, token) + elif args["program_verify"]: + projid = args["--projid"][0] + op = oneplus(None, projid=projid) + token = args[""] + prog_token = args[""] + pk = args[""] + items = op.setprojmodel_verify(pk, token) + op.program_verify(pk, prog_token, items) + elif args["setswprojmodel_verify"]: + projid = args["--projid"][0] + device_timestamp = args["--ts"] + op = oneplus(None, projid=projid, serial=123456) + token = args[""] + pk = args[""] + op.setswprojmodel_verify(pk, token, device_timestamp) + + +def test_setswprojmodel_verify(): + deviceresp = b"RX:\nRX:\nRX:\n" + projid = "20889" + op = oneplus(None, projid=projid, serial=123456) + data = deviceresp.decode('utf-8') + device_timestamp = data[data.rfind("device_timestamp"):].split("\"")[1] + pk, token = op.generatetoken(False, device_timestamp) + if not op.setswprojmodel_verify(pk, token, device_timestamp): + assert "Setswprojmodel error" + + +if __name__ == "__main__": + main() diff --git a/edl/Library/Modules/xiaomi.py b/edl/Library/Modules/xiaomi.py new file mode 100644 index 0000000..7167ce4 --- /dev/null +++ b/edl/Library/Modules/xiaomi.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 + +import logging +from edl.Library.utils import LogBase + + +class xiaomi(metaclass=LogBase): + def __init__(self, fh, projid="18825", serial=123456, ATOBuild=0, Flash_Mode=0, cf=0, supported_functions=None, + loglevel=logging.INFO): + self.fh = fh + self.xiaomi_authdata = b"\x93\x6E\x3A\x8E\x57\x3C\xAD\x07\xC1\x67\x64\x4B\x61\x21\x78\x35\xD8\x5A\xD4\xFD" + \ + b"\xDB\x7D\x84\x0A\x2B\x72\x25\x43\x2F\xCD\xA1\x3A\x7C\x19\x2C\xFA\x97\x9E\xD1\x65" + \ + b"\x17\xE6\x97\x0B\x1B\x07\xDF\x6C\x51\x6F\xEC\x81\xF6\x96\x8F\xCF\x7F\xFD\xDB\xC3" + \ + b"\x97\xA1\x62\xC2\xCA\x3E\x5D\x76\x12\x4A\xA1\x76\x9F\x1B\x21\x64\xB3\x9B\x76\x93" + \ + b"\x0B\x4C\xC6\x75\x19\xF7\xF3\x39\x87\x76\x77\xF4\xE8\xAF\x25\x82\x86\x82\xBC\xBF" + \ + b"\x4E\x59\x3A\x57\xE7\xE3\x05\x32\x69\x92\x53\xE0\xB1\xCC\x5D\x9D\x0D\x55\x4A\xF2" + \ + b"\xBD\x46\xD5\x6F\x18\xD6\xE5\x29\x0B\xA4\xA0\xCA\xC2\x43\x1F\x9F\x19\xC4\xC1\xA3" + \ + b"\x9D\x76\x64\xFF\xAB\x48\xA9\xE1\x1A\x55\x93\x86\x81\x98\x35\xB8\x4D\xF5\x67\x5E" + \ + b"\x70\xD2\x5F\xDB\x51\x23\xE7\xB0\x40\xFE\x21\x10\x8F\x0A\xE6\xD7\xD9\xD2\x67\xF2" + \ + b"\xC9\xC6\x1A\xD0\x54\xC6\x84\x93\xDC\x4D\x33\xF7\x4D\x0C\xF2\xD4\xAA\xDC\xD4\x30" + \ + b"\x15\x2D\xB6\x7C\x22\xA1\x81\xAD\x6D\x77\x61\x63\x7F\x70\xCB\xDA\x88\x4C\xDC\x11" + \ + b"\x33\x72\x03\x83\x77\x90\xE6\x84\x5C\xA5\xA8\x76\x79\x30\xB9\xC2\x6F\xDA\x71\x27" + \ + b"\x25\x64\xCA\x34\x76\x3D\x35\x2F\x5F\xE4\x2A\xB7\x38\xFB\x38\xA5" + self.__logger.setLevel(loglevel) + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def edl_auth(self): + """ + Poco F1, Redmi 5 Pro, 6 Pro, 7 Pro, 7A, 8, 8A, 8A Dual, 8A Pro, Y2, S2 + """ + authcmd = b" " + rsp = self.fh.xmlsend(authcmd) + if rsp[0]: + rsp = self.fh.xmlsend(self.xiaomi_authdata) + if len(rsp) > 1: + if rsp[0]: + if b"EDL Authenticated" in rsp[2] or b"ACK" in rsp[2]: + return True + return True + return False diff --git a/edl/Library/TestFiles/gpt_sm8180x.bin b/edl/Library/TestFiles/gpt_sm8180x.bin new file mode 100644 index 0000000000000000000000000000000000000000..7a14b20dc260977d2fba2353f02445f0a01dea3a GIT binary patch literal 24576 zcmeI$ODIH97{KvwEUXj@OL5C{Lm3M!6v~*%&2#M39T~-BO5?S#pk_r)B(fl{lCo3m zP?Sg(mXxO1VBrxPip6z?GYcD;9rZtT@AsYVIp;g)x4TD#_MZhD@@U((Lmfc{JK2-8 zZzel~@dzM*00IagfB*srAb9;=}<=)%D!%ipCtZA8E@S>N&V`I zPxsTJ!zaxN9v_fEb&T{dY6h*Dx~2v`#$)mo@FerUYrFR zrm~9{4o!P2WR9NXoV~gpvW|T_r(<)~da|Ws;$EUl{4)5{!oU2sc(7swir#7$Z(mGj z!NzRvL{G46IxjN$-Vr&PDY^IQbu3*tQQxoXhkjWjnPFG!Yw061Rc&rFb94W0DUi5t zNkqL?MxS)bfD*8zUu7Yc^(Iq>u?Qf500IagfB*srAb> i) & 1) # branchless + x = (x >> 1) ^ ((x & 1) * 0xE1000000000000000000000000000000) + assert res < 1 << 128 + return res + + def change_key(self, master_key): + if master_key >= (1 << 128): + raise InvalidInputException('Master key should be 128-bit') + + self.__master_key = long_to_bytes(master_key, 16) + self.__aes_ecb = AES.new(self.__master_key, AES.MODE_ECB) + self.__auth_key = bytes_to_long(self.__aes_ecb.encrypt(b'\x00' * 16)) + + # precompute the table for multiplication in finite field + table = [] # for 8-bit + for i in range(16): + row = [] + for j in range(256): + row.append(self.gf_2_128_mul(self.__auth_key, j << (8 * i))) + table.append(tuple(row)) + self.__pre_table = tuple(table) + + self.prev_init_value = None # reset + + def __times_auth_key(self, val): + res = 0 + for i in range(16): + res ^= self.__pre_table[i][val & 0xFF] + val >>= 8 + return res + + def __ghash(self, aad, txt): + len_aad = len(aad) + len_txt = len(txt) + + # padding + if 0 == len_aad % 16: + data = aad + else: + data = aad + b'\x00' * (16 - len_aad % 16) + if 0 == len_txt % 16: + data += txt + else: + data += txt + b'\x00' * (16 - len_txt % 16) + + tag = 0 + assert len(data) % 16 == 0 + for i in range(len(data) // 16): + tag ^= bytes_to_long(data[i * 16: (i + 1) * 16]) + tag = self.__times_auth_key(tag) + # print 'X\t', hex(tag) + tag ^= ((8 * len_aad) << 64) | (8 * len_txt) + tag = self.__times_auth_key(tag) + + return tag + + def encrypt(self, init_value, plaintext, auth_data=b''): + if init_value >= (1 << 96): + raise InvalidInputException('IV should be 96-bit') + # a naive checking for IV reuse + if init_value == self.prev_init_value: + raise InvalidInputException('IV must not be reused!') + self.prev_init_value = init_value + + len_plaintext = len(plaintext) + # len_auth_data = len(auth_data) + + if len_plaintext > 0: + counter = Counter.new( + nbits=32, + prefix=long_to_bytes(init_value, 12), + initial_value=2, # notice this + allow_wraparound=False) + aes_ctr = AES.new(self.__master_key, AES.MODE_CTR, counter=counter) + + if 0 != len_plaintext % 16: + padded_plaintext = plaintext + \ + b'\x00' * (16 - len_plaintext % 16) + else: + padded_plaintext = plaintext + ciphertext = aes_ctr.encrypt(padded_plaintext)[:len_plaintext] + + else: + ciphertext = b'' + + auth_tag = self.__ghash(auth_data, ciphertext) + # print 'GHASH\t', hex(auth_tag) + auth_tag ^= bytes_to_long(self.__aes_ecb.encrypt( + long_to_bytes((init_value << 32) | 1, 16))) + + # assert len(ciphertext) == len(plaintext) + assert auth_tag < (1 << 128) + return ciphertext, auth_tag + + def decrypt(self, init_value, ciphertext, auth_tag, auth_data=b''): + # if init_value >= (1 << 96): + # raise InvalidInputException('IV should be 96-bit') + # if auth_tag >= (1 << 128): + # raise InvalidInputException('Tag should be 128-bit') + + if auth_tag != self.__ghash(auth_data, ciphertext) ^ \ + bytes_to_long(self.__aes_ecb.encrypt( + long_to_bytes((init_value << 32) | 1, 16))): + raise InvalidTagException + + len_ciphertext = len(ciphertext) + if len_ciphertext > 0: + counter = Counter.new( + nbits=32, + prefix=long_to_bytes(init_value, 12), + initial_value=2, + allow_wraparound=True) + aes_ctr = AES.new(self.__master_key, AES.MODE_CTR, counter=counter) + + if 0 != len_ciphertext % 16: + padded_ciphertext = ciphertext + \ + b'\x00' * (16 - len_ciphertext % 16) + else: + padded_ciphertext = ciphertext + plaintext = aes_ctr.decrypt(padded_ciphertext)[:len_ciphertext] + + else: + plaintext = b'' + + return plaintext + + def aes_gcm(self, ciphertext, nounce, aes_key, hdr, tag_auth, decrypt=True): + cipher = AES.new(aes_key, AES.MODE_GCM, nounce) + if hdr is not None: + cipher.update(hdr) + if decrypt: + plaintext = cipher.decrypt(ciphertext) + try: + cipher.verify(tag_auth) + return plaintext + except ValueError: + return None + return None + + def aes_cbc(self, key, iv, data, decrypt=True): + if decrypt: + return AES.new(key, AES.MODE_CBC, IV=iv).decrypt(data) + else: + return AES.new(key, AES.MODE_CBC, IV=iv).encrypt(data) + + def aes_ecb(self, key, data, decrypt=True): + if decrypt: + return AES.new(key, AES.MODE_ECB).decrypt(data) + else: + return AES.new(key, AES.MODE_ECB).encrypt(data) + + def aes_ctr(self, key, counter, enc_data, decrypt=True): + ctr = Counter.new(128, initial_value=counter) + # Create the AES cipher object and decrypt the ciphertext, basically this here is just aes ctr 256 :) + cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + if decrypt: + data = cipher.decrypt(enc_data) + else: + data = cipher.encrypt(enc_data) + return data + + def aes_ccm(self, key, nounce, tag_auth, data, decrypt=True): + cipher = AES.new(key, AES.MODE_CCM, nounce) + if decrypt: + plaintext = cipher.decrypt(data) + try: + cipher.verify(tag_auth) + return plaintext + except ValueError: + return None + else: + ciphertext = cipher.encrypt(data) + return ciphertext + + def aes_cmac_verify(self, key, plain, compare): + ctx = CMAC.new(key, ciphermod=AES) + ctx.update(plain) + result = ctx.hexdigest() + if result != compare: + print("AES-CMAC failed !") + else: + print("AES-CMAC ok !") + + class rsa: # RFC8017 + def __init__(self, hashtype="SHA256"): + if hashtype == "SHA1": + self.hash = hashlib.sha1 + self.digestLen = 0x14 + elif hashtype == "SHA256": + self.hash = hashlib.sha256 + self.digestLen = 0x20 + + def pss_test(self): + N = "a2ba40ee07e3b2bd2f02ce227f36a195024486e49c19cb41bbbdfbba98b22b0e577c2eeaffa20d883a76e65e394c69d4b3c05a1e8fadda27edb2a42bc000fe888b9b32c22d15add0cd76b3e7936e19955b220dd17d4ea904b1ec102b2e4de7751222aa99151024c7cb41cc5ea21d00eeb41f7c800834d2c6e06bce3bce7ea9a5" + e = "010001" + D = "050e2c3e38d886110288dfc68a9533e7e12e27d2aa56d2cdb3fb6efa990bcff29e1d2987fb711962860e7391b1ce01ebadb9e812d2fbdfaf25df4ae26110a6d7a26f0b810f54875e17dd5c9fb6d641761245b81e79f8c88f0e55a6dcd5f133abd35f8f4ec80adf1bf86277a582894cb6ebcd2162f1c7534f1f4947b129151b71" + MSG = "859eef2fd78aca00308bdc471193bf55bf9d78db8f8a672b484634f3c9c26e6478ae10260fe0dd8c082e53a5293af2173cd50c6d5d354febf78b26021c25c02712e78cd4694c9f469777e451e7f8e9e04cd3739c6bbfedae487fb55644e9ca74ff77a53cb729802f6ed4a5ffa8ba159890fc " + salt = "e3b5d5d002c1bce50c2b65ef88a188d83bce7e61" + + N = int(N, 16) + e = int(e, 16) + D = int(D, 16) + MSG = unhexlify(MSG) + salt = unhexlify(salt) + signature = self.pss_sign(D, N, self.hash(MSG), salt, 1024) # pkcs_1_pss_encode_sha256 + isvalid = self.pss_verify(e, N, self.hash(MSG), signature, 1024) + if isvalid: + print("Test passed.") + else: + print("Test failed.") + + def i2osp(self, x, x_len): + """Converts the integer x to its big-endian representation of length + x_len. + """ + if x > 256 ** x_len: + raise IntegerTooLarge + h = hex(x)[2:] + if h[-1] == 'L': + h = h[:-1] + if len(h) & 1 == 1: + h = '0%s' % h + x = unhexlify(h) + return b'\x00' * int(x_len - len(x)) + x + + def os2ip(self, x): + """Converts the byte string x representing an integer reprented using the + big-endian convient to an integer. + """ + h = hexlify(x) + return int(h, 16) + + # def os2ip(self, X): + # return int.from_bytes(X, byteorder='big') + + def mgf1(self, input, length): + counter = 0 + output = b'' + while len(output) < length: + C = self.i2osp(counter, 4) + output += self.hash(input + C) + counter += 1 + return output[:length] + + def assert_int(self, var: int, name: str): + if isinstance(var, int): + return + raise TypeError('%s should be an integer, not %s' % (name, var.__class__)) + + def sign(self, tosign, D, N, emBits=1024): + self.assert_int(tosign, 'message') + self.assert_int(D, 'D') + self.assert_int(N, 'n') + + if tosign < 0: + raise ValueError('Only non-negative numbers are supported') + + if tosign > N: + tosign1 = divmod(tosign, N)[1] + signature = pow(tosign1, D, N) + raise OverflowError("The message %i is too long for n=%i" % (tosign, N)) + + signature = pow(tosign, D, N) + hexsign = self.i2osp(signature, emBits // 8) + return hexsign + + def pss_sign(self, D, N, msghash, salt, emBits=1024): + if isinstance(D, str): + D = unhexlify(D) + D = self.os2ip(D) + if isinstance(N, str): + N = unhexlify(N) + N = self.os2ip(N) + slen = len(salt) + emLen = self.ceil_div(emBits, 8) + inBlock = b"\x00" * 8 + msghash + salt + hhash = self.hash(inBlock) + PSlen = emLen - self.digestLen - slen - 1 - 1 + DB = (PSlen * b"\x00") + b"\x01" + salt + rlen = emLen - len(hhash) - 1 + dbMask = self.mgf1(hhash, rlen) + maskedDB = bytearray() + for i in range(0, len(dbMask)): + maskedDB.append(dbMask[i] ^ DB[i]) + maskedDB[0] = maskedDB[0] & 0x7F + EM = maskedDB + hhash + b"\xbc" + tosign = self.os2ip(EM) + # EM=hexlify(EM).decode('utf-8') + # tosign = int(EM,16) + return self.sign(tosign, D, N, emBits) + # 6B1EAA2042A5C8DA8B1B4A8320111A70A0CBA65233D1C6E418EF8156E82A8F96BD843F047FF25AB9702A6582C8387298753E628F23448B4580E09CBD2A483C623B888F47C4BD2C5EFF09013C6DFF67DB59BAB3037F0BEE05D5660264D28CC6251631FE75CE106D931A04FA032FEA31259715CE0FAB1AE0E2F8130807AF4019A61B9C060ECE59104F22156FEE8108F17DC80D7C2F8397AFB9780994F7C5A0652F93D1B48010B0B248AB9711235787D797FBA4D10A29BCF09628585D405640A866B15EE9D7526A2703E72A19811EF447F6E5C43F915B3808EBC79EA4BCF78903DBDE32E47E239CFB5F2B5986D0CBBFBE6BACDC29B2ADE006D23D0B90775B1AE4DD + + def ceil_div(self, a, b): + (q, r) = divmod(a, b) + if r: + return q + 1 + else: + return q + + def pss_verify(self, e, N, msghash, signature, emBits=1024, salt=None): + if salt == None: + slen = self.digestLen + else: + slen = len(salt) + sig = self.os2ip(signature) + + EM = pow(sig, e, N) + # EM = unhexlify(hex(EM)[2:]) + EM = self.i2osp(EM, emBits // 8) + + emLen = len(signature) + + valBC = EM[-1] + if valBC != 0xbc: + print("[rsa_pss] : 0xbc check failed") + return False + mhash = EM[emLen - self.digestLen - 1:-1] + maskedDB = EM[:emLen - self.digestLen - 1] + + lmask = ~(0xFF >> (8 * emLen + 1 - emBits)) + if EM[0] & lmask: + print("[rsa_pss] : lmask check failed") + return False + + dbMask = self.mgf1(mhash, emLen - self.digestLen - 1) + + DB = bytearray() + for i in range(0, len(dbMask)): + DB.append(dbMask[i] ^ maskedDB[i]) + + TS = bytearray() + TS.append(DB[0] & ~lmask) + TS.extend(DB[1:]) + + PS = (b"\x00" * (emLen - self.digestLen - slen - 2)) + b"\x01" + if TS[:len(PS)] != PS: + print(TS[:len(PS)]) + print(PS) + print("[rsa_pss] : 0x01 check failed") + return False + + if salt is not None: + inBlock = b"\x00" * 8 + msghash + salt + mhash = self.hash(inBlock) + if mhash == mhash: + return True + else: + return False + else: + salt = TS[-self.digestLen:] + inBlock = b"\x00" * 8 + msghash + salt + mhash = self.hash(inBlock) + if mhash == mhash: + return True + else: + return False + + class hash(): + def __init__(self, hashtype="SHA256"): + if hashtype == "SHA1": + self.hash = self.sha1 + self.digestLen = 0x14 + elif hashtype == "SHA256": + self.hash = self.sha256 + self.digestLen = 0x20 + elif hashtype == "MD5": + self.hash = self.md5 + self.digestLen = 0x10 + + def sha1(self, msg): + return hashlib.sha1(msg).digest() + + def sha256(self, msg): + return hashlib.sha256(msg).digest() + + def md5(self, msg): + return hashlib.md5(msg).digest() diff --git a/edl/Library/firehose.py b/edl/Library/firehose.py new file mode 100755 index 0000000..0158a23 --- /dev/null +++ b/edl/Library/firehose.py @@ -0,0 +1,1238 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2019 + +import binascii +import io +import platform +import time +import json +from struct import unpack +from binascii import hexlify +from queue import Queue +from threading import Thread +from edl.Library.utils import * +from edl.Library.gpt import gpt +from edl.Library.sparse import QCSparse + +try: + from edl.Library.Modules.init import modules +except ImportError as e: + pass + + +class nand_partition: + partentries = [] + + def __init__(self, parent, printer=None): + if printer is None: + self.printer = print + else: + self.printer = printer + self.partentries = [] + self.partitiontblsector = None + self.parent = parent + self.storage_info = {} + + def parse(self, partdata): + self.partentries = [] + + class partf: + sector = 0 + sectors = 0 + name = "" + attr1 = 0 + attr2 = 0 + attr3 = 0 + which_flash = 0 + + magic1, magic2, version, numparts = unpack(" 0: + wf.write(data) + q.task_done() + if stop() and q.empty(): + break + + +class asyncwriter(): + def __init__(self, wf): + self.writequeue = Queue() + self.worker = Thread(target=writefile, args=(wf, self.writequeue, lambda: self.stopthreads,)) + self.worker.setDaemon(True) + self.stopthreads = False + self.worker.start() + + def write(self, data): + self.writequeue.put_nowait(data) + + def stop(self): + self.stopthreads = True + self.writequeue.join() + + +class firehose(metaclass=LogBase): + class cfg: + TargetName = "" + Version = "" + ZLPAwareHost = 1 + SkipStorageInit = 0 + SkipWrite = 0 + MaxPayloadSizeToTargetInBytes = 1048576 + MaxPayloadSizeFromTargetInBytes = 8192 + MaxXMLSizeInBytes = 4096 + bit64 = True + + total_blocks = 0 + block_size = 0 + SECTOR_SIZE_IN_BYTES = 0 + MemoryName = "eMMC" + prod_name = "Unknown" + maxlun = 99 + + def __init__(self, cdc, xml, cfg, loglevel, devicemodel, serial, skipresponse, luns, args): + self.cdc = cdc + self.lasterror = b"" + self.loglevel = loglevel + self.args = args + self.xml = xml + self.cfg = cfg + self.prog = 0 + self.progtime = 0 + self.progpos = 0 + self.pk = None + self.modules = None + self.serial = serial + self.devicemodel = devicemodel + self.skipresponse = skipresponse + self.luns = luns + self.supported_functions = [] + self.lunsizes = {} + self.info = self.__logger.info + self.error = self.__logger.error + self.debug = self.__logger.debug + self.warning = self.__logger.warning + + self.__logger.setLevel(loglevel) + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + self.nandparttbl = None + self.nandpart = nand_partition(parent=self, printer=print) + + def show_progress(self, prefix, pos, total, display=True): + t0 = time.time() + if pos == 0: + self.prog = 0 + self.progtime = time.time() + self.progpos=pos + prog = round(float(pos) / float(total) * float(100), 2) + if pos != total: + if prog > self.prog: + if display: + tdiff=t0-self.progtime + datasize=(pos-self.progpos)/1024/1024 + throughput=(((datasize)/(tdiff))) + print_progress(prog, 100, prefix='Progress:', + suffix=prefix+' (Sector %d of %d) %0.2f MB/s' % + (pos // self.cfg.SECTOR_SIZE_IN_BYTES, + total // self.cfg.SECTOR_SIZE_IN_BYTES, + throughput), bar_length=50) + self.prog = prog + self.progpos = pos + self.progtime = t0 + else: + if display: + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + + + def detect_partition(self, arguments, partitionname): + fpartitions = {} + for lun in self.luns: + lunname = "Lun" + str(lun) + fpartitions[lunname] = [] + data, guid_gpt = self.get_gpt(lun, int(arguments["--gpt-num-part-entries"]), + int(arguments["--gpt-part-entry-size"]), + int(arguments["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + else: + for partition in guid_gpt.partentries: + fpartitions[lunname].append(partition.name) + if partition.name == partitionname: + return [True, lun, partition] + return [False, fpartitions] + + def getstatus(self, resp): + if "value" in resp: + value = resp["value"] + if value == "ACK": + return True + else: + return False + return True + + def decoder(self, data): + if isinstance(data, bytes) or isinstance(data, bytearray): + if data[:5] == b" timeout: + break + rdata += tmp + except Exception as err: + self.error(err) + return [False, resp, data] + try: + if b"raw hex token" in rdata: + rdata = rdata + try: + resp = self.xml.getresponse(rdata) + except Exception as e: # pylint: disable=broad-except + rdata = bytes(self.decoder(rdata), 'utf-8') + resp = self.xml.getresponse(rdata) + status = self.getstatus(resp) + except Exception as err: + status = True + self.debug(str(err)) + if isinstance(rdata, bytes) or isinstance(rdata, bytearray): + try: + self.debug("Error on getting xml response:" + rdata.decode('utf-8')) + except Exception as err: + self.debug("Error on getting xml response:" + hexlify(rdata).decode('utf-8') + + ", Error: " + str(err)) + elif isinstance(rdata, str): + self.debug("Error on getting xml response:" + rdata) + return [status, {"value": "NAK"}, rdata] + else: + status = True + resp = {"value": "ACK"} + return [status, resp, rdata] + + def cmd_reset(self): + data = "" + val = self.xmlsend(data) + try: + v = None + while v != b'': + v = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if v != b'': + resp = self.xml.getlog(v)[0] + else: + break + print(resp) + except Exception as err: + self.error(str(err)) + pass + if val[0]: + self.info("Reset succeeded.") + return True + else: + self.error("Reset failed.") + return False + + def cmd_xml(self, filename): + with open(filename, 'rb') as rf: + data = rf.read() + val = self.xmlsend(data) + if val[0]: + self.info("Command succeeded." + str(val[2])) + return val[2] + else: + self.error("Command failed:" + str(val[2])) + return val[2] + + def cmd_nop(self): + data = "" + self.xmlsend(data, True) + info = b"" + tmp = None + while tmp != b"": + tmp = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if tmp == b"": + break + info += tmp + if info != b"": + self.info("Nop succeeded.") + return self.xml.getlog(info) + else: + self.error("Nop failed.") + return False + + def cmd_getsha256digest(self, physical_partition_number, start_sector, num_partition_sectors): + data = f"\n" + val = self.xmlsend(data) + if val[0]: + res = self.xml.getlog(val[2]) + for line in res: + self.info(line) + if "Digest " in res: + return res.split("Digest ")[1] + else: + return res + else: + self.error("GetSha256Digest failed.") + return False + + def cmd_setbootablestoragedrive(self, partition_number): + data = f"\n" + val = self.xmlsend(data) + if val[0]: + self.info("Setbootablestoragedrive succeeded.") + return True + else: + self.error("Setbootablestoragedrive failed: %s" % val[2]) + return False + + def cmd_send(self, content, response=True): + data = f"\n<{content} />" + if response: + val = self.xmlsend(data) + if val[0] and b"log value=\"ERROR\"" not in val[1]: + return val[2] + else: + self.error(f"{content} failed.") + self.error(f"{val[2]}") + return val[1] + else: + self.xmlsend(data, True) + return True + + def cmd_patch(self, physical_partition_number, start_sector, byte_offset, value, size_in_bytes, display=True): + """ + + """ + + data = f"\n" + \ + f"\n" + + rsp = self.xmlsend(data) + if rsp[0]: + if display: + self.info(f"Patch:\n--------------------\n") + self.info(rsp[1]) + return True + else: + self.error(f"Error:{rsp}") + return False + + def wait_for_data(self): + tmp = bytearray() + timeout = 0 + while b'response value' not in tmp: + res = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if res == b'': + timeout += 1 + if timeout == 4: + break + time.sleep(0.1) + tmp += res + return tmp + + def cmd_program(self, physical_partition_number, start_sector, filename, display=True): + total = os.stat(filename).st_size + sparse = QCSparse(filename, self.loglevel) + sparseformat = False + if sparse.readheader(): + sparseformat = True + total = sparse.getsize() + bytestowrite = total + with open(filename, "rb") as rf: + # Make sure we fill data up to the sector size + num_partition_sectors = total // self.cfg.SECTOR_SIZE_IN_BYTES + if (total % self.cfg.SECTOR_SIZE_IN_BYTES) != 0: + num_partition_sectors += 1 + if display: + self.info(f"\nWriting to physical partition {str(physical_partition_number)}, " + + f"sector {str(start_sector)}, sectors {str(num_partition_sectors)}") + + data = f"\n" + \ + f"\n" + rsp = self.xmlsend(data, self.skipresponse) + self.show_progress("Write", 0, total, display) + if rsp[0]: + old = 0 + while bytestowrite > 0: + wlen = min(bytestowrite, self.cfg.MaxPayloadSizeToTargetInBytes) + + if sparseformat: + wdata = sparse.read(wlen) + else: + wdata = rf.read(wlen) + bytestowrite -= wlen + + if wlen % self.cfg.SECTOR_SIZE_IN_BYTES != 0: + filllen = (wlen // self.cfg.SECTOR_SIZE_IN_BYTES * self.cfg.SECTOR_SIZE_IN_BYTES) + \ + self.cfg.SECTOR_SIZE_IN_BYTES + wdata += b"\x00" * (filllen - wlen) + + self.cdc.write(wdata) + self.show_progress("Write", total-bytestowrite, total, display) + self.cdc.write(b'') + # time.sleep(0.2) + + wd = self.wait_for_data() + log = self.xml.getlog(wd) + rsp = self.xml.getresponse(wd) + if "value" in rsp: + if rsp["value"] != "ACK": + self.error(f"Error:") + for line in log: + self.error(line) + return False + else: + self.error(f"Error:{rsp}") + return False + return True + + def cmd_program_buffer(self, physical_partition_number, start_sector, wfdata, display=True): + bytestowrite = len(wfdata) + total = bytestowrite + # Make sure we fill data up to the sector size + num_partition_sectors = bytestowrite // self.cfg.SECTOR_SIZE_IN_BYTES + if (bytestowrite % self.cfg.SECTOR_SIZE_IN_BYTES) != 0: + num_partition_sectors += 1 + if display: + self.info(f"\nWriting to physical partition {str(physical_partition_number)}, " + + f"sector {str(start_sector)}, sectors {str(num_partition_sectors)}") + + data = f"\n" + \ + f"\n" + rsp = self.xmlsend(data, self.skipresponse) + self.show_progress("Write", 0, total, display) + if rsp[0]: + old = 0 + pos = 0 + while bytestowrite > 0: + wlen = min(bytestowrite, self.cfg.MaxPayloadSizeToTargetInBytes) + + wdata = data[pos:pos + wlen] + pos += wlen + bytestowrite -= wlen + + if wlen % self.cfg.SECTOR_SIZE_IN_BYTES != 0: + filllen = (wlen // self.cfg.SECTOR_SIZE_IN_BYTES * self.cfg.SECTOR_SIZE_IN_BYTES) + \ + self.cfg.SECTOR_SIZE_IN_BYTES + wdata += b"\x00" * (filllen - wlen) + + self.cdc.write(wdata) + + self.show_progress("Write", total - bytestowrite, total, display) + self.cdc.write(b'') + # time.sleep(0.2) + + wd = self.wait_for_data() + log = self.xml.getlog(wd) + rsp = self.xml.getresponse(wd) + if "value" in rsp: + if rsp["value"] != "ACK": + self.error(f"Error:") + for line in log: + self.error(line) + return False + else: + self.error(f"Error:{rsp}") + return False + return True + + def cmd_erase(self, physical_partition_number, start_sector, num_partition_sectors, display=True): + if display: + self.info(f"\nErasing from physical partition {str(physical_partition_number)}, " + + f"sector {str(start_sector)}, sectors {str(num_partition_sectors)}") + + data = f"\n" + \ + f"\n" + + rsp = self.xmlsend(data, self.skipresponse) + empty = b"\x00" * self.cfg.MaxPayloadSizeToTargetInBytes + pos = 0 + bytestowrite = self.cfg.SECTOR_SIZE_IN_BYTES * num_partition_sectors + total = self.cfg.SECTOR_SIZE_IN_BYTES * num_partition_sectors + self.show_progress("Erase", 0, total, display) + if rsp[0]: + while bytestowrite > 0: + wlen = min(bytestowrite, self.cfg.MaxPayloadSizeToTargetInBytes) + self.cdc.write(empty[:wlen]) + self.show_progress("Erase", total - bytestowrite, total, display) + bytestowrite -= wlen + pos += wlen + self.cdc.write(b'') + + res = self.wait_for_data() + info = self.xml.getlog(res) + rsp = self.xml.getresponse(res) + if "value" in rsp: + if rsp["value"] != "ACK": + self.error(f"Error:") + for line in info: + self.error(line) + return False + else: + self.error(f"Error:{rsp}") + return False + return True + + def cmd_read(self, physical_partition_number, start_sector, num_partition_sectors, filename, display=True): + self.lasterror = b"" + prog = 0 + if display: + self.info( + f"\nReading from physical partition {str(physical_partition_number)}, " + + f"sector {str(start_sector)}, sectors {str(num_partition_sectors)}") + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + + with open(file=filename, mode="wb", buffering=self.cfg.MaxPayloadSizeFromTargetInBytes) as wr: + + data = f"\n" + + rsp = self.xmlsend(data, self.skipresponse) + # time.sleep(0.01) + if rsp[0]: + if "value" in rsp[1]: + if rsp[1]["value"] == "NAK": + if display: + self.error(rsp[2].decode('utf-8')) + return b"" + bytestoread = self.cfg.SECTOR_SIZE_IN_BYTES * num_partition_sectors + total = bytestoread + show_progress = self.show_progress + usb_read = self.cdc.read + self.show_progress("Read", 0, total, display) + wMaxPacketSize = self.cdc.EP_IN.wMaxPacketSize + while bytestoread > 0: + data=usb_read(min(wMaxPacketSize, bytestoread)) + wr.write(data) + bytestoread -= len(data) + show_progress("Read", total - bytestoread, total, display) + # time.sleep(0.2) + wd = self.wait_for_data() + info = self.xml.getlog(wd) + rsp = self.xml.getresponse(wd) + if "value" in rsp: + if rsp["value"] != "ACK": + self.error(f"Error:") + for line in info: + self.error(line) + self.lasterror += bytes(line + "\n", "utf-8") + return False + else: + if display: + self.error(f"Error:{rsp[2]}") + return False + if display and prog != 100: + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + return True + + def cmd_read_buffer(self, physical_partition_number, start_sector, num_partition_sectors, display=True): + self.lasterror = b"" + prog = 0 + if display: + self.info( + f"\nReading from physical partition {str(physical_partition_number)}, " + + f"sector {str(start_sector)}, sectors {str(num_partition_sectors)}") + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + + data = f"\n" + + rsp = self.xmlsend(data, self.skipresponse) + resData = bytearray() + if rsp[0]: + if "value" in rsp[1]: + if rsp[1]["value"] == "NAK": + if display: + self.error(rsp[2].decode('utf-8')) + return -1 + bytestoread = self.cfg.SECTOR_SIZE_IN_BYTES * num_partition_sectors + total = bytestoread + if display: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + while bytestoread > 0: + tmp = self.cdc.read(min(self.cdc.EP_IN.wMaxPacketSize, bytestoread)) + size=len(tmp) + bytestoread -= size + resData.extend(tmp) + self.show_progress("Read", total-bytestoread, total, display) + + wd = self.wait_for_data() + info = self.xml.getlog(wd) + rsp = self.xml.getresponse(wd) + if "value" in rsp: + if rsp["value"] != "ACK": + self.error(f"Error:") + for line in info: + self.error(line) + return resData + else: + if len(rsp) > 1: + if b"Failed to open the UFS Device" in rsp[2]: + self.error(f"Error:{rsp[2]}") + self.lasterror = rsp[2] + return resData + if len(rsp) > 2 and not rsp[0]: + self.lasterror = rsp[2] + if display and prog != 100: + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + return resData # Do not remove, needed for oneplus + + def get_gpt(self, lun, gpt_num_part_entries, gpt_part_entry_size, gpt_part_entry_start_lba): + try: + data = self.cmd_read_buffer(lun, 0, 2, False) + except Exception as err: + self.debug(str(err)) + self.skipresponse = True + data = self.cmd_read_buffer(lun, 0, 2, False) + + if data == b"" or data == -1: + return None, None + magic = unpack(" 34: + sectors = 34 + data = self.cmd_read_buffer(lun, 0, sectors, False) + if data == b"": + return None, None + guid_gpt.parse(data, self.cfg.SECTOR_SIZE_IN_BYTES) + return data, guid_gpt + else: + return None, None + except Exception as err: + self.debug(str(err)) + return None, None + + def get_backup_gpt(self, lun, gpt_num_part_entries, gpt_part_entry_size, gpt_part_entry_start_lba): + data = self.cmd_read_buffer(lun, 0, 2, False) + if data == b"": + return None + guid_gpt = gpt( + num_part_entries=gpt_num_part_entries, + part_entry_size=gpt_part_entry_size, + part_entry_start_lba=gpt_part_entry_start_lba, + loglevel=self.__logger.level + ) + header = guid_gpt.parseheader(data, self.cfg.SECTOR_SIZE_IN_BYTES) + if "backup_lba" in header: + sectors = header.first_usable_lba - 1 + data = self.cmd_read_buffer(lun, header.backup_lba, sectors, False) + if data == b"": + return None + return data + else: + return None + + def calc_offset(self, sector, offset): + sector = sector + (offset // self.cfg.SECTOR_SIZE_IN_BYTES) + offset = offset % self.cfg.SECTOR_SIZE_IN_BYTES + return sector, offset + + def getluns(self, argument): + if argument["--lun"] is not None: + return [int(argument["--lun"])] + + luns = [] + if self.cfg.MemoryName.lower() == "ufs": + for i in range(0, self.cfg.maxlun): + luns.append(i) + else: + luns = [0] + return luns + + def configure(self, lvl): + if self.cfg.SECTOR_SIZE_IN_BYTES == 0: + if self.cfg.MemoryName.lower() == "emmc": + self.cfg.SECTOR_SIZE_IN_BYTES = 512 + else: + self.cfg.SECTOR_SIZE_IN_BYTES = 4096 + + connectcmd = f"" + \ + f"" + \ + "" + ''' + "" \ + "" + ''' + rsp = self.xmlsend(connectcmd) + if len(rsp) > 1: + if not rsp[0]: + if b"Only nop and sig tag can be" in rsp[2]: + self.info("Xiaomi EDL Auth detected.") + try: + self.modules = modules(fh=self, serial=self.serial, + supported_functions=self.supported_functions, + loglevel=self.__logger.level, + devicemodel=self.devicemodel, args=self.args) + except Exception as err: # pylint: disable=broad-except + self.modules = None + if self.modules.edlauth(): + rsp = self.xmlsend(connectcmd) + if len(rsp) > 1: + if rsp[0] and rsp[1] != {}: # On Ack + info = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if "MemoryName" not in rsp[1]: + # print(rsp[1]) + rsp[1]["MemoryName"] = "eMMC" + if "MaxXMLSizeInBytes" not in rsp[1]: + rsp[1]["MaxXMLSizeInBytes"] = "4096" + self.warning("Couldn't detect MaxPayloadSizeFromTargetinBytes") + if "MaxPayloadSizeToTargetInBytes" not in rsp[1]: + rsp[1]["MaxPayloadSizeToTargetInBytes"] = "1038576" + if "MaxPayloadSizeToTargetInBytesSupported" not in rsp[1]: + rsp[1]["MaxPayloadSizeToTargetInBytesSupported"] = "1038576" + if rsp[1]["MemoryName"].lower() != self.cfg.MemoryName.lower(): + self.warning("Memory type was set as " + self.cfg.MemoryName + " but device reported it is " + + rsp[1]["MemoryName"] + " instead.") + self.cfg.MemoryName = rsp[1]["MemoryName"] + self.cfg.MaxPayloadSizeToTargetInBytes = int(rsp[1]["MaxPayloadSizeToTargetInBytes"]) + self.cfg.MaxPayloadSizeToTargetInBytesSupported = int(rsp[1]["MaxPayloadSizeToTargetInBytesSupported"]) + self.cfg.MaxXMLSizeInBytes = int(rsp[1]["MaxXMLSizeInBytes"]) + if "MaxPayloadSizeFromTargetInBytes" in rsp[1]: + self.cfg.MaxPayloadSizeFromTargetInBytes = int(rsp[1]["MaxPayloadSizeFromTargetInBytes"]) + else: + self.cfg.MaxPayloadSizeFromTargetInBytes = self.cfg.MaxXMLSizeInBytes + self.warning("Couldn't detect MaxPayloadSizeFromTargetinBytes") + if "TargetName" in rsp[1]: + self.cfg.TargetName = rsp[1]["TargetName"] + if "MSM" not in self.cfg.TargetName: + self.cfg.TargetName = "MSM" + self.cfg.TargetName + else: + self.cfg.TargetName = "Unknown" + self.warning("Couldn't detect TargetName") + if "Version" in rsp[1]: + self.cfg.Version = rsp[1]["Version"] + else: + self.cfg.Version = 0 + self.warning("Couldn't detect Version") + else: # on NAK + if b"ERROR" in rsp[2]: + self.error(rsp[2].decode('utf-8')) + sys.exit() + if "MaxPayloadSizeToTargetInBytes" in rsp[1]: + try: + self.cfg.MemoryName = rsp[1]["MemoryName"] + self.cfg.MaxPayloadSizeToTargetInBytes = int(rsp[1]["MaxPayloadSizeToTargetInBytes"]) + self.cfg.MaxPayloadSizeToTargetInBytesSupported = int( + rsp[1]["MaxPayloadSizeToTargetInBytesSupported"]) + self.cfg.MaxXMLSizeInBytes = int(rsp[1]["MaxXMLSizeInBytes"]) + self.cfg.MaxPayloadSizeFromTargetInBytes = int(rsp[1]["MaxPayloadSizeFromTargetInBytes"]) + self.cfg.TargetName = rsp[1]["TargetName"] + if "MSM" not in self.cfg.TargetName: + self.cfg.TargetName = "MSM" + self.cfg.TargetName + self.cfg.Version = rsp[1]["Version"] + if lvl == 0: + return self.configure(lvl + 1) + else: + self.error(f"Error:{rsp}") + sys.exit() + except Exception as e: + pass + self.info(f"TargetName={self.cfg.TargetName}") + self.info(f"MemoryName={self.cfg.MemoryName}") + self.info(f"Version={self.cfg.Version}") + + rsp = self.cmd_read_buffer(0, 1, 1, False) + if rsp == b"" and self.args["--memory"] is None: + if b"Failed to open the SDCC Device" in self.lasterror: + self.warning( + "Memory type eMMC doesn't seem to match (Failed to init). Trying to use UFS instead.") + self.cfg.MemoryName = "UFS" + return self.configure(0) + if b"ERROR: Failed to initialize (open whole lun) UFS Device slot" in self.lasterror: + self.warning( + "Memory type UFS doesn't seem to match (Failed to init). Trying to use eMMC instead.") + self.cfg.MemoryName = "eMMC" + return self.configure(0) + elif b"Attribute \'SECTOR_SIZE_IN_BYTES\'=4096 must be equal to disk sector size 512" in self.lasterror: + self.cfg.SECTOR_SIZE_IN_BYTES = 512 + elif b"Attribute \'SECTOR_SIZE_IN_BYTES\'=512 must be equal to disk sector size 4096" in self.lasterror: + self.cfg.SECTOR_SIZE_IN_BYTES = 4096 + self.luns = self.getluns(self.args) + return True + + def getlunsize(self, lun): + if lun not in self.lunsizes: + try: + data, guid_gpt = self.get_gpt(lun, int(self.args["--gpt-num-part-entries"]), + int(self.args["--gpt-part-entry-size"]), + int(self.args["--gpt-part-entry-start-lba"])) + self.lunsizes[lun] = guid_gpt.totalsectors + except Exception as e: + self.error(e) + return -1 + else: + return self.lunsizes[lun] + return guid_gpt.totalsectors + + def connect(self): + v = b'-1' + if platform.system() == 'Windows': + self.cdc.timeout = 10 + else: + self.cdc.timeout = 10 + info = [] + while v != b'': + try: + v = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if v == b'': + break + data = self.xml.getlog(v) + if len(data) > 0: + info.append(data[0]) + if not info: + break + except Exception as err: # pylint: disable=broad-except + pass + supfunc = False + if info == [] or (len(info) > 0 and 'ERROR' in info[0]): + if len(info) > 0: + self.debug(info[0]) + info = self.cmd_nop() + if not info: + self.info("No supported functions detected, configuring qc generic commands") + self.supported_functions = ['configure', 'program', 'firmwarewrite', 'patch', 'setbootablestoragedrive', + 'ufs', 'emmc', 'power', 'benchmark', 'read', 'getstorageinfo', + 'getcrc16digest', 'getsha256digest', 'erase', 'peek', 'poke', 'nop', 'xml'] + else: + self.supported_functions = [] + for line in info: + if "chip serial num" in line.lower(): + self.info(line) + try: + serial = line.split("0x")[1][:-1] + self.serial = int(serial, 16) + except Exception as err: # pylint: disable=broad-except + self.debug(str(err)) + serial = line.split(": ")[2] + self.serial = int(serial.split(" ")[0]) + if supfunc and "end of supported functions" not in line.lower(): + rs = line.replace("\n", "") + if rs != "": + rs = rs.replace("INFO: ", "") + self.supported_functions.append(rs) + if "supported functions" in line.lower(): + supfunc = True + + if len(self.supported_functions) > 1: + info = "Supported Functions: " + for line in self.supported_functions: + info += line + "," + self.info(info[:-1]) + data = self.cdc.read(self.cfg.MaxXMLSizeInBytes) # logbuf + try: + self.info(data.decode('utf-8')) + except Exception as err: # pylint: disable=broad-except + self.debug(str(err)) + pass + + if not self.supported_functions: + self.supported_functions = ['configure', 'program', 'firmwarewrite', 'patch', 'setbootablestoragedrive', + 'ufs', 'emmc', 'power', 'benchmark', 'read', 'getstorageinfo', + 'getcrc16digest', 'getsha256digest', 'erase', 'peek', 'poke', 'nop', 'xml'] + + if "getstorageinfo" in self.supported_functions and self.args["--memory"] is None: + storageinfo = self.cmd_getstorageinfo() + if storageinfo is not None and storageinfo!=[]: + for info in storageinfo: + if "storage_info" in info: + try: + si = json.loads(info)["storage_info"] + except Exception as err: # pylint: disable=broad-except + self.debug(str(err)) + continue + self.info("Storage report:") + for sii in si: + self.info(f"{sii}:{si[sii]}") + if "total_blocks" in si: + self.cfg.total_blocks = si["total_blocks"] + + if "block_size" in si: + self.cfg.block_size = si["block_size"] + if "page_size" in si: + self.cfg.SECTOR_SIZE_IN_BYTES = si["page_size"] + if "mem_type" in si: + self.cfg.MemoryName = si["mem_type"] + if "prod_name" in si: + self.cfg.prod_name = si["prod_name"] + if "UFS Inquiry Command Output:" in info: + self.cfg.prod_name = info.split("Output: ")[1] + self.info(info) + if "UFS Erase Block Size:" in info: + self.cfg.block_size = int(info.split("Size: ")[1], 16) + self.info(info) + if "UFS Boot" in info: + self.cfg.MemoryName = "UFS" + self.cfg.SECTOR_SIZE_IN_BYTES = 4096 + if "UFS Boot Partition Enabled: " in info: + self.info(info) + if "UFS Total Active LU: " in info: + self.cfg.maxlun = int(info.split("LU: ")[1], 16) + + return self.supported_functions + + # OEM Stuff here below -------------------------------------------------- + + def cmd_writeimei(self, imei): + if len(imei) != 16: + self.info("IMEI must be 16 digits") + return False + data = "" + val = self.xmlsend(data) + if val[0]: + self.info("writeIMEI succeeded.") + return True + else: + self.error("writeIMEI failed.") + return False + + def cmd_getstorageinfo(self): + data = "" + val = self.xmlsend(data) + if val[0]: + try: + data = self.xml.getlog(val[2]) + return data + except: # pylint: disable=broad-except + return None + else: + self.warning("GetStorageInfo command isn't supported.") + return None + + def cmd_getstorageinfo_string(self): + data = "" + val = self.xmlsend(data) + if val[0]: + self.info(f"GetStorageInfo:\n--------------------\n") + data = self.xml.getlog(val[2]) + for line in data: + self.info(line) + return True + else: + self.warning("GetStorageInfo command isn't supported.") + return False + + def cmd_poke(self, address, data, filename="", info=False): + rf = None + if filename != "": + rf = open(filename, "rb") + SizeInBytes = os.stat(filename).st_size + else: + SizeInBytes = len(data) + if info: + self.info(f"Poke: Address({hex(address)}),Size({hex(SizeInBytes)})") + ''' + + ''' + maxsize = 8 + lengthtowrite = SizeInBytes + if lengthtowrite < maxsize: + maxsize = lengthtowrite + pos = 0 + old = 0 + datawritten = 0 + mode = 0 + if info: + print_progress(0, 100, prefix='Progress:', suffix='Complete', bar_length=50) + while lengthtowrite > 0: + if rf is not None: + content = hex(int(hexlify(rf.read(maxsize)).decode('utf-8'), 16)) + else: + content = 0 + if lengthtowrite < maxsize: + maxsize = lengthtowrite + for i in range(0, maxsize): + content = (content << 8) + int( + hexlify(data[pos + maxsize - i - 1:pos + maxsize - i]).decode('utf-8'), 16) + # content=hex(int(hexlify(data[pos:pos+maxsize]).decode('utf-8'),16)) + content = hex(content) + if mode == 0: + xdata = f"\n" + else: + xdata = f"\n" + try: + self.cdc.write(xdata[:self.cfg.MaxXMLSizeInBytes]) + except Exception as e: # pylint: disable=broad-except + self.debug(str(e)) + pass + addrinfo = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if b"SizeInBytes" in addrinfo or b"Invalid parameters" in addrinfo: + tmp = b"" + while b"NAK" not in tmp and b"ACK" not in tmp: + tmp += self.cdc.read(self.cfg.MaxXMLSizeInBytes) + xdata = f"\n" + self.cdc.write(xdata[:self.cfg.MaxXMLSizeInBytes]) + addrinfo = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if (b' old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + old = prog + if info: + self.info("Done writing.") + return True + + def cmd_peek(self, address, SizeInBytes, filename="", info=False): + if info: + self.info(f"Peek: Address({hex(address)}),Size({hex(SizeInBytes)})") + wf = None + if filename != "": + wf = open(filename, "wb") + ''' + + ''' + data = f"\n" + ''' + + + ''' + try: + self.cdc.write(data[:self.cfg.MaxXMLSizeInBytes]) + except Exception as err: # pylint: disable=broad-except + self.debug(str(err)) + pass + addrinfo = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if b"SizeInBytes" in addrinfo or b"Invalid parameters" in addrinfo: + tmp = b"" + while b"NAK" not in tmp and b"ACK" not in tmp: + tmp += self.cdc.read(self.cfg.MaxXMLSizeInBytes) + data = f"" + self.cdc.write(data[:self.cfg.MaxXMLSizeInBytes]) + addrinfo = self.cdc.read(self.cfg.MaxXMLSizeInBytes) + if (b' old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + old = prog + + if wf is not None: + wf.close() + if b'> 32) & 0xFFFFFF + socid = ((sahara.hwid >> 32) >> 16) + if hwid in msmids: + self.target_name = msmids[hwid] + self.info(f"Target detected: {self.target_name}") + if self.cfg.MemoryName == "": + if self.target_name in memory_type.preferred_memory: + type = memory_type.preferred_memory[self.target_name] + if type == memory_type.nand: + self.cfg.MemoryName = "nand" + if type == memory_type.spinor: + self.cfg.MemoryName = "spinor" + elif type == memory_type.emmc: + self.cfg.MemoryName = "eMMC" + elif type == memory_type.ufs: + self.cfg.MemoryName = "UFS" + self.warning("Based on the chipset, we assume " + + self.cfg.MemoryName + " as default memory type..., if it fails, try using " + + "--memory\" with \"UFS\",\"NAND\" or \"spinor\" instead !") + elif socid in sochw: + self.target_name = sochw[socid].split(",")[0] + + # We assume ufs is fine (hopefully), set it as default + if self.cfg.MemoryName == "": + self.warning( + "No --memory option set, we assume \"eMMC\" as default ..., if it fails, try using \"--memory\" " + + "with \"UFS\",\"NAND\" or \"spinor\" instead !") + self.cfg.MemoryName = "eMMC" + + if self.firehose.configure(0): + funcs = "Supported functions:\n-----------------\n" + for function in self.firehose.supported_functions: + funcs += function + "," + funcs = funcs[:-1] + self.info(funcs) + self.target_name = self.firehose.cfg.TargetName + self.connected = True + try: + if self.firehose.modules is None: + self.firehose.modules = modules(fh=self.firehose, serial=self.firehose.serial, + supported_functions=self.firehose.supported_functions, + loglevel=self.__logger.level, + devicemodel=self.firehose.devicemodel, args=self.arguments) + except Exception as err: # pylint: disable=broad-except + self.firehose.modules = None + + def check_cmd(self, func): + if not self.firehose.supported_functions: + return True + for sfunc in self.firehose.supported_functions: + if func.lower() == sfunc.lower(): + return True + return False + + def find_bootable_partition(self, rawprogram): + part = -1 + for xml in rawprogram: + with open(xml, "r") as fl: + for evt, elem in ET.iterparse(fl, events=["end"]): + if elem.tag == "program": + label = elem.get("label") + if label in ['xbl', 'xbl_a', 'sbl1']: + if part != -1: + self.error("[FIREHOSE] multiple bootloader found!") + return -1 + part = elem.get("physical_partition_number") + return part + + def getluns(self, argument): + if argument["--lun"] is not None: + return [int(argument["--lun"])] + + luns = [] + if self.cfg.MemoryName.lower() == "ufs": + for i in range(0, self.cfg.maxlun): + luns.append(i) + else: + luns = [0] + return luns + + def check_param(self, parameters): + error = False + params = "" + for parameter in parameters: + params += parameter + " " + if parameter not in parameters: + error = True + if error: + if len(parameters) == 1: + self.printer("Argument " + params + "required.") + else: + self.printer("Arguments " + params + "required.") + return False + return True + + def get_storage_info(self): + if "getstorageinfo" in self.firehose.supported_functions: + storageinfo = self.firehose.cmd_getstorageinfo() + for info in storageinfo: + if "storage_info" in info: + rs = info.replace("INFO: ", "") + field = json.loads(rs) + if "storage_info" in field: + info = field["storage_info"] + return info + return None + + def handle_firehose(self, cmd, options): + if cmd == "gpt": + luns = self.getluns(options) + directory = options[""] + if directory is None: + directory = "" + genxml = False + if "--genxml" in options: + if options["--genxml"]: + genxml = True + for lun in luns: + sfilename = os.path.join(directory, f"gpt_main{str(lun)}.bin") + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + with open(sfilename, "wb") as write_handle: + write_handle.write(data) + + self.printer(f"Dumped GPT from Lun {str(lun)} to {sfilename}") + sfilename = os.path.join(directory, f"gpt_backup{str(lun)}.bin") + with open(sfilename, "wb") as write_handle: + write_handle.write(data[self.firehose.cfg.SECTOR_SIZE_IN_BYTES * 2:]) + self.printer(f"Dumped Backup GPT from Lun {str(lun)} to {sfilename}") + if genxml: + guid_gpt.generate_rawprogram(lun, self.firehose.cfg.SECTOR_SIZE_IN_BYTES, directory) + return True + elif cmd == "printgpt": + luns = self.getluns(options) + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + self.printer(f"\nParsing Lun {str(lun)}:") + guid_gpt.print() + return True + elif cmd == "r": + if not self.check_param(["", ""]): + return False + partitionname = options[""] + filename = options[""] + filenames = filename.split(",") + partitions = partitionname.split(",") + if len(partitions) != len(filenames): + self.error("You need to gives as many filenames as given partitions.") + return False + i = 0 + for partition in partitions: + if partition=="gpt": + luns = self.getluns(options) + for lun in luns: + partfilename=filenames[i]+".lun%d" % lun + if self.firehose.cmd_read(lun, 0, 32, partfilename): + self.printer( + f"Dumped sector {str(0)} with sector count {str(32)} " + + f"as {partfilename}.") + continue + partfilename = filenames[i] + i += 1 + res = self.firehose.detect_partition(options, partition) + if res[0]: + lun = res[1] + rpartition = res[2] + if self.firehose.cmd_read(lun, rpartition.sector, rpartition.sectors, partfilename): + self.printer( + f"Dumped sector {str(rpartition.sector)} with sector count {str(rpartition.sectors)} " + + f"as {partfilename}.") + else: + fpartitions = res[1] + self.error(f"Error: Couldn't detect partition: {partition}\nAvailable partitions:") + for lun in fpartitions: + for rpartition in fpartitions[lun]: + if self.cfg.MemoryName == "emmc": + self.error("\t" + rpartition) + else: + self.error(lun + ":\t" + rpartition) + return False + return True + elif cmd == "rl": + if not self.check_param([""]): + return False + directory = options[""] + if options["--skip"]: + skip = options["--skip"].split(",") + else: + skip = [] + genxml = False + if "--genxml" in options: + if options["--genxml"]: + genxml = True + if not os.path.exists(directory): + os.mkdir(directory) + + luns = self.getluns(options) + + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + if len(luns) > 1: + storedir = os.path.join(directory, "lun" + str(lun)) + else: + storedir = directory + if not os.path.exists(storedir): + os.mkdir(storedir) + sfilename = os.path.join(storedir, f"gpt_main{str(lun)}.bin") + with open(sfilename, "wb") as write_handle: + write_handle.write(data) + + sfilename = os.path.join(storedir, f"gpt_backup{str(lun)}.bin") + with open(sfilename, "wb") as write_handle: + write_handle.write(data[self.firehose.cfg.SECTOR_SIZE_IN_BYTES * 2:]) + + if genxml: + guid_gpt.generate_rawprogram(lun, self.firehose.cfg.SECTOR_SIZE_IN_BYTES, storedir) + + for partition in guid_gpt.partentries: + partitionname = partition.name + if partition.name in skip: + continue + filename = os.path.join(storedir, partitionname + ".bin") + self.info( + f"Dumping partition {str(partition.name)} with sector count {str(partition.sectors)} " + + f"as {filename}.") + if self.firehose.cmd_read(lun, partition.sector, partition.sectors, filename): + self.info(f"Dumped partition {str(partition.name)} with sector count " + + f"{str(partition.sectors)} as {filename}.") + return True + elif cmd == "rf": + if not self.check_param([""]): + return False + filename = options[""] + storageinfo = self.get_storage_info() + if storageinfo is not None and self.cfg.MemoryName.lower() in ["spinor" , "nand"]: + totalsectors = None + if "total_blocks" in storageinfo: + totalsectors = storageinfo["total_blocks"] + if "num_physical" in storageinfo: + num_physical = storageinfo["num_physical"] + luns = [0] + if num_physical > 0: + luns=[] + for i in range(num_physical): + luns.append(i) + if totalsectors is not None: + for lun in luns: + buffer=self.firehose.cmd_read_buffer(physical_partition_number=lun, + start_sector=0, num_partition_sectors=1, display=False) + storageinfo = self.get_storage_info() + if "total_blocks" in storageinfo: + totalsectors = storageinfo["total_blocks"] + if len(luns) > 1: + sfilename = filename + f".lun{str(lun)}" + else: + sfilename = filename + self.printer(f"Dumping sector 0 with sector count {str(totalsectors)} as {filename}.") + if self.firehose.cmd_read(lun, 0, totalsectors, sfilename): + self.printer( + f"Dumped sector 0 with sector count {str(totalsectors)} as {filename}.") + else: + luns = self.getluns(options) + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + if len(luns) > 1: + sfilename = filename + f".lun{str(lun)}" + else: + sfilename = filename + self.printer(f"Dumping sector 0 with sector count {str(guid_gpt.totalsectors)} as {filename}.") + if self.firehose.cmd_read(lun, 0, guid_gpt.totalsectors, sfilename): + self.printer(f"Dumped sector 0 with sector count {str(guid_gpt.totalsectors)} as {filename}.") + return True + elif cmd == "pbl": + if not self.check_param([""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + filename = options[""] + if self.target_name in infotbl: + target_name = infotbl[self.target_name] + if len(target_name[0]) > 0: + if self.firehose.cmd_peek(target_name[0][0], target_name[0][1], filename, True): + self.printer(f"Dumped pbl at offset {hex(target_name[0][0])} as {filename}.") + return True + else: + self.error("No known pbl offset for this chipset") + else: + self.error("Unknown target chipset") + self.error("Error on dumping pbl") + return False + elif cmd == "qfp": + if not self.check_param([""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + filename = options[""] + if self.target_name not in infotbl: + self.error("Unknown target chipset") + else: + target_name = infotbl[self.target_name] + if len(target_name[1]) > 0: + if self.firehose.cmd_peek(target_name[1][0], target_name[1][1], filename): + self.printer(f"Dumped qfprom at offset {hex(target_name[1][0])} as {filename}.") + return True + else: + self.error("No known qfprom offset for this chipset") + self.error("Error on dumping qfprom") + return False + elif cmd == "secureboot": + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + if self.target_name in secureboottbl: + self.target_name = secureboottbl[self.target_name] + value = unpack("> (area * 8)) & 0xFF + pk_hashindex = sec_boot & 3 + oem_pkhash = True if ((sec_boot >> 4) & 1) == 1 else False + auth_enabled = True if ((sec_boot >> 5) & 1) == 1 else False + use_serial = True if ((sec_boot >> 6) & 1) == 1 else False + if auth_enabled: + is_secure = True + self.printer(f"Sec_Boot{str(area)} " + + f"PKHash-Index:{str(pk_hashindex)} " + + f"OEM_PKHash: {str(oem_pkhash)} " + + f"Auth_Enabled: {str(auth_enabled)}" + + f"Use_Serial: {str(use_serial)}") + if is_secure: + self.printer("Secure boot enabled.") + else: + self.printer("Secure boot disabled.") + return True + else: + self.error("Unknown target chipset") + return False + elif cmd == "memtbl": + if not self.check_param([""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + filename = options[""] + if self.target_name in infotbl: + self.target_name = infotbl[self.target_name] + if len(self.target_name[2]) > 0: + if self.firehose.cmd_peek(self.target_name[2][0], self.target_name[2][1], filename): + self.printer(f"Dumped memtbl at offset {hex(self.target_name[2][0])} as {filename}.") + return True + else: + self.error("No known memtbl offset for this chipset") + else: + self.error("Unknown target chipset") + self.error("Error on dumping memtbl") + return False + elif cmd == "footer": + if not self.check_param([""]): + return False + luns = self.getluns(options) + filename = options[""] + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + pnames = ["userdata2", "metadata", "userdata", "reserved1", "reserved2", "reserved3"] + for partition in guid_gpt.partentries: + if partition.name in pnames: + self.printer(f"Detected partition: {partition.name}") + data = self.firehose.cmd_read_buffer(lun, + partition.sector + + (partition.sectors - + (0x4000 // self.firehose.cfg.SECTOR_SIZE_IN_BYTES)), + (0x4000 // self.firehose.cfg.SECTOR_SIZE_IN_BYTES), False) + if data == b"": + continue + val = unpack("", "", ""]): + return False + start = int(options[""]) + sectors = int(options[""]) + filename = options[""] + if self.firehose.cmd_read(lun, start, sectors, filename, True): + self.printer(f"Dumped sector {str(start)} with sector count {str(sectors)} as {filename}.") + return True + elif cmd == "peek": + if not self.check_param(["", "", ""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + length = getint(options[""]) + filename = options[""] + self.firehose.cmd_peek(offset, length, filename, True) + self.info( + f"Peek data from offset {hex(offset)} and length {hex(length)} was written to {filename}") + return True + elif cmd == "peekhex": + if not self.check_param(["", ""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + length = getint(options[""]) + resp = self.firehose.cmd_peek(offset, length, "", True) + self.printer("\n") + self.printer(hexlify(resp)) + return True + elif cmd == "peekqword": + if not self.check_param([""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + resp = self.firehose.cmd_peek(offset, 8, "", True) + self.printer("\n") + self.printer(hex(unpack(""]): + return False + if not self.check_cmd("peek"): + self.error("Peek command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + resp = self.firehose.cmd_peek(offset, 4, "", True) + self.printer("\n") + self.printer(hex(unpack("", ""]): + return False + if not self.check_cmd("poke"): + self.error("Poke command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + filename = options[""] + return self.firehose.cmd_poke(offset, "", filename, True) + elif cmd == "pokehex": + if not self.check_param(["", ""]): + return False + if not self.check_cmd("poke"): + self.error("Poke command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + data = unhexlify(options[""]) + return self.firehose.cmd_poke(offset, data, "", True) + elif cmd == "pokeqword": + if not self.check_param(["", ""]): + return False + if not self.check_cmd("poke"): + self.error("Poke command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + data = pack(""])) + return self.firehose.cmd_poke(offset, data, "", True) + elif cmd == "pokedword": + if not self.check_param(["", ""]): + return False + if not self.check_cmd("poke"): + self.error("Poke command isn't supported by edl loader") + return False + else: + offset = getint(options[""]) + data = pack(""])) + return self.firehose.cmd_poke(offset, data, "", True) + elif cmd == "memcpy": + if not self.check_param(["", ""]): + return False + if not self.check_cmd("poke"): + self.printer("Poke command isn't supported by edl loader") + else: + srcoffset = getint(options[""]) + size = getint(options[""]) + dstoffset = srcoffset + size + if self.firehose.cmd_memcpy(dstoffset, srcoffset, size): + self.printer(f"Memcpy from {hex(srcoffset)} to {hex(dstoffset)} succeeded") + return True + else: + return False + elif cmd == "reset": + return self.firehose.cmd_reset() + elif cmd == "nop": + if not self.check_cmd("nop"): + self.error("Nop command isn't supported by edl loader") + return False + else: + return self.firehose.cmd_nop() + elif cmd == "setbootablestoragedrive": + if not self.check_param([""]): + return False + if not self.check_cmd("setbootablestoragedrive"): + self.error("setbootablestoragedrive command isn't supported by edl loader") + return False + else: + return self.firehose.cmd_setbootablestoragedrive(int(options[""])) + elif cmd == "getstorageinfo": + if not self.check_cmd("getstorageinfo"): + self.error("getstorageinfo command isn't supported by edl loader") + return False + else: + return self.firehose.cmd_getstorageinfo_string() + elif cmd == "w": + if not self.check_param(["", ""]): + return False + partitionname = options[""] + filename = options[""] + if options["--lun"] is not None: + lun = int(options["--lun"]) + else: + lun = 0 + startsector = 0 + if not os.path.exists(filename): + self.error(f"Error: Couldn't find file: {filename}") + return False + if partitionname.lower() == "gpt": + sectors = os.stat(filename).st_size // self.firehose.cfg.SECTOR_SIZE_IN_BYTES + res = [True, lun, sectors] + else: + res = self.firehose.detect_partition(options, partitionname) + if res[0]: + lun = res[1] + sectors = os.stat(filename).st_size // self.firehose.cfg.SECTOR_SIZE_IN_BYTES + if (os.stat(filename).st_size % self.firehose.cfg.SECTOR_SIZE_IN_BYTES) > 0: + sectors += 1 + if partitionname.lower() != "gpt": + partition = res[2] + if sectors > partition.sectors: + self.error( + f"Error: {filename} has {sectors} sectors but partition only has {partition.sectors}.") + return False + startsector = partition.sector + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + if self.firehose.cmd_program(lun, startsector, filename): + self.printer(f"Wrote {filename} to sector {str(startsector)}.") + return True + else: + self.printer(f"Error writing {filename} to sector {str(startsector)}.") + return False + else: + if len(res) > 0: + fpartitions = res[1] + self.error(f"Error: Couldn't detect partition: {partitionname}\nAvailable partitions:") + for lun in fpartitions: + for partition in fpartitions[lun]: + if self.cfg.MemoryName == "emmc": + self.error("\t" + partition) + else: + self.error(lun + ":\t" + partition) + return False + elif cmd == "wl": + if not self.check_param([""]): + return False + directory = options[""] + if options["--skip"]: + skip = options["--skip"].split(",") + else: + skip = [] + luns = self.getluns(options) + + if not os.path.exists(directory): + self.error(f"Error: Couldn't find directory: {directory}") + sys.exit() + filenames = [] + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + for dirName, subdirList, fileList in os.walk(directory): + for fname in fileList: + filenames.append(os.path.join(dirName, fname)) + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + if "partentries" in dir(guid_gpt): + for filename in filenames: + for partition in guid_gpt.partentries: + partname = filename[filename.rfind("/") + 1:] + if ".bin" in partname[-4:] or ".img" in partname[-4:] or ".mbn" in partname[-4:]: + partname = partname[:-4] + if partition.name == partname: + if partition.name in skip: + continue + sectors = os.stat(filename).st_size // self.firehose.cfg.SECTOR_SIZE_IN_BYTES + if (os.stat(filename).st_size % self.firehose.cfg.SECTOR_SIZE_IN_BYTES) > 0: + sectors += 1 + if sectors > partition.sectors: + self.error(f"Error: {filename} has {sectors} sectors but partition " + + f"only has {partition.sectors}.") + return False + self.printer(f"Writing {filename} to partition {str(partition.name)}.") + self.firehose.cmd_program(lun, partition.sector, filename) + else: + self.printer("Couldn't write partition. Either wrong memorytype given or no gpt partition.") + return False + return True + elif cmd == "ws": + if not self.check_param([""]): + return False + if options["--lun"] is not None: + lun = int(options["--lun"]) + else: + lun = 0 + start = int(options[""]) + filename = options[""] + if not os.path.exists(filename): + self.error(f"Error: Couldn't find file: {filename}") + return False + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + if self.firehose.cmd_program(lun, start, filename): + self.printer(f"Wrote {filename} to sector {str(start)}.") + return True + else: + self.error(f"Error on writing {filename} to sector {str(start)}") + return False + elif cmd == "wf": + if not self.check_param([""]): + return False + if options["--lun"] is not None: + lun = int(options["--lun"]) + else: + lun = 0 + start = 0 + filename = options[""] + if not os.path.exists(filename): + self.error(f"Error: Couldn't find file: {filename}") + return False + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + if self.firehose.cmd_program(lun, start, filename): + self.printer(f"Wrote {filename} to sector {str(start)}.") + return True + else: + self.error(f"Error on writing {filename} to sector {str(start)}") + return False + elif cmd == "e": + if not self.check_param([""]): + return False + luns = self.getluns(options) + partitionname = options[""] + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + if "partentries" in dir(guid_gpt): + for partition in guid_gpt.partentries: + if partition.name == partitionname: + self.firehose.cmd_erase(lun, partition.sector, partition.sectors) + self.printer( + f"Erased {partitionname} starting at sector {str(partition.sector)} " + + f"with sector count {str(partition.sectors)}.") + return True + else: + self.printer("Couldn't erase partition. Either wrong memorytype given or no gpt partition.") + return False + self.error(f"Error: Couldn't detect partition: {partitionname}") + return False + elif cmd == "ep": + if not self.check_param(["", ""]): + return False + luns = self.getluns(options) + partitionname = options[""] + sectors = int(options[""]) + + for lun in luns: + data, guid_gpt = self.firehose.get_gpt(lun, int(options["--gpt-num-part-entries"]), + int(options["--gpt-part-entry-size"]), + int(options["--gpt-part-entry-start-lba"])) + if guid_gpt is None: + break + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + if "partentries" in dir(guid_gpt): + for partition in guid_gpt.partentries: + if partition.name == partitionname: + self.firehose.cmd_erase(lun, partition.sector, sectors) + self.printer( + f"Erased {partitionname} starting at sector {str(partition.sector)} " + + f"with sector count {str(sectors)}.") + return True + else: + self.printer("Couldn't erase partition. Either wrong memorytype given or no gpt partition.") + return False + self.error(f"Error: Couldn't detect partition: {partitionname}") + return False + elif cmd == "es": + if not self.check_param(["", ""]): + return False + if options["--lun"] is not None: + lun = int(options["--lun"]) + else: + lun = 0 + start = int(options[""]) + sectors = int(options[""]) + if self.firehose.modules is not None: + self.firehose.modules.writeprepare() + if self.firehose.cmd_erase(lun, start, sectors): + self.printer(f"Erased sector {str(start)} with sector count {str(sectors)}.") + return True + return False + elif cmd == "xml": + if not self.check_param([""]): + return False + return self.firehose.cmd_xml(options[""]) + elif cmd == "rawxml": + if not self.check_param([""]): + return False + return self.firehose.cmd_rawxml(options[""]) + elif cmd == "send": + if not self.check_param([""]): + return False + command = options[""] + resp = self.firehose.cmd_send(command, True) + self.printer("\n") + self.printer(resp) + return True + elif cmd == "server": + return do_tcp_server(self, options, self.handle_firehose) + elif cmd == "modules": + if not self.check_param(["", ""]): + return False + mcommand = options[""] + moptions = options[""] + if self.firehose.modules is None: + self.error("Feature is not supported") + return False + else: + return self.firehose.modules.run(command=mcommand, args=moptions) + elif cmd == "qfil": + self.info("[qfil] raw programming...") + rawprogram = options[""].split(",") + imagedir = options[""] + patch = options[""].split(",") + for xml in rawprogram: + filename = os.path.join(imagedir, xml) + if os.path.exists(filename): + self.info("[qfil] programming %s" % xml) + fl = open(filename, "r") + for evt, elem in ET.iterparse(fl, events=["end"]): + if elem.tag == "program": + if elem.get("filename", ""): + filename = os.path.join(imagedir, elem.get("filename")) + if not os.path.isfile(filename): + self.error("%s doesn't exist!" % filename) + continue + partition_number = int(elem.get("physical_partition_number")) + num_disk_sectors = self.firehose.getlunsize(partition_number) + start_sector = elem.get("start_sector") + if "NUM_DISK_SECTORS" in start_sector: + start_sector = start_sector.replace("NUM_DISK_SECTORS", str(num_disk_sectors)) + if "-" in start_sector or "*" in start_sector or "/" in start_sector or \ + "+" in start_sector: + start_sector = start_sector.replace(".", "") + start_sector = eval(start_sector) + self.info(f"[qfil] programming {filename} to partition({partition_number})" + + f"@sector({start_sector})...") + + self.firehose.cmd_program(int(partition_number), int(start_sector), filename) + else: + self.warning(f"File : {filename} not found.") + self.info("[qfil] raw programming ok.") + + self.info("[qfil] patching...") + for xml in patch: + filename = os.path.join(imagedir, xml) + self.info("[qfil] patching with %s" % xml) + if os.path.exists(filename): + fl = open(filename, "r") + for evt, elem in ET.iterparse(fl, events=["end"]): + if elem.tag == "patch": + filename = elem.get("filename") + if filename != "DISK": + continue + start_sector = elem.get("start_sector") + size_in_bytes = elem.get("size_in_bytes") + self.info( + f"[qfil] patching {filename} sector({start_sector}), size={size_in_bytes}".format( + filename=filename, start_sector=start_sector, size_in_bytes=size_in_bytes)) + content = ElementTree.tostring(elem).decode("utf-8") + CMD = "\n {content} ".format( + content=content) + print(CMD) + self.firehose.xmlsend(CMD) + else: + self.warning(f"File : {filename} not found.") + self.info("[qfil] patching ok") + bootable = self.find_bootable_partition(rawprogram) + if bootable != -1: + if self.firehose.cmd_setbootablestoragedrive(bootable): + self.info("[qfil] partition({partition}) is now bootable\n".format(partition=bootable)) + else: + self.info( + "[qfil] set partition({partition}) as bootable failed\n".format(partition=bootable)) + + else: + self.error("Unknown/Missing command, a command is required.") + return False \ No newline at end of file diff --git a/edl/Library/gpt.py b/edl/Library/gpt.py new file mode 100755 index 0000000..86f1f8d --- /dev/null +++ b/edl/Library/gpt.py @@ -0,0 +1,357 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import argparse +import os +import sys +import logging +from enum import Enum +from struct import unpack, pack +from binascii import hexlify + +try: + from edl.Library.utils import LogBase, structhelper +except: + from utils import LogBase, structhelper + + +class gpt(metaclass=LogBase): + class gpt_header: + def __init__(self, data): + sh = structhelper(data) + self.signature = sh.bytes(8) + self.revision = sh.dword() + self.header_size = sh.dword() + self.crc32 = sh.dword() + self.reserved = sh.dword() + self.current_lba = sh.qword() + self.backup_lba = sh.qword() + self.first_usable_lba = sh.qword() + self.last_usable_lba = sh.qword() + self.disk_guid = sh.bytes(16) + self.part_entry_start_lba = sh.qword() + self.num_part_entries = sh.dword() + self.part_entry_size = sh.dword() + + class gpt_partition: + def __init__(self, data): + sh = structhelper(data) + self.type = sh.bytes(16) + self.unique = sh.bytes(16) + self.first_lba = sh.qword() + self.last_lba = sh.qword() + self.flags = sh.qword() + self.name = sh.string(72) + + class efi_type(Enum): + EFI_UNUSED = 0x00000000 + EFI_MBR = 0x024DEE41 + EFI_SYSTEM = 0xC12A7328 + EFI_BIOS_BOOT = 0x21686148 + EFI_IFFS = 0xD3BFE2DE + EFI_SONY_BOOT = 0xF4019732 + EFI_LENOVO_BOOT = 0xBFBFAFE7 + EFI_MSR = 0xE3C9E316 + EFI_BASIC_DATA = 0xEBD0A0A2 + EFI_LDM_META = 0x5808C8AA + EFI_LDM = 0xAF9B60A0 + EFI_RECOVERY = 0xDE94BBA4 + EFI_GPFS = 0x37AFFC90 + EFI_STORAGE_SPACES = 0xE75CAF8F + EFI_HPUX_DATA = 0x75894C1E + EFI_HPUX_SERVICE = 0xE2A1E728 + EFI_LINUX_DAYA = 0x0FC63DAF + EFI_LINUX_RAID = 0xA19D880F + EFI_LINUX_ROOT32 = 0x44479540 + EFI_LINUX_ROOT64 = 0x4F68BCE3 + EFI_LINUX_ROOT_ARM32 = 0x69DAD710 + EFI_LINUX_ROOT_ARM64 = 0xB921B045 + EFI_LINUX_SWAP = 0x0657FD6D + EFI_LINUX_LVM = 0xE6D6D379 + EFI_LINUX_HOME = 0x933AC7E1 + EFI_LINUX_SRV = 0x3B8F8425 + EFI_LINUX_DM_CRYPT = 0x7FFEC5C9 + EFI_LINUX_LUKS = 0xCA7D7CCB + EFI_LINUX_RESERVED = 0x8DA63339 + EFI_FREEBSD_BOOT = 0x83BD6B9D + EFI_FREEBSD_DATA = 0x516E7CB4 + EFI_FREEBSD_SWAP = 0x516E7CB5 + EFI_FREEBSD_UFS = 0x516E7CB6 + EFI_FREEBSD_VINUM = 0x516E7CB8 + EFI_FREEBSD_ZFS = 0x516E7CBA + EFI_OSX_HFS = 0x48465300 + EFI_OSX_UFS = 0x55465300 + EFI_OSX_ZFS = 0x6A898CC3 + EFI_OSX_RAID = 0x52414944 + EFI_OSX_RAID_OFFLINE = 0x52414944 + EFI_OSX_RECOVERY = 0x426F6F74 + EFI_OSX_LABEL = 0x4C616265 + EFI_OSX_TV_RECOVERY = 0x5265636F + EFI_OSX_CORE_STORAGE = 0x53746F72 + EFI_SOLARIS_BOOT = 0x6A82CB45 + EFI_SOLARIS_ROOT = 0x6A85CF4D + EFI_SOLARIS_SWAP = 0x6A87C46F + EFI_SOLARIS_BACKUP = 0x6A8B642B + EFI_SOLARIS_USR = 0x6A898CC3 + EFI_SOLARIS_VAR = 0x6A8EF2E9 + EFI_SOLARIS_HOME = 0x6A90BA39 + EFI_SOLARIS_ALTERNATE = 0x6A9283A5 + EFI_SOLARIS_RESERVED1 = 0x6A945A3B + EFI_SOLARIS_RESERVED2 = 0x6A9630D1 + EFI_SOLARIS_RESERVED3 = 0x6A980767 + EFI_SOLARIS_RESERVED4 = 0x6A96237F + EFI_SOLARIS_RESERVED5 = 0x6A8D2AC7 + EFI_NETBSD_SWAP = 0x49F48D32 + EFI_NETBSD_FFS = 0x49F48D5A + EFI_NETBSD_LFS = 0x49F48D82 + EFI_NETBSD_RAID = 0x49F48DAA + EFI_NETBSD_CONCAT = 0x2DB519C4 + EFI_NETBSD_ENCRYPT = 0x2DB519EC + EFI_CHROMEOS_KERNEL = 0xFE3A2A5D + EFI_CHROMEOS_ROOTFS = 0x3CB8E202 + EFI_CHROMEOS_FUTURE = 0x2E0A753D + EFI_HAIKU = 0x42465331 + EFI_MIDNIGHTBSD_BOOT = 0x85D5E45E + EFI_MIDNIGHTBSD_DATA = 0x85D5E45A + EFI_MIDNIGHTBSD_SWAP = 0x85D5E45B + EFI_MIDNIGHTBSD_UFS = 0x0394EF8B + EFI_MIDNIGHTBSD_VINUM = 0x85D5E45C + EFI_MIDNIGHTBSD_ZFS = 0x85D5E45D + EFI_CEPH_JOURNAL = 0x45B0969E + EFI_CEPH_ENCRYPT = 0x45B0969E + EFI_CEPH_OSD = 0x4FBD7E29 + EFI_CEPH_ENCRYPT_OSD = 0x4FBD7E29 + EFI_CEPH_CREATE = 0x89C57F98 + EFI_CEPH_ENCRYPT_CREATE = 0x89C57F98 + EFI_OPENBSD = 0x824CC7A0 + EFI_QNX = 0xCEF5A9AD + EFI_PLAN9 = 0xC91818F9 + EFI_VMWARE_VMKCORE = 0x9D275380 + EFI_VMWARE_VMFS = 0xAA31E02A + EFI_VMWARE_RESERVED = 0x9198EFFC + + def __init__(self, num_part_entries=0, part_entry_size=0, part_entry_start_lba=0, loglevel=logging.INFO, *args, + **kwargs): + self.num_part_entries = num_part_entries + self.__logger = self.__logger + self.part_entry_size = part_entry_size + self.part_entry_start_lba = part_entry_start_lba + self.totalsectors = None + self.header = None + self.sectorsize = None + self.partentries = [] + + self.error = self.__logger.error + self.__logger.setLevel(loglevel) + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def parseheader(self, gptdata, sectorsize=512): + return self.gpt_header(gptdata[sectorsize:sectorsize + 0x5C]) + + def parse(self, gptdata, sectorsize=512): + self.header = self.gpt_header(gptdata[sectorsize:sectorsize + 0x5C]) + self.sectorsize = sectorsize + if self.header.signature != b"EFI PART": + return False + if self.header.revision != 0x10000: + self.error("Unknown GPT revision.") + return False + if self.part_entry_start_lba != 0: + start = self.part_entry_start_lba + else: + start = self.header.part_entry_start_lba * sectorsize + + entrysize = self.header.part_entry_size + self.partentries = [] + + class partf: + unique = b"" + first_lba = 0 + last_lba = 0 + flags = 0 + sector = 0 + sectors = 0 + type = b"" + name = "" + + num_part_entries = self.header.num_part_entries + + for idx in range(0, num_part_entries): + data = gptdata[start + (idx * entrysize):start + (idx * entrysize) + entrysize] + if int(hexlify(data[16:32]), 16) == 0: + break + partentry = self.gpt_partition(data) + pa = partf() + guid1 = unpack("\n\n" + partofsingleimage = "false" + readbackverify = "false" + sparse = "false" + for partition in self.partentries: + filename = partition.name + ".bin" + mstr += f"\t\n" + partofsingleimage = "true" + sectors = self.header.first_usable_lba + mstr += f"\t\n" + sectors = self.header.first_usable_lba - 1 + mstr += f"\t\n" + mstr += "" + wf.write(bytes(mstr, 'utf-8')) + print(f"Wrote partition xml as {fname}") + + def print_gptfile(self, filename): + try: + filesize = os.stat(filename).st_size + with open(filename, "rb") as rf: + size = min(32 * 4096, filesize) + data = rf.read(size) + for sectorsize in [512, 4096]: + result = self.parse(data, sectorsize) + if result: + break + if result: + print(self.tostring()) + return result + except Exception as e: + self.error(str(e)) + return "" + + def test_gpt(self): + res = self.print_gptfile(os.path.join("TestFiles", "gpt_sm8180x.bin")) + assert res, "GPT Partition wasn't decoded properly" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="GPT utils") + subparsers = parser.add_subparsers(dest="command", help='sub-command help') + + parser_print = subparsers.add_parser("print", help="Print the gpt table") + parser_print.add_argument("image", help="The path of the GPT disk image") + + parser_test = subparsers.add_parser("test", help="Run self-test") + + parser_extract = subparsers.add_parser("extract", help="Extract the partitions") + parser_extract.add_argument("image", help="The path of the GPT disk image") + parser_extract.add_argument("-out", "-o", help="The path to extract the partitions") + parser_extract.add_argument("-partition", "-p", help="Extract specific partitions (separated by comma)") + + args = parser.parse_args() + if args.command not in ["print", "extract", "test"]: + parser.error("Command is mandatory") + + gp = gpt() + if args.command == "print": + if not os.path.exists(args.image): + print(f"File {args.image} does not exist. Aborting.") + sys.exit(1) + gp.print_gptfile(args.image) + elif args.command == "test": + gp.test_gpt() + elif args.command == "extract": + if not os.path.exists(args.image): + print(f"File {args.image} does not exist. Aborting.") + sys.exit(1) + filesize = os.stat(args.image).st_size + with open(args.image, "rb", buffering=1024 * 1024) as rf: + data = rf.read(min(32 * 4096, filesize)) + ssize = None + for sectorsize in [512, 4096]: + result = gp.parse(data, sectorsize) + if result: + ssize = sectorsize + break + if ssize is not None: + for partition in gp.partentries: + if args.partition is not None: + if partition != args.partition: + continue + name = partition.name + start = partition.sector * ssize + length = partition.sectors * ssize + out = args.out + if out is None: + out = "." + if not os.path.exists(out): + os.makedirs(out) + filename = os.path.join(out, name) + rf.seek(start) + bytestoread = length + with open(filename, "wb", buffering=1024 * 1024) as wf: + while bytestoread > 0: + size = min(bytestoread, 0x200000) + rf.read(size) + wf.write(size) + bytestoread -= size + print(f"Extracting {name} to {filename} at {hex(start)}, length {hex(length)}") diff --git a/edl/Library/hdlc.py b/edl/Library/hdlc.py new file mode 100755 index 0000000..4e4ce95 --- /dev/null +++ b/edl/Library/hdlc.py @@ -0,0 +1,240 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2019 + +import logging +from binascii import hexlify +from struct import unpack +import time + +MAX_PACKET_LEN = 4096 + +crcTbl = ( + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, + 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, + 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, + 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, + 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, + 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, + 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, + 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, + 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, + 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, + 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, + 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, + 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, + 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, + 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, + 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, + 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, + 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78) + + +def serial16le(data): + out = bytearray() + out.append(data & 0xFF) + out.append((data >> 8) & 0xFF) + return out + + +def serial16(data): + out = bytearray() + out.append((data >> 8) & 0xFF) + out.append(data & 0xFF) + return out + + +def serial32le(data): + out = bytearray() + out += serial16le(data & 0xFFFF) + out += serial16le((data >> 16) & 0xFFFF) + return out + + +def crc16(iv, data): + for byte in data: + iv = ((iv >> 8) & 0xFFFF) ^ crcTbl[(iv ^ byte) & 0xFF] + return ~iv & 0xFFFF + + +def serial32(data): + out = bytearray() + out += serial16((data >> 16) & 0xFFFF) + out += serial16(data & 0xFFFF) + return out + + +def escape(indata): + outdata = bytearray() + for i in range(0, len(indata)): + buf = indata[i] + if buf == 0x7e: + outdata.append(0x7d) + outdata.append(0x5e) + elif buf == 0x7d: + outdata.append(0x7d) + outdata.append(0x5d) + else: + outdata.append(buf) + return outdata + + +def unescape(indata): + mescape = False + out = bytearray() + for buf in indata: + if mescape: + if buf == 0x5e: + out.append(0x7e) + elif buf == 0x5d: + out.append(0x7d) + else: + logging.error("Fatal error unescaping buffer!") + return None + mescape = False + else: + if buf == 0x7d: + mescape = True + else: + out.append(buf) + if len(out) == 0: + return None + return out + + +def convert_cmdbuf(indata): + crc16val = crc16(0xFFFF, indata) + indata.extend(bytearray(serial16le(crc16val))) + outdata = escape(indata) + outdata.append(0x7E) + return outdata + + +class hdlc: + def __init__(self, cdc): + self.cdc = cdc + self.programmer = None + self.timeout = 1500 + + def receive_reply(self, timeout=None): + replybuf = bytearray() + if timeout is None: + timeout = self.timeout + tmp = self.cdc.read(MAX_PACKET_LEN, timeout) + if tmp == bytearray(): + return 0 + if tmp == b"": + return 0 + retry = 0 + while tmp[-1] != 0x7E: + time.sleep(0.01) + tmp += self.cdc.read(MAX_PACKET_LEN, timeout) + retry += 1 + if retry > 5: + break + replybuf.extend(tmp) + data = unescape(replybuf) + # print(hexlify(data)) + if len(data) > 3: + crc16val = crc16(0xFFFF, data[:-3]) + reccrc = int(data[-3]) + (int(data[-2]) << 8) + if crc16val != reccrc: + return -1 + else: + time.sleep(0.01) + data = self.cdc.read(MAX_PACKET_LEN, timeout) + if len(data) > 3: + crc16val = crc16(0xFFFF, data[:-3]) + reccrc = int(data[-3]) + (int(data[-2]) << 8) + if crc16val != reccrc: + return -1 + return data + return data[:-3] + + def receive_reply_nocrc(self, timeout=None): + replybuf = bytearray() + if timeout is None: + timeout = self.timeout + tmp = self.cdc.read(MAX_PACKET_LEN, timeout) + if tmp == bytearray(): + return 0 + if tmp == b"": + return 0 + retry = 0 + while tmp[-1] != 0x7E: + # time.sleep(0.05) + tmp += self.cdc.read(MAX_PACKET_LEN, timeout) + retry += 1 + if retry > 5: + break + replybuf.extend(tmp) + data = unescape(replybuf) + # print(hexlify(data)) + if len(data) > 3: + # crc16val = self.crc16(0xFFFF, data[:-3]) + # reccrc = int(data[-3]) + (int(data[-2]) << 8) + return data[:-3] + else: + time.sleep(0.5) + data = self.cdc.read(MAX_PACKET_LEN, timeout) + if len(data) > 3: + # crc16val = self.crc16(0xFFFF, data[:-3]) + # reccrc = int(data[-3]) + (int(data[-2]) << 8) + return data[:-3] + else: + return data + + def send_unframed_buf(self, outdata, prefixflag): + # ttyflush() + if prefixflag: + tmp = bytearray() + tmp.append(0x7E) + tmp.extend(outdata) + outdata = tmp + return self.cdc.write(outdata[:MAX_PACKET_LEN]) + # FlushFileBuffers(ser) + + def send_cmd_base(self, outdata, prefixflag, nocrc=False): + if isinstance(outdata, str): + outdata = bytes(outdata, 'utf-8') + packet = convert_cmdbuf(bytearray(outdata)) + if self.send_unframed_buf(packet, prefixflag): + if nocrc: + return self.receive_reply_nocrc() + else: + return self.receive_reply() + return b"" + + def send_cmd(self, outdata, nocrc=False): + return self.send_cmd_base(outdata, 1, nocrc) + + def send_cmd_np(self, outdata, nocrc=False): + return self.send_cmd_base(outdata, 0, nocrc) + + def show_errpacket(self, descr, pktbuf): + if len(pktbuf) == 0: + return + logging.error("Error: %s " % descr) + + if pktbuf[1] == 0x0e: + pktbuf[-4] = 0 + # puts(pktbuf+2) + ret = self.receive_reply() + errorcode = unpack("> 10 + flashinfo["block_size_kb"] = 64 << tid.block_size + flashinfo["pages_per_block"] = flashinfo["block_size_kb"] // flashinfo["page_size_kb"] + flashinfo["spare_size"] = (8 << tid.spare_size) * (flashinfo["page_size"] // 512) + if flashinfo["page_size"] == 2048 or flashinfo["page_size"] == 4096: + flashinfo["otp_sequence_cfg"] = "FLASH_NAND_OTP_SEQUENCE_CFG2" + else: + flashinfo["otp_sequence_cfg"] = "FLASH_NAND_OTP_SEQUENCE_UNKNOWN" + flashinfo["block_count"] = (1024 // 8 * density_mbits) // flashinfo["block_size_kb"] + if flashinfo["page_size"] == 2048 and flashinfo["feature_flags1_ecc"] > 0: + flashinfo["bad_block_info_byte_offset"] = 2048 + flashinfo["udata_max"] = 16 + flashinfo["max_corrected_udata_bytes"] = 16 + flashinfo["bad_block_info_byte_length"] = 1 if flashinfo["dev_width"] == 8 else 2 + elif flashinfo["page_size"] == 4096 and flashinfo["feature_flags1_ecc"] > 0: + flashinfo["bad_block_info_byte_offset"] = 4096 + flashinfo["udata_max"] = 32 + flashinfo["max_corrected_udata_bytes"] = 32 + flashinfo["bad_block_info_byte_length"] = 1 if flashinfo["dev_width"] == 8 else 2 + self.settings.PAGESIZE = flashinfo["page_size"] + self.settings.BLOCKSIZE = flashinfo["block_size_kb"] * 1024 + if flashinfo["dev_width"] == 8: + self.settings.IsWideFlash = 0 + else: + self.settings.IsWideFlash = 1 + self.settings.MAXBLOCK = flashinfo["block_count"] + + self.settings.BAD_BLOCK_IN_SPARE_AREA = flashinfo["bad_block_info_byte_offset"] + return flashinfo + + def toshiba_config(self, nandid): + flashinfo = self.gettbl(nandid, toshiba_tbl) + self.flashinfo = flashinfo + if (nandid >> 8) & 0xFF == 0xac: + self.settings.OOBSIZE = 256 + self.settings.PAGESIZE = 4096 + self.settings.MAXBLOCK = 2048 + # 8Bit_HW_ECC + elif (nandid >> 8) & 0xFF == 0xaa: + self.settings.OOBSIZE = 128 + self.settings.PAGESIZE = 2048 + self.settings.MAXBLOCK = 2048 + # 8Bit_HW_ECC + elif (nandid >> 8) & 0xFF == 0xa1: + self.settings.OOBSIZE = 128 + self.settings.PAGESIZE = 2048 + self.settings.MAXBLOCK = 1024 + # 8Bit_HW_ECC + + self.settings.CW_PER_PAGE = (self.settings.PAGESIZE >> 9) - 1 + self.settings.SPARE_SIZE_BYTES = 0 + + def samsung_config(self, nandid): + flashinfo = self.gettbl(nandid, samsung_tbl) + self.flashinfo = flashinfo + + # self.settings.SPARE_SIZE_BYTES = flashinfo["spare_size"] + self.settings.CW_PER_PAGE = (self.settings.PAGESIZE >> 9) - 1 + self.settings.SPARE_SIZE_BYTES = 0 + + def generic_config(self, nandid, chipsize): + devcfg = (nandid >> 24) & 0xff + self.settings.PAGESIZE = 1024 << (devcfg & 0x3) + self.settings.BLOCKSIZE = 64 << ((devcfg >> 4) & 0x3) + + if chipsize != 0: + self.settings.MAXBLOCK = chipsize * 1024 // self.settings.BLOCKSIZE + else: + self.settings.MAXBLOCK = 0x800 + self.settings.CW_PER_PAGE = (self.settings.PAGESIZE >> 9) - 1 + + def nand_setup(self, nandid): + """ + qcommand -p%qdl% -k11 -c "m 79b0020 295409c0" #NAND_DEV0_CFG0 + qcommand -p%qdl% -k11 -c "m 79b0024 08065d5d" #NAND_DEV0_CFG1 + qcommand -p%qdl% -k11 -c "m 79b0028 42040d10" #NAND_DEV0_ECC_CFG + qcommand -p%qdl% -k11 -c "m 79b00f0 00000203" NAND_EBI2_ECC_BUF_CFG + """ + + fid = (nandid >> 8) & 0xff + pid = nandid & 0xff + + self.settings.flash_mfr = "" + for info in nand_manuf_ids: + if info[0] == pid: + self.settings.flash_mfr = info[1] + break + + chipsize = 0 + for info in nand_ids: + if info[1] == fid: + chipsize = info[2] + self.settings.flash_descr = info[0] + break + + self.settings.cfg1_enable_bch_ecc = 1 + self.settings.IsWideFlash = 0 + self.settings.SPARE_SIZE_BYTES = 0 + self.settings.OOBSIZE = 0 + self.settings.ECC_PARITY_SIZE_BYTES = 0 + self.settings.BAD_BLOCK_BYTE_NUM = 0 + self.settings.ecc_bit = 4 + + if pid == 0x98: # Toshiba + self.toshiba_config(nandid) + if nandid == 0x2690AC98: + self.settings.ecc_bit = 8 + elif pid == 0xEC: # Samsung + self.samsung_config(nandid) + elif pid == 0x2C: # Micron + self.generic_config(nandid, chipsize) + # MT29AZ5A3CHHWD + if nandid == 0x2690AC2C or nandid == 0x26D0A32C: + self.settings.ecc_bit = 8 + elif pid == 0x01: + self.generic_config(nandid, chipsize) + if nandid == 0x1590AC01: # jsfc 4G + self.settings.OOBSIZE = 128 + self.settings.PAGESIZE = 2048 + self.settings.SPARE_SIZE_BYTES = 4 + else: + self.generic_config(nandid, chipsize) + + if nandid in supported_flash: + nd = supported_flash[nandid] + # density = nd[] + # width + chipsize = nd[0] // 1024 + self.settings.IsWideFlash = nd[1] + self.settings.PAGESIZE = nd[2] + self.settings.BLOCKSIZE = nd[3] + self.settings.OOBSIZE = nd[4] + self.settings.IsOneNand = nd[5] + + if chipsize != 0: + self.settings.MAXBLOCK = chipsize * 1024 // self.settings.BLOCKSIZE + else: + self.settings.MAXBLOCK = 0x800 + + self.settings.sectorsize = 512 + self.settings.sectors_per_page = self.settings.PAGESIZE // self.settings.sectorsize + + if self.settings.ecc_bit == 4: + self.settings.ECC_MODE = 0 # 0=4 bit ECC error + elif self.settings.ecc_bit == 8: + self.settings.ECC_MODE = 1 # 1=8 bit ECC error + elif self.settings.ecc_bit == 16: + self.settings.ECC_MODE = 2 # 2=16 bit ECC error + + if self.settings.ecc_size == 0: + if self.settings.ecc_bit == 4: + self.settings.ecc_size = 1 + elif self.settings.ecc_bit == 8 or self.settings.ecc_bit == 16: + self.settings.ecc_size = 2 + + if self.settings.OOBSIZE == 0: + self.settings.OOBSIZE = (8 << self.settings.ecc_size) * (self.settings.CW_PER_PAGE + 1) + + if 256 >= self.settings.OOBSIZE > 128: + self.settings.OOBSIZE = 256 + + if self.settings.SPARE_SIZE_BYTES == 0: + # HAM1 + if self.settings.ECC_MODE == 0: + self.settings.SPARE_SIZE_BYTES = 4 + else: + self.settings.SPARE_SIZE_BYTES = 2 + + if self.settings.cfg1_enable_bch_ecc: + hw_ecc_bytes = 0 + self.settings.UD_SIZE_BYTES = self.settings.SPARE_SIZE_BYTES + self.settings.sectorsize # 516 or 517 + if self.settings.SPARE_SIZE_BYTES == 2: + self.settings.UD_SIZE_BYTES += 2 + if self.settings.IsWideFlash: + self.settings.UD_SIZE_BYTES += 1 + else: + hw_ecc_bytes = 10 + self.settings.UD_SIZE_BYTES = 512 + + if self.settings.ECC_PARITY_SIZE_BYTES == 0: + self.settings.ECC_PARITY_SIZE_BYTES = 3 # HAM1 + if self.settings.ecc_bit == 4: # BCH4 + self.settings.ECC_PARITY_SIZE_BYTES = 7 + elif self.settings.ecc_bit == 8: # BCH8 + self.settings.ECC_PARITY_SIZE_BYTES = 13 + elif self.settings.ecc_bit == 16: # BCH16 + self.settings.ECC_PARITY_SIZE_BYTES = 26 + + linuxcwsize = 528 + if self.settings.cfg1_enable_bch_ecc and self.settings.ecc_bit == 8: + linuxcwsize = 532 + if nandid == 0x1590AC2C: # fixme + linuxcwsize = 532 + if self.settings.BAD_BLOCK_BYTE_NUM == 0: + self.settings.BAD_BLOCK_BYTE_NUM = ( + self.settings.PAGESIZE - (linuxcwsize * (self.settings.sectors_per_page - 1)) + 1) + + # UD_SIZE_BYTES must be 512, 516 or 517. If ECC-Protection 516 for x16Bit-Nand and 517 for x8-bit Nand + cfg0 = 0 << self.SET_RD_MODE_AFTER_STATUS \ + | 0 << self.STATUS_BFR_READ \ + | 5 << self.NUM_ADDR_CYCLES \ + | self.settings.SPARE_SIZE_BYTES << self.SPARE_SIZE_BYTES \ + | hw_ecc_bytes << self.ECC_PARITY_SIZE_BYTES_RS \ + | self.settings.UD_SIZE_BYTES << self.UD_SIZE_BYTES \ + | self.settings.CW_PER_PAGE << self.CW_PER_PAGE \ + | 0 << self.DISABLE_STATUS_AFTER_WRITE + + bad_block_byte = self.settings.BAD_BLOCK_BYTE_NUM + wide_bus = self.settings.IsWideFlash + bch_disabled = self.settings.args_disable_ecc # option in gui, implemented + + cfg1 = 0 << self.ECC_MODE_DEV1 \ + | 1 << self.ENABLE_NEW_ECC \ + | 0 << self.DISABLE_ECC_RESET_AFTER_OPDONE \ + | 0 << self.ECC_DECODER_CGC_EN \ + | 0 << self.ECC_ENCODER_CGC_EN \ + | 2 << self.WR_RD_BSY_GAP \ + | 0 << self.BAD_BLOCK_IN_SPARE_AREA \ + | bad_block_byte << self.BAD_BLOCK_BYTE_NUM \ + | 0 << self.CS_ACTIVE_BSY \ + | 7 << self.NAND_RECOVERY_CYCLES \ + | wide_bus << self.WIDE_FLASH \ + | bch_disabled << self.ENABLE_BCH_ECC + + """ + cfg0_raw = (self.settings.CW_PER_PAGE-1) << CW_PER_PAGE \ + | self.settings.UD_SIZE_BYTES << UD_SIZE_BYTES \ + | 5 << NUM_ADDR_CYCLES \ + | 0 << SPARE_SIZE_BYTES + + cfg1_raw = 7 << NAND_RECOVERY_CYCLES \ + | 0 << CS_ACTIVE_BSY \ + | 17 << BAD_BLOCK_BYTE_NUM \ + | 1 << BAD_BLOCK_IN_SPARE_AREA \ + | 2 << WR_RD_BSY_GAP \ + | wide_bus << WIDE_FLASH \ + | 1 << DEV0_CFG1_ECC_DISABLE + """ + ecc_bch_cfg = 1 << self.ECC_FORCE_CLK_OPEN \ + | 0 << self.ECC_DEC_CLK_SHUTDOWN \ + | 0 << self.ECC_ENC_CLK_SHUTDOWN \ + | self.settings.UD_SIZE_BYTES << self.ECC_NUM_DATA_BYTES \ + | self.settings.ECC_PARITY_SIZE_BYTES << self.ECC_PARITY_SIZE_BYTES_BCH \ + | self.settings.ECC_MODE << self.ECC_MODE \ + | 0 << self.ECC_SW_RESET \ + | bch_disabled << self.ECC_CFG_ECC_DISABLE + + if self.settings.UD_SIZE_BYTES == 516: + ecc_buf_cfg = 0x203 + elif self.settings.UD_SIZE_BYTES == 517: + ecc_buf_cfg = 0x204 + else: + ecc_buf_cfg = 0x1FF + + return cfg0, cfg1, ecc_buf_cfg, ecc_bch_cfg + + +class nandregs: + def __init__(self, parent): + self.register_mapping = { + } + self.reverse_mapping = {} + self.create_reverse_mapping() + self.parent = parent + + def __getattribute__(self, name): + if name in ("register_mapping", "parent"): + return super(nandregs, self).__getattribute__(name) + + if name in self.register_mapping: + return self.parent.mempeek(self.register_mapping[name]) + + return super(nandregs, self).__getattribute__(name) + + def __setattr__(self, name, value): + if name in ("register_mapping", "parent"): + super(nandregs, self).__setattr__(name, value) + + if name in self.register_mapping: + self.parent.mempoke(self.register_mapping[name], value) + else: + super(nandregs, self).__setattr__(name, value) + + def read(self, register): + if isinstance(register, str): + register = self.register_mapping.get(register.lower(), None) + return self.parent.mempeek(register) + + def write(self, register, value): + if isinstance(register, str): + register = self.register_mapping.get(register.lower(), None) + return self.parent.mempoke(register, value) + + def save(self): + reg_dict = {} + for reg in self.register_mapping: + reg_v = self.read(reg) + reg_dict[reg] = reg_v + return reg_dict + + def restore(self, value=None): + if value is None: + value = {} + for reg in self.register_mapping: + reg_v = value[reg] + self.write(reg, reg_v) + + def create_reverse_mapping(self): + self.reverse_mapping = {v: k for k, v in self.register_mapping.items()} diff --git a/edl/Library/pt.py b/edl/Library/pt.py new file mode 100755 index 0000000..758c720 --- /dev/null +++ b/edl/Library/pt.py @@ -0,0 +1,171 @@ +import struct + + +def get_n(x): + return int(x[6:8] + x[4:6] + x[2:4] + x[0:2], 16) + + +def parse_pt(data): + va = 0 + entries = [] + while va < len(data): + entry = struct.unpack(" 1: + return sld_xsp(msld) + + return "UNSUPPORTED" + + +class descriptor(object): + def __init__(self, mfld): + pass + + def get_name(self): + pass + + def __repr__(self): + s = "%8s " % self.get_name() + for attr, value in self.__dict__.items(): + try: + s += "%s=%s, " % (attr, hex(value)) + except: + s += "%s=%s, " % (attr, value) + + return s + + +class fld(descriptor): + pass + + +class fault_desc(fld): + + def get_name(self): + return "FAULT" + + +class reserved_desc(fld): + + def get_name(self): + return "RESERVED" + + +class pt_desc(fld): + + def __init__(self, desc): + self.coarse_base = (desc >> 10) << 10 + self.p = (desc >> 9) & 1 + self.domain = (desc >> 5) & 15 + self.sbz1 = (desc >> 4) & 1 + self.ns = (desc >> 3) & 1 + self.sbz2 = (desc >> 2) & 1 + + def get_name(self): + return "PT" + + +class section_desc(fld): + def __init__(self, desc): + self.section_base = (desc >> 20) << 20 + self.ns = (desc >> 19) & 1 + self.zero = ns = (desc >> 18) & 1 + self.ng = (desc >> 17) & 1 + self.s = (desc >> 16) & 1 + self.apx = (desc >> 15) & 1 + self.tex = (desc >> 12) & 7 + self.ap = (desc >> 10) & 3 + self.p = (desc >> 9) & 1 + self.domain = (desc >> 5) & 15 + self.nx = (desc >> 4) & 1 + self.c = (desc >> 3) & 1 + self.b = (desc >> 2) & 1 + + def get_name(self): + return "SECTION" + + +class sld(descriptor): + pass + + +class sld_lp(sld): + + def __init__(self, desc): + self.page_base = (desc >> 16) << 16 + self.nx = (desc >> 15) & 1 + self.tex = (desc >> 12) & 7 + self.ng = (desc >> 11) & 1 + self.s = (desc >> 10) & 1 + self.apx = (desc >> 9) & 1 + self.sbz = (desc >> 6) & 7 + self.ap = (desc >> 4) & 3 + self.c = (desc >> 3) & 1 + self.b = (desc >> 2) & 1 + + def get_name(self): + return "LARGEPAGE" + + +class sld_xsp(sld): + + def __init__(self, desc): + self.desc = desc + self.page_base = (desc >> 12) << 12 + self.ng = (desc >> 11) & 1 + self.s = (desc >> 10) & 1 + self.apx = (desc >> 9) & 1 + self.tex = (desc >> 6) & 7 + self.ap = (desc >> 4) & 3 + self.c = (desc >> 3) & 1 + self.b = (desc >> 2) & 1 + self.nx = desc & 1 + + def get_name(self): + return "XSMALLPAGE" diff --git a/edl/Library/pt64.py b/edl/Library/pt64.py new file mode 100755 index 0000000..4a82520 --- /dev/null +++ b/edl/Library/pt64.py @@ -0,0 +1,153 @@ +import struct + +""" +only supports 4KB granule w/ 25<=TnSZ<=33 +https://armv8-ref.codingbelief.com/en/chapter_d4/d42_7_the_algorithm_for_finding_the_translation_table_entries.html + +""" + + +def get_level_index(va, level): + if level == 1: + return (va >> 30) & 0x3F + + if level == 2: + return (va >> 21) & 0x1FF + + if level == 3: + return (va >> 12) & 0x1FF + + raise NotImplementedError() + + +def get_level_bits(level, tnsz): + if level == 1: + return 37 - tnsz + 26 + 1 - 30 + + if level == 2: + return 9 + + if level == 3: + return 9 + + raise NotImplementedError() + + +def get_level_size(tnsz, level): + return 2 ** get_level_bits(level, tnsz) * 8 + + +def get_va_for_level(va, index, level): + if level == 1: + return va + (index << 30) + + if level == 2: + return va + (index << 21) + + if level == 3: + return va + (index << 12) + + return va + + +def parse_pt(data, base, tnsz, level=1): + i = 0 + entries = [] + while i < min(len(data), get_level_size(tnsz, level)): + mentry = struct.unpack("> 63 + self.apx = (desc >> 61) & 3 + self.xn = (desc >> 60) & 1 + self.pxn = (desc >> 59) & 1 + self.attrindex = (desc >> 2) & 7 + self.ns = (desc >> 5) & 1 + self.ap = (desc >> 6) & 3 + self.sh = (desc >> 8) & 3 + self.af = (desc >> 10) & 1 + self.nG = (desc >> 11) & 1 + + +class entry4k(entry): + def __init__(self, desc, level): + entry.__init__(self, desc, level) + self.output = ((desc & 0xFFFFFFFFFFFF) >> 12) << 12 + + +class fault_entry(fld): + + def get_name(self): + return "FAULT" + + +class block_entry4k(entry4k): + + def __init__(self, desc, level): + entry4k.__init__(self, desc, level) + # shift = 39-9*level + # self.output = ((desc & 0xFFFFFFFFFFFFL) >> shift) << shift + + def get_name(self): + return "BLOCK4" + + +class table_entry4k(entry4k): + + def __init__(self, desc, level): + entry4k.__init__(self, desc, level) + + def get_name(self): + return "TABLE4" diff --git a/edl/Library/sahara.py b/edl/Library/sahara.py new file mode 100755 index 0000000..f165703 --- /dev/null +++ b/edl/Library/sahara.py @@ -0,0 +1,834 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import binascii +import time +import os +import sys +import logging +import inspect +from struct import unpack, pack +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) +from edl.Library.utils import read_object, print_progress, rmrf, LogBase +from edl.Config.qualcomm_config import sochw, msmids, root_cert_hash + + + + +def convertmsmid(msmid): + msmiddb = [] + if int(msmid, 16) & 0xFF == 0xe1 or msmid == '00000000': + return [msmid] + socid = int(msmid, 16) >> 16 + if socid in sochw: + names = sochw[socid].split(",") + for name in names: + for ids in msmids: + if msmids[ids] == name: + rmsmid = hex(ids)[2:].lower() + while len(rmsmid) < 8: + rmsmid = '0' + rmsmid + msmiddb.append(rmsmid) + return msmiddb + + +class sahara(metaclass=LogBase): + SAHARA_VERSION = 2 + SAHARA_MIN_VERSION = 1 + + class cmd: + SAHARA_HELLO_REQ = 0x1 + SAHARA_HELLO_RSP = 0x2 + SAHARA_READ_DATA = 0x3 + SAHARA_END_TRANSFER = 0x4 + SAHARA_DONE_REQ = 0x5 + SAHARA_DONE_RSP = 0x6 + SAHARA_RESET_REQ = 0x7 + SAHARA_RESET_RSP = 0x8 + SAHARA_MEMORY_DEBUG = 0x9 + SAHARA_MEMORY_READ = 0xA + SAHARA_CMD_READY = 0xB + SAHARA_SWITCH_MODE = 0xC + SAHARA_EXECUTE_REQ = 0xD + SAHARA_EXECUTE_RSP = 0xE + SAHARA_EXECUTE_DATA = 0xF + SAHARA_64BIT_MEMORY_DEBUG = 0x10 + SAHARA_64BIT_MEMORY_READ = 0x11 + SAHARA_64BIT_MEMORY_READ_DATA = 0x12 + SAHARA_RESET_STATE_MACHINE_ID = 0x13 + + class exec_cmd: + SAHARA_EXEC_CMD_NOP = 0x00 + SAHARA_EXEC_CMD_SERIAL_NUM_READ = 0x01 + SAHARA_EXEC_CMD_MSM_HW_ID_READ = 0x02 + SAHARA_EXEC_CMD_OEM_PK_HASH_READ = 0x03 + SAHARA_EXEC_CMD_SWITCH_TO_DMSS_DLOAD = 0x04 + SAHARA_EXEC_CMD_SWITCH_TO_STREAM_DLOAD = 0x05 + SAHARA_EXEC_CMD_READ_DEBUG_DATA = 0x06 + SAHARA_EXEC_CMD_GET_SOFTWARE_VERSION_SBL = 0x07 + + class sahara_mode: + SAHARA_MODE_IMAGE_TX_PENDING = 0x0 + SAHARA_MODE_IMAGE_TX_COMPLETE = 0x1 + SAHARA_MODE_MEMORY_DEBUG = 0x2 + SAHARA_MODE_COMMAND = 0x3 + + class status: + SAHARA_STATUS_SUCCESS = 0x00 # Invalid command received in current state + SAHARA_NAK_INVALID_CMD = 0x01 # Protocol mismatch between host and target + SAHARA_NAK_PROTOCOL_MISMATCH = 0x02 # Invalid target protocol version + SAHARA_NAK_INVALID_TARGET_PROTOCOL = 0x03 # Invalid host protocol version + SAHARA_NAK_INVALID_HOST_PROTOCOL = 0x04 # Invalid packet size received + SAHARA_NAK_INVALID_PACKET_SIZE = 0x05 # Unexpected image ID received + SAHARA_NAK_UNEXPECTED_IMAGE_ID = 0x06 # Invalid image header size received + SAHARA_NAK_INVALID_HEADER_SIZE = 0x07 # Invalid image data size received + SAHARA_NAK_INVALID_DATA_SIZE = 0x08 # Invalid image type received + SAHARA_NAK_INVALID_IMAGE_TYPE = 0x09 # Invalid tranmission length + SAHARA_NAK_INVALID_TX_LENGTH = 0x0A # Invalid reception length + SAHARA_NAK_INVALID_RX_LENGTH = 0x0B # General transmission or reception error + SAHARA_NAK_GENERAL_TX_RX_ERROR = 0x0C # Error while transmitting READ_DATA packet + SAHARA_NAK_READ_DATA_ERROR = 0x0D # Cannot receive specified number of program headers + SAHARA_NAK_UNSUPPORTED_NUM_PHDRS = 0x0E # Invalid data length received for program headers + SAHARA_NAK_INVALID_PDHR_SIZE = 0x0F # Multiple shared segments found in ELF image + SAHARA_NAK_MULTIPLE_SHARED_SEG = 0x10 # Uninitialized program header location + SAHARA_NAK_UNINIT_PHDR_LOC = 0x11 # Invalid destination address + SAHARA_NAK_INVALID_DEST_ADDR = 0x12 # Invalid data size received in image header + SAHARA_NAK_INVALID_IMG_HDR_DATA_SIZE = 0x13 # Invalid ELF header received + SAHARA_NAK_INVALID_ELF_HDR = 0x14 # Unknown host error received in HELLO_RESP + SAHARA_NAK_UNKNOWN_HOST_ERROR = 0x15 # Timeout while receiving data + SAHARA_NAK_TIMEOUT_RX = 0x16 # Timeout while transmitting data + SAHARA_NAK_TIMEOUT_TX = 0x17 # Invalid mode received from host + SAHARA_NAK_INVALID_HOST_MODE = 0x18 # Invalid memory read access + SAHARA_NAK_INVALID_MEMORY_READ = 0x19 # Host cannot handle read data size requested + SAHARA_NAK_INVALID_DATA_SIZE_REQUEST = 0x1A # Memory debug not supported + SAHARA_NAK_MEMORY_DEBUG_NOT_SUPPORTED = 0x1B # Invalid mode switch + SAHARA_NAK_INVALID_MODE_SWITCH = 0x1C # Failed to execute command + SAHARA_NAK_CMD_EXEC_FAILURE = 0x1D # Invalid parameter passed to command execution + SAHARA_NAK_EXEC_CMD_INVALID_PARAM = 0x1E # Unsupported client command received + SAHARA_NAK_EXEC_CMD_UNSUPPORTED = 0x1F # Invalid client command received for data response + SAHARA_NAK_EXEC_DATA_INVALID_CLIENT_CMD = 0x20 # Failed to authenticate hash table + SAHARA_NAK_HASH_TABLE_AUTH_FAILURE = 0x21 # Failed to verify hash for a given segment of ELF image + SAHARA_NAK_HASH_VERIFICATION_FAILURE = 0x22 # Failed to find hash table in ELF image + SAHARA_NAK_HASH_TABLE_NOT_FOUND = 0x23 # Target failed to initialize + SAHARA_NAK_TARGET_INIT_FAILURE = 0x24 # Failed to authenticate generic image + SAHARA_NAK_IMAGE_AUTH_FAILURE = 0x25 # Invalid ELF hash table size. Too bit or small. + SAHARA_NAK_INVALID_IMG_HASH_TABLE_SIZE = 0x26 + SAHARA_NAK_MAX_CODE = 0x7FFFFFFF # To ensure 32-bits wide */ + + ErrorDesc = { + 0x00: "Invalid command received in current state", + 0x01: "Protocol mismatch between host and target", + 0x02: "Invalid target protocol version", + 0x03: "Invalid host protocol version", + 0x04: "Invalid packet size received", + 0x05: "Unexpected image ID received", + 0x06: "Invalid image header size received", + 0x07: "Invalid image data size received", + 0x08: "Invalid image type received", + 0x09: "Invalid tranmission length", + 0x0A: "Invalid reception length", + 0x0B: "General transmission or reception error", + 0x0C: "Error while transmitting READ_DATA packet", + 0x0D: "Cannot receive specified number of program headers", + 0x0E: "Invalid data length received for program headers", + 0x0F: "Multiple shared segments found in ELF image", + 0x10: "Uninitialized program header location", + 0x11: "Invalid destination address", + 0x12: "Invalid data size received in image header", + 0x13: "Invalid ELF header received", + 0x14: "Unknown host error received in HELLO_RESP", + 0x15: "Timeout while receiving data", + 0x16: "Timeout while transmitting data", + 0x17: "Invalid mode received from host", + 0x18: "Invalid memory read access", + 0x19: "Host cannot handle read data size requested", + 0x1A: "Memory debug not supported", + 0x1B: "Invalid mode switch", + 0x1C: "Failed to execute command", + 0x1D: "Invalid parameter passed to command execution", + 0x1E: "Unsupported client command received", + 0x1F: "Invalid client command received for data response", + 0x20: "Failed to authenticate hash table", + 0x21: "Failed to verify hash for a given segment of ELF image", + 0x22: "Failed to find hash table in ELF image", + 0x23: "Target failed to initialize", + 0x24: "Failed to authenticate generic image", + 0x25: "Invalid ELF hash table size. Too bit or small.", + 0x26: "Invalid IMG Hash Table Size" + } + + def init_loader_db(self): + loaderdb = {} + for (dirpath, dirnames, filenames) in os.walk(os.path.join(parent_dir,"Loaders")): + for filename in filenames: + fn = os.path.join(dirpath, filename) + found = False + for ext in [".bin", ".mbn", ".elf"]: + if ext in filename[-4:]: + found = True + break + if not found: + continue + try: + hwid = filename.split("_")[0].lower() + msmid = hwid[:8] + devid = hwid[8:] + pkhash = filename.split("_")[1].lower() + for msmid in convertmsmid(msmid): + mhwid = msmid + devid + mhwid = mhwid.lower() + if mhwid not in loaderdb: + loaderdb[mhwid] = {} + if pkhash not in loaderdb[mhwid]: + loaderdb[mhwid][pkhash] = fn + else: + loaderdb[mhwid][pkhash].append(fn) + except Exception as e: # pylint: disable=broad-except + self.debug(str(e)) + continue + self.loaderdb = loaderdb + return loaderdb + + def get_error_desc(self, status): + if status in self.ErrorDesc: + return "Error: " + self.ErrorDesc[status] + else: + return "Unknown error" + + pkt_hello_req = [ + ('cmd', 'I'), + ('len', 'I'), + ('version', 'I'), + ('version_min', 'I'), + ('max_cmd_len', 'I'), + ('mode', 'I'), + ('res1', 'I'), + ('res2', 'I'), + ('res3', 'I'), + ('res4', 'I'), + ('res5', 'I'), + ('res6', 'I')] + + pkt_cmd_hdr = [ + ('cmd', 'I'), + ('len', 'I') + ] + + pkt_read_data = [ + ('id', 'I'), + ('data_offset', 'I'), + ('data_len', 'I') + ] + + pkt_read_data_64 = [ + ('id', 'Q'), + ('data_offset', 'Q'), + ('data_len', 'Q') + ] + + pkt_memory_debug = [ + ('memory_table_addr', 'I'), + ('memory_table_length', 'I') + ] + + pkt_memory_debug_64 = [ + ('memory_table_addr', 'Q'), + ('memory_table_length', 'Q') + ] + ''' + execute_cmd=[ + ('cmd', 'I'), + ('len', 'I'), + ('client_cmd','I') + ] + ''' + + pkt_execute_rsp_cmd = [ + ('cmd', 'I'), + ('len', 'I'), + ('client_cmd', 'I'), + ('data_len', 'I') + ] + + pkt_image_end = [ + ('id', 'I'), + ('status', 'I') + ] + + pkt_done = [ + ('cmd', 'I'), + ('len', 'I'), + ('status', 'I') + ] + + pbl_info = [ + ('serial', 'I'), + ('msm_id', 'I'), + ('pk_hash', '32s'), + ('pbl_sw', 'I') + ] + + parttbl = [ + ('save_pref', 'I'), + ('mem_base', 'I'), + ('length', 'I'), + ('desc', '20s'), + ('filename', '20s') + ] + + parttbl_64bit = [ + ('save_pref', 'Q'), + ('mem_base', 'Q'), + ('length', 'Q'), + ('desc', '20s'), + ('filename', '20s') + ] + + def __init__(self, cdc, loglevel): + self.cdc = cdc + self.__logger = self.__logger + self.info = self.__logger.info + self.debug = self.__logger.debug + self.error = self.__logger.error + self.warning = self.__logger.warning + self.id = None + self.loaderdb = None + self.version = 2.1 + self.programmer = None + self.mode = "" + self.serial = None + + self.serials = None + self.sblversion = None + self.hwid = None + self.pkhash = None + self.hwidstr = None + self.msm_id = None + self.oem_id = None + self.model_id = None + self.oem_str = None + self.msm_str = None + self.bit64 = False + self.pktsize = None + + self.init_loader_db() + + self.__logger.setLevel(loglevel) + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def get_rsp(self): + data = [] + try: + v = self.cdc.read() + if v == b'': + return [None, None] + if b" 1: + if v[0] == 0x01: + cmd = read_object(v[0:0x2 * 0x4], self.pkt_cmd_hdr) + if cmd['cmd'] == self.cmd.SAHARA_HELLO_REQ: + data = read_object(v[0x0:0xC * 0x4], self.pkt_hello_req) + self.pktsize = data['max_cmd_len'] + self.version = float(str(data['version']) + "." + str(data['version_min'])) + return ["sahara", data] + elif v[0] == self.cmd.SAHARA_END_TRANSFER: + return ["sahara", None] + elif b"" + self.cdc.write(data) + res = self.cdc.read() + if res == b"": + try: + data = b"\x7E\x06\x4E\x95\x7E" # Streaming nop + self.cdc.write(data) + res = self.cdc.read() + if b"\x7E\x0D\x16\x00\x00\x00\x00" in res or b"Invalid Command" in res: + return ["nandprg", None] + else: + return ["", None] + except Exception as e: # pylint: disable=broad-except + self.error(str(e)) + return ["", None] + if b" 0 and res[0] == self.cmd.SAHARA_END_TRANSFER: + print("Device is in Sahara error state, please reboot the device.") + return ["sahara", None] + else: + data = b"\x7E\x11\x00\x12\x00\xA0\xE3\x00\x00\xC1\xE5\x01\x40\xA0\xE3\x1E\xFF\x2F\xE1\x4B\xD9\x7E" + self.cdc.write(data) + res = self.cdc.read() + if len(res) > 0 and res[1] == 0x12: + return ["nandprg", None] + else: + self.cmd_modeswitch(self.sahara_mode.SAHARA_MODE_COMMAND) + return ["sahara", None] + + except Exception as e: # pylint: disable=broad-except + self.error(str(e)) + + self.cmd_modeswitch(self.sahara_mode.SAHARA_MODE_MEMORY_DEBUG) + cmd, pkt = self.get_rsp() + if None in [cmd , pkt]: + return ["", None] + return ["sahara", pkt] + + def enter_command_mode(self): + if not self.cmd_hello(self.sahara_mode.SAHARA_MODE_COMMAND): + return False + cmd, pkt = self.get_rsp() + if cmd["cmd"] == self.cmd.SAHARA_CMD_READY: + return True + elif "status" in pkt: + self.error(self.get_error_desc(pkt["status"])) + return False + return False + + def cmdexec_nop(self): + res = self.cmd_exec(self.exec_cmd.SAHARA_EXEC_CMD_NOP) + return res + + def cmdexec_get_serial_num(self): + res = self.cmd_exec(self.exec_cmd.SAHARA_EXEC_CMD_SERIAL_NUM_READ) + return unpack("=2.4: + # self.sblversion = "{:08x}".format(self.cmdexec_get_sbl_version()) + if self.hwid is not None: + self.hwidstr = "{:016x}".format(self.hwid) + self.msm_id = int(self.hwidstr[2:8], 16) + self.oem_id = int(self.hwidstr[-8:-4], 16) + self.model_id = int(self.hwidstr[-4:], 16) + self.oem_str = "{:04x}".format(self.oem_id) + self.model_id = "{:04x}".format(self.model_id) + self.msm_str = "{:08x}".format(self.msm_id) + if self.msm_id in msmids: + cpustr = f"CPU detected: \"{msmids[self.msm_id]}\"\n" + else: + cpustr = "Unknown CPU, please send log as issue to https://github.com/bkerler/edl\n" + """ + if self.version >= 2.4: + self.info(f"\n------------------------\n" + + f"HWID: 0x{self.hwidstr} (MSM_ID:0x{self.msm_str}," + + f"OEM_ID:0x{self.oem_str}," + + f"MODEL_ID:0x{self.model_id})\n" + + f"PK_HASH: 0x{self.pkhash}\n" + + f"Serial: 0x{self.serials}\n" + + f"SBL Version: 0x{self.sblversion}\n") + else: + """ + self.info(f"\n------------------------\n" + + f"HWID: 0x{self.hwidstr} (MSM_ID:0x{self.msm_str}," + + f"OEM_ID:0x{self.oem_str}," + + f"MODEL_ID:0x{self.model_id})\n" + + cpustr + + f"PK_HASH: 0x{self.pkhash}\n" + + f"Serial: 0x{self.serials}\n") + if self.programmer == "": + if self.hwidstr in self.loaderdb: + mt = self.loaderdb[self.hwidstr] + unfused = False + for rootcert in root_cert_hash: + if self.pkhash[0:16] in root_cert_hash[rootcert]: + unfused = True + if unfused: + self.info("Possibly unfused device detected, so any loader should be fine...") + if self.pkhash[0:16] in mt: + self.programmer = mt[self.pkhash[0:16]] + self.info(f"Trying loader: {self.programmer}") + else: + for loader in mt: + self.programmer = mt[loader] + self.info(f"Possible loader available: {self.programmer}") + for loader in mt: + self.programmer = mt[loader] + self.info(f"Trying loader: {self.programmer}") + break + elif self.pkhash[0:16] in mt: + self.programmer = self.loaderdb[self.hwidstr][self.pkhash[0:16]] + self.info(f"Detected loader: {self.programmer}") + else: + for loader in self.loaderdb[self.hwidstr]: + self.programmer = self.loaderdb[self.hwidstr][loader] + self.info(f"Trying loader: {self.programmer}") + break + # print("Couldn't find a loader for given hwid and pkhash :(") + # exit(0) + elif self.hwidstr is not None and self.pkhash is not None: + msmid = self.hwidstr[:8] + for hwidstr in self.loaderdb: + if msmid == hwidstr[:8]: + for pkhash in self.loaderdb[hwidstr]: + if self.pkhash[0:16] == pkhash: + self.programmer = self.loaderdb[hwidstr][pkhash] + self.info(f"Trying loader: {self.programmer}") + self.cmd_modeswitch(self.sahara_mode.SAHARA_MODE_COMMAND) + return True + self.error( + f"Couldn't find a loader for given hwid and pkhash ({self.hwidstr}_{self.pkhash[0:16]}" + + "_[FHPRG/ENPRG].bin) :(") + return False + else: + self.error(f"Couldn't find a suitable loader :(") + return False + + self.cmd_modeswitch(self.sahara_mode.SAHARA_MODE_COMMAND) + return True + return False + + def streaminginfo(self): + if self.enter_command_mode(): + self.serial = self.cmdexec_get_serial_num() + self.info(f"Device serial : {hex(self.serial)}") + self.cmd_modeswitch(self.sahara_mode.SAHARA_MODE_COMMAND) + return True + return False + + def cmd_done(self): + if self.cdc.write(pack(" 0: + if bytestoread > 0x080000: + length = 0x080000 + else: + length = bytestoread + bytesread = 0 + try: + self.cdc.read(1, 1) + except Exception as e: # pylint: disable=broad-except + self.debug(str(e)) + pass + if self.bit64: + if not self.cdc.write(pack(" 0: + try: + tmp = self.cdc.read(length) + except Exception as e: # pylint: disable=broad-except + self.debug(str(e)) + return None + length -= len(tmp) + pos += len(tmp) + bytesread += len(tmp) + if wf is not None: + wf.write(tmp) + else: + data += tmp + if display: + prog = round(float(pos) / float(total) * float(100), 1) + if prog > old: + if display: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + old = prog + bytestoread -= bytesread + if display: + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + ''' + try: + self.cdc.read(0) + except: + return data + ''' + return data + + def dump_partitions(self, partition): + for part in partition: + filename = part["filename"] + desc = part["desc"] + mem_base = part["mem_base"] + length = part["length"] + print(f"Dumping {filename}({desc}) at {hex(mem_base)}, length {hex(length)}") + fname = os.path.join("memory", filename) + with open(fname, "wb") as wf: + if self.read_memory(mem_base, length, True, wf): + print("Done dumping memory") + else: + self.error("Error dumping memory") + self.cmd_reset() + return True + + def debug_mode(self): + if not self.cmd_hello(self.sahara_mode.SAHARA_MODE_MEMORY_DEBUG): + return False + if os.path.exists("memory"): + rmrf("memory") + os.mkdir("memory") + cmd, pkt = self.get_rsp() + if cmd["cmd"] == self.cmd.SAHARA_MEMORY_DEBUG or cmd["cmd"] == self.cmd.SAHARA_64BIT_MEMORY_DEBUG: + memory_table_addr = pkt["memory_table_addr"] + memory_table_length = pkt["memory_table_length"] + if self.bit64: + pktsize = 8 + 8 + 8 + 20 + 20 + if memory_table_length % pktsize == 0: + if memory_table_length != 0: + print( + f"Reading 64-Bit partition from {hex(memory_table_addr)} with length of " + + "{hex(memory_table_length)}") + ptbldata = self.read_memory(memory_table_addr, memory_table_length) + num_entries = len(ptbldata) // pktsize + partitions = [] + for id_entry in range(0, num_entries): + pd = read_object(ptbldata[id_entry * pktsize:(id_entry * pktsize) + pktsize], + self.parttbl_64bit) + desc = pd["desc"].replace(b"\x00", b"").decode('utf-8') + filename = pd["filename"].replace(b"\x00", b"").decode('utf-8') + mem_base = pd["mem_base"] + save_pref = pd["save_pref"] + length = pd["length"] + partitions.append(dict(desc=desc, filename=filename, mem_base=mem_base, length=length, + save_pref=save_pref)) + print( + f"{filename}({desc}): Offset {hex(mem_base)}, Length {hex(length)}, " + + "SavePref {hex(save_pref)}") + + self.dump_partitions(partitions) + return True + + return True + else: + pktsize = (4 + 4 + 4 + 20 + 20) + if memory_table_length % pktsize == 0: + if memory_table_length != 0: + print(f"Reading 32-Bit partition from {hex(memory_table_addr)} " + + f"with length of {hex(memory_table_length)}") + ptbldata = self.read_memory(memory_table_addr, memory_table_length) + num_entries = len(ptbldata) // pktsize + partitions = [] + for id_entry in range(0, num_entries): + pd = read_object(ptbldata[id_entry * pktsize:(id_entry * pktsize) + pktsize], self.parttbl) + desc = pd["desc"].replace(b"\x00", b"").decode('utf-8') + filename = pd["filename"].replace(b"\x00", b"").decode('utf-8') + mem_base = pd["mem_base"] + save_pref = pd["save_pref"] + length = pd["length"] + partitions.append(dict(desc=desc, filename=filename, mem_base=mem_base, length=length, + save_pref=save_pref)) + print(f"{filename}({desc}): Offset {hex(mem_base)}, " + + f"Length {hex(length)}, SavePref {hex(save_pref)}") + + self.dump_partitions(partitions) + return True + elif "status" in pkt: + self.error(self.get_error_desc(pkt["status"])) + return False + return False + + def upload_loader(self): + if self.programmer == "": + return "" + try: + self.info(f"Uploading loader {self.programmer} ...") + with open(self.programmer, "rb") as rf: + programmer = rf.read() + except Exception as e: # pylint: disable=broad-except + self.error(str(e)) + sys.exit() + + if not self.cmd_hello(self.sahara_mode.SAHARA_MODE_IMAGE_TX_PENDING): + return "" + + try: + datalen = len(programmer) + done = False + while datalen > 0 or done: + cmd, pkt = self.get_rsp() + if cmd == -1 or pkt == -1: + if self.cmd_done(): + return self.mode # Do NOT remove + else: + self.error("Timeout while uploading loader. Wrong loader ?") + return "" + if cmd["cmd"] == self.cmd.SAHARA_64BIT_MEMORY_READ_DATA: + self.bit64 = True + elif cmd["cmd"] == self.cmd.SAHARA_READ_DATA: + self.bit64 = False + elif cmd["cmd"] == self.cmd.SAHARA_END_TRANSFER: + if pkt["status"] == self.status.SAHARA_STATUS_SUCCESS: + self.cmd_done() + return self.mode + else: + return "" + elif "status" in pkt: + self.error(self.get_error_desc(pkt["status"])) + return "" + else: + self.error("Unexpected error on uploading") + return "" + self.id = pkt["id"] + if self.id == 0x7: + self.mode = "nandprg" + elif self.id == 0xB: + self.mode = "enandprg" + elif self.id >= 0xC: + self.mode = "firehose" + + data_offset = pkt["data_offset"] + data_len = pkt["data_len"] + if data_offset + data_len > len(programmer): + while len(programmer) < data_offset + data_len: + programmer += b"\xFF" + data_to_send = programmer[data_offset:data_offset + data_len] + self.cdc.write(data_to_send) + datalen -= data_len + self.info("Loader uploaded.") + cmd, pkt = self.get_rsp() + if cmd["cmd"] == self.cmd.SAHARA_END_TRANSFER: + if pkt["status"] == self.status.SAHARA_STATUS_SUCCESS: + self.cmd_done() + return self.mode + return "" + except Exception as e: # pylint: disable=broad-except + self.error("Unexpected error on uploading, maybe signature of loader wasn't accepted ?\n" + str(e)) + return "" + + def cmd_modeswitch(self, mode): + data = pack(" ") + sys.exit() + sp = QCSparse(sys.argv[1], logging.INFO) + if sp.readheader(): + print("Extracting sectors to " + sys.argv[2]) + with open(sys.argv[2], "wb") as wf: + """ + old=0 + while sp.offsetold: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + old=prog + data=sp.read() + if data==b"" or data==-1: + break + wf.write(data) + if len(sp.tmpdata)>0: + wf.write(sp.tmpdata) + sp.tmpdata=bytearray() + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + """ + + fsize = sp.getsize() + SECTOR_SIZE_IN_BYTES = 4096 + num_partition_sectors = 5469709 + MaxPayloadSizeToTargetInBytes = 0x200000 + bytesToWrite = SECTOR_SIZE_IN_BYTES * num_partition_sectors + total = SECTOR_SIZE_IN_BYTES * num_partition_sectors + old = 0 + pos = 0 + while fsize > 0: + wlen = MaxPayloadSizeToTargetInBytes // SECTOR_SIZE_IN_BYTES * SECTOR_SIZE_IN_BYTES + if fsize < wlen: + wlen = fsize + wdata = sp.read(wlen) + bytesToWrite -= wlen + fsize -= wlen + pos += wlen + pv = wlen % SECTOR_SIZE_IN_BYTES + if pv != 0: + filllen = (wlen // SECTOR_SIZE_IN_BYTES * SECTOR_SIZE_IN_BYTES) + \ + SECTOR_SIZE_IN_BYTES + wdata += b"\x00" * (filllen - wlen) + wlen = len(wdata) + + wf.write(wdata) + + prog = round(float(pos) / float(total) * float(100), 1) + if prog > old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete (Sector %d)' + % (pos // SECTOR_SIZE_IN_BYTES), + bar_length=50) + + print("Done.") diff --git a/edl/Library/streaming.py b/edl/Library/streaming.py new file mode 100755 index 0000000..ced161c --- /dev/null +++ b/edl/Library/streaming.py @@ -0,0 +1,1069 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +from struct import pack +from binascii import unhexlify +from edl.Library.utils import * +from edl.Library.hdlc import * +from edl.Library.nand_config import BadFlags, SettingsOpt, nandregs, NandDevice + + +class Streaming(metaclass=LogBase): + def __init__(self, cdc, sahara, loglevel=logging.INFO): + self.__logger = self.__logger + self.regs = None + self.cdc = cdc + self.hdlc = hdlc(self.cdc) + self.mode = sahara.mode + self.sahara = sahara + self.settings = None + self.flashinfo = None + self.bbtbl = {} + self.nanddevice = None + self.nandbase = 0 + self.__logger.setLevel(loglevel) + self.info = self.__logger.info + self.debug = self.__logger.debug + self.error = self.__logger.error + self.warning = self.__logger.warning + self.modules = None + self.Qualcomm = 0 + self.Patched = 1 + self.streaming_mode = None + self.memread = None + + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def get_flash_config(self): + """ + self.regs.NAND_DEV0_CFG0=0x2a0408c0 + self.regs.NAND_DEV0_CFG1=0x804745c + self.regs.NAND_DEV0_ECC_CFG=0x42040700 + self.regs.NAND_EBI2_ECC_BUF_CFG=0x203 + """ + + self.regs.NAND_FLASH_CMD = 0x8000b + self.regs.NAND_EXEC_CMD = self.nanddevice.NAND_CMD_SOFT_RESET + self.nandwait() + # dev_cfg0 = self.regs.NAND_DEV0_CFG0 + # dev_cfg1 = self.regs.NAND_DEV0_CFG1 + # dev_ecc_cfg = self.regs.NAND_DEV0_ECC_CFG + + """ + self.nand_reset() + self.set_address(1, 0) + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ + self.regs.NAND_EXEC_CMD = 0x1 + self.nandwait() + tmp = self.memread(self.nanddevice.NAND_FLASH_BUFFER, 512) + """ + + nandid = self.regs.NAND_READ_ID + cfg0, cfg1, ecc_buf_cfg, ecc_bch_cfg = self.nanddevice.nand_setup(nandid) + self.regs.NAND_DEV0_CFG0 = cfg0 + self.regs.NAND_DEV0_CFG1 = cfg1 + self.regs.NAND_EBI2_ECC_BUF_CFG = ecc_buf_cfg + self.regs.NAND_DEV0_ECC_CFG = ecc_bch_cfg + + def nand_post(self): + self.mempoke(self.nanddevice.NAND_DEV0_ECC_CFG, + self.mempeek(self.nanddevice.NAND_DEV0_ECC_CFG) & 0xfffffffe) # ECC on BCH + self.mempoke(self.nanddevice.NAND_DEV0_CFG1, + self.mempeek(self.nanddevice.NAND_DEV0_CFG1) & 0xfffffffe) # ECC on R-S + + def nand_onfi(self): + cmd1 = self.regs.NAND_DEV_CMD1 + vld = self.regs.NAND_DEV_CMD_VLD + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ + self.regs.NAND_ADDR0 = 0 + self.regs.NAND_ADDR1 = 0 + self.regs.NAND_DEV0_CFG0 = 0 << self.nanddevice.CW_PER_PAGE | 512 << self.nanddevice.UD_SIZE_BYTES | \ + 5 << self.nanddevice.NUM_ADDR_CYCLES | 0 << self.nanddevice.SPARE_SIZE_BYTES + self.regs.NAND_DEV1_CFG1 = 7 << self.nanddevice.NAND_RECOVERY_CYCLES | 0 << self.nanddevice.CS_ACTIVE_BSY | \ + 17 << self.nanddevice.BAD_BLOCK_BYTE_NUM | \ + 1 << self.nanddevice.BAD_BLOCK_IN_SPARE_AREA | 2 << self.nanddevice.WR_RD_BSY_GAP | \ + 0 << self.nanddevice.WIDE_FLASH | \ + 1 << self.nanddevice.DEV0_CFG1_ECC_DISABLE + self.regs.NAND_EBI2_ECC_BUF_CFG = 1 << self.nanddevice.ECC_CFG_ECC_DISABLE + self.regs.NAND_DEV_CMD_VLD = self.regs.NAND_DEV_CMD_VLD & ~(1 << self.nanddevice.READ_START_VLD) + self.regs.NAND_DEV_CMD1 = (self.regs.NAND_DEV_CMD1 & ~( + 0xFF << self.nanddevice.READ_ADDR)) | self.nanddevice.NAND_CMD_PARAM << self.nanddevice.READ_ADDR + self.regs.NAND_EXEC_CMD = 1 + self.regs.NAND_DEV_CMD1_RESTORE = cmd1 + self.regs.DEV_CMD_VLD_RESTORE = vld + self.regs.NAND_DEV_CMD_VLD = 1 + self.regs.NAND_DEV_CMD1 = 1 + + # config_cw_read + self.regs.NAND_FLASH_CMD = 3 + self.regs.NAND_DEV0_CFG0 = 3 + self.regs.NAND_EBI2_ECC_BUF_CFG = 1 + self.regs.NAND_EXEC_CMD = 1 + + tmp = self.memread(self.nanddevice.NAND_FLASH_BUFFER, 512) + + self.regs.NAND_DEV_CMD1_RESTORE = 1 + self.regs.DEV_CMD_VLD_RESTORE = 1 + + (bytesperpage, spareperpage, + bytesperpartialpage, spareperpartialpage, # obsolete + pagesperblock, blocksperunit, units, + addresscycles) = unpack('> 4, + totalpages=pagesperblock * blocksperunit * units) + # self.pageaddressbits=bitsforaddress(self.pagesperblock) + return res + + def nand_init(self, xflag=0): + self.settings.cwsize = self.settings.sectorsize + if xflag: + # increase the codeword size by the OOB chunk size per sector˰ + self.settings.cwsize += self.settings.OOBSIZE // self.settings.sectors_per_page + + # ECC on/off + self.regs.NAND_DEV0_ECC_CFG = (self.regs.NAND_DEV0_ECC_CFG & 0xfffffffe) | self.settings.args_disable_ecc + # ECC on/off + self.regs.NAND_DEV0_CFG1 = (self.regs.NAND_DEV0_CFG1 & 0xfffffffe) | self.settings.args_disable_ecc + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_SOFT_RESET # Resetting all controller operations + self.regs.NAND_EXEC_CMD = 0x1 + self.nandwait() + + # set the command code˰ + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ_ALL # reading data+ecc+spare + + # clean the sector buffer˰ + for i in range(0, self.settings.cwsize, 4): + self.mempoke(self.nanddevice.NAND_FLASH_BUFFER + i, 0xffffffff) + + def secure_mode(self): + self.send(b"\x17\x01", True) + return 0 + + def qclose(self, errmode): + resp = self.send(b"\x15") + if not errmode: + time.sleep(0.5) + return True + if len(resp) > 2 and resp[1] == 0x16: + time.sleep(0.5) + return True + self.error("Error on closing stream") + return False + + def send_section_header(self, name): + # 0x1b open muliimage, 0xe for user-defined partition + resp = self.send(b"\x1b\x0e" + name + b"\x00") + if resp[1] == 0x1c: + return True + self.error("Error on sending section header") + return False + + def enter_flash_mode(self, ptable=None): + self.secure_mode() + self.qclose(0) + if ptable is None: + self.send_ptable(ptable, 0) # 1 for fullflash + + def write_flash(self, partname, filename): + wbsize = 1024 + filesize = os.stat(filename).st_size + with open(filename, 'rb') as rf: + if self.send_section_header(partname): + adr = 0 + while filesize > 0: + subdata = rf.read(wbsize) + if len(subdata) < wbsize + 1: + subdata += b'\xFF' * ((wbsize + 1) - len(subdata)) + scmd = b"\x07" + pack(" old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete (Sector %d)' % sector, + bar_length=50) + old = prog + self.nand_post() + if info: + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + return True + + def send_ptable(self, parttable, mode): + cmdbuf = b"\x19" + pack(" length: + blklen = length - i + iolen = 0 + resp = self.send(cmdbuf + pack(" 0: + iolen = len(resp) - 1 + if iolen < (blklen + 4): + tries -= 1 + time.sleep(1) + resp += self.hdlc.receive_reply_nocrc() + else: + break + if tries == 0: + self.error( + f"Error reading memory at addr {hex(address)}, {str(blklen)} bytes required, {str(iolen)} bytes " + f"received.") + errcount += 1 + result += b"\xeb" * blklen + else: + result += resp[5:] + + if errcount > 0: + return b"" + return result + + def mempeek(self, address): + res = self.memread(address, 4) + if res != b"": + data = unpack(" 1000: + data = data[0:1000] + length = 1000 + self.send(cmdbuf + pack("> 16) & 0xFF + # self.regs.NAND_FLASH_CHIP_SELECT = 0 | 4 # flash0 + undoc bit + + def exec_nand(self, cmd): + self.regs.NAND_FLASH_CMD = cmd + self.regs.NAND_EXEC_CMD = 1 + self.nandwait() + + def nand_reset(self): + self.exec_nand(1) + + def tst_badblock(self): + badflag = 0 + st = self.regs.NAND_BUFFER_STATUS & 0xFFFF0000 + if self.settings.IsWideFlash == 0: + if st != 0xFF0000: + badflag = 1 + elif st != 0xFFFF0000: + badflag = 1 + return badflag + + def check_block(self, block): + self.nand_reset() + self.set_address(block, 0) + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ_ALL + self.regs.NAND_FLASH_EXEC = 0x1 + self.nandwait() + return self.tst_badblock() + + """ + def check_block(self, block): + cwperpage = (self.settings.pagesize >> 9) + CFG1_WIDE_FLASH=1<<1 + #if self.settings.bch_mode: + # NAND_CFG0_RAW=0xA80420C0 + #else: + # NAND_CFG0_RAW=0xA80428C0 + self.nand_reset() + self.set_address(block, 0) + self.regs.NAND_FLASH_CMD=self.nanddevice.NAND_CMD_PAGE_READ + #self.regs.NAND_DEV0_CFG0=NAND_CFG0_RAW & ~(7 << 6) + self.regs.NAND_EXEC_CMD=0x1 + self.nandwait() + flashstatus = self.regs.NAND_FLASH_STATUS + if flashstatus & 0x110: + return 1 + + if self.settings.cfg1_enable_bch_ecc and self.settings.ecc_bit == 8: + val=532 * (cwperpage - 1) + else: + val=528 * (cwperpage - 1) + #val=self.settings.BAD_BLOCK_BYTE_NUM + ptr = self.nanddevice.NAND_FLASH_BUFFER + (self.settings.pagesize - val) + flag=self.memread(ptr,2) + if self.regs.NAND_DEV0_CFG1 & CFG1_WIDE_FLASH: + if flag[0] != 0xFF or flag[1] != 0xFF: + return 1 + elif flag[0] != 0xFF: + return 1 + return 0 + """ + + def check_ecc_status(self): + bs = self.regs.NAND_BUFFER_STATUS + if (bs & 0x100) != 0 and (self.regs.NAND_FLASH_CMD + 0xec) & 0x40 == 0: + return -1 + return bs & 0x1f + + def write_badmark(self, block, value): + udsize = 0x220 + cfg1bak = self.regs.NAND_DEV0_CFG1 + cfgeccbak = self.regs.NAND_DEV0_ECC_CFG + self.regs.NAND_DEV0_ECC_CFG = self.regs.NAND_DEV0_ECC_CFG | 1 + self.regs.NAND_DEV0_CFG1 = self.regs.NAND_DEV0_CFG1 | 1 + self.hardware_bad_off() + buf = bytearray([0xeb]) + for i in range(1, udsize): + buf.append(value) + self.nand_reset() + self.nandwait() + self.set_address(block, 0) + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PRG_PAGE_ALL + for i in range(0, self.settings.sectors_per_page): + self.memwrite(self.nanddevice.NAND_FLASH_BUFFER, buf[:udsize]) + self.regs.NAND_EXEC_CMD = 1 + self.nandwait() + self.hardware_bad_on() + self.regs.NAND_DEV0_CFG1 = cfg1bak + self.regs.NAND_DEV0_ECC_CFG = cfgeccbak + + def mark_bad(self, block): + if not self.check_block(block): + self.write_badmark(block, 0) + return 1 + return 0 + + def unmark_bad(self, block): + if self.check_block(block): + self.block_erase(block) + return 1 + return 0 + + """ + def tst_badpattern(self, buffer): + for i in range(0, len(buffer)): + if buffer[i] != 0xbb: + return 0 + return 1 + """ + + def flash_read(self, block, page, sectors, cwsize=None): + buffer = bytearray() + spare = bytearray() + cursize = 0 + newblock = False + cfg0 = self.regs.NAND_DEV0_CFG0 + if block not in self.bbtbl: + newblock = True + if self.settings.bad_processing_flag == BadFlags.BAD_DISABLE.value: + self.hardware_bad_off() + elif self.settings.bad_processing_flag != BadFlags.BAD_IGNORE.value: + if newblock: + res = 0 + if block in self.bbtbl: + res = self.bbtbl[block] + else: + if self.check_block(block): + res = 1 + self.bbtbl[block] = res + if res == 1: + for i in range(0, self.settings.PAGESIZE): + buffer.append(0xbb) + return buffer, spare + + self.nand_reset() + if self.settings.ECC_MODE == 1: + if cwsize >= self.settings.sectorsize + 4: + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ_ALL + else: + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ_ECC + else: + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_PAGE_READ_ALL + self.bch_reset() + + self.set_address(block, page) + bad_ecc = False + for sector in range(0, sectors): + self.regs.NAND_EXEC_CMD = 0x1 + self.nandwait() + ecc_status = self.check_ecc_status() + if ecc_status == -1: + bad_ecc = True + tmp = self.memread(self.nanddevice.NAND_FLASH_BUFFER, cwsize) + size = self.settings.UD_SIZE_BYTES + if cursize + size > self.settings.PAGESIZE: + size = cursize + size - self.settings.PAGESIZE + buffer.extend(tmp[:-size]) + spare.extend(tmp[-size:]) + else: + buffer.extend(tmp) + cursize += size + if bad_ecc: + self.debug("ECC error at : Block %08X Page %08X" % (block, page)) + + if self.settings.bad_processing_flag == BadFlags.BAD_DISABLE.value: + self.hardware_bad_off() + + self.regs.NAND_DEV0_CFG0 = cfg0 + return buffer, spare + + def hardware_bad_off(self): + cfg1 = self.regs.NAND_DEV0_CFG1 + cfg1 &= ~(0x3ff << 6) + self.regs.NAND_DEV0_CFG1 = cfg1 + + def hardware_bad_on(self): + cfg1 = self.regs.NAND_DEV0_CFG1 + cfg1 &= ~(0x7ff << 6) + cfg1 |= (self.settings.BAD_BLOCK_BYTE_NUM & 0x3FF) << 6 + cfg1 |= self.settings.BAD_BLOCK_IN_SPARE_AREA << 16 + self.regs.NAND_DEV0_CFG1 = cfg1 + + def set_badmark_pos(self, pos, place): + self.settings.BAD_BLOCK_BYTE_NUM = pos + self.settings.BAD_BLOCK_IN_SPARE_AREA = place & 1 + self.hardware_bad_on() + + def set_udsize(self, size): + tmpreg = self.regs.NAND_DEV0_CFG0 + tmpreg = (tmpreg & (~(0x3ff << 9))) | (size << 9) + self.regs.NAND_DEV0_CFG0 = tmpreg + if ((self.regs.NAND_DEV0_CFG1 >> 27) & 1) != 0: + tmpreg = self.regs.NAND_DEV0_ECC_CFG + tmpreg = (tmpreg & (~(0x3ff << 16))) | (size << 16) + self.regs.NAND_DEV0_ECC_CFG = tmpreg + + def set_sparesize(self, size): + cfg0 = self.regs.NAND_DEV0_CFG0 + cfg0 = cfg0 & (~(0xf << 23)) | (size << 23) + self.regs.NAND_DEV0_CFG0 = cfg0 + + def set_eccsize(self, size): + cfg1 = self.regs.NAND_DEV0_CFG1 + if ((cfg1 >> 27) & 1) != 0: + self.settings.cfg1_enable_bch_ecc = 1 + if self.settings.cfg1_enable_bch_ecc == 1: + ecccfg = self.regs.NAND_DEV0_ECC_CFG + ecccfg = (ecccfg & (~(0x1f << 8)) | (size << 8)) + self.regs.NAND_DEV0_ECC_CFG = ecccfg + else: + cfg0 = self.regs.NAND_DEV0_CFG0 + cfg0 = cfg0 & (~(0xf << 19)) | (size << 19) + self.regs.NAND_DEV0_CFG0 = cfg0 + + def bch_reset(self): + if not self.settings.cfg1_enable_bch_ecc: + return + cfgecc_temp = self.regs.NAND_DEV0_ECC_CFG + self.regs.NAND_DEV0_ECC_CFG = cfgecc_temp | 2 + self.regs.NAND_DEV0_ECC_CFG = cfgecc_temp + + def set_blocksize(self, udsize, ss, eccs): + self.set_udsize(udsize) + self.set_sparesize(ss) + self.set_eccsize(eccs) + + def get_udsize(self): + return (self.regs.NAND_DEV0_CFG0 & (0x3ff << 9)) >> 9 + + def block_erase(self, block): + self.nand_reset() + self.regs.NAND_ADDR0 = block * self.settings.num_pages_per_blk + self.regs.NAND_ADDR1 = 0 + oldcfg = self.regs.NAND_DEV0_CFG0 + self.regs.NAND_DEV0_CFG0 = oldcfg & ~0x1c0 + + self.regs.NAND_FLASH_CMD = self.nanddevice.NAND_CMD_BLOCK_ERASE + self.regs.NAND_EXEC_CMD = 1 + self.nandwait() + self.regs.NAND_DEV0_CFG0 = oldcfg + + def disable_bam(self): + nandcstate = {} + for i in range(0, 0xec, 4): + value = self.mempeek(self.nanddevice.NAND_FLASH_CMD + i) + nandcstate[i] = value + self.mempoke(self.settings.bcraddr, 1) + self.mempoke(self.settings.bcraddr, 0) + for i in nandcstate: + addr = self.nanddevice.NAND_FLASH_CMD + i + value = nandcstate[i] + self.mempoke(addr, value) + self.regs.NAND_EXEC_CMD = 1 + + def read_partition_table(self): + cwsize = self.settings.sectorsize + if self.settings.ECC_MODE == 1: + cwsize = self.settings.UD_SIZE_BYTES + for block in range(0, 12): + buffer, spare = self.flash_read(block, 0, 1, cwsize) + if buffer[0:8] != b"\xac\x9f\x56\xfe\x7a\x12\x7f\xcd": + continue + + buffer, spare = self.flash_read(block, 1, 2, cwsize) + magic1, magic2, version, numparts = unpack(" 0x2c: + self.info("Successfully uploaded programmer :)") + if b"Unrecognized flash device" in resp: + self.error("Unrecognized flash device, patch loader to match flash or use different loader!") + self.reset() + return False, hp + resp = bytearray(resp) + try: + hp.command = resp[1] + hp.magic = resp[2:2 + 32] + hp.version = resp[2 + 32] + hp.compatibleVersion = resp[3 + 32] + hp.maxPreferredBlockSize = unpack("I", resp[4 + 32:4 + 32 + 4])[0] + hp.baseFlashAddress = unpack("I", resp[4 + 4 + 32:4 + 32 + 4 + 4])[0] + hp.flashIdLength = resp[44] + offset = 45 + hp.flashId = resp[offset:offset + hp.flashIdLength] + offset += hp.flashIdLength + data = unpack("HH", resp[offset:offset + 4]) + hp.windowSize = data[0] + hp.numberOfSectors = data[1] + data = unpack(str(hp.numberOfSectors) + "I", resp[offset + 4:offset + 4 + (4 * hp.numberOfSectors)]) + hp.sectorSizes = data + hp.featureBits = resp[offset + 4 + hp.numberOfSectors * 4:offset + 4 + hp.numberOfSectors * 4 + 1] + """ + self.settings.PAGESIZE=512 + self.settings.UD_SIZE_BYTES=512 + self.settings.num_pages_per_blk=hp.maxPreferredBlockSize + self.settings.sectors_per_page=hp.numberOfSectors + """ + return True, hp + except Exception as e: # pylint: disable=broad-except + self.error(str(e)) + return False, hp + + def connect(self, mode=1): + time.sleep(0.200) + self.memread = self.patched_memread + if mode == 0: + cmdbuf = bytearray( + [0x11, 0x00, 0x12, 0x00, 0xa0, 0xe3, 0x00, 0x00, 0xc1, 0xe5, 0x01, 0x40, 0xa0, 0xe3, 0x1e, 0xff, 0x2f, + 0xe1]) + resp = self.send(cmdbuf, True) + self.hdlc.receive_reply(5) + i = resp[1] + if i == 0x12: + # if not self.tst_loader(): + # print("Unlocked bootloader being used, cannot continue") + # exit(2) + self.streaming_mode = self.Patched + chipset = self.identify_chipset() + if self.streaming_mode == self.Patched: + self.memread = self.patched_memread + self.settings = SettingsOpt(self, chipset) + self.nanddevice = NandDevice(self.settings) + self.setupregs() + self.get_flash_config() + return True + return True + else: + if b"Invalid" not in resp: + self.streaming_mode = self.Qualcomm + self.memread = self.qc_memread + self.settings = SettingsOpt(self, 0xFF) + return True + resp = self.hello() + if resp[0]: + sectorsize = 0 + sparebytes = 0 + if mode == 2: + if resp[1].flashId is not None: + self.info("Detected flash memory: %s" % resp[1].flashId.decode('utf-8')) + return True + self.streaming_mode = self.Patched + chipset = self.identify_chipset() + if chipset == 0xFF: + self.streaming_mode = self.Qualcomm + if self.streaming_mode == self.Qualcomm: + self.info("Unpatched loader detected. Using standard QC mode. Limited methods supported: peek") + self.settings = SettingsOpt(self, chipset) + self.memread = self.qc_memread + else: + self.memread = self.patched_memread + self.settings = SettingsOpt(self, chipset) + if self.settings.bad_loader: + self.error( + "Loader id doesn't match device, please fix config and patch loader. Rebooting.") + self.reset() + return False + self.nanddevice = NandDevice(self.settings) + self.setupregs() + + if self.cdc.pid == 0x900e or self.cdc.pid == 0x0076: + print("Boot to 0x9008") + self.mempoke(0x193d100, 1) + # dload-mode-addr, TCSR_BOOT_MISC_DETECT, iomap.h + # msm8916,8939,8953 0x193d100 + # msm8996 0x7b3000 + self.mempeek(0x7980000) + self.cdc.close() + sys.exit(0) + + if self.settings.bam: + self.disable_bam() # only for sahara + self.get_flash_config() + cfg0 = self.mempeek(self.nanddevice.NAND_DEV0_CFG0) + sectorsize = (cfg0 & (0x3ff << 9)) >> 9 + sparebytes = (cfg0 >> 23) & 0xf + self.info("HELLO protocol version: %i" % resp[1].version) + if self.streaming_mode == self.Patched: + self.info("Chipset: %s" % self.settings.chipname) + self.info("Base address of the NAND controller: %08x" % self.settings.nandbase) + self.info("Sector size: %d bytes" % sectorsize) + self.info("Spare bytes: %d bytes" % sparebytes) + markerpos = "spare" if self.nanddevice.BAD_BLOCK_IN_SPARE_AREA else "user" + self.info( + "Defective block marker position: %s+%x" % (markerpos, self.nanddevice.BAD_BLOCK_BYTE_NUM)) + self.info("The total size of the flash memory = %u blocks (%i MB)" % + (self.settings.MAXBLOCK, self.settings.MAXBLOCK * + self.settings.num_pages_per_blk / 1024 * self.settings.PAGESIZE / 1024)) + + val = resp[1].flashId.decode('utf-8') if resp[1].flashId[0] != 0x65 else "" + self.info("Flash memory: %s %s, %s" % (self.settings.flash_mfr, val, self.settings.flash_descr)) + # self.info("Maximum packet size: %i byte",*((unsigned int*)&rbuf[0x24])) + self.info( + "Page size: %d bytes (%d sectors)" % (self.settings.PAGESIZE, self.settings.sectors_per_page)) + self.info("The number of pages in the block: %d" % self.settings.num_pages_per_blk) + self.info("OOB size: %d bytes" % self.settings.OOBSIZE) + ecctype = "BCH" if self.settings.cfg1_enable_bch_ecc else "R-S" + self.info("ECC: %s, %i bit" % (ecctype, self.settings.ecc_bit)) + self.info("ЕСС size: %d bytes" % self.settings.ecc_size) + + return True + else: + self.error("Uploaded programmer doesn't respond :(") + return False + + def load_block(self, block, cwsize): + buffer = bytearray() + for page in range(0, self.settings.num_pages_per_blk): + tmp, spare = self.flash_read(block, page, self.settings.sectors_per_page, cwsize) + buffer.extend(tmp) + return buffer + + def memtofile(self, offset, length, filename, info=True): + old = 0 + pos = 0 + toread = length + if info: + print_progress(0, 100, prefix='Progress:', suffix='Complete', bar_length=50) + with open(filename, "wb") as wf: + while toread > 0: + size = 0x20000 + if self.streaming_mode == self.Qualcomm: + size = 0x200 + if toread < size: + size = toread + data = self.memread(offset + pos, size) + if data != b"": + wf.write(data) + else: + break + toread -= size + pos += size + if info: + prog = round(float(pos) / float(length) * float(100), 1) + if prog > old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete (Offset: %08X)' % (offset + pos), + bar_length=50) + old = prog + if info: + print_progress(100, 100, prefix='Progress:', suffix='Complete', bar_length=50) + return True + + def read_blocks(self, fw, block, length, cwsize, savespare=False, info=True): + badblocks = 0 + old = 0 + pos = 0 + totallength = length * self.settings.num_pages_per_blk * self.settings.PAGESIZE + if info: + print_progress(0, 100, prefix='Progress:', suffix='Complete', bar_length=50) + startoffset = block * self.settings.num_pages_per_blk * self.settings.PAGESIZE + endoffset = startoffset + totallength + + for offset in range(startoffset, endoffset, self.settings.PAGESIZE): + pages = int(offset / self.settings.PAGESIZE) + curblock = int(pages / self.settings.num_pages_per_blk) + curpage = int(pages - curblock * self.settings.num_pages_per_blk) + data, spare = self.flash_read(curblock, curpage, self.settings.sectors_per_page, cwsize) + if self.bbtbl[curblock] != 1 or (self.settings.bad_processing_flag != BadFlags.BAD_SKIP.value): + fw.write(data) + if savespare: + fw.write(spare) + else: + self.debug("Bad block at block %d" % curblock) + badblocks += 1 + pos += self.settings.PAGESIZE + if info: + prog = round(float(pos) / float(totallength) * float(100), 1) + if prog > old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + old = prog + return badblocks + + """ + def read_blocks_ext(self, fw, block, length, yaffsmode, info=True): + buffer = bytearray() + badblocks = 0 + pos = 0 + old = 0 + totallength = length * self.settings.num_pages_per_blk * self.settings.PAGESIZE + if info: + print_progress(0, 100, prefix='Progress:', suffix='Complete', bar_length=50) + for curblock in range(block, block + length): + for page in range(0, self.settings.num_pages_per_blk): + tmp, spare = self.flash_read(block, page, self.settings.sectors_per_page, self.settings.sectorsize + 4) + buffer.extend(tmp) + if info: + prog = int(float(pos) / float(totallength) * float(100)) + if prog > old: + print_progress(prog, 100, prefix='Progress:', suffix='Complete', bar_length=50) + old = prog + + if self.bbtbl[block] == 1 and (self.settings.bad_processing_flag != BadFlags.BAD_SKIP.value): + print("Bad block at block %d" % curblock) + badblocks += 1 + else: + for page in range(0, self.settings.num_pages_per_blk): + pgoffset = page * self.settings.sectors_per_page * (self.settings.sectorsize + 4) + for sec in range(0, self.settings.sectors_per_page): + udoffset = pgoffset + sec * (self.settings.sectorsize + 4) + if sec != (self.settings.sectors_per_page - 1): + fw.write(buffer[udoffset:udoffset + self.settings.sectorsize - 4]) + else: + fw.write(buffer[udoffset:udoffset + self.settings.sectorsize - 4 * ( + self.settings.sectors_per_page - 1)]) + + if yaffsmode == 1: + extbuf = bytearray() + soff = pgoffset + (self.settings.sectorsize + 4) * (self.settings.sectors_per_page - 1) + ( + self.settings.sectorsize - 4 * (self.settings.sectors_per_page - 1)) + extbuf.extend(buffer[soff]) + for i in range(0, self.settings.OOBSIZE): + extbuf.append(0xff) + fw.write(extbuf) + return badblocks + """ + + def read_raw(self, start, length, cwsize, filename): + with open(filename, 'wb') as fw: + if self.settings.rflag == 0: # normal + self.read_blocks(fw, start, length, cwsize) + """ + elif self.settings.rflag == 1: # linux + self.read_blocks_ext(fw, start, length, 0) # Fixme + elif self.settings.rflag == 2: # yaffs + self.read_blocks_ext(fw, start, length, 1) # Fixme + """ + + def send(self, cmd, nocrc=False): + if self.hdlc is not None: + return self.hdlc.send_cmd_np(cmd, nocrc) + return False + + def identify_chipset(self): + """ + PUSH {R1} + MOV R0, LR + BIC R0, R0, #3 + ADD R3, R0, #0xFF + LDR R1, =0xDEADBEEF + LDR R2, [R0],#4 + CMP R2, R1 + BEQ loc_30 + CMP R0, R3 + BCC loc_14 + MOV R0, #0 + B loc_34 + LDR R0, [R0] + POP {R1} + STRB R0, [R1,#1] + MOV R0, #0xAA + STRB R0, [R1] + MOV R4, #2 + BX LR + """ + search = unhexlify("04102de50e00a0e10300c0e3ff3080e234109fe5042090e4010052e10300000a030050e1" + + "faffff3a0000a0e3000000ea000090e504109de40100c1e5aa00a0e30000c1e50240a0e31eff2fe1efbeadde") + cmd = b"\x11\x00" + search + resp = self.send(cmd, True) + self.hdlc.receive_reply(5) + if b"Power off not supported" in resp: + self.streaming_mode = self.Qualcomm + self.memread = self.qc_memread + return 0xFF + if resp[1] != 0xaa: + resp = self.send(cmd, True) + if resp[1] != 0xaa: + return -1 + self.streaming_mode = self.Patched + self.memread = self.patched_memread + return resp[2] # 08 + + +def test_nand_config(): + class sahara: + mode = None + + qs = Streaming(None, sahara(), logging.INFO) + qs.settings = SettingsOpt(qs, 8) + qs.nanddevice = NandDevice(qs.settings) + testconfig = [ + # nandid, buswidth, density, pagesize, blocksize, oobsize, bchecc, cfg0,cfg1,eccbufcfg, bccbchcfg, badblockbyte + [0x1590aaad, 8, 256, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + # ZTE MF920V, MDM9x07 + [0x1590ac01, 8, 256, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + # ZTE OSH-150 + [0x1590acad, 8, 256, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x1590aac8, 8, 256, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x1590acc8, 8, 512, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x1d00f101, 8, 128, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x1d80f101, 8, 128, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + + # Sierra 9x15 + [0x1900aaec, 8, 256, 2048, 131072, 64, 8, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + # [0x1590aa98, 8, 256, 2048, 131072, 64, 8, 0xa8d408c0, 0x0004745c, 0x00000203, 0x42040d10, 0x000001d1], + [0x1590aa98, 8, 256, 2048, 131072, 64, 8, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x2690ac2c, 8, 512, 4096, 262144, 224, 8, 0x290409c0, 0x08045d5c, 0x00000203, 0x42040d10, 0x00000175], + # End + [0x2690dc98, 8, 512, 4096, 262144, 128, 4, 0x2a0409c0, 0x0804645c, 0x00000203, 0x42040700, 0x00000191], + # Sierra Wireless EM7455, MDM9x35, Quectel EC25, Toshiba KSLCMBL2VA2M2A + [0x2690ac98, 8, 512, 4096, 262144, 256, 8, 0x290409c0, 0x08045d5c, 0x00000203, 0x42040d10, 0x00000175], + [0x9590daef, 8, 256, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x9580f1c2, 8, 128, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x9580f1c2, 8, 128, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x9590dac2, 8, 256, 2048, 131072, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + [0x1590ac01, 8, 512, 2048, 128, 64, 4, 0x2a0408c0, 0x0804745c, 0x00000203, 0x42040700, 0x000001d1], + # Netgear MR5100 + [0x26d0a32c, 8, 1024, 4096, 262144, 256, 8, 0x290409c0, 0x08045d5c, 0x00000203, 0x42040d10, 0x00000175], + ] + errorids = [] + for test in testconfig: + nandid, buswidth, density, pagesize, blocksize, oobsize, bchecc, cfg0, \ + cfg1, eccbufcfg, bccbchcfg, badblockbyte = test + res_cfg0, res_cfg1, res_ecc_buf_cfg, res_ecc_bch_cfg = qs.nanddevice.nand_setup(nandid) + if cfg0 != res_cfg0 or cfg1 != res_cfg1 or eccbufcfg != res_ecc_buf_cfg or res_ecc_bch_cfg != bccbchcfg: + errorids.append([nandid, res_cfg0, res_cfg1, res_ecc_buf_cfg, res_ecc_bch_cfg]) + res_cfg0, res_cfg1, res_ecc_buf_cfg, res_ecc_bch_cfg = qs.nanddevice.nand_setup(nandid) + + if len(errorids) > 0: + st = "" + for id in errorids: + st += hex(id[0]) + f" {hex(id[1]), hex(id[2]), hex(id[3]), hex(id[4])}," + st = st[:-1] + print("Error at: " + st) + assert ("Error at : " + st) + else: + print("Yay, all nand_config tests are ok !!!!") + + +if __name__ == "__main__": + print("Running nand config tests...") + test_nand_config() diff --git a/edl/Library/streaming_client.py b/edl/Library/streaming_client.py new file mode 100644 index 0000000..e7412cd --- /dev/null +++ b/edl/Library/streaming_client.py @@ -0,0 +1,436 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import sys +import os +import logging +from binascii import hexlify, unhexlify +from struct import unpack, pack +from edl.Library.streaming import Streaming +from edl.Library.utils import do_tcp_server, LogBase, getint + + +class streaming_client(metaclass=LogBase): + def __init__(self, arguments, cdc, sahara, loglevel, printer): + self.cdc = cdc + self.__logger = self.__logger + self.sahara = sahara + self.arguments = arguments + self.streaming = Streaming(cdc, sahara, loglevel) + self.printer = printer + self.__logger.setLevel(loglevel) + self.error = self.__logger.error + self.info = self.__logger.info + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + def disconnect(self): + self.cdc.close() + sys.exit(0) + + def check_param(self, parameters): + error = False + params = "" + for parameter in parameters: + params += parameter + " " + if parameter not in parameters: + error = True + if error: + if len(parameters) == 1: + self.printer("Argument " + params + "required.") + else: + self.printer("Arguments " + params + "required.") + return False + return True + + def print_partitions(self, partitions): + self.printer("Name Offset\t\tLength\t\tAttr\t\t\tFlash") + self.printer("-------------------------------------------------------------") + for name in partitions: + partition = partitions[name] + for i in range(0x10 - len(name)): + name += " " + offset = partition[ + "offset"] * self.streaming.settings.num_pages_per_blk * self.streaming.settings.PAGESIZE + length = partition[ + "length"] * self.streaming.settings.num_pages_per_blk * self.streaming.settings.PAGESIZE + attr1 = partition["attr1"] + attr2 = partition["attr2"] + attr3 = partition["attr3"] + which_flash = partition["which_flash"] + self.printer( + f"{name}\t%08X\t%08X\t{hex(attr1)}/{hex(attr2)}/{hex(attr3)}\t{which_flash}" % (offset, length)) + + def handle_streaming(self, cmd, options): + mode = 0 + """ + offset = getint(options[""]) + length = getint(options[""]) + filename = options[""] + self.streaming.streaming_mode=self.streaming.Qualcomm + self.streaming.memread=self.streaming.qc_memread + self.streaming.memtofile(offset, length, filename) + """ + + if "" in options: + mode = options[""] + if self.streaming.connect(mode): + xflag = 0 + self.streaming.hdlc.receive_reply(5) + if self.streaming.streaming_mode == self.streaming.Patched: + self.streaming.nand_init(xflag) + if cmd == "gpt": + directory = options[""] + if directory is None: + directory = "" + data = self.streaming.read_partition_table() + sfilename = os.path.join(directory, f"partition.bin") + if data != b"": + with open(sfilename, "wb") as write_handle: + write_handle.write(data) + self.printer(f"Dumped Partition Table to {sfilename}") + else: + self.error(f"Error on dumping partition table to {sfilename}") + elif cmd == "printgpt": + partitions = self.streaming.get_partitions() + self.print_partitions(partitions) + self.streaming.nand_post() + elif cmd == "r": + partitionname = options[""] + filename = options[""] + filenames = filename.split(",") + partitions = partitionname.split(",") + if len(partitions) != len(filenames): + self.error("You need to gives as many filenames as given partitions.") + return + i = 0 + rpartitions = self.streaming.get_partitions() + for partition in partitions: + if partition.lower() in rpartitions: + spartition = rpartitions[partition] + offset = spartition["offset"] + length = spartition["length"] + # attr1 = spartition["attr1"] + # attr2 = spartition["attr2"] + # attr3 = spartition["attr3"] + partfilename = filenames[i] + self.printer(f"Dumping Partition {partition}...") + self.streaming.read_raw(offset, length, self.streaming.settings.UD_SIZE_BYTES, partfilename) + self.printer(f"Dumped sector {str(offset)} with sector count {str(length)} as {partfilename}.") + else: + self.error(f"Error: Couldn't detect partition: {partition}\nAvailable partitions:") + self.print_partitions(rpartitions) + elif cmd == "rs": + sector = getint(options[""]) # Page + sectors = getint(options[""]) + filename = options[""] + self.printer(f"Dumping at Sector {hex(sector)} with Sectorcount {hex(sectors)}...") + if self.streaming.read_sectors(sector, sectors, filename, True): + self.printer(f"Dumped sector {str(sector)} with sector count {str(sectors)} as {filename}.") + elif cmd == "rf": + sector = 0 + sectors = self.streaming.settings.MAXBLOCK * self.streaming.settings.num_pages_per_blk * \ + self.streaming.settings.sectors_per_page + filename = options[""] + self.printer(f"Dumping Flash from sector 0 to sector {hex(sectors)}...") + if self.streaming.read_sectors(sector, sectors, filename, True): + self.printer(f"Dumped sector {str(sector)} with sector count {str(sectors)} as {filename}.") + elif cmd == "rl": + directory = options[""] + if options["--skip"]: + skip = options["--skip"].split(",") + else: + skip = [] + if not os.path.exists(directory): + os.mkdir(directory) + storedir = directory + if not os.path.exists(storedir): + os.mkdir(storedir) + sfilename = os.path.join(storedir, f"partition.bin") + partdata = self.streaming.read_partition_table() + if partdata != -1: + with open(sfilename, "wb") as write_handle: + write_handle.write(partdata) + else: + self.error(f"Couldn't detect partition header.") + return + partitions = self.streaming.get_partitions() + for partition in partitions: + if partition in skip: + continue + filename = os.path.join(storedir, partition + ".bin") + spartition = partitions[partition] + offset = spartition["offset"] + length = spartition["length"] + # attr1 = spartition["attr1"] + # attr2 = spartition["attr2"] + # attr3 = spartition["attr3"] + partfilename = filename + self.info(f"Dumping partition {str(partition)} with block count {str(length)} as " + + f"{filename}.") + self.streaming.read_raw(offset, length, self.streaming.settings.UD_SIZE_BYTES, partfilename) + elif cmd == "peek": + offset = getint(options[""]) + length = getint(options[""]) + filename = options[""] + if self.streaming.memtofile(offset, length, filename): + self.info( + f"Peek data from offset {hex(offset)} and length {hex(length)} was written to {filename}") + elif cmd == "peekhex": + offset = getint(options[""]) + length = getint(options[""]) + resp = self.streaming.memread(offset, length) + self.printer("\n") + self.printer(hexlify(resp)) + elif cmd == "peekqword": + offset = getint(options[""]) + resp = self.streaming.memread(offset, 8) + self.printer("\n") + self.printer(hex(unpack(""]) + resp = self.streaming.mempeek(offset) + self.printer("\n") + self.printer(hex(resp)) + elif cmd == "poke": + offset = getint(options[""]) + filename = unhexlify(options[""]) + try: + with open(filename, "rb") as rf: + data = rf.read() + if self.streaming.memwrite(offset, data): + self.info("Poke succeeded.") + else: + self.error("Poke failed.") + except Exception as e: + self.error(str(e)) + elif cmd == "pokehex": + offset = getint(options[""]) + data = unhexlify(options[""]) + if self.streaming.memwrite(offset, data): + self.info("Poke succeeded.") + else: + self.error("Poke failed.") + elif cmd == "pokeqword": + offset = getint(options[""]) + data = pack(""])) + if self.streaming.memwrite(offset, data): + self.info("Poke succeeded.") + else: + self.error("Poke failed.") + elif cmd == "pokedword": + offset = getint(options[""]) + data = pack(""])) + if self.streaming.mempoke(offset, data): + self.info("Poke succeeded.") + else: + self.error("Poke failed.") + elif cmd == "reset": + if self.streaming.reset(): + self.info("Reset succeeded.") + elif cmd == "memtbl": + filename = options[""] + memtbl = self.streaming.settings.memtbl + data = self.streaming.memread(memtbl[0], memtbl[1]) + if data != b"": + with open(filename, "wb") as wf: + wf.write(data) + self.printer(f"Dumped memtbl at offset {hex(memtbl[0])} as {filename}.") + else: + self.error("Error on dumping memtbl") + elif cmd == "secureboot": + value = self.streaming.mempeek(self.streaming.settings.secureboot) + if value != -1: + is_secure = False + for area in range(0, 4): + sec_boot = (value >> (area * 8)) & 0xFF + pk_hashindex = sec_boot & 3 + oem_pkhash = True if ((sec_boot >> 4) & 1) == 1 else False + auth_enabled = True if ((sec_boot >> 5) & 1) == 1 else False + use_serial = True if ((sec_boot >> 6) & 1) == 1 else False + if auth_enabled: + is_secure = True + self.printer(f"Sec_Boot{str(area)} PKHash-Index:{str(pk_hashindex)} " + + f"OEM_PKHash: {str(oem_pkhash)} " + + f"Auth_Enabled: {str(auth_enabled)} " + + f"Use_Serial: {str(use_serial)}") + if is_secure: + self.printer("Secure boot enabled.") + else: + self.printer("Secure boot disabled.") + else: + self.error("Unknown target chipset") + elif cmd == "pbl": + filename = options[""] + pbl = self.streaming.settings.pbl + self.printer("Dumping pbl....") + data = self.streaming.memread(pbl[0], pbl[1]) + if data != b"": + with open(filename, "wb") as wf: + wf.write(data) + self.printer(f"Dumped pbl at offset {hex(pbl[0])} as {filename}.") + else: + self.error("Error on dumping pbl") + elif cmd == "qfp": + filename = options[""] + qfp = self.streaming.settings.qfprom + self.printer("Dumping qfprom....") + data = self.streaming.memread(qfp[0], qfp[1]) + if data != b"": + with open(filename, "wb") as wf: + wf.write(data) + self.printer(f"Dumped qfprom at offset {hex(qfp[0])} as {filename}.") + else: + self.error("Error on dumping qfprom") + elif cmd == "memcpy": + if not self.check_param(["", ""]): + return False + srcoffset = getint(options[""]) + size = getint(options[""]) + dstoffset = srcoffset + size + if self.streaming.cmd_memcpy(dstoffset, srcoffset, size): + self.printer(f"Memcpy from {hex(srcoffset)} to {hex(dstoffset)} succeeded") + return True + else: + return False + ############################### + elif cmd == "nop": + # resp=self.streaming.send(b"\x7E\x09") + self.error("Nop command isn't supported by streaming loader") + return True + elif cmd == "setbootablestoragedrive": + self.error("setbootablestoragedrive command isn't supported by streaming loader") + return True + elif cmd == "getstorageinfo": + self.error("getstorageinfo command isn't supported by streaming loader") + return True + elif cmd == "w": + if not self.check_param(["", ""]): + return False + partitionname = options[""] + filename = options[""] + partitionfilename = "" + if "--partitionfilename" in options: + partitionfilename = options["--partitionfilename"] + if not os.path.exists(partitionfilename): + self.error(f"Error: Couldn't find partition file: {partitionfilename}") + return False + if not os.path.exists(filename): + self.error(f"Error: Couldn't find file: {filename}") + return False + if partitionfilename == "": + rpartitions = self.streaming.get_partitions() + else: + rpartitions = self.streaming.get_partitions(partitionfilename) + if self.streaming.enter_flash_mode(): + if partitionname in rpartitions: + spartition = rpartitions[partitionname] + offset = spartition["offset"] + length = spartition["length"] + # attr1 = spartition["attr1"] + # attr2 = spartition["attr2"] + # attr3 = spartition["attr3"] + sectors = int(os.stat( + filename).st_size / self.streaming.settings.num_pages_per_blk / + self.streaming.settings.PAGESIZE) + if sectors > length: + self.error( + f"Error: {filename} has {sectors} sectors but partition only has {length}.") + return False + if self.streaming.modules is not None: + self.streaming.modules.writeprepare() + if self.streaming.write_flash(partitionname, filename): + self.printer(f"Wrote {filename} to sector {str(offset)}.") + return True + else: + self.printer(f"Error writing {filename} to sector {str(offset)}.") + return False + else: + self.error(f"Error: Couldn't detect partition: {partitionname}\nAvailable partitions:") + self.print_partitions(rpartitions) + return False + elif cmd == "wl": + if not self.check_param([""]): + return False + directory = options[""] + if options["--skip"]: + skip = options["--skip"].split(",") + else: + skip = [] + if not os.path.exists(directory): + self.error(f"Error: Couldn't find directory: {directory}") + return False + filenames = [] + if self.streaming.enter_flash_mode(): + if self.streaming.modules is not None: + self.streaming.modules.writeprepare() + rpartitions = self.streaming.get_partitions() + for dirName, subdirList, fileList in os.walk(directory): + for fname in fileList: + filenames.append(os.path.join(dirName, fname)) + for filename in filenames: + for partition in rpartitions: + partname = filename[filename.rfind("/") + 1:] + if ".bin" in partname[-4:]: + partname = partname[:-4] + if partition == partname: + if partition in skip: + continue + spartition = rpartitions[partition] + offset = spartition["offset"] + length = spartition["length"] + # attr1 = spartition["attr1"] + # attr2 = spartition["attr2"] + # attr3 = spartition["attr3"] + sectors = int(os.stat(filename).st_size / + self.streaming.settings.num_pages_per_blk / + self.streaming.settings.PAGESIZE) + if sectors > length: + self.error( + f"Error: {filename} has {sectors} sectors but partition only has {length}.") + return False + self.printer(f"Writing {filename} to partition {str(partition)}.") + self.streaming.write_flash(partition, filename) + else: + self.printer("Couldn't write partition. Either wrong memorytype given or no gpt partition.") + return False + return True + elif cmd == "ws": + self.error("ws command isn't supported by streaming loader") # todo + return False + elif cmd == "wf": + self.error("wf command isn't supported by streaming loader") # todo + return False + elif cmd == "e": + self.error("e command isn't supported by streaming loader") # todo + return False + elif cmd == "es": + self.error("es command isn't supported by streaming loader") # todo + return False + elif cmd == "xml": + self.error("xml command isn't supported by streaming loader") + return False + elif cmd == "rawxml": + self.error("rawxml command isn't supported by streaming loader") + return False + elif cmd == "send": + self.error("send command isn't supported by streaming loader") + return False + ############################### + elif cmd == "server": + return do_tcp_server(self, options, self.handle_streaming) + elif cmd == "modules": + if not self.check_param(["", ""]): + return False + command = options[""] + options = options[""] + if self.streaming.modules is None: + self.error("Feature is not supported") + return False + else: + return self.streaming.modules.run(mainargs=options, command=command) + else: + self.error("Unknown/Missing command, a command is required.") + return False diff --git a/edl/Library/usblib.py b/edl/Library/usblib.py new file mode 100755 index 0000000..2b554ad --- /dev/null +++ b/edl/Library/usblib.py @@ -0,0 +1,658 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import io +import logging + +import usb.core # pyusb +import usb.util +import time +import inspect +import array +import usb.backend.libusb0 +import usb.backend.libusb1 +from enum import Enum +from binascii import hexlify +from ctypes import c_void_p, c_int +from edl.Library.utils import * + +USB_DIR_OUT = 0 # to device +USB_DIR_IN = 0x80 # to host + +# USB types, the second of three bRequestType fields +USB_TYPE_MASK = (0x03 << 5) +USB_TYPE_STANDARD = (0x00 << 5) +USB_TYPE_CLASS = (0x01 << 5) +USB_TYPE_VENDOR = (0x02 << 5) +USB_TYPE_RESERVED = (0x03 << 5) + +# USB recipients, the third of three bRequestType fields +USB_RECIP_MASK = 0x1f +USB_RECIP_DEVICE = 0x00 +USB_RECIP_INTERFACE = 0x01 +USB_RECIP_ENDPOINT = 0x02 +USB_RECIP_OTHER = 0x03 +# From Wireless USB 1.0 +USB_RECIP_PORT = 0x04 +USB_RECIP_RPIPE = 0x05 + +tag = 0 + +CDC_CMDS = { + "SEND_ENCAPSULATED_COMMAND": 0x00, + "GET_ENCAPSULATED_RESPONSE": 0x01, + "SET_COMM_FEATURE": 0x02, + "GET_COMM_FEATURE": 0x03, + "CLEAR_COMM_FEATURE": 0x04, + "SET_LINE_CODING": 0x20, + "GET_LINE_CODING": 0x21, + "SET_CONTROL_LINE_STATE": 0x22, + "SEND_BREAK": 0x23, # wValue is break time +} + + +class UsbClass(metaclass=LogBase): + + def load_windows_dll(self): + if os.name == 'nt': + windows_dir = None + try: + # add pygame folder to Windows DLL search paths + windows_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "Windows") + try: + os.add_dll_directory(windows_dir) + except Exception: + pass + os.environ['PATH'] = windows_dir + ';' + os.environ['PATH'] + except Exception: + pass + del windows_dir + + def __init__(self, loglevel=logging.INFO, portconfig=None, devclass=-1): + self.load_windows_dll() + self.connected = False + self.timeout = None + self.vid = None + self.pid = None + self.device = None + self.EP_IN = None + self.EP_OUT = None + self.interface = None + self.stopbits = None + self.databits = None + self.baudrate = None + self.parity = None + self.configuration = None + self.backend = None + self.loglevel = loglevel + self.portconfig = portconfig + self.devclass = devclass + self.__logger = self.__logger + self.info = self.__logger.info + self.error = self.__logger.error + self.warning = self.__logger.warning + self.debug = self.__logger.debug + self.__logger.setLevel(loglevel) + self.buffer = array.array('B', [0]) * 1048576 + if loglevel == logging.DEBUG: + logfilename = "log.txt" + fh = logging.FileHandler(logfilename) + self.__logger.addHandler(fh) + + if sys.platform.startswith('freebsd') or sys.platform.startswith('linux'): + self.backend = usb.backend.libusb1.get_backend(find_library=lambda x: "libusb-1.0.so") + elif sys.platform.startswith('win32'): + self.backend = usb.backend.libusb1.get_backend(find_library=lambda x: "libusb-1.0.dll") + if self.backend is not None: + self.backend.lib.libusb_set_option.argtypes = [c_void_p, c_int] + self.backend.lib.libusb_set_option(self.backend.ctx, 1) + + def verify_data(self, data, pre="RX:"): + self.debug("", stack_info=True) + if isinstance(data, bytes) or isinstance(data, bytearray): + if data[:5] == b"= self.__logger.level: + self.debug(pre + hexlify(data).decode('utf-8')) + else: + if logging.DEBUG >= self.__logger.level: + self.debug(pre + data) + return data + + def getinterfacecount(self): + if self.vid is not None: + self.device = usb.core.find(idVendor=self.vid, idProduct=self.pid) + if self.device is None: + self.debug("Couldn't detect the device. Is it connected ?") + return False + try: + self.device.set_configuration() + except Exception as e: + self.debug(str(e)) + pass + self.configuration = self.device.get_active_configuration() + self.debug(2, self.configuration) + return self.configuration.bNumInterfaces + else: + self.error("No device detected. Is it connected ?") + return 0 + + def setlinecoding(self, baudrate=None, parity=0, databits=8, stopbits=1): + sbits = {1: 0, 1.5: 1, 2: 2} + dbits = {5, 6, 7, 8, 16} + pmodes = {0, 1, 2, 3, 4} + brates = {300, 600, 1200, 2400, 4800, 9600, 14400, + 19200, 28800, 38400, 57600, 115200, 230400} + + if stopbits is not None: + if stopbits not in sbits.keys(): + valid = ", ".join(str(k) for k in sorted(sbits.keys())) + raise ValueError("Valid stopbits are " + valid) + self.stopbits = stopbits + else: + self.stopbits = 0 + + if databits is not None: + if databits not in dbits: + valid = ", ".join(str(d) for d in sorted(dbits)) + raise ValueError("Valid databits are " + valid) + self.databits = databits + else: + self.databits = 0 + + if parity is not None: + if parity not in pmodes: + valid = ", ".join(str(pm) for pm in sorted(pmodes)) + raise ValueError("Valid parity modes are " + valid) + self.parity = parity + else: + self.parity = 0 + + if baudrate is not None: + if baudrate not in brates: + brs = sorted(brates) + dif = [abs(br - baudrate) for br in brs] + best = brs[dif.index(min(dif))] + raise ValueError( + "Invalid baudrates, nearest valid is {}".format(best)) + self.baudrate = baudrate + + linecode = [ + self.baudrate & 0xff, + (self.baudrate >> 8) & 0xff, + (self.baudrate >> 16) & 0xff, + (self.baudrate >> 24) & 0xff, + sbits[self.stopbits], + self.parity, + self.databits] + + txdir = 0 # 0:OUT, 1:IN + req_type = 1 # 0:std, 1:class, 2:vendor + recipient = 1 # 0:device, 1:interface, 2:endpoint, 3:other + req_type = (txdir << 7) + (req_type << 5) + recipient + data = bytearray(linecode) + wlen = self.device.ctrl_transfer( + req_type, CDC_CMDS["SET_LINE_CODING"], + data_or_wlength=data, windex=1) + self.debug("Linecoding set, {}b sent".format(wlen)) + + def setbreak(self): + txdir = 0 # 0:OUT, 1:IN + req_type = 1 # 0:std, 1:class, 2:vendor + recipient = 1 # 0:device, 1:interface, 2:endpoint, 3:other + req_type = (txdir << 7) + (req_type << 5) + recipient + wlen = self.device.ctrl_transfer( + bmrequesttype=req_type, brequest=CDC_CMDS["SEND_BREAK"], + wvalue=0, data_or_wlength=0, windex=1) + self.debug("Break set, {}b sent".format(wlen)) + + def setcontrollinestate(self, rts=None, dtr=None, isftdi=False): + ctrlstate = (2 if rts else 0) + (1 if dtr else 0) + if isftdi: + ctrlstate += (1 << 8) if dtr is not None else 0 + ctrlstate += (2 << 8) if rts is not None else 0 + txdir = 0 # 0:OUT, 1:IN + req_type = 2 if isftdi else 1 # 0:std, 1:class, 2:vendor + # 0:device, 1:interface, 2:endpoint, 3:other + recipient = 0 if isftdi else 1 + req_type = (txdir << 7) + (req_type << 5) + recipient + + wlen = self.device.ctrl_transfer( + bmrequesttype=req_type, + brequest=1 if isftdi else CDC_CMDS["SET_CONTROL_LINE_STATE"], + wvalue=ctrlstate, + windex=1, + data_or_wlength=0) + self.debug("Linecoding set, {}b sent".format(wlen)) + + def connect(self, EP_IN=-1, EP_OUT=-1): + if self.connected: + self.close() + self.connected = False + for usbid in self.portconfig: + vid = usbid[0] + pid = usbid[1] + interface = usbid[2] + self.device = usb.core.find(idVendor=vid, idProduct=pid, backend=self.backend) + if self.device is not None: + self.vid = vid + self.pid = pid + self.interface = interface + break + + if self.device is None: + self.debug("Couldn't detect the device. Is it connected ?") + return False + + + try: + if self.device is not None: + self.configuration = self.device.get_active_configuration() + except usb.core.USBError as e: + if e.strerror == "Configuration not set": + self.device.set_configuration() + self.configuration = self.device.get_active_configuration() + if e.errno == 13: + self.backend = usb.backend.libusb0.get_backend() + self.device = usb.core.find(idVendor=vid, idProduct=pid, backend=self.backend) + + if self.interface == -1: + for interfacenum in range(0, self.configuration.bNumInterfaces): + itf = usb.util.find_descriptor(self.configuration, bInterfaceNumber=interfacenum) + try: + if self.device.is_kernel_driver_active(interfacenum): + self.debug("Detaching kernel driver") + self.device.detach_kernel_driver(interfacenum) + except Exception as err: + self.debug("No kernel driver supported: " + str(err)) + + usb.util.claim_interface(self.device, interfacenum) + if self.devclass != -1: + if itf.bInterfaceClass == self.devclass: # MassStorage + self.interface = interfacenum + break + else: + self.interface = interfacenum + break + + self.debug(self.configuration) + if self.interface > self.configuration.bNumInterfaces: + print("Invalid interface, max number is %d" % self.configuration.bNumInterfaces) + return False + if self.interface != -1: + itf = usb.util.find_descriptor(self.configuration, bInterfaceNumber=self.interface) + if EP_OUT == -1: + # match the first OUT endpoint + self.EP_OUT = usb.util.find_descriptor(itf, + custom_match=lambda em: usb.util.endpoint_direction( + em.bEndpointAddress) == usb.util.ENDPOINT_OUT) + else: + self.EP_OUT = EP_OUT + if EP_IN == -1: + # match the first OUT endpoint + self.EP_IN = usb.util.find_descriptor(itf, + custom_match=lambda em: usb.util.endpoint_direction( + em.bEndpointAddress) == usb.util.ENDPOINT_IN) + else: + self.EP_IN = EP_IN + + self.connected = True + return True + else: + print("Couldn't find MassStorage interface. Aborting.") + self.connected = False + return False + + def close(self, reset=False): + if self.connected: + usb.util.dispose_resources(self.device) + try: + if not self.device.is_kernel_driver_active(self.interface): + # self.device.attach_kernel_driver(self.interface) #Do NOT uncomment + self.device.attach_kernel_driver(0) + if reset: + self.device.reset() + except Exception as e: # pylint: disable=broad-except + self.debug(str(e)) + pass + del self.device + + def write(self, command): + pktsize=self.EP_OUT.wMaxPacketSize + if isinstance(command, str): + command = bytes(command, 'utf-8') + if command == b'': + try: + self.EP_OUT.write(b'') + except usb.core.USBError as e: + error = str(e.strerror) + if "timeout" in error: + time.sleep(0.01) + try: + self.EP_OUT.write(b'') + except Exception as e: # pylint: disable=broad-except + self.debug(str(e)) + return False + return True + else: + i = 0 + try: + buffer=array.array('B',command) + self.EP_OUT.write(buffer) + except Exception as e: # pylint: disable=broad-except + # print("Error while writing") + if "timed out" in str(e): + self.debug(str(e)) + time.sleep(0.01) + i += 1 + if i == 3: + return False + pass + else: + self.error(str(e)) + return False + if self.loglevel == logging.DEBUG: + self.verify_data(bytearray(command), "TX:") + return True + + def read(self, length=0x80, timeout=None): + if self.loglevel==logging.DEBUG: + self.debug(inspect.currentframe().f_back.f_code.co_name + ":" + hex(length)) + tmp = bytearray() + extend = tmp.extend + if timeout is None: + timeout = self.timeout + buffer = self.buffer + ep_read = self.EP_IN.read + while len(tmp) == 0: + try: + length=ep_read(buffer, timeout) + extend(buffer[:length]) + if len(tmp)>0: + return tmp + except usb.core.USBError as e: + error = str(e.strerror) + if "timed out" in error: + # if platform.system()=='Windows': + # time.sleep(0.05) + # print("Waiting...") + self.debug("Timed out") + self.debug(tmp) + return tmp + elif "Overflow" in error: + self.error("USB Overflow") + sys.exit(0) + elif e.errno is not None: + print(repr(e), type(e), e.errno) + sys.exit(0) + else: + break + if self.loglevel == logging.DEBUG: + self.verify_data(tmp, "RX:") + return tmp + + def ctrl_transfer(self, bmrequesttype, brequest, wvalue, windex, data_or_wlength): + ret = self.device.ctrl_transfer(bmrequesttype=bmrequesttype, brequest=brequest, wvalue=wvalue, windex=windex, + data_or_wlength=data_or_wlength) + return ret[0] | (ret[1] << 8) + + +class ScsiCmds(Enum): + SC_TEST_UNIT_READY = 0x00, + SC_REQUEST_SENSE = 0x03, + SC_FORMAT_UNIT = 0x04, + SC_READ_6 = 0x08, + SC_WRITE_6 = 0x0a, + SC_INQUIRY = 0x12, + SC_MODE_SELECT_6 = 0x15, + SC_RESERVE = 0x16, + SC_RELEASE = 0x17, + SC_MODE_SENSE_6 = 0x1a, + SC_START_STOP_UNIT = 0x1b, + SC_SEND_DIAGNOSTIC = 0x1d, + SC_PREVENT_ALLOW_MEDIUM_REMOVAL = 0x1e, + SC_READ_FORMAT_CAPACITIES = 0x23, + SC_READ_CAPACITY = 0x25, + SC_WRITE_10 = 0x2a, + SC_VERIFY = 0x2f, + SC_READ_10 = 0x28, + SC_SYNCHRONIZE_CACHE = 0x35, + SC_READ_TOC = 0x43, + SC_READ_HEADER = 0x44, + SC_MODE_SELECT_10 = 0x55, + SC_MODE_SENSE_10 = 0x5a, + SC_READ_12 = 0xa8, + SC_WRITE_12 = 0xaa, + SC_PASCAL_MODE = 0xff + + +command_block_wrapper = [ + ('dCBWSignature', '4s'), + ('dCBWTag', 'I'), + ('dCBWDataTransferLength', 'I'), + ('bmCBWFlags', 'B'), + ('bCBWLUN', 'B'), + ('bCBWCBLength', 'B'), + ('CBWCB', '16s'), +] +command_block_wrapper_len = 31 + +command_status_wrapper = [ + ('dCSWSignature', '4s'), + ('dCSWTag', 'I'), + ('dCSWDataResidue', 'I'), + ('bCSWStatus', 'B') +] +command_status_wrapper_len = 13 + + +class Scsi: + """ + FIHTDC, PCtool + """ + SC_READ_NV = 0xf0 + SC_SWITCH_STATUS = 0xf1 + SC_SWITCH_PORT = 0xf2 + SC_MODEM_STATUS = 0xf4 + SC_SHOW_PORT = 0xf5 + SC_MODEM_DISCONNECT = 0xf6 + SC_MODEM_CONNECT = 0xf7 + SC_DIAG_RUT = 0xf8 + SC_READ_BATTERY = 0xf9 + SC_READ_IMAGE = 0xfa + SC_ENABLE_ALL_PORT = 0xfd + SC_MASS_STORGE = 0xfe + SC_ENTER_DOWNLOADMODE = 0xff + SC_ENTER_FTMMODE = 0xe0 + SC_SWITCH_ROOT = 0xe1 + """ + //Div2-5-3-Peripheral-LL-ADB_ROOT-00+/* } FIHTDC, PCtool */ + //StevenCPHuang 2011/08/12 porting base on 1050 -- + //StevenCPHuang_20110820,add Moto's mode switch cmd to support PID switch function ++ + """ + SC_MODE_SWITCH = 0xD6 + + # /StevenCPHuang_20110820,add Moto's mode switch cmd to support PID switch function -- + + def __init__(self, loglevel=logging.INFO, vid=None, pid=None, interface=-1): + self.vid = vid + self.pid = pid + self.interface = interface + self.Debug = False + self.usb = None + self.loglevel = loglevel + + def connect(self): + self.usb = UsbClass(loglevel=self.loglevel, portconfig=[self.vid, self.pid, self.interface], devclass=8) + if self.usb.connect(): + return True + return False + + # htcadb = "55534243123456780002000080000616687463800100000000000000000000"; + # Len 0x6, Command 0x16, "HTC" 01 = Enable, 02 = Disable + def send_mass_storage_command(self, lun, cdb, direction, data_length): + global tag + cmd = cdb[0] + if 0 <= cmd < 0x20: + cdb_len = 6 + elif 0x20 <= cmd < 0x60: + cdb_len = 10 + elif 0x60 <= cmd < 0x80: + cdb_len = 0 + elif 0x80 <= cmd < 0xA0: + cdb_len = 16 + elif 0xA0 <= cmd < 0xC0: + cdb_len = 12 + else: + cdb_len = 6 + + if len(cdb) != cdb_len: + print("Error, cdb length doesn't fit allowed cbw packet length") + return 0 + + if (cdb_len == 0) or (cdb_len > command_block_wrapper_len): + print("Error, invalid data packet length, should be max of 31 bytes.") + return 0 + else: + data = write_object(command_block_wrapper, b"USBC", tag, data_length, direction, lun, cdb_len, cdb)[ + 'raw_data'] + print(hexlify(data)) + if len(data) != 31: + print("Error, invalid data packet length, should be 31 bytes, but length is %d" % len(data)) + return 0 + tag += 1 + self.usb.write(data, 31) + return tag + + def send_htc_adbenable(self): + # do_reserve from f_mass_storage.c + print("Sending HTC adb enable command") + common_cmnd = b"\x16htc\x80\x01" # reserve_cmd + 'htc' + len + flag + ''' + Flag values: + 1: Enable adb daemon from mass_storage + 2: Disable adb daemon from mass_storage + 3: cancel unmount BAP cdrom + 4: cancel unmount HSM rom + ''' + lun = 0 + datasize = common_cmnd[4] + timeout = 5000 + ret_tag = self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, datasize) + ret_tag += self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, datasize) + if datasize > 0: + data = self.usb.read(datasize, timeout) + print("DATA: " + hexlify(data).decode('utf-8')) + print("Sent HTC adb enable command") + + def send_htc_ums_adbenable(self): # HTC10 + # ums_ctrlrequest from f_mass_storage.c + print("Sending HTC ums adb enable command") + brequesttype = USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE + brequest = 0xa0 + wvalue = 1 + ''' + wValue: + 0: Disable adb daemon + 1: Enable adb daemon + ''' + windex = 0 + w_length = 1 + ret = self.usb.ctrl_transfer(brequesttype, brequest, wvalue, windex, w_length) + print("Sent HTC ums adb enable command: %x" % ret) + + def send_zte_adbenable(self): # zte blade + common_cmnd = b"\x86zte\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserve_cmd + 'zte' + len + flag + common_cmnd2 = b"\x86zte\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserve_cmd + 'zte' + len + flag + ''' + Flag values: + 0: disable adbd ---for 736T + 1: enable adbd ---for 736T + 2: disable adbd ---for All except 736T + 3: enable adbd ---for All except 736T + ''' + lun = 0 + datasize = common_cmnd[4] + timeout = 5000 + ret_tag = self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, datasize) + ret_tag += self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, datasize) + ret_tag = self.send_mass_storage_command(lun, common_cmnd2, USB_DIR_IN, datasize) + ret_tag += self.send_mass_storage_command(lun, common_cmnd2, USB_DIR_IN, datasize) + if datasize > 0: + data = self.usb.read(datasize, timeout) + print("DATA: " + hexlify(data).decode('utf-8')) + print("Send HTC adb enable command") + + def send_fih_adbenable(self): # motorola xt560, nokia 3.1, #f_mass_storage.c + if self.usb.connect(): + print("Sending FIH adb enable command") + datasize = 0x24 + # reserve_cmd + 'FI' + flag + len + none + common_cmnd = bytes([self.SC_SWITCH_PORT]) + b"FI1" + struct.pack("1: Enable adb daemon from mass_storage + common_cmnd[3]->0: Disable adb daemon from mass_storage + ''' + lun = 0 + # datasize=common_cmnd[4] + timeout = 5000 + ret_tag = None + ret_tag += self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, 0x600) + # ret_tag+=self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, 0x600) + if datasize > 0: + data = self.usb.read(datasize, timeout) + print("DATA: " + hexlify(data).decode('utf-8')) + print("Sent FIH adb enable command") + self.usb.close() + + def send_alcatel_adbenable(self): # Alcatel MW41 + if self.usb.connect(): + print("Sending alcatel adb enable command") + datasize = 0x24 + common_cmnd = b"\x16\xf9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + lun = 0 + timeout = 5000 + ret_tag = None + ret_tag += self.send_mass_storage_command(lun, common_cmnd, USB_DIR_IN, 0x600) + if datasize > 0: + data = self.usb.read(datasize, timeout) + print("DATA: " + hexlify(data).decode('utf-8')) + print("Sent alcatel adb enable command") + self.usb.close() + + def send_fih_root(self): + # motorola xt560, nokia 3.1, huawei u8850, huawei Ideos X6, + # lenovo s2109, triumph M410, viewpad 7, #f_mass_storage.c + if self.usb.connect(): + print("Sending FIH root command") + datasize = 0x24 + # reserve_cmd + 'FIH' + len + flag + none + common_cmnd = bytes([self.SC_SWITCH_ROOT]) + b"FIH" + struct.pack(" 0: + data = self.usb.read(datasize, timeout) + print("DATA: " + hexlify(data).decode('utf-8')) + print("Sent FIH root command") + self.usb.close() + + def close(self): + self.usb.close() + return True diff --git a/edl/Library/usbscsi.py b/edl/Library/usbscsi.py new file mode 100755 index 0000000..d550f75 --- /dev/null +++ b/edl/Library/usbscsi.py @@ -0,0 +1,52 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import argparse +from edl.Library.usblib import * + + +def main(): + info = 'MassStorageBackdoor (c) B.Kerler 2019.' + parser = argparse.ArgumentParser(description=info) + print("\n" + info + "\n\n") + parser.add_argument('-vid', metavar="", help='[Option] Specify vid, default=0x2e04)', default="0x2e04") + parser.add_argument('-pid', metavar="", help='[Option] Specify pid, default=0xc025)', default="0xc025") + parser.add_argument('-interface', metavar="", help='[Option] Specify interface number)', default="") + parser.add_argument('-nokia', help='[Option] Enable Nokia adb backdoor', action='store_true') + parser.add_argument('-alcatel', help='[Option] Enable alcatel adb backdoor', action='store_true') + parser.add_argument('-zte', help='[Option] Enable zte adb backdoor', action='store_true') + parser.add_argument('-htc', help='[Option] Enable htc adb backdoor', action='store_true') + parser.add_argument('-htcums', help='[Option] Enable htc ums adb backdoor', action='store_true') + args = parser.parse_args() + vid = None + pid = None + if args.vid != "": + vid = int(args.vid, 16) + if args.pid != "": + pid = int(args.pid, 16) + if args.interface != "": + interface = int(args.interface, 16) + else: + interface = -1 + + usbscsi = Scsi(vid, pid, interface) + if usbscsi.connect(): + if args.nokia: + usbscsi.send_fih_adbenable() + usbscsi.send_fih_root() + elif args.zte: + usbscsi.send_zte_adbenable() + elif args.htc: + usbscsi.send_htc_adbenable() + elif args.htcums: + usbscsi.send_htc_ums_adbenable() + elif args.alcatel: + usbscsi.send_alcatel_adbenable() + else: + print("A command is required. Use -h to see options.") + exit(0) + usbscsi.close() + + +if __name__ == '__main__': + main() diff --git a/edl/Library/utils.py b/edl/Library/utils.py new file mode 100755 index 0000000..9c4c7ac --- /dev/null +++ b/edl/Library/utils.py @@ -0,0 +1,587 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import sys +import logging +import logging.config +import codecs +import struct +import os +import shutil +import stat +import colorama +import copy +from struct import unpack, pack + +try: + from capstone import * +except ImportError: + print("Capstone library is missing (optional).") +try: + from keystone import * +except ImportError: + print("Keystone library is missing (optional).") + + + + + +class structhelper: + pos = 0 + + def __init__(self, data, pos=0): + self.pos = 0 + self.data = data + + def qword(self): + dat = unpack(""] = opts[0] + elif cmd == "r": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "rl": + options[""] = opts[0] + elif cmd == "rf": + options[""] = opts[0] + elif cmd == "rs": + options[""] = opts[0] + options[""] = opts[1] + options[""] = opts[2] + elif cmd == "w": + options[""] = opts[0] + options[""] = opts[0] + elif cmd == "wl": + options[""] = opts[0] + elif cmd == "wf": + options[""] = opts[0] + elif cmd == "ws": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "e": + options[""] = opts[0] + elif cmd == "es": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "footer": + options[""] = opts[0] + elif cmd == "peek": + options[""] = opts[0] + options[""] = opts[1] + options[""] = opts[2] + elif cmd == "peekhex": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "peekdword": + options[""] = opts[0] + elif cmd == "peekqword": + options[""] = opts[0] + elif cmd == "memtbl": + options[""] = opts[0] + elif cmd == "poke": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "pokehex": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "pokedword": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "pokeqword": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "memcpy": + options[""] = opts[0] + options[""] = opts[1] + elif cmd == "pbl": + options[""] = opts[0] + elif cmd == "qfp": + options[""] = opts[0] + elif cmd == "setbootablestoragedrive": + options[""] = opts[0] + elif cmd == "send": + options[""] = opts[0] + elif cmd == "xml": + options[""] = opts[0] + elif cmd == "rawxml": + options[""] = opts[0] + return options + + +def getint(valuestr): + try: + return int(valuestr) + except: + try: + return int(valuestr, 16) + except: + return 0 + + +class ColorFormatter(logging.Formatter): + LOG_COLORS = { + logging.ERROR: colorama.Fore.RED, + logging.DEBUG: colorama.Fore.LIGHTMAGENTA_EX, + logging.WARNING: colorama.Fore.YELLOW, + } + + def format(self, record, *args, **kwargs): + # if the corresponding logger has children, they may receive modified + # record, so we want to keep it intact + new_record = copy.copy(record) + if new_record.levelno in self.LOG_COLORS: + pad = "" + if new_record.name != "root": + print(new_record.name) + pad = "[LIB]: " + # we want levelname to be in different color, so let's modify it + new_record.msg = "{pad}{color_begin}{msg}{color_end}".format( + pad=pad, + msg=new_record.msg, + color_begin=self.LOG_COLORS[new_record.levelno], + color_end=colorama.Style.RESET_ALL, + ) + # now we can let standart formatting take care of the rest + return super(ColorFormatter, self).format(new_record, *args, **kwargs) + + +class LogBase(type): + debuglevel = logging.root.level + + def __init__(cls, *args): + super().__init__(*args) + logger_attribute_name = '_' + cls.__name__ + '__logger' + logger_debuglevel_name = '_' + cls.__name__ + '__debuglevel' + logger_name = '.'.join([c.__name__ for c in cls.mro()[-2::-1]]) + LOG_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "root": { + "()": ColorFormatter, + "format": "%(name)s - %(message)s", + } + }, + "handlers": { + "root": { + # "level": cls.__logger.level, + "formatter": "root", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + } + }, + "loggers": { + "": { + "handlers": ["root"], + # "level": cls.debuglevel, + "propagate": False + } + }, + } + logging.config.dictConfig(LOG_CONFIG) + logger = logging.getLogger(logger_name) + + setattr(cls, logger_attribute_name, logger) + setattr(cls, logger_debuglevel_name, cls.debuglevel) + + +def del_rw(action, name, exc): + os.chmod(name, stat.S_IWRITE) + os.remove(name) + + +def rmrf(path): + if os.path.exists(path): + if os.path.isfile(path): + del_rw("", path, "") + else: + shutil.rmtree(path, onerror=del_rw) + + +class elf: + class memorysegment: + phy_addr = 0 + virt_start_addr = 0 + virt_end_addr = 0 + file_start_addr = 0 + file_end_addr = 0 + + def __init__(self, indata, filename): + self.data = indata + self.filename = filename + self.header, self.pentry = self.parse() + self.memorylayout = [] + for entry in self.pentry: + ms = self.memorysegment() + ms.phy_addr = entry.phy_addr + ms.virt_start_addr = entry.virt_addr + ms.virt_end_addr = entry.virt_addr + entry.seg_mem_len + ms.file_start_addr = entry.from_file + ms.file_end_addr = entry.from_file + entry.seg_file_len + self.memorylayout.append(ms) + + def getfileoffset(self, offset): + for memsegment in self.memorylayout: + if memsegment.virt_end_addr >= offset >= memsegment.virt_start_addr: + return offset - memsegment.virt_start_addr + memsegment.file_start_addr + return None + + def getvirtaddr(self, fileoffset): + for memsegment in self.memorylayout: + if memsegment.file_end_addr >= fileoffset >= memsegment.file_start_addr: + return memsegment.virt_start_addr + fileoffset - memsegment.file_start_addr + return None + + def getbaseaddr(self, offset): + for memsegment in self.memorylayout: + if memsegment.virt_end_addr >= offset >= memsegment.virt_start_addr: + return memsegment.virt_start_addr + return None + + class programentry: + p_type = 0 + from_file = 0 + virt_addr = 0 + phy_addr = 0 + seg_file_len = 0 + seg_mem_len = 0 + p_flags = 0 + p_align = 0 + + def parse_programentry(self, dat): + pe = self.programentry() + if self.elfclass == 1: + (pe.p_type, pe.from_file, pe.virt_addr, pe.phy_addr, pe.seg_file_len, pe.seg_mem_len, pe.p_flags, + pe.p_align) = struct.unpack("> 16 + a = ((offset + div) & 0xFFFF) + strasm = "" + if div > 0: + strasm += "# " + hex(offset) + "\n" + strasm += "mov " + reg + ", #" + hex(a) + ";\n" + strasm += "movk " + reg + ", #" + hex(abase) + ", LSL#16;\n" + strasm += "sub " + reg + ", " + reg + ", #" + hex(div) + ";\n" + else: + strasm += "# " + hex(offset) + "\n" + strasm += "mov " + reg + ", #" + hex(a) + ";\n" + strasm += "movk " + reg + ", #" + hex(abase) + ", LSL#16;\n" + strasm += "add " + reg + ", " + reg + ", #" + hex(-div) + ";\n" + return strasm + + def uart_valid_sc(self, sc): + badchars = [b'\x00', b'\n', b'\r', b'\x08', b'\x7f', b'\x20', b'\x09'] + for idx, c in enumerate(sc): + c = bytes([c]) + if c in badchars: + print("bad char 0x%s in SC at offset %d, opcode # %d!\n" % (codecs.encode(c, 'hex'), idx, idx / 4)) + print(codecs.encode(sc, 'hex')) + return False + return True + + def disasm(self, code, size): + cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) + instr = [] + for i in cs.disasm(code, size): + # print("0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str)) + instr.append("%s\t%s" % (i.mnemonic, i.op_str)) + return instr + + def assembler(self, code): + ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) + if self.bDebug: + try: + encoding, count = ks.asm(code) + except KsError as e: + print(e) + print(e.stat_count) + print(code[e.stat_count:e.stat_count + 10]) + exit(0) + if self.bDebug: + # walk every line to find the (first) error + for idx, line in enumerate(code.splitlines()): + print("%02d: %s" % (idx, line)) + if len(line) and line[0] != '.': + try: + encoding, count = ks.asm(line) + except Exception as e: + print("bummer: " + str(e)) + else: + exit(0) + else: + encoding, count = ks.asm(code) + + sc = "" + count = 0 + out = "" + for i in encoding: + if self.cstyle: + out += ("\\x%02x" % i) + else: + out += ("%02x" % i) + sc += "%02x" % i + + count += 1 + # if bDebug and count % 4 == 0: + # out += ("\n") + + return out + + def find_binary(self, data, strf, pos=0): + t = strf.split(b".") + pre = 0 + offsets = [] + while pre != -1: + pre = data[pos:].find(t[0], pre) + if pre == -1: + if len(offsets) > 0: + for offset in offsets: + error = 0 + rt = offset + len(t[0]) + for i in range(1, len(t)): + if t[i] == b'': + rt += 1 + continue + rt += 1 + prep = data[rt:].find(t[i]) + if (prep != 0): + error = 1 + break + rt += len(t[i]) + if error == 0: + return offset + else: + return None + else: + offsets.append(pre) + pre += 1 + return None + + +def read_object(data: object, definition: object) -> object: + """ + Unpacks a structure using the given data and definition. + """ + obj = {} + object_size = 0 + pos = 0 + for (name, stype) in definition: + object_size += struct.calcsize(stype) + obj[name] = struct.unpack(stype, data[pos:pos + struct.calcsize(stype)])[0] + pos += struct.calcsize(stype) + obj['object_size'] = object_size + obj['raw_data'] = data + return obj + + +def write_object(definition, *args): + """ + Unpacks a structure using the given data and definition. + """ + obj = {} + object_size = 0 + data = b"" + i = 0 + for (name, stype) in definition: + object_size += struct.calcsize(stype) + arg = args[i] + try: + data += struct.pack(stype, arg) + except Exception as e: + print("Error:" + str(e)) + break + i += 1 + obj['object_size'] = len(data) + obj['raw_data'] = data + return obj + + +def print_progress(iteration, total, prefix='', suffix='', decimals=1, bar_length=100): + """ + Call in a loop to create terminal progress bar + @params: + iteration - Required : current iteration (Int) + total - Required : total iterations (Int) + prefix - Optional : prefix string (Str) + suffix - Optional : suffix string (Str) + decimals - Optional : positive number of decimals in percent complete (Int) + bar_length - Optional : character length of bar (Int) + """ + str_format = "{0:." + str(decimals) + "f}" + percents = str_format.format(100 * (iteration / float(total))) + filled_length = int(round(bar_length * iteration / float(total))) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + + sys.stdout.write('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix)) + + if iteration == total: + sys.stdout.write('\n') + sys.stdout.flush() diff --git a/edl/Library/xmlparser.py b/edl/Library/xmlparser.py new file mode 100755 index 0000000..918b6e6 --- /dev/null +++ b/edl/Library/xmlparser.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# (c) B.Kerler 2018-2021 +import xml.etree.ElementTree as ET + + +class xmlparser: + def getresponse(self, input): + lines = input.split(b"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