Misc Notes - tnballo/notebook GitHub Wiki
References:
1 . "Why do Windows functions all begin with a pointless MOV EDI, EDI instruction?” by Raymond Chen
-
ARM Interrupt Vector Table (IVT) entries are interrupt handler addresses. The table is indexed by interrupt number.
-
RTOSs use a default value of
0xEAFFFFFEfor table entries. This encodesb #0- an infinite spin loop (unconditional branch to current PC). This default allows the following error recovery sequence:-
Unsupported interrupt is fired, indexing into IVT hits the default entry. CPU enters infinite spin loop.
-
Code to mask/block interrupts has not run, as no handler has yet been executed.
-
Watchdog timer (typically private on-chip peripheral, not external off-chip component) fires while CPU is spinning, sending RESET interrupt.
-
System is reset to a (theoretically) good state in a finite and fixed amount of time. Ideally, the unsupported interrupt won't be raised again.
-
-
An IVT's entries can be populated during boot, replacing most or all defaults with handler addresses.
- See function
early_trap_init()in the Linux kernel.
- See function
-
The
/hotpatchcompiler flag ensures that every function starts with a 2 byte NOP (ex.mov edi, edi) that is preceded by an additional 5 bytes of NOP slack space (7 total bytes of NOP). -
During normal operation a function call jumps into those 2 NOP bytes and then slides into the actual code, executing all 7 bytes of NOP on every entry would be wasteful (by comparison the 2 byte
mov edi, ediis only 1 clock cycle, 1 pipe). -
Should we need to hook (hijack and redirect) a function call, that 2 byte entry can be replaced with a jump. However only small relative jumps can be encoded in 2 bytes. Arbitrary jumps can be encoded with 5 bytes, hence the preceding padding.
-
Control flow is re-directed to a hook by patching in a small offset jump backwards (replacing the 2 byte NOP), the target of which is a patched-in jump to the hooked function address (replacement of the 5 byte NOP). This can all be done at runtime, hence "hot-patching".
-
2 byte writes are atomic given the register size of most architectures, which is why this works cleanly at runtime. A more generic approach would be overwriting the first 5 bytes in any function with a jump target, and making sure the target destination starts with the same 5 bytes that were overwritten (preserve the function prologue/functionality). But this introduces race conditions, if 5 bytes can't be written atomically than a thread may start executing instructions as they are being overwritten.
Extract the firmware image to find the CPIO archive:
binwalk -eM <firmware.img>
Extract the archive to current directory, then make changes:
cat 0.cpio | sudo cpio -i
Re-pack current directory as ASCII cpio archive (SVR4 with no CRC) (binwalk description):
find . | cpio -H newc -o > 0.cpio.modded
ASCII/hex/decimal:
>>> hex(70)
'0x46'
>>> 0x46
70
>>> chr(0x46)
'F'
>>> ord('F')
70Float to little-endian hex string:
>>> import struct
>>> float_val = 13.37
>>> struct.pack("<f", float_val).encode('hex')
'85eb5541'Little-endian hex string to float:
>>> hex_const_str = '85eb5541'
>>> struct.unpack('<f', hex_const_str.decode('hex'))[0]
13.369999885559082-
structlibrary docs here. - ASCII/hex/decimal table in man page (
man ascii).