Ledger-Donjon / rainbow

Makes Unicorn traces. Generic Side-Channel and Fault Injection simulator
GNU Lesser General Public License v3.0
172 stars 34 forks source link

Merge contiguous mapped zones #9

Closed protopyte closed 3 years ago

protopyte commented 3 years ago

I faced an issue during the Ledger CTF where a call to map_space ended up with boundaries in two distinct, contiguous, previously mapped spaces. The rainbow logic ends up trying to remap already mapped regions, which the underlying unicorn doesn't like.

$ cat diff
diff --git a/rainbow/rainbow.py b/rainbow/rainbow.py
index 4f78b1a..a1ecb89 100644
--- a/rainbow/rainbow.py
+++ b/rainbow/rainbow.py
@@ -77,9 +77,13 @@ class rainbowBase:
         self.sca_values_trace = []

     # convenience function
-    def map_space(self, a, b, verbose=False):
+    def map_space(self, a, b, verbose=True):
         """ Maps area into the unicorn emulator between a and b, or nothing if it was already mapped.
         Only completes missing portions if there is overlapping with a previously-mapped segment """
+        if verbose:
+          print()
+          print(f"map_space called with {a:x} {b-a:x}")
+          print(f"current maps:", ["%08X -> %08X" % (s[0], s[1]) for s in self.mapped_regions])
         if any(map(lambda x: a >= x[0] and b <= x[1], self.mapped_regions)):
             if verbose:
                 print(f"Did not map {a:x} {b-a:x} as it is already mapped.")

$ cat bug_rainbow_map_space.py
from rainbow.generics import rainbow_arm

emu = rainbow_arm()
emu.load("picohsm-speed-dating/firmware-mcu.elf")

$ python3 bug_rainbow_map_space.py

map_space called with affffe00 220
current maps: []
Mapping : affffc00 800

map_space called with 8000000 1268
current maps: ['AFFFFC00 -> B0000400']
Mapping : 8000000 1400

map_space called with 80016d3 12
current maps: ['AFFFFC00 -> B0000400', '08000000 -> 08001400']
Mapping : 8001400 400

map_space called with 80016e8 8
current maps: ['AFFFFC00 -> B0000400', '08000000 -> 08001400', '08001400 -> 08001800']
Did not map 80016e8 8 as it is already mapped.

map_space called with 8001268 27
current maps: ['AFFFFC00 -> B0000400', '08000000 -> 08001400', '08001400 -> 08001800']
Did not map 8001268 27 as it is already mapped.

