Skip to content

Commit 99d21af

Browse files
committed
refactor!: ql own mem as bytearray, ref passed to uc and r2
BREAKING CHANGE: MapInfoEntry now has 6 elements instead of 5 BREAKING CHANGE: mem is managed in Python instead of uc BREAKING CHANGE: r2 map io from ql.mem, no full binary, now missing symbols BREAKING CHANGE: del_mapinfo and change_mapinfo recreate and remap mem Add unit tests for ql mem operations Also fix potential bug in syscall_munmap
1 parent cb7ca60 commit 99d21af

File tree

6 files changed

+157
-49
lines changed

6 files changed

+157
-49
lines changed

examples/extensions/r2/hello_r2.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def my_sandbox(path, rootfs):
3535
ql.hook_address(func, r2.functions['main'].offset)
3636
# enable trace powered by r2 symsmap
3737
# r2.enable_trace()
38-
r2.bt(0x401906)
38+
r2.set_backtrace(0x401906)
3939
ql.run()
4040

4141
if __name__ == "__main__":

qiling/arch/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, ql: Qiling):
2727

2828
@lru_cache(maxsize=64)
2929
def get_base_and_name(self, addr: int) -> Tuple[int, str]:
30-
for begin, end, _, name, _ in self.ql.mem.map_info:
30+
for begin, end, _, name, _, _ in self.ql.mem.map_info:
3131
if begin <= addr < end:
3232
return begin, basename(name)
3333

qiling/extensions/r2/r2.py

+16-17
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,8 @@ def __init__(self, ql: "Qiling", baseaddr=(1 << 64) - 1, loadaddr=0):
142142
self.loadaddr = loadaddr # r2 -m [addr] map file at given address
143143
self.analyzed = False
144144
self._r2c = libr.r_core.r_core_new()
145-
if ql.code:
146-
self._setup_code(ql.code)
147-
else:
148-
self._setup_file(ql.path)
145+
self._r2i = ctypes.cast(self._r2c.contents.io, ctypes.POINTER(libr.r_io.struct_r_io_t))
146+
self._setup_mem(ql)
149147

150148
def _qlarch2r(self, archtype: QL_ARCH) -> str:
151149
return {
@@ -162,20 +160,21 @@ def _qlarch2r(self, archtype: QL_ARCH) -> str:
162160
QL_ARCH.PPC: "ppc",
163161
}[archtype]
164162

165-
def _setup_code(self, code: bytes):
166-
path = f'malloc://{len(code)}'.encode()
167-
fh = libr.r_core.r_core_file_open(self._r2c, path, UC_PROT_ALL, self.loadaddr)
168-
libr.r_core.r_core_bin_load(self._r2c, path, self.baseaddr)
169-
self._cmd(f'wx {code.hex()}')
163+
def _rbuf_map(self, buf: bytearray, perm: int = UC_PROT_ALL, addr: int = 0, delta: int = 0):
164+
rbuf = libr.r_buf_new_with_pointers(ctypes.c_ubyte.from_buffer(buf), len(buf), False)
165+
rbuf = ctypes.cast(rbuf, ctypes.POINTER(libr.r_io.struct_r_buf_t))
166+
desc = libr.r_io_open_buffer(self._r2i, rbuf, perm, 0) # last arg `mode` is always 0 in r2 code
167+
libr.r_io.r_io_map_add(self._r2i, desc.contents.fd, desc.contents.perm, delta, addr, len(buf))
168+
169+
def _setup_mem(self, ql: 'Qiling'):
170+
if not hasattr(ql, '_mem'):
171+
return
172+
for start, end, perms, _label, _mmio, buf in ql.mem.map_info:
173+
self._rbuf_map(buf, perms, start)
170174
# set architecture and bits for r2 asm
171-
arch = self._qlarch2r(self.ql.arch.type)
172-
self._cmd(f"e,asm.arch={arch},asm.bits={self.ql.arch.bits}")
173-
174-
def _setup_file(self, path: str):
175-
path = path.encode()
176-
fh = libr.r_core.r_core_file_open(self._r2c, path, UC_PROT_READ | UC_PROT_EXEC, self.loadaddr)
177-
libr.r_core.r_core_bin_load(self._r2c, path, self.baseaddr)
178-
175+
arch = self._qlarch2r(ql.arch.type)
176+
self._cmd(f"e,asm.arch={arch},asm.bits={ql.arch.bits}")
177+
179178
def _cmd(self, cmd: str) -> str:
180179
r = libr.r_core.r_core_cmd_str(
181180
self._r2c, ctypes.create_string_buffer(cmd.encode("utf-8")))

