Repository: angr/angr

Issue:
display bug in block.pp()
`block.pp()` does not show instructions correctly in mips
output from angr:
~~~
3fe364e4  sw      $ra, [$sp]
3fe364e8  addiu   $v0, $zero, 0x2
3fe364ec  move    $t9, $a0
3fe364f0  sw      $v0, [$sp]
3fe364f4  jalr    $t9
3fe364f8  addiu   $a0, $sp, 0x18
~~~
output from gdb
~~~
   0x3fe4d4e4 <xdr_free+4>:	sw	ra,52(sp)
   0x3fe4d4e8 <xdr_free+8>:	li	v0,2
   0x3fe4d4ec <xdr_free+12>:	move	t9,a0
   0x3fe4d4f0 <xdr_free+16>:	sw	v0,24(sp)
   0x3fe4d4f4 <xdr_free+20>:	jalr	t9
   0x3fe4d4f8 <xdr_free+24>:	addiu	a0,sp,24
~~~


Discussion:
binary: https://gist.github.com/Kyle-Kyle/075165a81e046978c3e01502d59536dd
^ the offset to `sp`  is missing
Someone brought up this issue in the angr slack, which I believe means we should try to prioritize it. 
I have a branch that solves this issue. I'll try to finish it today.

Resolving patch:
diff --git a/angr/analyses/disassembly.py b/angr/analyses/disassembly.py
--- a/angr/analyses/disassembly.py
+++ b/angr/analyses/disassembly.py
@@ -600,6 +600,17 @@ def __init__(self, op_num, children, parentinsn):
         self.segment_selector = None
         self.prefix = [ ]
         self.values = [ ]
+        self.offset = [ ]
+        # offset_location
+        # - prefix: -0xff00($gp)
+        # - before_value: 0xff00+rax
+        # - after_value: rax+0xff00
+        self.offset_location = "after_value"
+        # values_style
+        # - square: [rax+0x10]
+        # - curly: {rax+0x10}
+        # - paren: (rax+0x10)
+        self.values_style = "square"
 
         try:
             if '[' in self.children:
@@ -644,6 +655,9 @@ def _parse_memop_squarebracket(self):
         self.values = self.children[square_bracket_pos + 1: len(self.children) - 1]
 
     def _parse_memop_paren(self):
+        offset = [ ]
+        self.values_style = "paren"
+
         if self.children[0] != '(':
             try:
                 paren_pos = self.children.index('(')
@@ -651,7 +665,14 @@ def _parse_memop_paren(self):
                 raise
 
             if all(isinstance(item, str) for item in self.children[:paren_pos]):
+                # parse prefix
                 self.prefix = self.children[ : paren_pos]
+            elif all(isinstance(item, Value) for item in self.children[:paren_pos]):
+                # parse offset
+                # force each piece to be rendered with its sign (+/-)
+                offset += self.children[:paren_pos]
+                # offset appears before the left parenthesis
+                self.offset_location = "prefix"
 
         else:
             paren_pos = 0
@@ -659,13 +680,14 @@ def _parse_memop_paren(self):
             self.segment_selector = None
 
         self.values = self.children[paren_pos + 1 : len(self.children) - 1]
+        self.offset = offset
 
     def _render(self, formatting):
         if self.prefix is None:
             # we failed in parsing. use the default rendering
             return super(MemoryOperand, self)._render(formatting)
         else:
-            values_style = "square"
+            values_style = self.values_style
             show_prefix = True
             custom_values_str = None
 
@@ -691,15 +713,34 @@ def _render(self, formatting):
                     x.render(formatting)[0] if not isinstance(x, (bytes, str)) else x for x in self.values
                 )
 
+            if values_style == "curly":
+                left_paren, right_paren = "{", "}"
+            elif values_style == "paren":
+                left_paren, right_paren = "(", ")"
+            else:  # square
+                left_paren, right_paren = "[", "]"
+
+            if self.offset:
+                offset_str = "".join(
+                    x.render(formatting)[0] if not isinstance(x, (bytes, str)) else x for x in self.offset
+                )
+
+                # combine values and offsets according to self.offset_location
+                if self.offset_location == "prefix":
+                    value_str = ''.join([offset_str, left_paren, value_str, right_paren])
+                elif self.offset_location == "before_value":
+                    value_str = ''.join([left_paren, offset_str, value_str, right_paren])
+                else:  # after_value
+                    value_str = ''.join([left_paren, value_str, offset_str, right_paren])
+            else:
+                value_str = left_paren + value_str + right_paren
+
             segment_selector_str = "" if self.segment_selector is None else self.segment_selector
 
             if segment_selector_str and prefix_str:
                 prefix_str += ' '
 
