Full revamp
This commit is contained in:
@@ -2,28 +2,21 @@
|
|||||||
|
|
||||||
## What's this?
|
## What's this?
|
||||||
|
|
||||||
MTreader is a simple and small (under 200 SLOC) Python-based utility that aims to serve a single goal: read chunks of flash memory from MediaTek MT6261-based feature phones. It can also read firmware version before creating the readback, and supports displaying some decimal error codes from the platform itself if anything goes wrong. No other features are supported or even planned here.
|
MTreader is a simple and small (under 100 SLOC) Python-based utility that aims to serve a single goal: read chunks of flash memory from MediaTek MT6261-based feature phones. No other features are supported or even planned here.
|
||||||
|
|
||||||
MTreader runs on Python 3.2 and above and only requires pySerial module as a dependency.
|
MTreader runs on Python 3.2 and above and only requires pySerial module as a dependency.
|
||||||
|
|
||||||
## How to use it?
|
## How to use it?
|
||||||
|
|
||||||
### Basic usage
|
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 mtreader.py port file start length
|
python3 mtreader.py port file start length
|
||||||
```
|
```
|
||||||
|
|
||||||
Example - backup all 4MB of flash memory (positions 0 to 4194304 on the device flash) into `rom.bin` file on Mac: `python3 mtreader.py /dev/tty.usbmodem14100 rom.bin 0 4194304`
|
Example - backup all 4MB of flash memory (positions 0 to 4194304 on the device flash) into `rom.bin` file on Mac:
|
||||||
|
|
||||||
### Advanced usage
|
```
|
||||||
|
python3 mtreader.py /dev/tty.usbmodem14100 rom.bin 0 4194304
|
||||||
In case your phone requires a different DA (Download Agent) binary than the provided one (`default-da.bin`), you can also specify its parameters:
|
```
|
||||||
|
|
||||||
- `-a` - path to DA file
|
|
||||||
- `-s1` - DA stage 1 loading parameters in the `offset:size:addr` format with each value in hex (defaults to `0:0x718:0x70007000`)
|
|
||||||
- `-s2` - DA stage 2 loading parameters in the `offset:size:addr` format with each value in hex (defaults to `0x718:0x1e5c8:0x10020000`)
|
|
||||||
- `-fio` - offset to the flash information block within the DA file in hex (defaults to `0x1ece0`)
|
|
||||||
|
|
||||||
You can also adjust the readback block size as well with the `-bs` parameter. Changing it is not recommended if the default of 1024 bytes works for you.
|
You can also adjust the readback block size as well with the `-bs` parameter. Changing it is not recommended if the default of 1024 bytes works for you.
|
||||||
|
|
||||||
@@ -32,7 +25,7 @@ You can also adjust the readback block size as well with the `-bs` parameter. Ch
|
|||||||
|
|
||||||
### Which hardware is currently supported?
|
### Which hardware is currently supported?
|
||||||
|
|
||||||
MT6261-based OEM phones that can open BROM port that can be seen as USB-Serial. Nokias (genuine ones) will generally not work. The rule of thumb is: if it can be handled by FlashTool v5.1420, then it can be handled by MTreader as well.
|
MT6261-based OEM phones that can open BROM port that can be seen as USB-Serial. Nokias (genuine ones) will generally not work.
|
||||||
|
|
||||||
As for different MT6261 versions support, the tool had been successfully tested on various MT6261D and MT6261M handsets (provided they can also be supported by FlashTool). MT6261D still remains the primary target though.
|
As for different MT6261 versions support, the tool had been successfully tested on various MT6261D and MT6261M handsets (provided they can also be supported by FlashTool). MT6261D still remains the primary target though.
|
||||||
|
|
||||||
@@ -43,11 +36,12 @@ No, it isn't. FlashTool is proprietary and single-platform. Having no viable ope
|
|||||||
Moreover, to in order to just create a flash dump with FlashTool, you'll need to:
|
Moreover, to in order to just create a flash dump with FlashTool, you'll need to:
|
||||||
|
|
||||||
1. Obtain some scatter file (even though it's totally unnecessary for the readback functionality) and load it into FlashTool.
|
1. Obtain some scatter file (even though it's totally unnecessary for the readback functionality) and load it into FlashTool.
|
||||||
2. Go to the "Readback" tab.
|
2. Probably also obtain the necessary Download Agent file if the one shipped with FlashTool doesn't work with your model.
|
||||||
3. Remove all blocks there and create the block with the memory range you want in a tiny GUI window.
|
3. Go to the "Readback" tab.
|
||||||
4. Select the path to save in another GUI window.
|
4. Remove all blocks there and create the block with the memory range you want in a tiny GUI window.
|
||||||
5. Press "Readback" and connect the device with perfect timing for it to be seen by the tool.
|
5. Select the path to save in another GUI window.
|
||||||
6. Probably reinsert the battery (if it's possible at all) after an unsuccessful or even successful operation.
|
6. Press "Readback" and connect the device with perfect timing for it to be seen by the tool.
|
||||||
|
7. Probably reinsert the battery (if it's possible at all) after an unsuccessful or even successful operation.
|
||||||
|
|
||||||
With MTreader, you just need to:
|
With MTreader, you just need to:
|
||||||
|
|
||||||
@@ -64,14 +58,18 @@ Turn the phone off, insert the cable while holding the bootkey and see what devi
|
|||||||
|
|
||||||
It's a key that you hold to open the BROM serial port. It depends on the vendor. On most phones, it's Call key. On some phones, it can be arrow down key, # or something else.
|
It's a key that you hold to open the BROM serial port. It depends on the vendor. On most phones, it's Call key. On some phones, it can be arrow down key, # or something else.
|
||||||
|
|
||||||
|
### How is the tool working without a DA binary?
|
||||||
|
|
||||||
|
Yes, MTreader used a DA in the past, but now it's completely blob-free and works via BROM itself. The trick is in using the correct mapping register setting: when we set the 32-bit value at the `0xa0510000` to 3, all the ROM contents (in 32-bit little-endian chunks) are mapped onto `0x10000000` base address by the MT6261 chipset itself.
|
||||||
|
|
||||||
### Where was information collected from?
|
### Where was information collected from?
|
||||||
|
|
||||||
There were three main sources of information: [mtk-open-tools](https://github.com/mtek-hack-hack/mtk-open-tools), [platform-quectel](https://github.com/Wiz-IO/platform-quectel/blob/master/builder/frameworks/MT6261.py) and - most importantly - my own research of FlashTool (running on a VM) USB traffic dumps necessary to combine the above sources, streamline them and make them work on real phone targets, not developer boards. Making the phone load the DA correctly was one thing but complete absence of understanding what to do next and figuring out the protocol to talk to DA itself was another challenge altogether.
|
There were several main sources of information: [mtk-open-tools](https://github.com/mtek-hack-hack/mtk-open-tools), [platform-quectel](https://github.com/Wiz-IO/platform-quectel/blob/master/builder/frameworks/MT6261.py), [Fernly](https://github.com/xobs/fernly) and - most importantly - my own research of FlashTool (running on a VM) USB traffic dumps necessary to combine the above sources, streamline them and make them work on real phone targets, not developer boards.
|
||||||
|
|
||||||
### Will there be other chipset support?
|
### Will there be other chipset support?
|
||||||
|
|
||||||
Most probably, no. MT6261 is the simplest to implement because it uses 2-stage DA, while most other MediaTeks use 4 stages. However, the implementation is as straightforward and hackable as possible, so it shouldn't be a problem to add two other stages and thus other chipset support if absolutely necessary.
|
Most probably, no, but you may try.
|
||||||
|
|
||||||
### Are there any plans to support flashing in addition to readbacks?
|
### Are there any plans to support flashing in addition to readbacks?
|
||||||
|
|
||||||
No. Not in this utility. Flashing MediaTek SoCs via USB is much more complicated than dumping. The process depends on the kind of partitions we're trying to flash, is generally quite fragile, relies on some proprietary, internal and undocumented logic, and in total requires much more research and rigorous testing on different devices with potential risk of bricking them to an unflashable state. When such research is complete, another utility will be published.
|
No. Not in this utility. Flashing MediaTek SoCs via USB is much more complicated than dumping. The process is generally quite fragile, relies on more proprietary, internal and undocumented logic, and in total requires much more research and rigorous testing on different devices with potential risk of bricking them to an unflashable state. When such research is complete, another utility will be published.
|
||||||
|
|||||||
Binary file not shown.
+40
-131
@@ -3,19 +3,9 @@
|
|||||||
import os, sys, struct, time
|
import os, sys, struct, time
|
||||||
from serial import Serial
|
from serial import Serial
|
||||||
|
|
||||||
# status constants
|
|
||||||
NONE = b''
|
|
||||||
CONT = b'\x69'
|
|
||||||
ACK = b'\x5A'
|
|
||||||
NACK = b'\xA5'
|
|
||||||
|
|
||||||
class MTreader:
|
class MTreader:
|
||||||
|
|
||||||
def __init__(self, devfile, da_path, stage_1, stage_2, fio):
|
def __init__(self, devfile):
|
||||||
self.da_path = da_path
|
|
||||||
self.st1_offset, self.st1_size, self.st1_addr = [int(x, 16) for x in stage_1.split(":")]
|
|
||||||
self.st2_offset, self.st2_size, self.st2_addr = [int(x, 16) for x in stage_2.split(":")]
|
|
||||||
self.fio = int(fio, 16)
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
self.s = Serial(devfile, 115200)
|
self.s = Serial(devfile, 115200)
|
||||||
@@ -26,13 +16,8 @@ class MTreader:
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def crc_word(self, data, chs=0):
|
|
||||||
for b in data:
|
|
||||||
chs += b
|
|
||||||
return chs & 0xFFFF
|
|
||||||
|
|
||||||
def send(self, data, sz = 0):
|
def send(self, data, sz = 0):
|
||||||
r = ""
|
r = ''
|
||||||
if len(data):
|
if len(data):
|
||||||
self.s.write(data)
|
self.s.write(data)
|
||||||
if sz > 0:
|
if sz > 0:
|
||||||
@@ -40,7 +25,7 @@ class MTreader:
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
def cmd(self, cmd, sz = 0):
|
def cmd(self, cmd, sz = 0):
|
||||||
r = ""
|
r = ''
|
||||||
cmdlen = len(cmd)
|
cmdlen = len(cmd)
|
||||||
if cmdlen > 0:
|
if cmdlen > 0:
|
||||||
assert(self.send(cmd, cmdlen) == cmd)
|
assert(self.send(cmd, cmdlen) == cmd)
|
||||||
@@ -48,126 +33,58 @@ class MTreader:
|
|||||||
r = self.s.read(sz)
|
r = self.s.read(sz)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def da_read16(self, addr, sz=1):
|
def read16(self, addr, sz=1, endianness='>'):
|
||||||
r = self.cmd(b'\xA2' + struct.pack(">II", addr, sz), sz*2)
|
r = self.cmd(b'\xA2' + struct.pack('>II', addr, sz), sz*2)
|
||||||
return struct.unpack(">" + sz * 'H', r)
|
return struct.unpack(endianness + sz * 'H', r)
|
||||||
|
|
||||||
def da_write16(self, addr, val):
|
def write16(self, addr, val):
|
||||||
r = self.cmd(b'\xD2' + struct.pack(">II", addr, 1), 2)
|
r = self.cmd(b'\xD2' + struct.pack(">II", addr, 1), 2)
|
||||||
assert(r == b"\0\1")
|
assert(r == b"\0\1")
|
||||||
r = self.cmd(struct.pack(">H", val), 2)
|
r = self.cmd(struct.pack(">H", val), 2)
|
||||||
assert(r == b"\0\1")
|
assert(r == b"\0\1")
|
||||||
|
|
||||||
def da_write32(self, addr, val):
|
def read32(self, addr, sz=1, endianness=">"):
|
||||||
|
r = self.cmd(b'\xD1' + struct.pack(">II", addr, sz), (sz*4)+4)
|
||||||
|
return struct.unpack(endianness + "H" + (sz*'I')+"H", r)
|
||||||
|
|
||||||
|
def write32(self, addr, val):
|
||||||
r = self.cmd(b'\xD4' + struct.pack(">II", addr, 1), 2)
|
r = self.cmd(b'\xD4' + struct.pack(">II", addr, 1), 2)
|
||||||
assert(r == b"\0\1")
|
assert(r == b"\0\1")
|
||||||
r = self.cmd(struct.pack(">I", val), 2)
|
r = self.cmd(struct.pack(">I", val), 2)
|
||||||
assert(r == b"\0\1")
|
assert(r == b"\0\1")
|
||||||
|
|
||||||
def da_send_da(self, address, size, data, block=4096):
|
|
||||||
r = self.cmd(b'\xD7' + struct.pack(">III", address, size, block), 2)
|
|
||||||
assert(r == b"\0\0")
|
|
||||||
while data:
|
|
||||||
self.s.write(data[:block])
|
|
||||||
data = data[block:]
|
|
||||||
r = self.cmd(NONE, 4) # checksum
|
|
||||||
|
|
||||||
def get_da(self, offset, size):
|
|
||||||
self.fd.seek(offset)
|
|
||||||
data = self.fd.read(size)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def connect(self, timeout = 9.0):
|
def connect(self, timeout = 9.0):
|
||||||
self.s.timeout = 0.02
|
self.s.timeout = 0.1
|
||||||
while True:
|
while True:
|
||||||
self.s.write(b"\xA0")
|
self.s.write(b'\xA0')
|
||||||
if self.s.read(1) == b"\x5F":
|
r = self.s.read(1)
|
||||||
self.s.write(b"\x0A\x50\x05")
|
if r == b'\x5F':
|
||||||
|
break
|
||||||
|
self.s.write(b'\x0A\x50\x05')
|
||||||
r = self.s.read(3)
|
r = self.s.read(3)
|
||||||
if r == b"\xF5\xAF\xFA":
|
assert(r == b'\xF5\xAF\xFA')
|
||||||
break
|
self.chip = self.read16(0x80000008)[0]
|
||||||
else:
|
self.write16(0xa0030000, 0x2200) # disable system watchdog
|
||||||
print("BROM connection error")
|
self.write16(0xa0710074, 0x0001) # enable transfers from core to RTC
|
||||||
exit(2)
|
self.write16(0xa0700a28, 0x8000) # enable USB download mode
|
||||||
timeout -= self.s.timeout
|
self.write16(0xa0700a24, 2) # disable battery watchdog
|
||||||
if timeout < 0:
|
self.write32(0xa0510000, 3) # enter memory map mode 3
|
||||||
print("Timeout error")
|
# now all flash is mapped as little-endian 32-bit chunks at 0x10000000
|
||||||
exit(2)
|
|
||||||
self.s.timeout = 1.0
|
|
||||||
self.CPU_HW = self.da_read16(0x80000000)[0]
|
|
||||||
self.CPU_SW = self.da_read16(0x80000004)[0]
|
|
||||||
self.chip = self.da_read16(0x80000008)[0]
|
|
||||||
self.CPU_SB = self.da_read16(0x8000000C)[0]
|
|
||||||
self.da_write16(0xa0700a28, 0x4010)
|
|
||||||
self.da_write16(0xa0700a00, 0xF210)
|
|
||||||
self.da_write16(0xa0030000, 0x2200)
|
|
||||||
self.da_write16(0xa071004c, 0x1a57)
|
|
||||||
self.da_write16(0xa071004c, 0x2b68)
|
|
||||||
self.da_write16(0xa071004c, 0x042e)
|
|
||||||
self.da_write16(0xa0710068, 0x586a)
|
|
||||||
self.da_write16(0xa0710074, 0x0001)
|
|
||||||
self.da_write16(0xa0710068, 0x9136)
|
|
||||||
self.da_write16(0xa0710074, 0x0001)
|
|
||||||
self.da_write16(0xa0710000, 0x430e)
|
|
||||||
self.da_write16(0xa0710074, 0x0001)
|
|
||||||
self.da_write32(0xa0510000, 0x00000002)
|
|
||||||
|
|
||||||
def da_start(self):
|
def read_flash(self, outfile, start, size, blk_size=1024):
|
||||||
assert(os.path.isfile(self.da_path) == True)
|
|
||||||
self.fd = open(self.da_path, "rb")
|
|
||||||
# Send DA first stage
|
|
||||||
offset = self.st1_offset
|
|
||||||
size = self.st1_size
|
|
||||||
data = self.get_da(offset, size)
|
|
||||||
self.da_send_da(self.st1_addr, size, data, 0x400)
|
|
||||||
# Send DA second stage
|
|
||||||
offset = self.st2_offset
|
|
||||||
size = self.st2_size
|
|
||||||
data = self.get_da(offset, size)
|
|
||||||
self.da_send_da(self.st2_addr, size, data, 0x800)
|
|
||||||
# Pass execution to DA
|
|
||||||
r = self.cmd(b'\xD5' + struct.pack(">I", self.st1_addr), 2)
|
|
||||||
assert r == b"\0\0"
|
|
||||||
r = self.cmd(NONE, 4)
|
|
||||||
self.send(b"\xa5\x05\xfe\x00\x08\x00\x70\x07\xff\xff\x02\x00\x00\x01\x08", 1) # something undocumented
|
|
||||||
#Set flash ID info and other stuff
|
|
||||||
offset = self.fio
|
|
||||||
for i in range(512):
|
|
||||||
data = self.get_da(offset, 36)
|
|
||||||
assert(data[:4] != b'\xFF\xFF\0\0')
|
|
||||||
offset += 36
|
|
||||||
r = self.send(data, 1)
|
|
||||||
if r == ACK:
|
|
||||||
assert(self.cmd(NONE, 2) == b'\xA5\x69')
|
|
||||||
break
|
|
||||||
assert(r == CONT)
|
|
||||||
r = self.send(b"\0\0\0\0", 256)
|
|
||||||
|
|
||||||
def get_version(self):
|
|
||||||
self.send(b'\xEF', 1)
|
|
||||||
r = self.send(b'\xEF', 256)
|
|
||||||
r = r[:64].strip(b'\0')
|
|
||||||
return r
|
|
||||||
|
|
||||||
def da_read_flash(self, start, size, outfile, blk_size=1024):
|
|
||||||
r = self.send(b'\xD6\0' + struct.pack(">LL", start, size), 1)
|
|
||||||
if r == NACK:
|
|
||||||
errorcode = self.s.read(4)
|
|
||||||
print('Flash read error: %d, exiting' % int.from_bytes(errorcode, 'big'))
|
|
||||||
self.da_reset()
|
|
||||||
exit(1)
|
|
||||||
self.send(struct.pack(">L", blk_size), 0)
|
|
||||||
outf = open(outfile, "wb")
|
outf = open(outfile, "wb")
|
||||||
|
offset = 0
|
||||||
|
addr = 0x10000000 + start
|
||||||
while size > 0:
|
while size > 0:
|
||||||
chunk = self.s.read(min(size, blk_size))
|
rsize = min(size, blk_size)
|
||||||
size -= len(chunk)
|
chunk = self.read32(addr, rsize>>2, '<')
|
||||||
chksum = struct.unpack(">H", self.s.read(2))[0]
|
outchunk = struct.pack('>' + (len(chunk) - 2)*'L', *chunk[1:-1])
|
||||||
chksum_ref = self.crc_word(chunk)
|
size -= rsize
|
||||||
assert(chksum_ref == chksum)
|
addr += rsize
|
||||||
outf.write(chunk)
|
outf.write(outchunk)
|
||||||
sys.stdout.write(".")
|
sys.stdout.write(".")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
self.s.write(ACK) # continue with the next packet
|
outf.close()
|
||||||
|
|
||||||
def da_reset(self):
|
def da_reset(self):
|
||||||
r = self.send(b'\xB9', 1)
|
r = self.send(b'\xB9', 1)
|
||||||
@@ -181,25 +98,17 @@ if __name__ == "__main__": # main app start
|
|||||||
parser.add_argument('file', help='File to write the dump into')
|
parser.add_argument('file', help='File to write the dump into')
|
||||||
parser.add_argument('start', type=int, help='start position (in the phone flash memory)')
|
parser.add_argument('start', type=int, help='start position (in the phone flash memory)')
|
||||||
parser.add_argument('length', type=int, help='data length')
|
parser.add_argument('length', type=int, help='data length')
|
||||||
parser.add_argument('-da','--agent', default=os.path.dirname(os.path.realpath(__file__))+'/default-da.bin', help='Path to download agent (DA) binary')
|
|
||||||
parser.add_argument('-s1','--stage-1', default='0:0x718:0x70007000', help='Data for first stage DA loading (offset:size:addr format)')
|
|
||||||
parser.add_argument('-s2','--stage-2', default='0x718:0x1e5c8:0x10020000', help='Data for second stage DA loading (offset:size:addr format)')
|
|
||||||
parser.add_argument('-fio','--flash-info-offset', default='0x1ece0', help='Flash info offset in the DA file (hex)')
|
|
||||||
parser.add_argument('-bs','--block-size', type=int, default=1024, help='Readback block size (in bytes), defaults to 1024')
|
parser.add_argument('-bs','--block-size', type=int, default=1024, help='Readback block size (in bytes), defaults to 1024')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if(args.length < 1):
|
if(args.length < 1):
|
||||||
print('Zero length of data, exiting')
|
print('Zero length of data, exiting')
|
||||||
exit(1)
|
exit(1)
|
||||||
print("Turn the phone off, hold the boot key and connect the cable")
|
print("Turn the phone off, hold the boot key and connect the cable")
|
||||||
m = MTreader(args.port, args.agent, args.stage_1, args.stage_2, args.flash_info_offset)
|
m = MTreader(args.port)
|
||||||
|
print("Sending BROM handshake...")
|
||||||
m.connect()
|
m.connect()
|
||||||
if m.chip != 0x6261:
|
print("Connected, chip ID: %04x" % m.chip)
|
||||||
print("Warning: chip ID is detected as %04x instead of 6261, readback process might fail!" % m.chip)
|
|
||||||
m.da_start()
|
|
||||||
print("DA sent and responsive, starting the operation")
|
|
||||||
ver = m.get_version().decode('ascii')
|
|
||||||
print("Firmware version: %s\n" % ver)
|
|
||||||
print("Dumping, don't disconnect...")
|
print("Dumping, don't disconnect...")
|
||||||
m.da_read_flash(args.start, args.length, args.file, args.block_size)
|
m.read_flash(args.file, args.start, args.length, args.block_size)
|
||||||
print("\nROM dumped, disconnect the cable")
|
print("\nROM dumped, disconnect the cable")
|
||||||
m.da_reset()
|
m.da_reset()
|
||||||
|
|||||||
Reference in New Issue
Block a user