|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# |
| 3 | +# Known-answer tests for the BNIL emulator. |
| 4 | +# |
| 5 | +# emulator_test.py and emulator_il_test.py both test in the small: a handful of |
| 6 | +# instructions, one assertion each. These tests instead emulate whole compiled |
| 7 | +# algorithms -- base64 and MD5 -- against published test vectors. |
| 8 | +# |
| 9 | +# That matters because the two styles fail differently. A per-instruction test |
| 10 | +# only catches a bug you already thought to write a case for; MD5's 64 rounds of |
| 11 | +# rotate-and-wrapping-add catch carry, rotate, masking and truncation errors |
| 12 | +# unconditionally, because a single wrong bit anywhere changes the digest |
| 13 | +# completely. |
| 14 | +# |
| 15 | +# The kernels are built freestanding (see kat/build.sh) with no libc and no |
| 16 | +# syscalls, so each entry point is a pure function over caller-supplied buffers |
| 17 | +# and the emulator core is what is under test, not the libc stub layer. The same |
| 18 | +# C is compiled for every architecture below, so one set of vectors validates |
| 19 | +# each lifter/emulator pair. |
| 20 | +# |
| 21 | +# Run headless with, e.g.: |
| 22 | +# |
| 23 | +# PYTHONPATH=<bn>/python python3 -m pytest emulator_kat_test.py |
| 24 | +# |
| 25 | +# Requires the `emulator` core plugin to be present in the Binary Ninja install. |
| 26 | + |
| 27 | +import hashlib |
| 28 | +import os |
| 29 | +import unittest |
| 30 | + |
| 31 | +import binaryninja |
| 32 | + |
| 33 | +try: |
| 34 | + from emulator import LLILEmulator, ILEmulatorStopReason |
| 35 | +except ImportError: |
| 36 | + from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason |
| 37 | + |
| 38 | + |
| 39 | +KAT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kat', 'prebuilt') |
| 40 | + |
| 41 | +# Emulator address space. The loaded object's own sections sit at 0x400000; these |
| 42 | +# regions are placed well clear of it. |
| 43 | +STACK_BASE = 0x7000_0000 |
| 44 | +STACK_SIZE = 0x10000 |
| 45 | +IN_BUF = 0x1000_0000 |
| 46 | +OUT_BUF = 0x2000_0000 |
| 47 | +BUF_SIZE = 0x1000 |
| 48 | + |
| 49 | +MAX_INSTRUCTIONS = 20_000_000 |
| 50 | + |
| 51 | + |
| 52 | +class KATBase: |
| 53 | + """Emulates the freestanding KAT kernels for one architecture.""" |
| 54 | + |
| 55 | + obj_name = None # set by subclasses |
| 56 | + |
| 57 | + @classmethod |
| 58 | + def setUpClass(cls): |
| 59 | + path = os.path.join(KAT_DIR, cls.obj_name) |
| 60 | + if not os.path.exists(path): |
| 61 | + raise unittest.SkipTest(f'missing KAT object {path}; run kat/build.sh') |
| 62 | + cls.bv = binaryninja.load(path) |
| 63 | + cls.bv.update_analysis_and_wait() |
| 64 | + |
| 65 | + def emulator_at(self, symbol): |
| 66 | + """Build an emulator positioned at ``symbol`` with a stack and buffers mapped.""" |
| 67 | + funcs = self.bv.get_functions_by_name(symbol) |
| 68 | + self.assertTrue(funcs, f'{symbol} not found in {self.obj_name}') |
| 69 | + |
| 70 | + emu = LLILEmulator(self.bv) |
| 71 | + |
| 72 | + # The emulator has its own address space and does not read the view, so the |
| 73 | + # object's sections (crucially .rodata, holding the base64 alphabet and the |
| 74 | + # MD5 constants) must be copied in explicitly. |
| 75 | + for section in self.bv.sections.values(): |
| 76 | + data = self.bv.read(section.start, section.length) |
| 77 | + if data: |
| 78 | + emu.map_memory(section.start, data, section.name) |
| 79 | + |
| 80 | + emu.map_memory(STACK_BASE, STACK_SIZE, 'stack') |
| 81 | + emu.map_memory(IN_BUF, BUF_SIZE, 'input') |
| 82 | + emu.map_memory(OUT_BUF, BUF_SIZE, 'output') |
| 83 | + |
| 84 | + self.assertTrue(emu.set_entry_point(funcs[0].start)) |
| 85 | + emu.set_max_instructions(MAX_INSTRUCTIONS) |
| 86 | + # Leave headroom on both sides so a push or a red zone stays mapped. |
| 87 | + emu.set_register(self.bv.arch.stack_pointer, STACK_BASE + STACK_SIZE // 2) |
| 88 | + return emu |
| 89 | + |
| 90 | + def run_to_halt(self, emu, what): |
| 91 | + emu.run() |
| 92 | + self.assertEqual( |
| 93 | + emu.stop_reason, ILEmulatorStopReason.ILEmulatorHalt, |
| 94 | + f'{what} did not run to completion on {self.obj_name}: ' |
| 95 | + f'stop_reason={emu.stop_reason} msg={emu.stop_message!r} ' |
| 96 | + f'at {emu.current_address:#x}') |
| 97 | + |
| 98 | + # -- kernels ------------------------------------------------------------ |
| 99 | + |
| 100 | + def b64_encode(self, data: bytes) -> bytes: |
| 101 | + emu = self.emulator_at('b64_encode') |
| 102 | + emu.write_memory(IN_BUF, data) |
| 103 | + emu.set_arguments([IN_BUF, len(data), OUT_BUF]) |
| 104 | + self.run_to_halt(emu, 'b64_encode') |
| 105 | + out = emu.read_memory(OUT_BUF, BUF_SIZE) |
| 106 | + return out[:out.index(b'\x00')] |
| 107 | + |
| 108 | + def md5(self, data: bytes) -> bytes: |
| 109 | + emu = self.emulator_at('md5') |
| 110 | + emu.write_memory(IN_BUF, data) |
| 111 | + emu.set_arguments([IN_BUF, len(data), OUT_BUF]) |
| 112 | + self.run_to_halt(emu, 'md5') |
| 113 | + return emu.read_memory(OUT_BUF, 16) |
| 114 | + |
| 115 | + # -- tests -------------------------------------------------------------- |
| 116 | + |
| 117 | + # RFC 4648 section 10. |
| 118 | + B64_VECTORS = [ |
| 119 | + (b'', b''), |
| 120 | + (b'f', b'Zg=='), |
| 121 | + (b'fo', b'Zm8='), |
| 122 | + (b'foo', b'Zm9v'), |
| 123 | + (b'foob', b'Zm9vYg=='), |
| 124 | + (b'fooba', b'Zm9vYmE='), |
| 125 | + (b'foobar', b'Zm9vYmFy'), |
| 126 | + ] |
| 127 | + |
| 128 | + def test_base64_rfc4648_vectors(self): |
| 129 | + for data, expected in self.B64_VECTORS: |
| 130 | + with self.subTest(input=data): |
| 131 | + self.assertEqual(self.b64_encode(data), expected) |
| 132 | + |
| 133 | + def test_base64_matches_reference(self): |
| 134 | + import base64 |
| 135 | + # Every residue of the 3-byte group, plus a payload spanning many groups. |
| 136 | + for n in (1, 2, 3, 4, 5, 16, 17, 18, 61): |
| 137 | + data = bytes((i * 7 + 11) & 0xff for i in range(n)) |
| 138 | + with self.subTest(length=n): |
| 139 | + self.assertEqual(self.b64_encode(data), base64.b64encode(data)) |
| 140 | + |
| 141 | + # RFC 1321 appendix A.5. |
| 142 | + MD5_VECTORS = [ |
| 143 | + (b'', 'd41d8cd98f00b204e9800998ecf8427e'), |
| 144 | + (b'a', '0cc175b9c0f1b6a831c399e269772661'), |
| 145 | + (b'abc', '900150983cd24fb0d6963f7d28e17f72'), |
| 146 | + (b'message digest', 'f96b697d7cb7938d525a2f31aaf161d0'), |
| 147 | + (b'abcdefghijklmnopqrstuvwxyz', 'c3fcd3d76192e4007dfb496cca67e13b'), |
| 148 | + (b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', |
| 149 | + 'd174ab98d277d9f5a5611c2c9f419d9f'), |
| 150 | + (b'1234567890' * 8, '57edf4a22be3c955ac49da2e2107b67a'), |
| 151 | + ] |
| 152 | + |
| 153 | + def test_md5_rfc1321_vectors(self): |
| 154 | + for data, expected in self.MD5_VECTORS: |
| 155 | + with self.subTest(input=data[:24]): |
| 156 | + self.assertEqual(self.md5(data).hex(), expected) |
| 157 | + |
| 158 | + def test_md5_block_boundaries(self): |
| 159 | + # 55/56 and 63/64 are where MD5's padding spills into an extra block -- |
| 160 | + # the cases most likely to expose a length or carry bug. |
| 161 | + for n in (54, 55, 56, 57, 63, 64, 65, 119, 120): |
| 162 | + data = bytes((i * 31 + 7) & 0xff for i in range(n)) |
| 163 | + with self.subTest(length=n): |
| 164 | + self.assertEqual(self.md5(data).hex(), hashlib.md5(data).hexdigest()) |
| 165 | + |
| 166 | + |
| 167 | +class X86_64KATTests(KATBase, unittest.TestCase): |
| 168 | + obj_name = 'kat-x86_64.o' |
| 169 | + |
| 170 | + |
| 171 | +class AArch64KATTests(KATBase, unittest.TestCase): |
| 172 | + obj_name = 'kat-aarch64.o' |
| 173 | + |
| 174 | + # These fail on a known emulator/lifter disagreement about shift counts, not on |
| 175 | + # anything wrong with the vectors -- base64 passes here, and both algorithms pass |
| 176 | + # on x86-64. |
| 177 | + # |
| 178 | + # When the rotate amount comes from a table, the aarch64 lifter emits |
| 179 | + # |
| 180 | + # w9 = neg.d(w8) ; -7 -> 0xfffffff9 |
| 181 | + # w8 = w0 << w8 |
| 182 | + # w9 = w0 u>> w9 ; relies on the ISA masking the count to 5 bits |
| 183 | + # w0 = w9 | w8 |
| 184 | + # |
| 185 | + # relying on ARM64 masking a register shift amount to the operand width. The |
| 186 | + # emulator shifts literally (see the LLIL_LSR/LLIL_LSL comment in |
| 187 | + # llilemulator.cpp), so the second shift yields 0 and every rotate collapses to |
| 188 | + # `x << c`. MD5 is 64 such rotates, so the digest is wrong for every input. |
| 189 | + # |
| 190 | + # Masking by operand width in the emulator would fix aarch64 but break 8/16-bit |
| 191 | + # x86 shifts, whose counts the x86 lifter has already masked to 5 bits and which |
| 192 | + # must yield 0 rather than wrapping -- so the resolution belongs in the aarch64 |
| 193 | + # lifter, which is out of tree. These expectations flip to unexpected successes |
| 194 | + # once that is settled. |
| 195 | + # Defined as wrappers rather than |
| 196 | + # test_x = unittest.expectedFailure(KATBase.test_x) |
| 197 | + # because expectedFailure marks the function object itself, which the base class |
| 198 | + # shares with the x86-64 subclass. |
| 199 | + @unittest.expectedFailure |
| 200 | + def test_md5_rfc1321_vectors(self): |
| 201 | + KATBase.test_md5_rfc1321_vectors(self) |
| 202 | + |
| 203 | + @unittest.expectedFailure |
| 204 | + def test_md5_block_boundaries(self): |
| 205 | + KATBase.test_md5_block_boundaries(self) |
| 206 | + |
| 207 | + |
| 208 | +if __name__ == '__main__': |
| 209 | + unittest.main() |
0 commit comments