-            if values_style == 'curly':
-                return [ '%s%s{%s}' % (prefix_str, segment_selector_str, value_str) ]
-            else:
-                return [ '%s%s[%s]' % (prefix_str, segment_selector_str, value_str) ]
+            return [ '%s%s%s' % (prefix_str, segment_selector_str, value_str) ]
 
 
 class OperandPiece(DisassemblyPiece): # pylint: disable=abstract-method
@@ -967,7 +1008,7 @@ def parse_block(self, block: BlockNode) -> None:
             self._add_block_ir_to_results(block, b.vex)
 
     def render(self, formatting=None, show_edges: bool = True, show_addresses: bool = True,
-               show_bytes: bool = False, ascii_only: Optional[bool] = None) -> str:
+               show_bytes: bool = False, ascii_only: Optional[bool] = None, color: bool = True) -> str:
         """
         Render the disassembly to a string, with optional edges and addresses.
 
@@ -989,7 +1030,7 @@ def render(self, formatting=None, show_edges: bool = True, show_addresses: bool
                     MemoryOperand:   'yellow',
                     Comment:         'gray',
                     Hook:            'green',
-                } if ansi_color_enabled else {},
+                } if ansi_color_enabled and color else {},
                 'format_callback': lambda item, s: ansi_color(s, formatting['colors'].get(type(item), None))
             }
 


Test patch:
diff --git a/tests/test_disassembly.py b/tests/test_disassembly.py
new file mode 100644
--- /dev/null
+++ b/tests/test_disassembly.py
@@ -0,0 +1,57 @@
+# pylint:disable=missing-class-docstring,no-self-use
+from unittest import TestCase, main
+
+import angr
+from angr.analyses import Disassembly
+from angr.analyses.disassembly import MemoryOperand
+
+
+class TestDisassembly(TestCase):
+    def test_mips32_missing_offset_in_instructions(self):
+        proj = angr.load_shellcode(b"\x8f\xbc\x00\x10"
+                                   b"\x02\x20\x30\x21"
+                                   b"\x8F\x85\x80\x28"
+                                   b"\x8F\x99\x81\x20"
+                                   b"\x02\x40\x38\x21"
+                                   b"\x24\xA5\x5E\x38"
+                                   b"\x03\x20\xF8\x09"
+                                   b"\x24\x04\x00\x02",
+                                   "MIPS32",
+                                   0)
+        # 0x0:    lw      $gp, 0x10($sp)
+        # 0x4:    move    $a2, $s1
+        # 0x8:    lw      $a1, -0x7fd8($gp)
+        # 0xc:    lw      $t9, -0x7ee0($gp)
+        # 0x10:   move    $a3, $s2
+        # 0x14:   addiu   $a1, $a1, 0x5e38
+        # 0x18:   jalr    $t9
+        # 0x1c:   addiu   $a0, $zero, 2
+
+        block = proj.factory.block(0)
+        disass = proj.analyses[Disassembly].prep()(
+            ranges=[(block.addr, block.addr + block.size)]
+        )
+        result = disass.raw_result
+        assert len(result) == 10, f"Incorrect number of instructions ({len(result)})"
+
+        ins = result[4]
+        operand_1 = ins.operands[1]
+        assert isinstance(operand_1, MemoryOperand)
+        assert len(operand_1.children) == 4
+        assert len(operand_1.values) == 1
+        assert len(operand_1.offset) == 1
+        assert operand_1.offset_location == "prefix"
+        rendered = disass.render(color=False)
+        assert rendered == """   _start:
+0  lw      $gp, 0x10($sp)
+4  move    $a2, $s1
+8  lw      $a1, -0x7fd8($gp)
+c  lw      $t9, -0x7ee0($gp)
+10  move    $a3, $s2
+14  addiu   $a1, $a1, 0x5e38
+18  jalr    $t9
+1c  addiu   $a0, $zero, 0x2"""
+
+
+if __name__ == "__main__":
+    main()