qiling/os/memory.py

+53-28
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

6+
import ctypes
67
import os, re
78
from typing import Any, Callable, Iterator, List, Mapping, MutableSequence, Optional, Pattern, Sequence, Tuple, Union
89

@@ -11,8 +12,8 @@
1112
from qiling import Qiling
1213
from qiling.exception import *
1314

14-
# tuple: range start, range end, permissions mask, range label, is mmio?
15-
MapInfoEntry = Tuple[int, int, int, str, bool]
15+
# tuple: range start, range end, permissions mask, range label, is mmio?, bytearray
16+
MapInfoEntry = Tuple[int, int, int, str, bool, bytearray]
1617

1718
MmioReadCallback = Callable[[Qiling, int, int], int]
1819
MmioWriteCallback = Callable[[Qiling, int, int, int], None]
@@ -80,7 +81,7 @@ def string(self, addr: int, value=None, encoding='utf-8') -> Optional[str]:
8081

8182
self.__write_string(addr, value, encoding)
8283

83-
def add_mapinfo(self, mem_s: int, mem_e: int, mem_p: int, mem_info: str, is_mmio: bool = False):
84+
def add_mapinfo(self, mem_s: int, mem_e: int, mem_p: int, mem_info: str, is_mmio: bool = False, data : bytearray = None):
8485
"""Add a new memory range to map.
8586
8687
Args:
@@ -90,12 +91,11 @@ def add_mapinfo(self, mem_s: int, mem_e: int, mem_p: int, mem_info: str, is_mmio
9091
mem_info: map entry label
9192
is_mmio: memory range is mmio
9293
"""
93-
94-
self.map_info.append((mem_s, mem_e, mem_p, mem_info, is_mmio))
95-
self.map_info = sorted(self.map_info, key=lambda tp: tp[0])
94+
self.map_info.append((mem_s, mem_e, mem_p, mem_info, is_mmio, data))
95+
self.map_info.sort(key=lambda tp: tp[0])
9696

9797
def del_mapinfo(self, mem_s: int, mem_e: int):
98-
"""Subtract a memory range from map.
98+
"""Subtract a memory range from map, will destroy data and unmap uc mem in the range.
9999
100100
Args:
101101
mem_s: memory range start
@@ -104,30 +104,36 @@ def del_mapinfo(self, mem_s: int, mem_e: int):
104104

105105
tmp_map_info: MutableSequence[MapInfoEntry] = []
106106

107-
for s, e, p, info, mmio in self.map_info:
107+
for s, e, p, info, mmio, data in self.map_info:
108108
if e <= mem_s:
109-
tmp_map_info.append((s, e, p, info, mmio))
109+
tmp_map_info.append((s, e, p, info, mmio, data))
110110
continue
111111

112112
if s >= mem_e:
113-
tmp_map_info.append((s, e, p, info, mmio))
113+
tmp_map_info.append((s, e, p, info, mmio, data))
114114
continue
115115

116116
if s < mem_s:
117-
tmp_map_info.append((s, mem_s, p, info, mmio))
117+
self.ql.uc.mem_unmap(s, mem_s - s)
118+
self.map_ptr(s, mem_s - s, p, data[:mem_s - s])
119+
tmp_map_info.append((s, mem_s, p, info, mmio, data[:mem_s - s]))
118120

