Skip to content

Commit a1e07b4

Browse files
committed
Add memory feature
1 parent 4da3f95 commit a1e07b4

File tree

7 files changed

+389
-5
lines changed

7 files changed

+389
-5
lines changed

ptrlib/algorithm/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .shortestpath import *
1+
from .shortestpath import *

ptrlib/arch/linux/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .consts import *
2+
from .memory import *
23
from .ospath import *
34
from .sig import *
4-
from .syscall import *
5-
5+
from .syscall import *

ptrlib/arch/linux/memory.py

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import ctypes
2+
import ctypes.util
3+
import re
4+
from logging import getLogger
5+
from typing import Generator, NamedTuple, List, Optional, Union
6+
from ptrlib.binary.encoding import str2bytes
7+
8+
logger = getLogger(__name__)
9+
10+
11+
class LinuxMemoryRegion(NamedTuple):
12+
start: int
13+
end: int
14+
perm: str
15+
offset: int
16+
path: Optional[str]
17+
18+
def __repr__(self):
19+
return f"LinuxMemoryRegion('{str(self)}')"
20+
21+
def __str__(self):
22+
return f"0x{self.start:016x}-0x{self.end:016x} {self.perm} {self.path}"
23+
24+
class LinuxProcessMemory(object):
25+
"""Memory inspector for Linux
26+
"""
27+
def __init__(self, pid: int, sudo: bool=True):
28+
"""
29+
Args:
30+
pid (int): Process ID to attach
31+
sudo (bool): If this parameter is set to true and a permission error occurs, ask pkexec prompt to get root privilege (Default is true)
32+
"""
33+
self._libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
34+
self._pid = pid
35+
36+
self._libc.strerror.argtypes = [ctypes.c_int]
37+
self._libc.strerror.restype = ctypes.c_char_p
38+
39+
self._libc.process_vm_readv.argtypes = [
40+
ctypes.c_int,
41+
ctypes.POINTER(ctypes.c_void_p),
42+
ctypes.c_ulong,
43+
ctypes.POINTER(ctypes.c_void_p),
44+
ctypes.c_ulong,
45+
ctypes.c_ulong,
46+
]
47+
self._libc.process_vm_readv.restype = ctypes.c_ssize_t
48+
49+
self._libc.process_vm_writev.argtypes = self._libc.process_vm_readv.argtypes
50+
self._libc.process_vm_writev.resype = ctypes.c_ssize_t
51+
52+
@property
53+
def vmmap(self) -> List[LinuxMemoryRegion]:
54+
maps = []
55+
with open(f"/proc/{self._pid}/maps", "r") as f:
56+
for line in f:
57+
m = re.match(r"^([0-9a-f]+)-([0-9a-f]+) ([-rwxsp]+) ([0-9a-f]+) \S+ \d+\s+(.*)", line)
58+
start = int(m.groups()[0], 16)
59+
end = int(m.groups()[1], 16)
60+
perm = m.groups()[2]
61+
offset = int(m.groups()[3], 16)
62+
path = m.groups()[4]
63+
maps.append(LinuxMemoryRegion(start, end, perm, offset, path))
64+
return maps
65+
66+
def read(self, addr: int, size: int) -> bytes:
67+
"""Attempt to read memory
68+
69+
Args:
70+
addr (int): Remote address to read data from
71+
size (int): Size to read
72+
73+
Returns:
74+
bytes: Data read from the memory
75+
"""
76+
e1 = e2 = None
77+
78+
# 1. /proc/pid/mem is the most reliable
79+
try:
80+
return self.proc_mem_read(addr, size)
81+
except OSError as e:
82+
e2 = e
83+
84+
# 2. process_vm_readv can bypass anti-debug
85+
try:
86+
return self.process_vm_read(addr, size)
87+
except OSError as e:
88+
e1 = e
89+
90+
# 3. TODO: ptrace
91+
raise e1 or e2
92+
93+
def write(self, addr: int, data: bytes) -> int:
94+
"""Attempt to write memory
95+
96+
Args:
97+
addr (int): Remote address to write data to
98+
data (bytes): Data to write
99+
100+
Returns:
101+
int: Number of bytes written to the memory
102+
"""
103+
e1 = e2 = None
104+
105+
# 1. /proc/pid/mem is the most reliable
106+
try:
107+
return self.proc_mem_write(addr, data)
108+
except OSError as e:
109+
e2 = e
110+
111+
# 2. process_vm_writev can bypass anti-debug
112+
try:
113+
return self.process_vm_write(addr, data)
114+
except OSError as e:
115+
e1 = e
116+
117+
# 3. TODO: ptrace
118+
raise e1 or e2
119+
120+
def search(self,
121+
data: Union[str, bytes],
122+
start: Optional[int]=None,
123+
end: Optional[int]=None,
124+
length: Optional[int]=None) -> Generator[int, None, None]:
125+
"""Search for memory
126+
127+
Args:
128+
data (bytes): Data to search
129+
start (int): Lower bound for search
130+
end (int): Upper bound for search
131+
len (int): Length of region to search (Requires either `start` or `end`)
132+
133+
Returns:
134+
generator: A generator to yield matching addresses
135+
"""
136+
if isinstance(data, str):
137+
data = str2bytes(data)
138+
139+
if length is not None:
140+
if (start or end) is None:
141+
raise ValueError("`len` is specified but neither `start` nor `end` is set")
142+
elif start is None:
143+
start = end - length
144+
else:
145+
end = start + length
146+
147+
if start is None:
148+
start = 0
149+
if end is None:
150+
end = 1 << 64 # Works fine both for 32-bit and 64-bit
151+
152+
prev_end = -1
153+
region_start = 0
154+
offset = 0
155+
haystack = b''
156+
for mem in self.vmmap:
157+
if mem.end <= start or mem.start > end:
158+
continue
159+
elif mem.start >= 0x8000_0000_0000: # Skip non-canonical and kernel memory
160+
continue
161+
elif mem.path == '[vvar]': # NOTE: Heuristic skip
162+
continue
163+
164+
if mem.start != prev_end:
165+
region_start = mem.start
166+
offset = 0
167+
haystack = b''
168+
prev_end = mem.end
169+
170+
# Search page by page
171+
for addr in range(mem.start, mem.end, 0x1000):
172+
try:
173+
haystack += self.read(addr, 0x1000)
174+
except OSError as e:
175+
logger.warning(f"Could not read memory ({addr}, {addr+0x1000}): {e}")
176+
continue
177+
178+
# TODO: Implement stream KMP
179+
while True:
180+
found = haystack.find(data, offset)
181+
if found == -1: break
182+
yield region_start + found
183+
offset = found + 1
184+
185+
if offset <= len(haystack) - len(data):
186+
offset = len(haystack) - len(data) + 1
187+
188+
def proc_mem_read(self, addr: int, size: int):
189+
"""Read memory with using /proc/pid/mem
190+
191+
Args:
192+
addr (int): Remote address to read data from
193+
size (int): Size to read
194+
195+
Returns:
196+
bytes: Data read from the memory
197+
"""
198+
with open(f"/proc/{self._pid}/mem", "rb") as f:
199+
f.seek(addr, 0)
200+
return f.read(size)
201+
202+
def proc_mem_write(self, addr: int, data: Union[str, bytes]) -> int:
203+
"""Write memory with using process_vm_writev
204+
205+
Args:
206+
addr (int): Remote address to write data to
207+
data (bytes): Data to write
208+
209+
Returns:
210+
int: Number of bytes written to the memory
211+
"""
212+
if isinstance(data, str):
213+
data = str2bytes(data)
214+
215+
with open(f"/proc/{self._pid}/mem", "wb") as f:
216+
f.seek(addr, 0)
217+
return f.write(data)
218+
219+
def process_vm_read(self, addr: int, size: int) -> bytes:
220+
"""Read memory with using process_vm_readv
221+
222+
Args:
223+
addr (int): Remote address to read data from
224+
size (int): Size to read
225+
226+
Returns:
227+
bytes: Data read from the memory
228+
"""
229+
buf = ctypes.create_string_buffer(size)
230+
local_iov = (ctypes.c_void_p * 2)(ctypes.addressof(buf), size)
231+
remote_iov = (ctypes.c_void_p * 2)(addr, size)
232+
233+
n_read = self._libc.process_vm_readv(
234+
self._pid, local_iov, 1, remote_iov, 1, 0
235+
)
236+
if n_read == -1:
237+
e = ctypes.get_errno()
238+
s = self._libc.strerror(e).decode()
239+
raise OSError(e, f"process_vm_readv failed: {s}")
240+
241+
return buf.raw
242+
243+
def process_vm_write(self, addr: int, data: bytes) -> int:
244+
"""Write memory with using process_vm_writev
245+
246+
Args:
247+
addr (int): Remote address to write data to
248+
data (bytes): Data to write
249+
250+
Returns:
251+
int: Number of bytes written to the memory
252+
"""
253+
buf = ctypes.create_string_buffer(data)
254+
local_iov = (ctypes.c_void_p * 2)(ctypes.addressof(buf), len(data))
255+
remote_iov = (ctypes.c_void_p * 2)(addr, len(data))
256+
257+
n_written = self._libc.process_vm_writev(
258+
self._pid, local_iov, 1, remote_iov, 1, 0
259+
)
260+
if n_written == -1:
261+
e = ctypes.get_errno()
262+
s = self._libc.strerror(e).decode()
263+
raise OSError(e, f"process_vm_writev failed: {s}")
264+
265+
return n_written
266+

