#!/usr/bin/env python3 # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # import sys import struct import unittest import platform sys.path.append("..") from qiling import Qiling from qiling.const import QL_INTERCEPT, QL_VERBOSE @unittest.skipUnless(platform.system() == 'Linux', 'kernel proxy requires Linux host') class KernelProxyTest(unittest.TestCase): """Tests for the hybrid kernel proxy (Phase 0).""" ROOTFS = "../examples/rootfs/x8664_linux" HELLO_BIN = "../examples/rootfs/x8664_linux/bin/x8664_hello" # ------------------------------------------------------------------------- # Proxy lifecycle # ------------------------------------------------------------------------- def test_proxy_start_stop(self): """Proxy starts and stops cleanly.""" from qiling.os.posix.kernel_proxy import KernelProxy ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) proxy = KernelProxy(ql) self.assertIsNotNone(proxy._process) self.assertIsNotNone(proxy._client) self.assertTrue(proxy._process.poll() is None) # still running proxy.stop() self.assertIsNone(proxy._process) self.assertIsNone(proxy._client) del ql def test_no_proxy_no_change(self): """Without proxy, Qiling behaves identically.""" from qiling.extensions import pipe ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) ql.os.stdout = pipe.SimpleOutStream(1) ql.run() self.assertIn(b"Hello", ql.os.stdout.read(1024)) del ql def test_proxy_attached_no_forwarding(self): """Proxy attached but no syscalls forwarded — no behavior change.""" from qiling.os.posix.kernel_proxy import KernelProxy from qiling.extensions import pipe ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) ql.os.stdout = pipe.SimpleOutStream(1) proxy = KernelProxy(ql) ql.run() proxy.stop() self.assertIn(b"Hello", ql.os.stdout.read(1024)) del ql # ------------------------------------------------------------------------- # Raw syscall forwarding (integer-only args) # ------------------------------------------------------------------------- def test_forward_getpid(self): """Forward getpid — proxy returns its own PID.""" from qiling.os.posix.kernel_proxy import KernelProxy ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) proxy = KernelProxy(ql) proxy.forward_syscall('getpid') results = [] def on_getpid_exit(ql, *args): results.append(args[-1]) ql.os.set_syscall('getpid', on_getpid_exit, QL_INTERCEPT.EXIT) ql.run() # if getpid was called, it should return the proxy's PID if results: self.assertEqual(results[0], proxy._process.pid) proxy.stop() del ql def test_forward_brk(self): """Forward brk to real kernel — binary still runs correctly.""" from qiling.os.posix.kernel_proxy import KernelProxy from qiling.extensions import pipe ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) ql.os.stdout = pipe.SimpleOutStream(1) proxy = KernelProxy(ql) proxy.forward_syscall('brk') brk_results = [] def on_brk_exit(ql, *args): brk_results.append(args[-1]) ql.os.set_syscall('brk', on_brk_exit, QL_INTERCEPT.EXIT) ql.run() proxy.stop() # brk is called during libc init self.assertGreater(len(brk_results), 0, "brk was never called") # binary should still produce correct output self.assertIn(b"Hello", ql.os.stdout.read(1024)) del ql # ------------------------------------------------------------------------- # FD-returning syscalls (returns_fd=True) # ------------------------------------------------------------------------- def test_forward_returns_fd(self): """forward_syscall with returns_fd=True creates ql_proxy_fd in FD table.""" from qiling.os.posix.kernel_proxy import KernelProxy from qiling.os.posix.kernel_proxy.proxy_fd import ql_proxy_fd ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) proxy = KernelProxy(ql) proxy.forward_syscall('eventfd2', returns_fd=True) # call the forwarder directly (simulates binary calling eventfd2) hook = ql.os.posix_syscall_hooks[QL_INTERCEPT.CALL].get('ql_syscall_eventfd2') self.assertIsNotNone(hook, "forwarder hook not registered") guest_fd = hook(ql, 0, 0) self.assertGreaterEqual(guest_fd, 0) fd_obj = ql.os.fd[guest_fd] self.assertIsInstance(fd_obj, ql_proxy_fd) # write and read through the proxy FD fd_obj.write(struct.pack('=0) and distinct self.assertGreaterEqual(rfd, 0) self.assertGreaterEqual(wfd, 0) self.assertNotEqual(rfd, wfd) # clean up the proxy-side FDs the kernel just gave us import os as _os _os.close(rfd) _os.close(wfd) proxy.stop() del ql def test_ptr_in_reads_guest_memory(self): """PtrIn buffer is copied from guest memory and the proxy sees the data. Uses write(int fd, const void *buf, size_t count) on stderr (fd 2). We can't easily inspect proxy's stderr, but a successful return value equal to count proves the data was forwarded — write would otherwise return -EFAULT for a bad pointer or short for less data. """ from qiling.os.posix.kernel_proxy import KernelProxy, PtrIn ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) proxy = KernelProxy(ql) proxy.forward_syscall('write', arg_types=('int', PtrIn(size=lambda a: a[2]), 'int')) addr = 0x800000 payload = b'kernel_proxy ptr_in roundtrip\n' ql.mem.map(addr, 0x1000) ql.mem.write(addr, payload) # forward stderr (fd 2 in the proxy is our subprocess's stderr, # which goes to the test runner — harmless) hook = ql.os.posix_syscall_hooks[QL_INTERCEPT.CALL]['ql_syscall_write'] retval = hook(ql, 2, addr, len(payload)) self.assertEqual(retval, len(payload)) proxy.stop() del ql def test_ptr_size_callable(self): """PtrIn/PtrOut accept a size callable that depends on other args.""" from qiling.os.posix.kernel_proxy import PtrIn, PtrOut ptr = PtrIn(size=lambda args: args[2] * 4) self.assertEqual(ptr.resolve((0, 0, 5)), 20) ptr2 = PtrOut(size=12) self.assertEqual(ptr2.resolve((0, 0, 0)), 12) # ------------------------------------------------------------------------- # Reference cycle (#4) # ------------------------------------------------------------------------- def test_no_reference_cycle_via_hook(self): """The forwarder closure holds only a weakref to KernelProxy, so the proxy can be garbage-collected once the user drops their reference, even though the hook is still registered on ql.os.""" import gc import weakref from qiling.os.posix.kernel_proxy import KernelProxy ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) proxy = KernelProxy(ql) proxy.forward_syscall('getpid') proxy.forward_syscall('eventfd2', returns_fd=True) wref = weakref.ref(proxy) proxy.stop() # tear down the subprocess but leave the hooks registered del proxy gc.collect() self.assertIsNone(wref(), "KernelProxy survived after stop()+del — closure must hold " "a strong ref (cycle), defeating the weakref design") del ql if __name__ == "__main__": unittest.main()