Vakumat P80C552 - Ghidra, BinaryNinja, IDA or Cutter?
After getting a good firmware image, the next question is which program to use for the disassembly. The chip used is an extension of the 8052, so any program that supports this out of the box would make the work much easier. Lets check out the usual 4 programs.
Ghidra:
Ghidra has 3 options to select for chips in this family:
I tried all 3 and the result was the same as far as disassembly was concerned. The decompiler window varied a bit. Here the first few lines executed right after the reset.
And there are some clear errors. MOV FIFLG, #0xff should be MOV P4,#0xff for example. Many of the special function registers have incorrect or missing names. After fixing those, the disassembly is much better. The decompiler seems to make mistakes, though. I was thinking that maybe it's optimizing all the P1.5 calls and lumping the end result together into a single assignment, but then it should also remove all the nop() calls. I could not find any option in Ghidra to get a better result. I submitted a bug report. So hopefully this will be fixed or maybe I get a beat-down for stupidly not using some sort of decompiler option...
https://github.com/NationalSecurityAgency/ghidra/issues/3061
Binary Ninja:
Binary Ninja does not support this chip family out of the box. After asking around in the forum, I was pointed to the Intel 8051 Family Architecture Plugin by amtal:
https://github.com/amtal/i8051
The plugin is still in development and does not support my chip out of the box, but amtal was awesome and helped me out with a custom loader script for this firmware image:
https://github.com/amtal/i8051/pull/7
from binaryninja.types import Symbol
from binaryninja.enums import SymbolType, SegmentFlag, SectionSemantics
from .. import mem
from ..binaryview import Family8051View
class UnkPhilips(Family8051View):
"""
"""
name = "Philips? 80C51"
long_name = "Unknown Philips? 80C51"
xram_size = 0x10000
@classmethod
def is_valid_for_data(self, data):
return data.read(0xBD70, 11) == b'VACUMAT 500'
def perform_get_entry_point(self):
return 0
def load_memory(self):
super().load_memory()
seg_f = SegmentFlag
r__ = seg_f.SegmentReadable
rw_ = seg_f.SegmentReadable | seg_f.SegmentWritable
r_x = (seg_f.SegmentReadable | seg_f.SegmentExecutable |
seg_f.SegmentContainsCode)
self.add_auto_segment(mem.CODE+0x0000, 0xBD70,
0x0000, 0xBD70, r_x)
self.add_auto_segment(mem.CODE+0xBD70, 0x10000-0xBD70,
0xBD70, 0x10000-0xBD70, r__)
sem_rod = SectionSemantics.ReadOnlyDataSectionSemantics
self.add_auto_section('.tables',
mem.CODE + 0x36a0, 0x5e3e - 0x36a0, sem_rod)
def load_symbols(self):
super().load_symbols()
def isr(name, ea):
self.define_auto_symbol(Symbol(SymbolType.FunctionSymbol,
mem.CODE+ea, name))
self.add_function(mem.CODE+ea)
isr('isr_ext0', 0)
isr('isr_timer_ctr_0', 0x03)
isr('isr_unknown_0', 0x0b)
isr('isr_unknown_1', 0x0e)
isr('isr_ext1', 0x13)
def load_patches(self):
super().load_patches()
UnkPhilips.register()
and some other super-helpful information. I then got a list of where the registers are mapped to:
And since this plugin supports 8051, as in Ghidra P4 does not have the right name, though in this case it's obvious that the name is missing.
But, no problem, as we can easily change the names in both programs. Both programs produce decompiled output, but one has to be careful as it's sometimes wrong. On to the next program.
amtal later gave some extra helpful patches:
diff --git a/lowlevelil.py b/lowlevelil.py
index 6242953..1670da2 100644
--- a/lowlevelil.py
+++ b/lowlevelil.py
@@ -212,9 +212,9 @@ def low_level_il(size, name, ops):
sz = 1 if ops == ['A'] else 0
def _tmp():
def clr(il,vs,ea):
- w(ops[0], il, il.const(sz, 0), 0 if is_reg else vs[0])
+ w(ops[0], il, il.const(sz, 0) if is_reg else 0, 0 if is_reg else vs[0])
def setb(il,vs,ea):
- w(ops[0], il, il.const(sz,1), 0 if is_reg else vs[0])
+ w(ops[0], il, il.const(sz, 1) if is_reg else 1, 0 if is_reg else vs[0])
def cpl(il,vs,ea):
v = 0 if is_reg else vs[0]
val = il.neg_expr(sz, r(ops[0], il, v))
@@ -359,8 +359,15 @@ def w(kind, il, val, v=0):
return il.append(il.set_reg(1, mem.regs[byte], il.or_expr(1, src, mask)))
# TODO sketchy bit-write endianness
addr = il.const_pointer(6, byte) # should be properly mapped by ana
- mask = il.shift_left(1, il.const(1, 1), il.const(1, bit))
- val = il.or_expr(1, il.load(1, addr), mask) # <- also only sets, never clears :|
+ if val == 1:
+ mask = il.const(1, 1 << bit)
+ val = il.or_expr(1, il.load(1, addr), mask)
+ elif val == 0:
+ mask = il.const(1, (1 << bit) ^ 0xff)
+ val = il.and_expr(1, il.load(1, addr), mask)
+ else:
+ mask = il.shift_left(1, il.const(1, 1), il.const(1, bit))
+ val = il.or_expr(1, il.load(1, addr), mask) # <- also only sets, never clears :|
return il.append(il.store(1, addr, val))
if kind.startswith('R') or kind in ['A', 'B']:
return il.append(il.set_reg(1, kind, val))
and
"Because it's not (afaict) using register banks you can also apply this:"
diff --git a/lowlevelil.py b/lowlevelil.py
index 1670da2..a67b950 100644
--- a/lowlevelil.py
+++ b/lowlevelil.py
@@ -284,6 +284,7 @@ def low_level_il(size, name, ops):
return unimpl
+PRETEND_RBANK_0 = True
def r(kind, il, v=0):
"""
@@ -304,6 +305,8 @@ def r(kind, il, v=0):
if kind == 'data addr':
if v in mem.regs:
return il.reg(1, mem.regs[v])
+ if v in range(mem.IRAM, mem.IRAM+8) and PRETEND_RBANK_0:
+ return il.reg(1, f'R{v - mem.IRAM}')
# TODO: overlay PSW as register? how to compute from flags?
return il.load(1, il.const_pointer(6, v))
if kind.endswith('bit addr'): # cosmetic / prefix, optional
@@ -346,6 +349,8 @@ def w(kind, il, val, v=0):
if kind == 'data addr':
if v in mem.regs:
return il.append(il.set_reg(1, mem.regs[v], val)) # aa5b good test aa68
+ if v in range(mem.IRAM, mem.IRAM+8) and PRETEND_RBANK_0:
+ return il.append(il.set_reg(1, f'R{v - mem.IRAM}', val))
# TODO: overlay PSW as register? how to compute from flags?
return il.append(il.store(1, il.const_pointer(6, v), val))
if kind.endswith('bit addr'): # cosmetic / prefix, optional
IDA:
IDA shows two varieties of 8051
But selecting either one shows the same list of sub-options
Going through the options sometimes results in only registers with generic names
Or sometimes in partial name resolution with helpful comments.
Or sometimes incorrect register names, like in Ghidra
But there is not an option that covers our particular Philips chip. IDA does not have any decompilation at all for this chip family.
Cutter:
Cutter offers two options for the 8051 family
The first produces some basic disassembly with correct register names or register address if the name is not known. The second produces garbage.
Like in IDA, this processor is not supported by the Cutter decompiler.
The SFR at 0x92:
One thing in common for all the tools is that they don't know anything about a SFR at address 0x92, but there is an access to it in the code. Neither does the datasheet for this chip mention this register: https://www.nxp.com/docs/en/data-sheet/80C552_83C552.pdf
It will be interesting to see how important this is for the front panel communication.
I expect to be using a combination of Binary Ninja and Ghidra, which has sort of become my go-to approach lately....
References:
80C51 FAMILY DERIVATIVES, 8XC552/562 overview - https://www.keil.com/dd/docs/datashts/philips/8xc5x2_ov.pdf
Intel 8051 Family Architecture Plugin - https://github.com/amtal/i8051
80C552/83C552 Datasheet - https://www.nxp.com/docs/en/data-sheet/80C552_83C552.pdf