Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions ubireader/ubifs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ubireader.ubifs.defines import *
from ubireader.ubifs import nodes, display
from typing import Optional
from zlib import crc32

class ubifs():
"""UBIFS object
Expand Down Expand Up @@ -61,23 +62,46 @@ def __init__(self, ubifs_file, master_key: Optional[bytes] = None):

self._mst_nodes = [None, None]
for i in range(0, 2):
s_mst_offset = self.leb_size * (UBIFS_MST_LNUM + i)
mst_offset = s_mst_offset
mst_nodes = []
try:
mst_offset = self.leb_size * (UBIFS_MST_LNUM + i)
self.file.seek(mst_offset)
mst_chdr = nodes.common_hdr(self.file.read(UBIFS_COMMON_HDR_SZ))
log(self , '%s file addr: %s' % (mst_chdr, self.file.last_read_addr()))
verbose_display(mst_chdr)
while mst_offset < self.leb_size + s_mst_offset:
self.file.seek(mst_offset)
mst_chdr = nodes.common_hdr(self.file.read(UBIFS_COMMON_HDR_SZ))
log(self , '%s file addr: %s' % (mst_chdr, self.file.last_read_addr()))
verbose_display(mst_chdr)
# crc
cpos = self.file.tell()
self.file.seek(mst_offset + 8)
crcdata = self.file.read(mst_chdr.len-8)
self.file.seek(cpos)
crc = (~crc32(crcdata) & 0xFFFFFFFF)
if crc != mst_chdr.crc:
break
Comment on lines +80 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would log that the CRC32 does not match:

if crc != mst_chdr.crc:
    log(self, 'Master node CRC check failed: expected 0x%x got 0x%x' % (crc, mst_chdr.crc))

I'd continue working even in the presence of corrupt master node, this way the tool still extracts something. It may not be the most recent, but at least one that is not corrupt.

if crc != mst_chdr.crc:
    log(self, 'Master node CRC check failed: expected 0x%x got 0x%x' % (crc, mst_chdr.crc))
    mst_offset += mst_chdr.len
    continue

What do you think ?

Copy link
Author

@scriptkitz scriptkitz Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, masternodes are recorded in sequence. If a CRC failure occurs, there should be no normal ones afterwards. However, you can try continuing the scan. I'm not quite sure if the old data has been completely cleared.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objective is to be fault-tolerant since ubireader is used by unblob on a variety of weird firmware and image dumps.


if mst_chdr.node_type == UBIFS_MST_NODE:
self.file.seek(mst_offset + UBIFS_COMMON_HDR_SZ)
buf = self.file.read(UBIFS_MST_NODE_SZ)
self._mst_nodes[i] = nodes.mst_node(buf, self.file.last_read_addr())
log(self , '%s%s file addr: %s' % (self._mst_nodes[i], i, self.file.last_read_addr()))
verbose_display(self._mst_nodes[i])
else:
raise Exception('Wrong node type.')
if mst_chdr.node_type == UBIFS_MST_NODE:
buf = self.file.read(UBIFS_MST_NODE_SZ)
mst_node = nodes.mst_node(buf, self.file.last_read_addr())
mst_nodes.append(mst_node)
log(self , '%s%s file addr: %s' % (mst_node, i, self.file.last_read_addr()))
verbose_display(mst_node)
elif mst_chdr.node_type == UBIFS_PAD_NODE:
buf = self.file.read(UBIFS_PAD_NODE_SZ)
padnode = nodes.pad_node(buf, self.file.last_read_addr())
mst_offset += padnode.pad_len
log(self , '%s%s file addr: %s' % (padnode, i, self.file.last_read_addr()))
verbose_display(padnode)
else:
raise Exception('Wrong node type.')
mst_offset += mst_chdr.len
except Exception as e:
error(self, 'Warn', 'Master block %s error: %s' % (i, e))

# Get the valid master node with the highest sequence number.
if len(mst_nodes):
self._mst_nodes[i] = max(mst_nodes, key=lambda x: x.cmt_no)

if self._mst_nodes[0] is None and self._mst_nodes[1] is None:
error(self, 'Fatal', 'No valid Master Node found.')
Expand Down
8 changes: 8 additions & 0 deletions ubireader/ubifs/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ def mst_node(node, tab=''):
buf += '%s%s: %r\n' % (tab, key, value)
return buf

def pad_node(node, tab=''):
buf = '%s%s\n' % (tab, node)
buf += '%sFile offset: %s\n' % (tab, node.file_offset)
buf += '%s---------------------\n' % (tab)
tab += '\t'
for key, value in node:
buf += '%s%s: %r\n' % (tab, key, value)
return buf

def dent_node(node, tab=''):
buf = '%s%s\n' % (tab, node)
Expand Down
28 changes: 28 additions & 0 deletions ubireader/ubifs/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,31 @@ def __iter__(self):

def display(self, tab=''):
return display.mst_node(self, tab)

class pad_node(object):
"""Get pad node at given LEB number + offset.

Arguments:
Bin:buf -- Raw data to extract header information from.
Int:offset -- Offset in LEB of data node.

See ubifs/defines.py for object attributes.
"""
def __init__(self, buf, file_offset=-1):
self.file_offset = file_offset
fields = dict(list(zip(UBIFS_PAD_NODE_FIELDS, struct.unpack(UBIFS_PAD_NODE_FORMAT, buf))))
for key in fields:
setattr(self, key, fields[key])

setattr(self, 'errors', [])

def __repr__(self):
return 'UBIFS Pad Node'

def __iter__(self):
for key in dir(self):
if not key.startswith('_'):
yield key, getattr(self, key)

def display(self, tab=''):
return display.pad_node(self, tab)
4 changes: 3 additions & 1 deletion ubireader/ubifs/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ def _set_file_perms(path, inode):
verbose_log(_set_file_perms, 'perms:%s, owner: %s.%s, path: %s' % (inode['ino'].mode, inode['ino'].uid, inode['ino'].gid, path))

def _set_file_timestamps(path, inode):
os.utime(path, (inode['ino'].atime_sec, inode['ino'].mtime_sec), follow_symlinks = False)
follow_symlinks = False
if os.name == 'nt': follow_symlinks = True
os.utime(path, (inode['ino'].atime_sec, inode['ino'].mtime_sec), follow_symlinks=follow_symlinks)
Comment on lines -170 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems to be unrelated. Please put it in a dedicated commit with a description explaining why you need to follow symlinks on Windows.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, On Windows, this parameter to False will error with Python 3.12, while True will not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. Please put that in a dedicated commit.

verbose_log(_set_file_timestamps, 'timestamps: access: %s, modify: %s, path: %s' % (inode['ino'].atime_sec, inode['ino'].mtime_sec, path))

def _write_reg_file(path, data):
Expand Down