Skip to content

Commit 0594a52

Browse files
authored
Merge pull request #1567 from elicn/fix-blob-coverage
Fix blob coverage
2 parents 7bcb5ab + 70255af commit 0594a52

File tree

6 files changed

+127
-82
lines changed

6 files changed

+127
-82
lines changed

qiling/extensions/coverage/formats/base.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

6+
from __future__ import annotations
7+
68
from abc import ABC, abstractmethod
9+
from typing import TYPE_CHECKING
10+
711

8-
from qiling import Qiling
12+
if TYPE_CHECKING:
13+
from qiling import Qiling
914

1015

1116
class QlBaseCoverage(ABC):
@@ -15,25 +20,21 @@ class QlBaseCoverage(ABC):
1520
all the methods marked with the @abstractmethod decorator.
1621
"""
1722

23+
FORMAT_NAME: str
24+
1825
def __init__(self, ql: Qiling):
1926
super().__init__()
2027

2128
self.ql = ql
2229

23-
@property
24-
@staticmethod
25-
@abstractmethod
26-
def FORMAT_NAME() -> str:
27-
raise NotImplementedError
28-
2930
@abstractmethod
30-
def activate(self):
31+
def activate(self) -> None:
3132
pass
3233

3334
@abstractmethod
34-
def deactivate(self):
35+
def deactivate(self) -> None:
3536
pass
3637

3738
@abstractmethod
38-
def dump_coverage(self, coverage_file: str):
39+
def dump_coverage(self, coverage_file: str) -> None:
3940
pass

qiling/extensions/coverage/formats/drcov.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

6-
from ctypes import Structure
7-
from ctypes import c_uint32, c_uint16
6+
from __future__ import annotations
7+
8+
from ctypes import Structure, c_uint32, c_uint16
9+
from functools import lru_cache
10+
from typing import TYPE_CHECKING, BinaryIO, Dict, Tuple
811

912
from .base import QlBaseCoverage
1013

1114

15+
if TYPE_CHECKING:
16+
from qiling import Qiling
17+
from qiling.loader.loader import QlLoader
18+
19+
1220
# Adapted from https://www.ayrx.me/drcov-file-format
1321
class bb_entry(Structure):
1422
_fields_ = [
@@ -29,36 +37,61 @@ class QlDrCoverage(QlBaseCoverage):
2937

3038
FORMAT_NAME = "drcov"
3139

32-
def __init__(self, ql):
40+
def __init__(self, ql: Qiling):
3341
super().__init__(ql)
3442

3543
self.drcov_version = 2
3644
self.drcov_flavor = 'drcov'
37-
self.basic_blocks = []
45+
self.basic_blocks: Dict[int, bb_entry] = {}
3846
self.bb_callback = None
3947

40-
@staticmethod
41-
def block_callback(ql, address, size, self):
42-
for mod_id, mod in enumerate(ql.loader.images):
43-
if mod.base <= address <= mod.end:
44-
ent = bb_entry(address - mod.base, size, mod_id)
45-
self.basic_blocks.append(ent)
46-
break
48+
@lru_cache(maxsize=64)
49+
def _get_img_base(self, loader: QlLoader, address: int) -> Tuple[int, int]:
50+
"""Retrieve the containing image of a given address.
51+
52+
Addresses are expected to be aligned to page boundary, and cached for faster retrieval.
53+
"""
54+
55+
return next((i, img.base) for i, img in enumerate(loader.images) if img.base <= address < img.end)
56+
57+
def block_callback(self, ql: Qiling, address: int, size: int):
58+
if address not in self.basic_blocks:
59+
try:
60+
# we rely on the fact that images are allocated on page size boundary and
61+
# use it to speed up image retrieval. we align the basic block address to
62+
# page boundary, knowing basic blocks within the same page belong to the
63+
# same image. then we use the aligned address to retreive the containing
64+
# image. returned values are cached so subsequent retrievals for basic
65+
# blocks within the same page will return the cached value instead of
66+
# going through the retreival process again (up to maxsize cached pages)
4767

48-
def activate(self):
49-
self.bb_callback = self.ql.hook_block(self.block_callback, user_data=self)
68+
i, img_base = self._get_img_base(ql.loader, address & ~(0x1000 - 1))
69+
except StopIteration:
70+
pass
71+
else:
72+
self.basic_blocks[address] = bb_entry(address - img_base, size, i)
5073

51-
def deactivate(self):
52-
self.ql.hook_del(self.bb_callback)
74+
def activate(self) -> None:
75+
self.bb_callback = self.ql.hook_block(self.block_callback)
76+
77+
def deactivate(self) -> None:
78+
if self.bb_callback:
79+
self.ql.hook_del(self.bb_callback)
80+
81+
def dump_coverage(self, coverage_file: str) -> None:
82+
def __write_line(bio: BinaryIO, line: str) -> None:
83+
bio.write(f'{line}\n'.encode())
5384

54-
def dump_coverage(self, coverage_file):
5585
with open(coverage_file, "wb") as cov:
56-
cov.write(f"DRCOV VERSION: {self.drcov_version}\n".encode())
57-
cov.write(f"DRCOV FLAVOR: {self.drcov_flavor}\n".encode())
58-
cov.write(f"Module Table: version {self.drcov_version}, count {len(self.ql.loader.images)}\n".encode())
59-
cov.write("Columns: id, base, end, entry, checksum, timestamp, path\n".encode())
86+
__write_line(cov, f"DRCOV VERSION: {self.drcov_version}")
87+
__write_line(cov, f"DRCOV FLAVOR: {self.drcov_flavor}")
88+
__write_line(cov, f"Module Table: version {self.drcov_version}, count {len(self.ql.loader.images)}")
89+
__write_line(cov, "Columns: id, base, end, entry, checksum, timestamp, path")
90+
6091
for mod_id, mod in enumerate(self. ql.loader.images):
61-
cov.write(f"{mod_id}, {mod.base}, {mod.end}, 0, 0, 0, {mod.path}\n".encode())
62-
cov.write(f"BB Table: {len(self.basic_blocks)} bbs\n".encode())
63-
for bb in self.basic_blocks:
92+
__write_line(cov, f"{mod_id}, {mod.base}, {mod.end}, 0, 0, 0, {mod.path}")
93+
94+
__write_line(cov, f"BB Table: {len(self.basic_blocks)} bbs")
95+
96+
for bb in self.basic_blocks.values():
6497
cov.write(bytes(bb))

qiling/extensions/coverage/formats/drcov_exact.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
#
2+
#
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

@@ -17,10 +17,6 @@ class QlDrCoverageExact(QlDrCoverage):
1717

1818
FORMAT_NAME = "drcov_exact"
1919

20-
def __init__(self, ql):
21-
super().__init__(ql)
22-
23-
def activate(self):
20+
def activate(self) -> None:
2421
# We treat every instruction as a block on its own.
25-
self.bb_callback = self.ql.hook_code(self.block_callback, user_data=self)
26-
22+
self.bb_callback = self.ql.hook_code(self.block_callback)
Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
#!/usr/bin/env python3
2-
#
2+
#
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

6-
from collections import namedtuple
7-
from os.path import basename
6+
from __future__ import annotations
7+
8+
import os
9+
from typing import Any, TYPE_CHECKING, List, NamedTuple
810

911
from .base import QlBaseCoverage
1012

1113

14+
if TYPE_CHECKING:
15+
from qiling import Qiling
16+
17+
1218
# Adapted from https://github.com/nccgroup/Cartographer/blob/main/EZCOV.md#coverage-data
13-
class bb_entry(namedtuple('bb_entry', 'offset size mod_id')):
14-
def csvline(self):
15-
offset = '0x{:08x}'.format(self.offset)
19+
class bb_entry(NamedTuple):
20+
offset: int
21+
size: int
22+
mod_id: Any
23+
24+
def as_csv(self) -> str:
25+
offset = f'{self.offset:#010x}'
1626
mod_id = f"[ {self.mod_id if self.mod_id is not None else ''} ]"
27+
1728
return f"{offset},{self.size},{mod_id}\n"
1829

1930
class QlEzCoverage(QlBaseCoverage):
@@ -27,29 +38,30 @@ class QlEzCoverage(QlBaseCoverage):
2738

2839
FORMAT_NAME = "ezcov"
2940

30-
def __init__(self, ql):
41+
def __init__(self, ql: Qiling):
3142
super().__init__(ql)
43+
3244
self.ezcov_version = 1
33-
self.ezcov_flavor = 'ezcov'
34-
self.basic_blocks = []
35-
self.bb_callback = None
45+
self.ezcov_flavor = 'ezcov'
46+
self.basic_blocks: List[bb_entry] = []
47+
self.bb_callback = None
3648

37-
@staticmethod
38-
def block_callback(ql, address, size, self):
39-
mod = ql.loader.find_containing_image(address)
40-
if mod is not None:
41-
ent = bb_entry(address - mod.base, size, basename(mod.path))
42-
self.basic_blocks.append(ent)
49+
def block_callback(self, ql: Qiling, address: int, size: int):
50+
img = ql.loader.find_containing_image(address)
4351

44-
def activate(self):
45-
self.bb_callback = self.ql.hook_block(self.block_callback, user_data=self)
52+
if img is not None:
53+
self.basic_blocks.append(bb_entry(address - img.base, size, os.path.basename(img.path)))
4654

47-
def deactivate(self):
48-
self.ql.hook_del(self.bb_callback)
55+
def activate(self) -> None:
56+
self.bb_callback = self.ql.hook_block(self.block_callback)
4957

50-
def dump_coverage(self, coverage_file):
58+
def deactivate(self) -> None:
59+
if self.bb_callback:
60+
self.ql.hook_del(self.bb_callback)
61+
62+
def dump_coverage(self, coverage_file: str) -> None:
5163
with open(coverage_file, "w") as cov:
5264
cov.write(f"EZCOV VERSION: {self.ezcov_version}\n")
5365
cov.write("# Qiling EZCOV exporter tool\n")
54-
for bb in self.basic_blocks:
55-
cov.write(bb.csvline())
66+
67+
cov.writelines(bb.as_csv() for bb in self.basic_blocks)
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
#
2+
#
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
# This code structure is copied and modified from the coverage extension
55

@@ -12,24 +12,20 @@ class QlBaseTrace(ABC):
1212
To add support for a new coverage format, just derive from this class and implement
1313
all the methods marked with the @abstractmethod decorator.
1414
"""
15-
15+
16+
FORMAT_NAME: str
17+
1618
def __init__(self):
1719
super().__init__()
1820

