Skip to content

Basic Usage

xwings edited this page Jul 6, 2025 · 2 revisions

Basic Usage

Essential patterns and techniques for using Qiling Framework effectively.

Core Concepts

Binary Emulation

Qiling emulates binaries by providing a virtual environment that mimics the target operating system and architecture.

from qiling import Qiling

# Basic binary emulation
ql = Qiling(['path/to/binary'], 'path/to/rootfs')
ql.run()

Shellcode Emulation

Emulate raw shellcode without a full binary:

from qiling import Qiling
from qiling.const import QL_ARCH, QL_OS

# x86_64 Linux shellcode
shellcode = b"\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\xff\xc0\x48\x89\xc7\x48\xff\xc0\x0f\x05"

ql = Qiling(code=shellcode, 
           archtype=QL_ARCH.X8664, 
           ostype=QL_OS.LINUX)
ql.run()

Memory Operations

Reading and Writing Memory

from qiling import Qiling

ql = Qiling(['binary'], 'rootfs')

# Read memory
data = ql.mem.read(0x1000, 100)  # Read 100 bytes from 0x1000

# Write memory
ql.mem.write(0x1000, b"Hello World!")

# Read/write pointers
pointer_value = ql.mem.read_ptr(0x2000)
ql.mem.write_ptr(0x2000, 0x12345678)

# Read null-terminated strings
string_data = ql.mem.string(0x3000)

Memory Mapping

from unicorn import UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC

# Map memory region
ql.mem.map(0x10000000, 0x1000, 
          perms=UC_PROT_READ | UC_PROT_WRITE, 
          info="custom_region")

# Map memory anywhere
addr = ql.mem.map_anywhere(0x1000, info="dynamic_region")

# Unmap memory
ql.mem.unmap(0x10000000, 0x1000)

# Check if memory is mapped
if ql.mem.is_mapped(0x10000000, 0x1000):
    print("Memory is mapped")

Memory Information

# Get memory map information
map_info = ql.mem.get_mapinfo()
for start, end, perms, label, container in map_info:
    print(f"0x{start:08x}-0x{end:08x} {label}")

# Get formatted memory map
print(ql.mem.get_formatted_mapinfo())

# Search for patterns
for addr in ql.mem.search(b"password"):
    print(f"Found pattern at 0x{addr:x}")

Register Operations

Reading and Writing Registers

# Read registers (x86_64 example)
rax_value = ql.arch.regs.rax
rip_value = ql.arch.regs.rip

# Write registers
ql.arch.regs.rax = 0x12345678
ql.arch.regs.rip = 0x401000

# Generic register access
value = ql.arch.regs.read("rax")
ql.arch.regs.write("rax", 0x87654321)

# Get all registers
all_regs = ql.arch.regs.save()
print(f"RAX: 0x{all_regs['rax']:x}")

Architecture-Specific Registers

# x86/x86_64
ql.arch.regs.eax    # 32-bit
ql.arch.regs.rax    # 64-bit
ql.arch.regs.esp    # Stack pointer (32-bit)
ql.arch.regs.rsp    # Stack pointer (64-bit)

# ARM
ql.arch.regs.r0     # General purpose
ql.arch.regs.sp     # Stack pointer
ql.arch.regs.lr     # Link register
ql.arch.regs.pc     # Program counter

# ARM64
ql.arch.regs.x0     # 64-bit general purpose
ql.arch.regs.w0     # 32-bit view of x0
ql.arch.regs.sp     # Stack pointer

Hook System Basics

Code Hooks

Monitor instruction execution:

def code_hook(ql, address, size):
    print(f"Executing instruction at 0x{address:x}, size: {size}")

# Hook all instructions
ql.hook_code(code_hook)

# Hook specific address
def specific_hook(ql):
    print(f"Hit specific address: 0x{ql.arch.regs.rip:x}")

ql.hook_address(specific_hook, 0x401000)

Memory Hooks

Monitor memory access:

def mem_read_hook(ql, access, address, size, value):
    print(f"Memory read: 0x{address:x} -> 0x{value:x}")

def mem_write_hook(ql, access, address, size, value):
    print(f"Memory write: 0x{address:x} = 0x{value:x}")

ql.hook_mem_read(mem_read_hook)
ql.hook_mem_write(mem_write_hook)

API Hooks

Intercept function calls:

def custom_malloc(ql, size):
    """Custom malloc implementation"""
    print(f"malloc({size}) called")
    # Allocate memory using Qiling's heap
    addr = ql.os.heap.alloc(size)
    print(f"Allocated at 0x{addr:x}")
    return addr

# Hook malloc function
ql.set_api('malloc', custom_malloc)

Stack Operations

Stack Manipulation

# Push values to stack
ql.stack_push(0x12345678)
ql.stack_push(b"Hello")

# Pop values from stack
value = ql.stack_pop()

# Read/write at stack offsets
data = ql.stack_read(8)        # Read 8 bytes above stack pointer
ql.stack_write(8, b"data")     # Write at offset

File System Operations

File System Mapping

# Map host paths to guest paths
ql.add_fs_mapper("/tmp", "./sandbox")
ql.add_fs_mapper("/etc/passwd", "./fake_passwd")

# Remove mappings
ql.remove_fs_mapper("/tmp")

Standard I/O

import sys
from io import StringIO

# Redirect stdout to capture output
captured_output = StringIO()
ql = Qiling(['binary'], 'rootfs', stdout=captured_output)
ql.run()

# Get captured output
output = captured_output.getvalue()
print(f"Program output: {output}")

# Provide custom stdin
input_data = StringIO("input data\n")
ql = Qiling(['binary'], 'rootfs', stdin=input_data)
ql.run()

Environment and Arguments

Command Line Arguments

# Pass arguments to the emulated binary
argv = ['binary_name', '--flag', 'value', 'input.txt']
ql = Qiling(argv, 'rootfs')
ql.run()

Environment Variables

# Set environment variables
env = {
    'PATH': '/bin:/usr/bin',
    'HOME': '/home/user',
    'LANG': 'en_US.UTF-8',
    'DEBUG': '1'
}

ql = Qiling(['binary'], 'rootfs', env=env)
ql.run()

Execution Control

Execution Constraints

# Run with timeout (microseconds)
ql.run(timeout=1000000)  # 1 second

# Run with instruction count limit
ql.run(count=1000)  # Max 1000 instructions

# Run between specific addresses
ql.run(begin=0x401000, end=0x402000)

Stopping Execution

def stop_hook(ql):
    print("Stopping execution")
    ql.emu_stop()  # Stop emulation

ql.hook_address(stop_hook, 0x401234)
ql.run()

State Management

Saving and Restoring State

# Save complete state
saved_state = ql.save()

# Modify state and run
ql.arch.regs.rax = 0x999
ql.run()

# Restore previous state
ql.restore(saved_state)
ql.run()  # Runs from saved state

Snapshots

# Save with custom name
ql.save(name="checkpoint1")

# Restore specific snapshot
ql.restore(saved_states, snapshot="checkpoint1")

Debugging Integration

Built-in Debugger (QDB)

# Enable QDB debugger
ql.debugger = "qdb"
ql.run()

# QDB will provide interactive debugging
# Commands: run, step, continue, breakpoint, info reg, x/10x $rsp

GDB Integration

# Start GDB server
ql.debugger = "gdb:localhost:9999"
ql.run()

# In another terminal:
# gdb -ex "target remote localhost:9999"

Error Handling

Exception Handling

from qiling.exception import *

try:
    ql = Qiling(['binary'], 'rootfs')
    ql.run()
except QlErrorFileNotFound:
    print("Binary file not found")
except QlErrorArch:
    print("Architecture not supported")
except QlErrorOS:
    print("Operating system not supported")
except QlErrorException as e:
    print(f"Qiling error: {e}")

Memory Exception Handling

def mem_invalid_hook(ql, access, address, size, value):
    print(f"Invalid memory access at 0x{address:x}")
    # Map the page to continue execution
    page_addr = address & 0xfffff000
    ql.mem.map(page_addr, 0x1000)
    print(f"Mapped page at 0x{page_addr:x}")

ql.hook_mem_invalid(mem_invalid_hook)

Configuration

Verbosity Control

from qiling.const import QL_VERBOSE

# Different verbosity levels
ql = Qiling(['binary'], 'rootfs', verbose=QL_VERBOSE.OFF)      # Silent
ql = Qiling(['binary'], 'rootfs', verbose=QL_VERBOSE.DEFAULT)  # Basic
ql = Qiling(['binary'], 'rootfs', verbose=QL_VERBOSE.DEBUG)    # Detailed
ql = Qiling(['binary'], 'rootfs', verbose=QL_VERBOSE.DISASM)   # With disassembly
ql = Qiling(['binary'], 'rootfs', verbose=QL_VERBOSE.DUMP)     # With memory dumps