map_space called with 800128f 444
current maps: ['AFFFFC00 -> B0000400', '08000000 -> 08001400', '08001400 -> 08001800']
Mapping : 8001400 400
Traceback (most recent call last):
  File "bug_rainbow_map_space.py", line 9, in <module>
    emu.load(target)
  File "[snip]/rainbow/rainbow/rainbow.py", line 195, in load
    return load_selector(filename, self, typ, verbose=verbose)
  File "[snip]/rainbow/rainbow/loaders/__init__.py", line 33, in load_selector
    return loader(filename, rainbow_instance, verbose=verbose)
  File "[snip]/rainbow/rainbow/loaders/elfloader.py", line 36, in elfloader
    emu.map_space(
  File "[snip]/rainbow/rainbow/rainbow.py", line 124, in map_space
    ret = self.emu.mem_map(base, size)
  File "/usr/local/lib/python3.8/dist-packages/unicorn/unicorn.py", line 448, in mem_map
    raise UcError(status)
unicorn.unicorn.UcError: Invalid memory mapping (UC_ERR_MAP)

My quick fix was to merge contiguous regions at each map_space call, but you might prefer to handle this straight upon appending a newly mapped region. Obfuscated code follows:

$ cat patch
diff --git a/rainbow/rainbow.py b/rainbow/rainbow.py
index 4f78b1a..948467d 100644
--- a/rainbow/rainbow.py
+++ b/rainbow/rainbow.py
@@ -89,19 +89,30 @@ class rainbowBase:
             return

         overlap = 0
+        to_unmap = []
         for r_start, r_end in self.mapped_regions:
             # check for overlaps
-            if a < r_start and r_start < b <= r_end:
+            # Full overlap
+            if a < r_start and r_end < b:
+                # Unicorn doesn't like mapping multiple times the same area
+                # Unmap before remapping a larger area
+                self.emu.mem_unmap(r_start, r_end-r_start)
+                to_unmap += [(r_start, r_end)]
+            # Partial high overlap
+            elif a < r_start and r_start < b <= r_end:
                 overlap = 1
                 aa = a
                 bb = r_start
                 break
+            # Partial low overlap
             elif r_end > a >= r_start and b > r_end:
                 overlap = 1
                 aa = r_end
                 bb = b
                 break

+        self.mapped_regions = [r for r in self.mapped_regions if r not in to_unmap]
+
         if overlap == 0:
             aa = a
             bb = b
@@ -120,7 +131,14 @@ class rainbowBase:
         ret = self.emu.mem_map(base, size)
         if ret is not None:
             print(ret)
-        self.mapped_regions.append((base, base + size))
+
+        # Merge contiguous intervals to avoid overlaps:
+        # - discard elements that appear both as a lower and upper interval boundary
+        # - as intervals don't overlap, sorted list contains alternating lower and upper boundaries
+        regions = sorted(list(set([t[0] for t in self.mapped_regions] + [base]) ^ set([t[1] for t in self.mapped_regions] + [base + size])))
+
+        # Rebuild mapped regions list
+        self.mapped_regions = list(zip(regions[::2], regions[1::2]))

     def __setitem__(self, inp, val):
         """ Sets a register, memory address or memory range to a value. Handles writing ints or bytes. 

$ cat test_patch.py
from rainbow.generics import rainbow_arm
import unicorn as uc

tests = [
          [
            (0x1000, 0x2000), 

            # Test: same map
            (0x1000, 0x2000), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: included map
            (0x1400, 0x1800), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: distinct map
            (0x5000, 0x6000), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: contiguous map above
            (0x2000, 0x3000), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: contiguous map below
            (0x800, 0x1000), 
          ],
          [
            (0x1000, 0x2000), 
            (0x3000, 0x4000), 

            # Test: contiguous map below and above
            (0x2000, 0x3000), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: overlapping below
            (0x1800, 0x3000), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: exact overlapping below
            (0x1800, 0x2000), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: overlapping above
            (0x800, 0x1800), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: exact overlapping above
            (0x1000, 0x1800), 
          ],
          [
            (0x1000, 0x2000), 

            # Test: inclusion of previous map
            (0x800, 0x2800), 
          ],
          [
            (0x1000, 0x2000), 
            (0x3000, 0x4000), 

            # Test: multiple inclusions of previous maps
            (0x800, 0x5000), 
          ],
          [
            (0x1000, 0x2000), 
            (0x3000, 0x4000), 

            # Test: inclusion and overlap
            (0x800, 0x3800), 
          ],
        ] 

for test in tests:
  emu = rainbow_arm()

  for t in test:
    try:
      print("Mapping:", "%04X -> %04X" % t)
      emu.map_space(*t)
      print(" Mapped:", ", ".join(["%04X -> %04X" % r for r in emu.mapped_regions if r[0] < 0x10000]))
    except uc.unicorn.UcError as e:
      print(">>> CRASHED! <<<")
  print("")
$ python3 test_patch.py
Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1400 -> 1800
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 5000 -> 6000
 Mapped: 1000 -> 2000, 5000 -> 6000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 2000 -> 3000
 Mapped: 1000 -> 3000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 0800 -> 1000
 Mapped: 0800 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 3000 -> 4000
 Mapped: 1000 -> 2000, 3000 -> 4000
Mapping: 2000 -> 3000
 Mapped: 1000 -> 4000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1800 -> 3000
 Mapped: 1000 -> 3000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1800 -> 2000
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 0800 -> 1800
 Mapped: 0800 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1000 -> 1800
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 0800 -> 2800
 Mapped: 0800 -> 2800

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 3000 -> 4000
 Mapped: 1000 -> 2000, 3000 -> 4000
Mapping: 0800 -> 5000
 Mapped: 0800 -> 5000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 3000 -> 4000
 Mapped: 1000 -> 2000, 3000 -> 4000
Mapping: 0800 -> 3800
 Mapped: 0800 -> 4000

Current results are as follows:

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1400 -> 1800
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 5000 -> 6000
 Mapped: 1000 -> 2000, 5000 -> 6000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 2000 -> 3000
 Mapped: 1000 -> 2000, 2000 -> 3000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 0800 -> 1000
 Mapped: 1000 -> 2000, 0800 -> 1000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 3000 -> 4000
 Mapped: 1000 -> 2000, 3000 -> 4000
Mapping: 2000 -> 3000
 Mapped: 1000 -> 2000, 3000 -> 4000, 2000 -> 3000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1800 -> 3000
 Mapped: 1000 -> 2000, 2000 -> 3000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1800 -> 2000
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 0800 -> 1800
 Mapped: 1000 -> 2000, 0800 -> 1000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 1000 -> 1800
 Mapped: 1000 -> 2000

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 0800 -> 2800
>>> CRASHED! <<<

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 3000 -> 4000
 Mapped: 1000 -> 2000, 3000 -> 4000
Mapping: 0800 -> 5000
>>> CRASHED! <<<

Mapping: 1000 -> 2000
 Mapped: 1000 -> 2000
Mapping: 3000 -> 4000
 Mapped: 1000 -> 2000, 3000 -> 4000
Mapping: 0800 -> 3800
>>> CRASHED! <<<
yhql commented 3 years ago

Thanks for the report! I added a fix, together with (most of) your tests :)