Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions qiling/extensions/coverage/formats/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from typing import List
from qiling import Qiling
from qiling.core_hooks_types import HookRet
import re

class History:
history_hook_handle: HookRet = None
history: List[int] = []
ql: Qiling

def __init__(self, ql: Qiling) -> None:
self.ql = ql
self.track_block_coverage()

def clear_history(self) -> None:
"""Clears the current state of the history

"""
self.history = []

def clear_hooks(self) -> None:
"""Clears the current history hook from the Qiling instance

Returns:
None
"""

self.ql.hook_del(self.history_hook_handle)

def track_block_coverage(self) -> None:
"""Configures the history plugin to track all of the basic blocks that are executed. Removes any existing hooks

Returns:
None
"""
if self.history_hook_handle:
self.clear_hooks()

def __hook_block(ql, address, size):
self.history.append(address)

self.history_hook_handle = self.ql.hook_block(__hook_block)

def track_instruction_coverage(self) -> None:
"""Configures the history plugin to track all of the instructions that are executed. Removes any existing hooks

Returns:
None
"""
if self.history_hook_handle:
self.clear_hooks()

def __hook_block(ql, address, size):
self.history.append(address)

self.history_hook_handle = self.ql.hook_code(__hook_block)

def get_ins_only_lib(self, libs: List[str]) -> List[int]:
"""Returns a list of addresses that have been executed that are only in mmaps for objects that match the regex of items in the list

Args:
libs (List[str]): A list of regex strings to match against the library names in the memory maps

Returns:
List[int]: A list of addresses that have been executed and in the memory maps that match the regex

Examples:
>>> history.get_ins_only_lib(["libc.so", "libpthread.so"])
"""

executable_maps = self.get_regex_matching_exec_maps(libs)
return [x for x in self.history if any([x >= start and x <= end for start, end, _, _, _ in executable_maps])]

def get_ins_exclude_lib(self, libs: list) -> List:
'''Returns a list of history instructions that are not in the libraries that match the regex in the libs list

Args:
libs (List): A list of regex strings to match against the library names in the memory maps

Returns:
List: A list of addresses that have been executed and are not in the memory maps that match the regex

Examples:
>>> history.get_ins_exclude_lib(["libc.so", "libpthread.so"])
'''

executable_maps = self.get_regex_matching_exec_maps(libs)
return [x for x in self.history if any([x < start or x > end for start, end, _, _, _ in executable_maps])]

def get_mem_map_from_addr(self, ins: int) -> tuple:
'''Returns the memory map that contains the instruction

Args:
ins (int): The instruction address to search for

Returns:
tuple: A tuple that contains the memory map that contains the instruction
this tuple is in the format of (start_addr, end_addr, perms, name, path)

Examples:
>>> history.get_mem_map_from_addr(0x7ffff7dd1b97)
'''

#get the memory map that contains the instruction
mem_map = [x for x in self.ql.mem.get_mapinfo() if x[0] <= ins and x[1] >= ins]

if len(mem_map) == 0:
return None

# i sure hope theres not more than one map that contains the instruction lol
return mem_map[0]

def get_regex_matching_exec_maps(self, libs: List) -> List:
'''Returns a list of tuples for current mmaps whose names match the regex of libs in the list

This is a wrapper around ql.mem.get_mapinfo() and just filters the results by the regex of the library names and also only returns maps that are executable

Args:
libs (List): A list of regex strings to match against the library names in the memory maps

Returns:
List: A list of tuples that match the regex and are executable

Examples:
>>> history.get_regex_matching_exec_maps(["libc.so", "libpthread.so"])
>>> history.get_regex_matching_exec_maps(".*libc.*")
'''

# if libs is a string, convert it to a list
if isinstance(libs, str):
libs = [libs]

# filter the history list by the library name, using a list of regex
regex = [re.compile(lib) for lib in libs]

# filter the list of tuples
# so that we return only the ones where the library name matches the regex
regex_matching_libs = [x for x in self.ql.mem.get_mapinfo() if any([r.match(x[3]) for r in regex])]

# filter viable_libs for items that have the executable bit set
executable_maps = [x for x in regex_matching_libs if 'x' in x[2]]

return executable_maps
79 changes: 79 additions & 0 deletions tests/test_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import unittest
from qiling import Qiling
from qiling.const import QL_VERBOSE
from qiling.extensions.coverage.formats.history import History
from typing import List

class HistoryTest(unittest.TestCase):

@staticmethod
def sanitize_mmap_path(mmap: List[tuple]) -> List[tuple]:
'''Removes the path from the mmap tuple so that it can be compared to other mmaps
currently because the loader is handling loading ld and the main binary, we get the annotation of the path in element 5 of the tuple (index 4)
this path is going to be dependent on the users filesystem, so it doesnt quite make sense to test for it
'''
if isinstance(mmap, tuple):
mmap = [mmap]

return list(map(lambda x: (x[0], x[1], x[2], x[3], ''), mmap))

def test_get_regex_matching_exec_maps(self):
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
history = History(ql)
ql.run()

self.assertEqual([(0x7fffb7dd6000, 0x7fffb7fbd000, 'r-x', '[mmap] libc.so.6', '')], history.get_regex_matching_exec_maps(".*libc.so.*"))

self.assertEqual(
[
(0x7fffb7dd6000, 0x7fffb7fbd000, 'r-x', '[mmap] libc.so.6', ''),
(0x7ffff7dd5000, 0x7ffff7dfc000, 'r-x', 'ld-linux-x86-64.so.2', '')
],
self.sanitize_mmap_path(history.get_regex_matching_exec_maps([".*libc.so.*", "ld.*"]))
)

del ql

def test_get_mem_map_from_addr(self):
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
history = History(ql)
ql.run()

self.assertEqual(
self.sanitize_mmap_path(history.get_mem_map_from_addr(0x7ffff7df4830))[0],
(
0x7ffff7dd5000,
0x7ffff7dfc000,
'r-x',
'ld-linux-x86-64.so.2',
''))


def test_get_ins_exclude_lib(self):
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
history = History(ql)
ql.run(end=0x55555555465a)

non_libc_blocks = history.get_ins_exclude_lib(".*libc.so.*")

# this test is going to take a while but oh well
# also assumes that the get_mem_map_from_addr function works
for block in non_libc_blocks:
map_for_ins = history.get_mem_map_from_addr(block)
self.assertNotRegex(map_for_ins[3], ".*libc.so.*")

def test_get_ins_only_lib(self):
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
history = History(ql)
ql.run(end=0x55555555465a)

non_libc_blocks = history.get_ins_only_lib(".*libc.so.*")

# this test is going to take a while but oh well
# also assumes that the get_mem_map_from_addr function works
for block in non_libc_blocks:
map_for_ins = history.get_mem_map_from_addr(block)
self.assertRegex(map_for_ins[3], ".*libc.so.*")

if __name__ == "__main__":
unittest.main()