19-
@property
20-
@staticmethod
21-
@abstractmethod
22-
def FORMAT_NAME():
23-
raise NotImplementedError
24-
2521
@abstractmethod
26-
def activate(self):
22+
def activate(self) -> None:
2723
pass
2824

2925
@abstractmethod
30-
def deactivate(self):
26+
def deactivate(self) -> None:
3127
pass
3228

3329
@abstractmethod
34-
def dump_trace(self, trace_file):
30+
def dump_trace(self, trace_file: str) -> None:
3531
pass

qiling/loader/blob.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
#
55

66
from qiling import Qiling
7-
from qiling.loader.loader import QlLoader
7+
from qiling.loader.loader import QlLoader, Image
88
from qiling.os.memory import QlMemoryHeap
99

10+
1011
class QlLoaderBLOB(QlLoader):
1112
def __init__(self, ql: Qiling):
1213
super().__init__(ql)
@@ -16,13 +17,19 @@ def __init__(self, ql: Qiling):
1617
def run(self):
1718
self.load_address = self.ql.os.entry_point # for consistency
1819

19-
self.ql.mem.map(self.ql.os.entry_point, self.ql.os.code_ram_size, info="[code]")
20-
self.ql.mem.write(self.ql.os.entry_point, self.ql.code)
20+
code_begins = self.load_address
21+
code_size = self.ql.os.code_ram_size
22+
code_ends = code_begins + code_size
2123

22-
heap_address = self.ql.os.entry_point + self.ql.os.code_ram_size
23-
heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16)
24-
self.ql.os.heap = QlMemoryHeap(self.ql, heap_address, heap_address + heap_size)
24+
self.ql.mem.map(code_begins, code_size, info="[code]")
25+
self.ql.mem.write(code_begins, self.ql.code)
2526

26-
self.ql.arch.regs.arch_sp = heap_address - 0x1000
27+
# allow image-related functionalities
28+
self.images.append(Image(code_begins, code_ends, 'blob_code'))
29+
30+
# FIXME: heap starts above end of ram??
31+
heap_base = code_ends
32+
heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16)
33+
self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size)
2734

28-
return
35+
self.ql.arch.regs.arch_sp = code_ends - 0x1000

0 commit comments

Comments
 (0)