Multithread Support

# Enable multithread support for threaded applications
ql = Qiling(['threaded_binary'], 'rootfs', multithread=True)
ql.run()

Library Caching

# Enable library caching for faster Windows emulation
ql = Qiling(['windows_binary.exe'], 'rootfs_windows', libcache=True)
ql.run()

Common Patterns

API Monitoring

api_calls = []

def monitor_api(ql, *args, **kwargs):
    """Generic API monitor"""
    func_name = kwargs.get('func_name', 'unknown')
    api_calls.append({
        'function': func_name,
        'args': args,
        'pc': ql.arch.regs.rip
    })
    print(f"API call: {func_name}")
    return None  # Call original function

# Hook multiple APIs
apis_to_monitor = ['CreateFileW', 'ReadFile', 'WriteFile', 'CloseHandle']
for api in apis_to_monitor:
    ql.set_api(api, lambda ql, *args, **kwargs: monitor_api(ql, *args, func_name=api, **kwargs))

ql.run()

# Print API call summary
print(f"Total API calls: {len(api_calls)}")
for call in api_calls:
    print(f"  {call['function']} at 0x{call['pc']:x}")

Memory Pattern Analysis

def analyze_memory_patterns(ql):
    """Analyze common memory patterns"""
    patterns = {
        'strings': [],
        'urls': [],
        'registry_keys': []
    }
    
    # Search for different patterns
    for addr in ql.mem.search(b"http"):
        try:
            url = ql.mem.string(addr)
            patterns['urls'].append((addr, url))
        except:
            pass
    
    for addr in ql.mem.search(b"HKEY_"):
        try:
            key = ql.mem.string(addr)
            patterns['registry_keys'].append((addr, key))
        except:
            pass
    
    return patterns

# Run analysis after emulation
ql.run()
patterns = analyze_memory_patterns(ql)
print(f"Found {len(patterns['urls'])} URLs")
print(f"Found {len(patterns['registry_keys'])} registry keys")

Execution Tracing

class ExecutionTracer:
    def __init__(self):
        self.trace = []
        self.call_stack = []
    
    def trace_instruction(self, ql, address, size):
        self.trace.append({
            'address': address,
            'instruction': ql.mem.read(address, size),
            'registers': ql.arch.regs.save()
        })
    
    def trace_function_call(self, ql, address, size):
        # Simple function call detection (CALL instruction)
        instruction = ql.mem.read(address, size)
        if instruction[0] == 0xe8:  # CALL rel32
            self.call_stack.append(address)
    
    def get_trace_summary(self):
        return {
            'instruction_count': len(self.trace),
            'unique_addresses': len(set(t['address'] for t in self.trace)),
            'call_depth': len(self.call_stack)
        }

tracer = ExecutionTracer()
ql.hook_code(tracer.trace_instruction)
ql.hook_code(tracer.trace_function_call)

ql.run()
summary = tracer.get_trace_summary()
print(f"Executed {summary['instruction_count']} instructions")
print(f"Visited {summary['unique_addresses']} unique addresses")

Best Practices

Performance Optimization

  1. Use appropriate verbosity levels - Only enable detailed output when needed
  2. Enable library caching - For Windows binaries with many DLL loads
  3. Use targeted hooks - Hook specific addresses instead of all instructions
  4. Memory management - Unmap unused memory regions
  5. Timeouts - Always set timeouts for unknown binaries

Security Considerations

  1. Isolate rootfs - Use separate rootfs for each analysis
  2. Monitor resource usage - Watch memory and CPU consumption
  3. Validate inputs - Check binary files before emulation
  4. Limit execution - Use timeouts and instruction limits
  5. Network isolation - Block network access for malware analysis

Debugging Tips

  1. Start simple - Begin with basic emulation before adding hooks
  2. Use QDB - Interactive debugging for complex issues
  3. Check memory maps - Verify memory layout with get_mapinfo()
  4. Monitor registers - Track register changes at critical points
  5. Incremental development - Add features one at a time

This covers the essential patterns for using Qiling Framework effectively. For more advanced topics, see:

Clone this wiki locally