119121
if s == mem_s:
120122
pass
121123

122124
if e > mem_e:
123-
tmp_map_info.append((mem_e, e, p, info, mmio))
125+
self.ql.uc.mem_unmap(mem_e, e - mem_e)
126+
self.map_ptr(mem_e, e - mem_e, p, data[mem_e - e:])
127+
tmp_map_info.append((mem_e, e, p, info, mmio, data[mem_e - e:]))
124128

125129
if e == mem_e:
126130
pass
127131

132+
del data[mem_s - s:mem_e - s]
133+
128134
self.map_info = tmp_map_info
129135

130-
def change_mapinfo(self, mem_s: int, mem_e: int, mem_p: Optional[int] = None, mem_info: Optional[str] = None):
136+
def change_mapinfo(self, mem_s: int, mem_e: int, mem_p: Optional[int] = None, mem_info: Optional[str] = None, data: Optional[bytearray] = None):
131137
tmp_map_info: Optional[MapInfoEntry] = None
132138
info_idx: int = None
133139

@@ -142,12 +148,15 @@ def change_mapinfo(self, mem_s: int, mem_e: int, mem_p: Optional[int] = None, me
142148
return
143149

144150
if mem_p is not None:
145-
self.del_mapinfo(mem_s, mem_e)
146-
self.add_mapinfo(mem_s, mem_e, mem_p, mem_info if mem_info else tmp_map_info[3])
151+
data = data or self.read(mem_s, mem_e - mem_s)
152+
assert(len(data) == mem_e - mem_s)
153+
self.unmap(mem_s, mem_e - mem_s)
154+
self.map_ptr(mem_s, mem_e - mem_s, mem_p, data)
155+
self.add_mapinfo(mem_s, mem_e, mem_p, mem_info or tmp_map_info[3], tmp_map_info[4], data)
147156
return
148157

149158
if mem_info is not None:
150-
self.map_info[info_idx] = (tmp_map_info[0], tmp_map_info[1], tmp_map_info[2], mem_info, tmp_map_info[4])
159+
self.map_info[info_idx] = (tmp_map_info[0], tmp_map_info[1], tmp_map_info[2], mem_info, tmp_map_info[4], tmp_map_info[5])
151160

152161
def get_mapinfo(self) -> Sequence[Tuple[int, int, str, str, str]]:
153162
"""Get memory map info.
@@ -166,7 +175,7 @@ def __perms_mapping(ps: int) -> str:
166175

167176
return ''.join(val if idx & ps else '-' for idx, val in perms_d.items())
168177

169-
def __process(lbound: int, ubound: int, perms: int, label: str, is_mmio: bool) -> Tuple[int, int, str, str, str]:
178+
def __process(lbound: int, ubound: int, perms: int, label: str, is_mmio: bool, _data: bytearray) -> Tuple[int, int, str, str, str]:
170179
perms_str = __perms_mapping(perms)
171180

172181
if hasattr(self.ql, 'loader'):
@@ -211,7 +220,7 @@ def get_lib_base(self, filename: str) -> Optional[int]:
211220

212221
# some info labels may be prefixed by boxed label which breaks the search by basename.
213222
# iterate through all info labels and remove all boxed prefixes, if any
214-
stripped = ((lbound, p.sub('', info)) for lbound, _, _, info, _ in self.map_info)
223+
stripped = ((lbound, p.sub('', info)) for lbound, _, _, info, _, _ in self.map_info)
215224

216225
return next((lbound for lbound, info in stripped if os.path.basename(info) == filename), None)
217226

@@ -268,11 +277,10 @@ def save(self):
268277
"mmio" : []
269278
}
270279

271-
for lbound, ubound, perm, label, is_mmio in self.map_info:
280+
for lbound, ubound, perm, label, is_mmio, data in self.map_info:
272281
if is_mmio:
273282
mem_dict['mmio'].append((lbound, ubound, perm, label, *self.mmio_cbs[(lbound, ubound)]))
274283
else:
275-
data = self.read(lbound, ubound - lbound)
276284
mem_dict['ram'].append((lbound, ubound, perm, label, bytes(data)))
277285

278286
return mem_dict
@@ -393,7 +401,7 @@ def search(self, needle: Union[bytes, Pattern[bytes]], begin: Optional[int] = No
393401
assert begin < end, 'search arguments do not make sense'
394402

395403
# narrow the search down to relevant ranges; mmio ranges are excluded due to potential read size effects
396-
ranges = [(max(begin, lbound), min(ubound, end)) for lbound, ubound, _, _, is_mmio in self.map_info if not (end < lbound or ubound < begin or is_mmio)]
404+
ranges = [(max(begin, lbound), min(ubound, end)) for lbound, ubound, _, _, is_mmio, _data in self.map_info if not (end < lbound or ubound < begin or is_mmio)]
397405
results = []
398406

399407
# if needle is a bytes sequence use it verbatim, not as a pattern
@@ -439,10 +447,10 @@ def __mapped_regions(self) -> Iterator[Tuple[int, int]]:
439447

440448
iter_memmap = iter(self.map_info)
441449

442-
p_lbound, p_ubound, _, _, _ = next(iter_memmap)
450+
p_lbound, p_ubound, _, _, _, _ = next(iter_memmap)
443451

444452
# map_info is assumed to contain non-overlapping regions sorted by lbound
445-
for lbound, ubound, _, _, _ in iter_memmap:
453+
for lbound, ubound, _, _, _, _ in iter_memmap:
446454
if lbound == p_ubound:
447455
p_ubound = ubound
448456
else:
@@ -514,8 +522,8 @@ def find_free_space(self, size: int, minaddr: Optional[int] = None, maxaddr: Opt
514522
assert minaddr < maxaddr
515523

516524
# get gap ranges between mapped ones and memory bounds
517-
gaps_ubounds = tuple(lbound for lbound, _, _, _, _ in self.map_info) + (mem_ubound,)
518-
gaps_lbounds = (mem_lbound,) + tuple(ubound for _, ubound, _, _, _ in self.map_info)
525+
gaps_ubounds = tuple(lbound for lbound, _, _, _, _, _ in self.map_info) + (mem_ubound,)
526+
gaps_lbounds = (mem_lbound,) + tuple(ubound for _, ubound, _, _, _, _ in self.map_info)
519527
gaps = zip(gaps_lbounds, gaps_ubounds)
520528

521529
for lbound, ubound in gaps:
@@ -563,7 +571,7 @@ def protect(self, addr: int, size: int, perms):
563571
self.change_mapinfo(aligned_address, aligned_address + aligned_size, mem_p = perms)
564572

565573

566-
def map(self, addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None):
574+
def map(self, addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None, ptr: Optional[bytearray] = None):
567575
"""Map a new memory range.
568576
569577
Args:
@@ -582,8 +590,25 @@ def map(self, addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str
582590
if not self.is_available(addr, size):
583591
raise QlMemoryMappedError('Requested memory is unavailable')
584592

585-
self.ql.uc.mem_map(addr, size, perms)
586-
self.add_mapinfo(addr, addr + size, perms, info or '[mapped]', is_mmio=False)
593+
buf = self.map_ptr(addr, size, perms, ptr)
594+
self.add_mapinfo(addr, addr + size, perms, info or '[mapped]', is_mmio=False, data=buf)
595+
596+
def map_ptr(self, addr: int, size: int, perms: int = UC_PROT_ALL, buf: Optional[bytearray] = None) -> bytearray:
597+
"""Map a new memory range allocated as Python bytearray, will not affect map_info
598+
599+
Args:
600+
addr: memory range base address
601+
size: memory range size (in bytes)
602+
perms: requested permissions mask
603+
buf: bytearray already allocated (if any)
604+
605+
Returns:
606+
bytearray with size, should be added to map_info by caller
607+
"""
608+
buf = buf or bytearray(size)
609+
buf_type = ctypes.c_byte * size
610+
self.ql.uc.mem_map_ptr(addr, size, perms, buf_type.from_buffer(buf))
611+
return buf
587612

588613
def map_mmio(self, addr: int, size: int, read_cb: Optional[MmioReadCallback], write_cb: Optional[MmioWriteCallback], info: str = '[mmio]'):
589614
# TODO: mmio memory overlap with ram? Is that possible?

qiling/os/posix/syscall/mman.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def ql_syscall_munmap(ql: Qiling, addr: int, length: int):
1616
mapped_fd = [fd for fd in ql.os.fd if fd != 0 and isinstance(fd, ql_file) and fd._is_map_shared and not (fd.name.endswith(".so") or fd.name.endswith(".dylib"))]
1717

1818
if mapped_fd:
19-
all_mem_info = [_mem_info for _, _, _, _mem_info in ql.mem.map_info if _mem_info not in ("[mapped]", "[stack]", "[hook_mem]")]
19+
all_mem_info = [_mem_info for _, _, _, _mem_info, _mmio, _data in ql.mem.map_info if _mem_info not in ("[mapped]", "[stack]", "[hook_mem]")]
2020

2121
for _fd in mapped_fd:
2222
if _fd.name in [each.split()[-1] for each in all_mem_info]:
@@ -110,7 +110,7 @@ def syscall_mmap_impl(ql: Qiling, addr: int, mlen: int, prot: int, flags: int, f
110110
if mmap_base == 0:
111111
mmap_base = ql.loader.mmap_address
112112
ql.loader.mmap_address = mmap_base + mmap_size
113-
ql.log.debug("%s - mapping needed for 0x%x" % (api_name, mmap_base))
113+
ql.log.debug("%s - mapping needed at 0x%x with size 0x%x" % (api_name, mmap_base, mmap_size))
114114
try:
115115
ql.mem.map(mmap_base, mmap_size, prot, "[syscall_%s]" % api_name)
116116
except Exception as e:

tests/test_mem.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python3
2+
3+
import sys, unittest
4+
sys.path.append("..")
5+
6+
from unicorn import UC_PROT_ALL, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC, UC_PROT_NONE, UcError
7+
from unicorn.x86_const import UC_X86_REG_EAX, UC_X86_REG_ESI
8+
9+
from qiling import Qiling
10+
from qiling.exception import QlMemoryMappedError
11+
from test_shellcode import X8664_LIN, X86_LIN
12+
13+
14+
class MemTest(unittest.TestCase):
15+
def test_map_correct(self):
16+
ql = Qiling(code=X8664_LIN, archtype="x86_64", ostype="linux")
17+
ql.mem.map(0x40000, 0x1000 * 16, UC_PROT_ALL) # [0x40000, 0x50000]
18+
ql.mem.map(0x60000, 0x1000 * 16, UC_PROT_ALL) # [0x60000, 0x70000]
19+
ql.mem.map(0x20000, 0x1000 * 16, UC_PROT_ALL) # [0x20000, 0x30000]
20+
self.assertRaises(QlMemoryMappedError, ql.mem.map, 0x10000, 0x2000 * 16, UC_PROT_ALL)
21+
self.assertRaises(QlMemoryMappedError, ql.mem.map, 0x25000, 0x1000 * 16, UC_PROT_ALL)
22+
self.assertRaises(QlMemoryMappedError, ql.mem.map, 0x35000, 0x1000 * 16, UC_PROT_ALL)
23+
self.assertRaises(QlMemoryMappedError, ql.mem.map, 0x45000, 0x1000 * 16, UC_PROT_ALL)
24+
self.assertRaises(QlMemoryMappedError, ql.mem.map, 0x55000, 0x2000 * 16, UC_PROT_ALL)
25+
ql.mem.map(0x50000, 0x5000, UC_PROT_ALL)
26+
ql.mem.map(0x35000, 0x5000, UC_PROT_ALL)
27+
28+
def test_mem_protect(self):
29+
ql = Qiling(code=X86_LIN, archtype="x86", ostype="linux")
30+
code = bytes([0x01, 0x70, 0x04])
31+
r_eax = 0x2000
32+
r_esi = 0xdeadbeef
33+
ql.arch.regs.write(UC_X86_REG_EAX, r_eax)
34+
ql.arch.regs.write(UC_X86_REG_ESI, r_esi)
35+
ql.mem.map(0x1000, 0x1000, UC_PROT_READ | UC_PROT_EXEC)
36+
ql.mem.map(0x2000, 0x1000, UC_PROT_READ)
37+
ql.mem.protect(0x2000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
38+
ql.mem.write(0x1000, code)
39+
ql.emu_start(0x1000, 0x1000 + len(code) - 1, 0, 1)
40+
buf = ql.mem.read(0x2000 + 4, 4)
41+
self.assertEqual(int.from_bytes(buf, "little"), 0xdeadbeef)
42+
43+
def test_splitting_mem_unmap(self):
44+
ql = Qiling(code=X86_LIN, archtype="x86", ostype="linux")
45+
ql.mem.map(0x20000, 0x1000, UC_PROT_NONE)
46+
ql.mem.map(0x21000, 0x2000, UC_PROT_NONE)
47+
try:
48+
ql.mem.unmap(0x21000, 0x1000)
49+
except UcError as e:
50+
print(e)
51+
for s, e, p in ql.uc.mem_regions():
52+
print(hex(s), hex(e), p)
53+
for line in ql.mem.get_formatted_mapinfo():
54+
print(line)
55+
56+
def test_mem_protect_map_ptr(self):
57+
ql = Qiling(code=X8664_LIN, archtype="x86_64", ostype="linux")
58+
val = 0x114514
59+
data1 = bytearray(0x4000)
60+
data2 = bytearray(0x2000)
61+
ql.mem.map_ptr(0x4000, 0x4000, UC_PROT_ALL, data1)
62+
ql.mem.add_mapinfo(0x4000, 0x4000 + 0x4000, UC_PROT_ALL, "data1", False, data1)
63+
ql.mem.unmap(0x6000, 0x2000)
64+
ql.mem.map(0x6000, 0x2000, UC_PROT_ALL, data2)
65+
ql.mem.add_mapinfo(0x6000, 0x6000 + 0x2000, UC_PROT_ALL, "data2", False, data2)
66+
67+
ql.mem.write(0x6004, val.to_bytes(8, "little"))
68+
ql.mem.protect(0x6000, 0x1000, UC_PROT_READ)
69+
buf = ql.mem.read(0x6004, 8)
70+
self.assertEqual(int.from_bytes(buf, 'little'), val)
71+
72+
def test_map_at_the_end(self):
73+
ql = Qiling(code=X8664_LIN, archtype="x86_64", ostype="linux")
74+
mem = bytearray(0x1000)
75+
mem[:0x100] = [0xff] * 0x100
76+
mem = bytes(mem)
77+
ql.mem.map(0xfffffffffffff000, 0x1000, UC_PROT_ALL)
78+
ql.mem.write(0xfffffffffffff000, mem)
79+
self.assertRaises(UcError, ql.mem.write, 0xffffffffffffff00, mem)
80+
self.assertRaises(UcError, ql.mem.write, 0, mem)
81+
82+
83+
if __name__ == "__main__":
84+
unittest.main()

0 commit comments

Comments
 (0)