ptrlib/connection/proc.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import subprocess
44
from logging import getLogger
55
from typing import List, Mapping, Optional, Union
6+
from ptrlib.arch.linux.memory import LinuxProcessMemory
67
from ptrlib.arch.linux.sig import signal_name
78
from ptrlib.binary.encoding import bytes2str
89
from .tube import Tube, tube_is_open
@@ -128,6 +129,9 @@ def __init__(self,
128129
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
129130
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
130131

132+
# Memory interface
133+
self._memory = LinuxProcessMemory(self.pid)
134+
131135
logger.info(f"Successfully created new process {str(self)}")
132136
self._init_done = True
133137

@@ -142,6 +146,11 @@ def returncode(self) -> Optional[int]:
142146
def pid(self) -> int:
143147
return self._proc.pid
144148

149+
@property
150+
@tube_is_open
151+
def memory(self) -> LinuxProcessMemory:
152+
return self._memory
153+
145154
#
146155
# Implementation of Tube methods
147156
#
@@ -290,6 +299,5 @@ def wait(self, timeout: Optional[Union[int, float]]=None) -> int:
290299
"""
291300
return self._proc.wait(timeout)
292301

293-
294302
Process = WinProcess if _is_windows else UnixProcess
295303
process = Process # alias for the Process

ptrlib/connection/sock.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import errno
21
import select
32
import socket
43
from logging import getLogger

0 commit comments

Comments
 (0)