diff --git a/tests/test_assembler.nim b/tests/test_assembler.nim index 69ceb6e..bb12865 100644 --- a/tests/test_assembler.nim +++ b/tests/test_assembler.nim @@ -19,46 +19,52 @@ suite "Assembler Module Tests": test "assembleInstruction encodes NOP correctly": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("nop", archX64) - check bytes.len == 1 - check bytes[0] == 0x90'u8 + else: + let bytes = assembleInstruction("nop", archX64) + check bytes.len == 1 + check bytes[0] == 0x90'u8 test "assembleInstruction encodes RET correctly": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("ret", archX64) - check bytes.len == 1 - check bytes[0] == 0xC3'u8 + else: + let bytes = assembleInstruction("ret", archX64) + check bytes.len == 1 + check bytes[0] == 0xC3'u8 test "assembleInstruction returns empty seq for invalid instruction": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("notavalidinstruction xyz_!!!", archX64) - check bytes.len == 0 + else: + let bytes = assembleInstruction("notavalidinstruction xyz_!!!", archX64) + check bytes.len == 0 test "assembleBlock encodes multiple semicolon-separated instructions": if not isKeystoneAvailable(): skip() - let output = assembleBlock("nop; nop; ret", archX64) - check output.bytes.len == 3 - check output.statCount == 3 - check output.bytes[0] == 0x90'u8 - check output.bytes[1] == 0x90'u8 - check output.bytes[2] == 0xC3'u8 + else: + let output = assembleBlock("nop; nop; ret", archX64) + check output.bytes.len == 3 + check output.statCount == 3 + check output.bytes[0] == 0x90'u8 + check output.bytes[1] == 0x90'u8 + check output.bytes[2] == 0xC3'u8 test "assembleBlock returns empty output for invalid instructions": if not isKeystoneAvailable(): skip() - let output = assembleBlock("notvalid !!!", archX64) - check output.bytes.len == 0 - check output.statCount == 0 + else: + let output = assembleBlock("notvalid !!!", archX64) + check output.bytes.len == 0 + check output.statCount == 0 test "assembleInstruction handles x86 32-bit mode": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("nop", archX86) - check bytes.len == 1 - check bytes[0] == 0x90'u8 + else: + let bytes = assembleInstruction("nop", archX86) + check bytes.len == 1 + check bytes[0] == 0x90'u8 test "makeNops generates the correct number of NOP bytes": let nops = makeNops(4) diff --git a/tests/test_disassembler.nim b/tests/test_disassembler.nim index 668b85a..ac61625 100644 --- a/tests/test_disassembler.nim +++ b/tests/test_disassembler.nim @@ -6,13 +6,6 @@ # unconditionally. import unittest, disassembler, binary -# --------------------------------------------------------------------------- -# Helper: skip a test when Capstone is not installed. -# --------------------------------------------------------------------------- -template requireCapstone() = - if not isCapstoneAvailable(): - skip() - # --------------------------------------------------------------------------- # Suite: Capstone availability probe # --------------------------------------------------------------------------- @@ -34,59 +27,71 @@ suite "disassembleBytes": check result.len == 0 test "NOP byte (0x90) decodes as 'nop' on x64": - requireCapstone() - let data: seq[byte] = @[0x90'u8] - let instrs = disassembleBytes(data, 0x1000'u64, archX64) - check instrs.len == 1 - check instrs[0].mnemonic == "nop" - check instrs[0].address == 0x1000'u64 - check instrs[0].rawBytes == @[0x90'u8] + if not isCapstoneAvailable(): + skip() + else: + let data: seq[byte] = @[0x90'u8] + let instrs = disassembleBytes(data, 0x1000'u64, archX64) + check instrs.len == 1 + check instrs[0].mnemonic == "nop" + check instrs[0].address == 0x1000'u64 + check instrs[0].rawBytes == @[0x90'u8] test "RET byte (0xC3) decodes as 'ret' on x64": - requireCapstone() - let data: seq[byte] = @[0xC3'u8] - let instrs = disassembleBytes(data, 0x2000'u64, archX64) - check instrs.len == 1 - check instrs[0].mnemonic == "ret" - check instrs[0].address == 0x2000'u64 + if not isCapstoneAvailable(): + skip() + else: + let data: seq[byte] = @[0xC3'u8] + let instrs = disassembleBytes(data, 0x2000'u64, archX64) + check instrs.len == 1 + check instrs[0].mnemonic == "ret" + check instrs[0].address == 0x2000'u64 test "Function prologue bytes decode to push/mov on x64": - requireCapstone() - # 55 push rbp - # 48 89 E5 mov rbp, rsp - let data: seq[byte] = @[0x55'u8, 0x48'u8, 0x89'u8, 0xE5'u8] - let instrs = disassembleBytes(data, 0x4000'u64, archX64) - check instrs.len == 2 - check instrs[0].mnemonic == "push" - check instrs[0].address == 0x4000'u64 - check instrs[1].mnemonic == "mov" - check instrs[1].address == 0x4001'u64 + if not isCapstoneAvailable(): + skip() + else: + # 55 push rbp + # 48 89 E5 mov rbp, rsp + let data: seq[byte] = @[0x55'u8, 0x48'u8, 0x89'u8, 0xE5'u8] + let instrs = disassembleBytes(data, 0x4000'u64, archX64) + check instrs.len == 2 + check instrs[0].mnemonic == "push" + check instrs[0].address == 0x4000'u64 + check instrs[1].mnemonic == "mov" + check instrs[1].address == 0x4001'u64 test "Multiple instructions have sequential addresses": - requireCapstone() - # 90 = nop (1 byte), C3 = ret (1 byte) - let data: seq[byte] = @[0x90'u8, 0xC3'u8] - let instrs = disassembleBytes(data, 0x5000'u64, archX64) - check instrs.len == 2 - check instrs[0].address == 0x5000'u64 - check instrs[1].address == 0x5001'u64 + if not isCapstoneAvailable(): + skip() + else: + # 90 = nop (1 byte), C3 = ret (1 byte) + let data: seq[byte] = @[0x90'u8, 0xC3'u8] + let instrs = disassembleBytes(data, 0x5000'u64, archX64) + check instrs.len == 2 + check instrs[0].address == 0x5000'u64 + check instrs[1].address == 0x5001'u64 test "Invalid/garbage bytes return empty or partial result without crashing": - requireCapstone() - # 0xFF 0xFF is not a valid x64 instruction at this position; Capstone - # may decode it partially or return empty, but must not raise. - let data: seq[byte] = @[0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8] - let instrs = disassembleBytes(data, 0'u64, archX64) - # We only check it did not crash; result may be empty or partial. - check instrs.len >= 0 + if not isCapstoneAvailable(): + skip() + else: + # 0xFF 0xFF is not a valid x64 instruction at this position; Capstone + # may decode it partially or return empty, but must not raise. + let data: seq[byte] = @[0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8] + let instrs = disassembleBytes(data, 0'u64, archX64) + # We only check it did not crash; result may be empty or partial. + check instrs.len >= 0 test "Base address is reflected in instruction addresses": - requireCapstone() - let data: seq[byte] = @[0x90'u8] - let base = 0xDEAD0000'u64 - let instrs = disassembleBytes(data, base, archX64) - check instrs.len == 1 - check instrs[0].address == base + if not isCapstoneAvailable(): + skip() + else: + let data: seq[byte] = @[0x90'u8] + let base = 0xDEAD0000'u64 + let instrs = disassembleBytes(data, base, archX64) + check instrs.len == 1 + check instrs[0].address == base # --------------------------------------------------------------------------- # Suite: disassembleSection (pure-Nim paths that do not need Capstone) diff --git a/tests/test_emulator.nim b/tests/test_emulator.nim index e99a14b..5cfbfb3 100644 --- a/tests/test_emulator.nim +++ b/tests/test_emulator.nim @@ -35,9 +35,10 @@ suite "Emulator Module Tests": test "closeEmulator is safe to call twice on a real context": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - closeEmulator(ctx) - closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + closeEmulator(ctx) + closeEmulator(ctx) test "loadMemory returns false when engine is nil": var ctx = EmulatorContext() @@ -51,10 +52,11 @@ suite "Emulator Module Tests": test "loadMemory succeeds with valid data when Unicorn available": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - let data: seq[byte] = @[0x90'u8, 0x90'u8] - check ctx.loadMemory(0x00401000'u64, data) == true + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + let data: seq[byte] = @[0x90'u8, 0x90'u8] + check ctx.loadMemory(0x00401000'u64, data) == true test "readRegister returns 0 for nil engine": let ctx = EmulatorContext() @@ -83,86 +85,92 @@ suite "Emulator Module Tests": test "register write and read round-trip": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - check ctx.writeRegister(UC_X86_REG_RAX, 0xDEADBEEF'u64) == true - check ctx.readRegister(UC_X86_REG_RAX) == 0xDEADBEEF'u64 + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + check ctx.writeRegister(UC_X86_REG_RAX, 0xDEADBEEF'u64) == true + check ctx.readRegister(UC_X86_REG_RAX) == 0xDEADBEEF'u64 test "register write and read round-trip for RBX": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - check ctx.writeRegister(UC_X86_REG_RBX, 0x1234'u64) == true - check ctx.readRegister(UC_X86_REG_RBX) == 0x1234'u64 + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + check ctx.writeRegister(UC_X86_REG_RBX, 0x1234'u64) == true + check ctx.readRegister(UC_X86_REG_RBX) == 0x1234'u64 test "emulate a NOP sled executes without error": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - # Four NOP bytes. The emulator stops after maxInstructions = 4. - let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] - let base = 0x00401000'u64 - check ctx.loadMemory(base, code) == true + # Four NOP bytes. The emulator stops after maxInstructions = 4. + let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] + let base = 0x00401000'u64 + check ctx.loadMemory(base, code) == true - let res = ctx.emulateRange(base, base + uint64(code.len), 4) - check res.success == true + let res = ctx.emulateRange(base, base + uint64(code.len), 4) + check res.success == true test "emulate mov eax, 42 and read register result": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - # Encoding: b8 2a 00 00 00 (mov eax, 42) - let code: seq[byte] = @[0xb8'u8, 0x2a'u8, 0x00'u8, 0x00'u8, 0x00'u8] - let base = 0x00401000'u64 - check ctx.loadMemory(base, code) == true + # Encoding: b8 2a 00 00 00 (mov eax, 42) + let code: seq[byte] = @[0xb8'u8, 0x2a'u8, 0x00'u8, 0x00'u8, 0x00'u8] + let base = 0x00401000'u64 + check ctx.loadMemory(base, code) == true - # Execute exactly 1 instruction and verify EAX. - let res = ctx.emulateRange(base, base + uint64(code.len), 1) - check res.success == true - check ctx.readRegister(UC_X86_REG_EAX) == 42'u64 + # Execute exactly 1 instruction and verify EAX. + let res = ctx.emulateRange(base, base + uint64(code.len), 1) + check res.success == true + check ctx.readRegister(UC_X86_REG_EAX) == 42'u64 test "emulate xor rax, rax zeroes the register": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - check ctx.writeRegister(UC_X86_REG_RAX, 0xFFFF'u64) == true + check ctx.writeRegister(UC_X86_REG_RAX, 0xFFFF'u64) == true - # Encoding: 48 31 c0 (xor rax, rax) - let code: seq[byte] = @[0x48'u8, 0x31'u8, 0xc0'u8] - let base = 0x00401000'u64 - check ctx.loadMemory(base, code) == true + # Encoding: 48 31 c0 (xor rax, rax) + let code: seq[byte] = @[0x48'u8, 0x31'u8, 0xc0'u8] + let base = 0x00401000'u64 + check ctx.loadMemory(base, code) == true - let res = ctx.emulateRange(base, base + uint64(code.len), 1) - check res.success == true - check ctx.readRegister(UC_X86_REG_RAX) == 0'u64 + let res = ctx.emulateRange(base, base + uint64(code.len), 1) + check res.success == true + check ctx.readRegister(UC_X86_REG_RAX) == 0'u64 test "code hook fires for each instruction in a NOP sled": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - var counter: int = 0 + var counter: int = 0 - proc countHook(uc: ptr UcEngine, address: uint64, size: uint32, - userData: pointer) {.cdecl.} = - var p = cast[ptr int](userData) - p[] += 1 + proc countHook(uc: ptr UcEngine, address: uint64, size: uint32, + userData: pointer) {.cdecl.} = + var p = cast[ptr int](userData) + p[] += 1 - let base = 0x00401000'u64 - let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] - check ctx.loadMemory(base, code) == true - check ctx.addCodeHook(countHook, addr counter) == true + let base = 0x00401000'u64 + let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] + check ctx.loadMemory(base, code) == true + check ctx.addCodeHook(countHook, addr counter) == true - discard ctx.emulateRange(base, base + uint64(code.len), 4) - check counter == 4 + discard ctx.emulateRange(base, base + uint64(code.len), 4) + check counter == 4 test "loadBinary returns false when engine is nil": var ctx = EmulatorContext() @@ -172,11 +180,12 @@ suite "Emulator Module Tests": test "loadBinary returns false for unknown binary format": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - # parseBinary on a nonexistent file returns bfUnknown with no sections. - let info = parseBinary("nonexistent_binary_xyz.elf") - check ctx.loadBinary(info, ".text") == false + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + # parseBinary on a nonexistent file returns bfUnknown with no sections. + let info = parseBinary("nonexistent_binary_xyz.elf") + check ctx.loadBinary(info, ".text") == false test "isUnicornAvailable does not raise when library is missing": # Regardless of whether Unicorn is installed, the call must not raise. diff --git a/tests/test_patcher.nim b/tests/test_patcher.nim index 6b95386..a310368 100644 --- a/tests/test_patcher.nim +++ b/tests/test_patcher.nim @@ -121,16 +121,17 @@ suite "Patcher Module Tests": test "assembleAndPatch writes assembled bytes when Keystone available": if not isKeystoneAvailable(): skip() - let srcFile = "test_aap_src.bin" - let dstFile = "test_aap_dst.bin" - defer: - removeFile(srcFile) - removeFile(dstFile) - writeFile(srcFile, newString(8)) - check assembleAndPatch(srcFile, dstFile, 0, "nop", archX64) == true - let result = readFile(dstFile) - check result.len == 8 - check byte(result[0]) == 0x90'u8 + else: + let srcFile = "test_aap_src.bin" + let dstFile = "test_aap_dst.bin" + defer: + removeFile(srcFile) + removeFile(dstFile) + writeFile(srcFile, newString(8)) + check assembleAndPatch(srcFile, dstFile, 0, "nop", archX64) == true + let result = readFile(dstFile) + check result.len == 8 + check byte(result[0]) == 0x90'u8 suite "Emulation-Based Patch Testing": @@ -146,35 +147,39 @@ suite "Emulation-Based Patch Testing": test "testPatchInEmulator returns false for non-existent source file": if not isUnicornAvailable(): skip() - check testPatchInEmulator("no_such_binary_xyz.bin", 0, - @[0x90'u8], archX64) == false + else: + check testPatchInEmulator("no_such_binary_xyz.bin", 0, + @[0x90'u8], archX64) == false test "testPatchInEmulator returns false for empty patch bytes": if not isUnicornAvailable(): skip() - let srcFile = "test_emu_patch_src.bin" - defer: removeFile(srcFile) - writeFile(srcFile, newString(16)) - check testPatchInEmulator(srcFile, 0, @[], archX64) == false + else: + let srcFile = "test_emu_patch_src.bin" + defer: removeFile(srcFile) + writeFile(srcFile, newString(16)) + check testPatchInEmulator(srcFile, 0, @[], archX64) == false test "testPatchInEmulator returns false when offset is out of range": if not isUnicornAvailable(): skip() - let srcFile = "test_emu_oob.bin" - defer: removeFile(srcFile) - writeFile(srcFile, newString(4)) - check testPatchInEmulator(srcFile, 100, @[0x90'u8], archX64) == false + else: + let srcFile = "test_emu_oob.bin" + defer: removeFile(srcFile) + writeFile(srcFile, newString(4)) + check testPatchInEmulator(srcFile, 100, @[0x90'u8], archX64) == false test "testPatchInEmulator executes a NOP sled patch without error": if not isUnicornAvailable(): skip() - let srcFile = "test_emu_nop.bin" - defer: removeFile(srcFile) - # Write a page of NOP bytes so emulation has valid instructions to run. - var buf = newString(0x1000) - for i in 0 ..< 0x1000: - buf[i] = char(0x90'u8) - writeFile(srcFile, buf) - # Patch the first 4 bytes with NOPs and emulate from offset 0. - check testPatchInEmulator(srcFile, 0, @[0x90'u8, 0x90'u8, 0x90'u8, - 0x90'u8], archX64, 4) == true + else: + let srcFile = "test_emu_nop.bin" + defer: removeFile(srcFile) + # Write a page of NOP bytes so emulation has valid instructions to run. + var buf = newString(0x1000) + for i in 0 ..< 0x1000: + buf[i] = char(0x90'u8) + writeFile(srcFile, buf) + # Patch the first 4 bytes with NOPs and emulate from offset 0. + check testPatchInEmulator(srcFile, 0, @[0x90'u8, 0x90'u8, 0x90'u8, + 0x90'u8], archX64, 4) == true diff --git a/tests/test_runtime.nim b/tests/test_runtime.nim index 1c9c0c6..178e0ec 100644 --- a/tests/test_runtime.nim +++ b/tests/test_runtime.nim @@ -13,7 +13,7 @@ suite "Runtime Module - Platform": test "attachProcess returns failure on non-Linux": when not defined(linux): - let r = attachProcess(1) + let r = runtime.attachProcess(1) check r.success == false else: # On Linux this would actually attach; skip the negative test. @@ -21,31 +21,31 @@ suite "Runtime Module - Platform": test "detachProcess returns failure on non-Linux": when not defined(linux): - let r = detachProcess(1) + let r = runtime.detachProcess(1) check r.success == false else: check true test "patchProcessMemory returns failure for empty bytes on all platforms": - let r = patchProcessMemory(1, 0x1000'u64, @[]) + let r = runtime.patchProcessMemory(1, 0x1000'u64, @[]) check r.success == false test "injectBreakpoint returns failure on non-Linux": when not defined(linux): - let (_, r) = injectBreakpoint(1, 0x1000'u64) + let (_, r) = runtime.injectBreakpoint(1, 0x1000'u64) check r.success == false else: check true test "monitorSyscalls returns empty seq on non-Linux": when not defined(linux): - let events = monitorSyscalls(1, 5) + let events = runtime.monitorSyscalls(1, 5) check events.len == 0 else: check true test "disassembleAtAddress returns empty seq for zero count": - let instrs = disassembleAtAddress(1, 0x1000'u64, 0) + let instrs = runtime.disassembleAtAddress(1, 0x1000'u64, 0) check instrs.len == 0 # ---------------------------------------------------------------------------