diff --git a/examples/scripts/dllscollector.bat b/examples/scripts/dllscollector.bat index d45a55fa9..048d5f9c3 100644 --- a/examples/scripts/dllscollector.bat +++ b/examples/scripts/dllscollector.bat @@ -57,6 +57,7 @@ CALL :collect_dll32 kdcom.dll CALL :collect_dll32 kernel32.dll CALL :collect_dll32 KernelBase.dll CALL :collect_dll32 mpr.dll +CALL :collect_dll32 mscoree.dll CALL :collect_dll32 msvcp_win.dll CALL :collect_dll32 msvcp60.dll CALL :collect_dll32 msvcr120_clr0400.dll, msvcr110.dll @@ -102,6 +103,7 @@ CALL :collect_dll64 advapi32.dll CALL :collect_dll64 gdi32.dll CALL :collect_dll64 kernel32.dll CALL :collect_dll64 KernelBase.dll +CALL :collect_dll64 mscoree.dll CALL :collect_dll64 msvcrt.dll CALL :collect_dll64 ntdll.dll CALL :collect_dll64 ntoskrnl.exe diff --git a/qiling/arch/x86_utils.py b/qiling/arch/x86_utils.py index de6e24f24..f0f58706f 100644 --- a/qiling/arch/x86_utils.py +++ b/qiling/arch/x86_utils.py @@ -1,5 +1,6 @@ from abc import abstractmethod +from typing import Optional from qiling import Qiling from qiling.arch.x86 import QlArchIntel @@ -32,7 +33,7 @@ def __setitem__(self, index: int, data: bytes) -> None: self.mem.write(self.base + (index * self.entsize), data) - def get_next_free(self, start: int = None, end: int = None) -> int: + def get_next_free(self, start: Optional[int] = None, end: Optional[int] = None) -> int: # The Linux kernel determines whether the segment is empty by judging whether the content in the current GDT segment is 0. null_entry = b'\x00' * self.entsize @@ -112,7 +113,7 @@ def get_entry(self, index: int) -> bytes: def set_entry(self, index: int, data: bytes) -> None: self.array[index] = data - def get_free_idx(self, start: int = None, end: int = None) -> int: + def get_free_idx(self, start: Optional[int] = None, end: Optional[int] = None) -> int: return self.array.get_next_free(start, end) diff --git a/qiling/debugger/gdb/gdb.py b/qiling/debugger/gdb/gdb.py index f559a2a83..4e48b6c57 100644 --- a/qiling/debugger/gdb/gdb.py +++ b/qiling/debugger/gdb/gdb.py @@ -378,7 +378,7 @@ def handle_q(subcmd: str) -> Reply: if query == 'Supported': # list of supported features excluding the multithreading-related ones - common = ( + features = [ 'BreakpointCommands+', 'ConditionalBreakpoints+', 'ConditionalTracepoints+', @@ -408,23 +408,21 @@ def handle_q(subcmd: str) -> Reply: 'multiprocess+', 'no-resumed+', 'qXfer:auxv:read+', - 'qXfer:exec-file:read+', 'qXfer:features:read+', # 'qXfer:libraries-svr4:read+', # 'qXfer:osdata:read+', 'qXfer:siginfo:read+', 'qXfer:siginfo:write+', 'qXfer:statictrace:read+', - 'qXfer:threads:read+', 'qXfer:traceframe-info:read+', 'swbreak+', 'tracenz+', 'vfork-events+' - ) + ] # might or might not need for multi thread if self.ql.multithread: - features = ( + features += [ 'PacketSize=47ff', 'FastTracepoints+', 'QThreadEvents+', @@ -436,16 +434,26 @@ def handle_q(subcmd: str) -> Reply: 'qXfer:btrace-conf:read+', 'qXfer:btrace:read+', 'vContSupported+' - ) + ] else: - features = ( + features += [ 'PacketSize=3fff', 'qXfer:spu:read+', 'qXfer:spu:write+' - ) + ] + + # os dependent features + if not self.ql.interpreter: + # filesystem dependent features + if hasattr(self.ql.os, 'path'): + features.append('qXfer:exec-file:read+') - return ';'.join(common + features) + # process dependent features + if hasattr(self.ql.os, 'pid'): + features.append('qXfer:threads:read+') + + return ';'.join(features) elif query == 'Xfer': feature, op, annex, params = data @@ -462,15 +470,11 @@ def handle_q(subcmd: str) -> Reply: return f'{"l" if len(content) < length else "m"}{content}' elif feature == 'threads' and op == 'read': - if not self.ql.baremetal and hasattr(self.ql.os, 'pid'): - content = '\r\n'.join(( - '', - f'', - '' - )) - - else: - content = '' + content = '\r\n'.join(( + '', + f'', + '' + )) return f'l{content}' @@ -494,7 +498,7 @@ def handle_q(subcmd: str) -> Reply: return b'l' + auxv_data[:length] elif feature == 'exec-file' and op == 'read': - return f'l{os.path.abspath(self.ql.path)}' + return f'l{self.ql.os.path.host_to_virtual_path(self.ql.path)}' elif feature == 'libraries-svr4' and op == 'read': # TODO: this one requires information of loaded libraries which currently not provided @@ -584,16 +588,20 @@ def handle_v(subcmd: str) -> Reply: flags = int(flags, 16) mode = int(mode, 16) - # try to guess whether this is an emulated path or real one - if path.startswith(os.path.abspath(self.ql.rootfs)): - host_path = path + virtpath = self.ql.os.path.virtual_abspath(path) + + if virtpath.startswith(r'/proc'): + # TODO: we really need a centralized virtual filesystem to open + # both emulated (like procfs) and real files, and manage their + # file descriptors seamlessly + fd = -1 else: host_path = self.ql.os.path.virtual_to_host_path(path) - self.ql.log.debug(f'{PROMPT} target file: {host_path}') + self.ql.log.debug(f'{PROMPT} target host path: {host_path}') - if os.path.exists(host_path) and not path.startswith(r'/proc'): - fd = os.open(host_path, flags, mode) + if os.path.exists(host_path): + fd = os.open(host_path, flags, mode) return f'F{fd:x}' diff --git a/qiling/extensions/trace.py b/qiling/extensions/trace.py index 878c6a4b7..ca99ecdb7 100644 --- a/qiling/extensions/trace.py +++ b/qiling/extensions/trace.py @@ -97,7 +97,7 @@ def __parse_op(op: X86Op) -> str: """ if op.type == CS_OP_REG: - return insn.reg_name(op.value.reg) + return insn.reg_name(op.value.reg) or '?' elif op.type == CS_OP_IMM: imm = op.value.imm @@ -113,6 +113,7 @@ def __parse_op(op: X86Op) -> str: disp = mem.disp ea = base + index * scale + disp + seg = f'{insn.reg_name(mem.segment)}:' if mem.segment else '' # we construct the string representation for each operand; denote memory # dereferenes with the appropriate 'ptr' prefix. the 'lea' instruction is @@ -131,7 +132,7 @@ def __parse_op(op: X86Op) -> str: qualifier = f'{ptr} ptr ' - return f'{qualifier}[{__resolve(ea) or f"{ea:#x}"}]' + return f'{qualifier}{seg}[{__resolve(ea) or f"{ea:#x}"}]' # unexpected op type raise RuntimeError diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 1b05e6d13..ea740da1c 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -248,7 +248,12 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: # in case of a dll loaded from a hooked API call, failures would not be # recoverable and we have to give up its DllMain. if not self.ql.os.PE_RUN: + + # temporarily set PE_RUN to allow proper fcall unwinding during + # execution of DllMain + self.ql.os.PE_RUN = True self.call_dll_entrypoint(dll, dll_base, dll_len, dll_name) + self.ql.os.PE_RUN = False self.ql.log.info(f'Done loading {dll_name}') @@ -622,7 +627,9 @@ def init_ki_user_shared_data(self): user_shared_data_obj = KUSER_SHARED_DATA() user_shared_data_size = ctypes.sizeof(KUSER_SHARED_DATA) - self.ql.mem.map(addr, self.ql.mem.align_up(user_shared_data_size)) + # TODO: initialize key fields in this structure + + self.ql.mem.map(addr, self.ql.mem.align_up(user_shared_data_size), info='[kuser shared]') self.ql.mem.write(addr, bytes(user_shared_data_obj)) def init_security_cookie(self, pe: pefile.PE, image_base: int): @@ -657,6 +664,7 @@ def run(self): self.sys_dlls = ( 'ntdll.dll', 'kernel32.dll', + 'mscoree.dll', 'ucrtbase.dll' ) @@ -695,7 +703,6 @@ def run(self): cmdline = ntpath.join(self.ql.os.userprofile, 'Desktop', self.ql.targetname) cmdargs = ' '.join(f'"{arg}"' if ' ' in arg else arg for arg in self.argv[1:]) - self.filepath = bytes(f'{cmdline}\x00', "utf-8") self.cmdline = bytes(f'{cmdline} {cmdargs}\x00', "utf-8") self.load(pe) @@ -722,14 +729,13 @@ def load(self, pe: Optional[pefile.PE]): self.ql.log.info(f'Loading {self.path} to {image_base:#x}') self.ql.log.info(f'PE entry point at {self.entry_point:#x}') - self.ql.mem.map(image_base, image_size, info=f'[{image_name}]') + self.ql.mem.map(image_base, image_size, info=f'{image_name}') self.images.append(Image(image_base, image_base + pe.NT_HEADERS.OPTIONAL_HEADER.SizeOfImage, os.path.abspath(self.path))) if self.is_driver: self.init_driver_object() self.init_registry_path() self.init_eprocess() - self.init_ki_user_shared_data() # set IRQ Level in CR8 to PASSIVE_LEVEL self.ql.arch.regs.write(UC_X86_REG_CR8, 0) @@ -748,6 +754,8 @@ def load(self, pe: Optional[pefile.PE]): # add image to ldr table self.add_ldr_data_table_entry(image_name) + self.init_ki_user_shared_data() + pe.parse_data_directories() # done manipulating pe file; write its contents into memory @@ -817,8 +825,6 @@ def load(self, pe: Optional[pefile.PE]): self.ql.os.fcall.call_native(self.entry_point, args, None) elif pe is None: - self.filepath = b"" - self.ql.mem.map(self.entry_point, self.ql.os.code_ram_size, info="[shellcode]") self.init_teb() diff --git a/qiling/os/fcall.py b/qiling/os/fcall.py index c9bd95d2c..30fff7594 100644 --- a/qiling/os/fcall.py +++ b/qiling/os/fcall.py @@ -2,7 +2,7 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework -from typing import Any, Callable, Iterable, MutableMapping, Optional, Mapping, Tuple, Sequence +from typing import Any, Callable, Iterable, Iterator, MutableMapping, Optional, Mapping, Tuple, Sequence from qiling import Qiling from qiling.cc import QlCC @@ -50,6 +50,21 @@ def __make_accessor(nbits: int) -> Accessor: # let the user override default accessors or add custom ones self.accessors.update(accessors) + def readEllipsis(self, ptypes: Sequence[Any]) -> Iterator[int]: + """ + """ + + default = self.accessors[PARAM_INTN] + + # count skipped slots + si = sum(self.accessors.get(typ, default)[2] for typ in ptypes) + + while True: + read, _, nslots = default + + yield read(si) + si += nslots + def readParams(self, ptypes: Sequence[Any]) -> Sequence[int]: """Walk the function parameters list and get their values. diff --git a/qiling/os/linux/kernel_api/kernel_api.py b/qiling/os/linux/kernel_api/kernel_api.py index 5fb9d7e12..c16ed3256 100644 --- a/qiling/os/linux/kernel_api/kernel_api.py +++ b/qiling/os/linux/kernel_api/kernel_api.py @@ -31,12 +31,10 @@ def hook_printk(ql: Qiling, address: int, params): return 0 level = int(format[1]) if format[1].isdigit() else 4 - nargs = format.count("%") - ptypes = (POINTER, ) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[1:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.printf(f'{PRINTK_LEVEL[level]} {format[2:]}', args, wstring=False) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.printf(f'{PRINTK_LEVEL[level]} {format[2:]}', args, wstring=False) + upd_args(params) return count diff --git a/qiling/os/linux/thread.py b/qiling/os/linux/thread.py index 1e2c5c1ba..512dee6f4 100644 --- a/qiling/os/linux/thread.py +++ b/qiling/os/linux/thread.py @@ -11,6 +11,7 @@ from unicorn.unicorn import UcError from qiling import Qiling +from qiling.const import QL_ARCH from qiling.os.thread import * from qiling.arch.x86_const import * from qiling.exception import QlErrorExecutionStop diff --git a/qiling/os/mapper.py b/qiling/os/mapper.py index 6b79524fb..c5b2914ae 100644 --- a/qiling/os/mapper.py +++ b/qiling/os/mapper.py @@ -101,15 +101,17 @@ def open_ql_file(self, path: str, openflags: int, openmode: int): if self.has_mapping(path): return self._open_mapping_ql_file(path, openflags, openmode) - real_path = self.path.transform_to_real_path(path) - return ql_file.open(real_path, openflags, openmode) + host_path = self.path.virtual_to_host_path(path) + + return ql_file.open(host_path, openflags, openmode) def open(self, path: str, openmode: str): if self.has_mapping(path): return self._open_mapping(path, openmode) - real_path = self.path.transform_to_real_path(path) - return open(real_path, openmode) + host_path = self.path.virtual_to_host_path(path) + + return open(host_path, openmode) def _parse_path(self, p: Union[os.PathLike, str]) -> str: fspath = getattr(p, '__fspath__', None) diff --git a/qiling/os/path.py b/qiling/os/path.py index 2cab19913..61c5b8db1 100644 --- a/qiling/os/path.py +++ b/qiling/os/path.py @@ -42,9 +42,9 @@ def __init__(self, rootfs: str, cwd: str, emulos: QL_OS) -> None: self.cwd = cwd # + # temporary aliases for backward compatibility self.transform_to_relative_path = self.virtual_abspath self.transform_to_real_path = self.virtual_to_host_path - self.transform_to_link_path = self.virtual_to_host_path # @staticmethod @@ -258,6 +258,23 @@ def __is_safe_host_path(self, hostpath: Path, strict: bool = False) -> bool: else: return True + def host_to_virtual_path(self, hostpath: str) -> str: + """Convert a host path to its corresponding virtual path relative to rootfs. + + Args: + hostpath : host path + + Returns: the corresponding virtual path + + Raises: `ValueError` in case the host path does not map to a rootfs location + """ + + resolved = Path(hostpath).resolve(strict=False) + virtpath = self._cwd_anchor / resolved.relative_to(self._rootfs_path) + + return str(virtpath) + + def virtual_abspath(self, virtpath: str) -> str: """Convert a relative virtual path to an absolute virtual path based on the current working directory. diff --git a/qiling/os/posix/filestruct.py b/qiling/os/posix/filestruct.py index daa978ce8..f20fd38fd 100644 --- a/qiling/os/posix/filestruct.py +++ b/qiling/os/posix/filestruct.py @@ -3,133 +3,136 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # import os - -from qiling.exception import * +from socket import socket, AddressFamily, SocketKind +from typing import Union try: import fcntl except ImportError: pass -import socket class ql_socket: - def __init__(self, socket): + def __init__(self, socket: socket): self.__fd = socket.fileno() self.__socket = socket def __getstate__(self, *args, **kwargs): - _state = self.__dict__.copy() - _state["_ql_socket__socket"] = { - "family": self.__dict__["_ql_socket__socket"].family, - "type": self.__dict__["_ql_socket__socket"].type, - "proto": self.__dict__["_ql_socket__socket"].proto, - "laddr": self.__dict__["_ql_socket__socket"].getsockname(), - } + fname = f'_{self.__class__.__name__}__socket' + sock = self.__dict__[fname] + + _state[fname] = { + "family" : sock.family, + "type" : sock.type, + "proto" : sock.proto, + "laddr" : sock.getsockname(), + } return _state def __setstate__(self, state): self.__dict__ = state - + @classmethod - def open(self, socket_domain, socket_type, socket_protocol, opts=None): - s = socket.socket(socket_domain, socket_type, socket_protocol) - if opts: - s.setsockopt(*opts) - return self(s) - - def read(self, read_len): - return os.read(self.__fd, read_len) - - def write(self, write_buf): - return os.write(self.__fd, write_buf) - - def fileno(self): + def open(cls, domain: Union[AddressFamily, int], socktype: Union[SocketKind, int], protocol: int): + s = socket(domain, socktype, protocol) + + return cls(s) + + def read(self, length: int) -> bytes: + return os.read(self.__fd, length) + + def write(self, data: bytes) -> int: + return os.write(self.__fd, data) + + def fileno(self) -> int: return self.__fd - - def close(self): - return os.close(self.__fd) - - def fcntl(self, fcntl_cmd, fcntl_arg): + + def close(self) -> None: + os.close(self.__fd) + + def fcntl(self, cmd, arg): try: - return fcntl.fcntl(self.__fd, fcntl_cmd, fcntl_arg) + return fcntl.fcntl(self.__fd, cmd, arg) except Exception: pass - def ioctl(self, ioctl_cmd, ioctl_arg): + def ioctl(self, cmd, arg): try: - return fcntl.ioctl(self.__fd, ioctl_cmd, ioctl_arg) + return fcntl.ioctl(self.__fd, cmd, arg) except Exception: - pass - - def dup(self): + pass + + def dup(self) -> 'ql_socket': new_s = self.__socket.dup() - new_ql_socket = ql_socket(new_s) - return new_ql_socket - - def connect(self, connect_addr): - return self.__socket.connect(connect_addr) - - def shutdown(self, shutdown_how): - return self.__socket.shutdown(shutdown_how) - - def bind(self, bind_addr): - return self.__socket.bind(bind_addr) - - def listen(self, listen_num): - return self.__socket.listen(listen_num) + + return ql_socket(new_s) + + def connect(self, address) -> None: + self.__socket.connect(address) + + def shutdown(self, how: int) -> None: + return self.__socket.shutdown(how) + + def bind(self, address) -> None: + return self.__socket.bind(address) + + def listen(self, backlog: int) -> None: + return self.__socket.listen(backlog) def getsockname(self): return self.__socket.getsockname() - + def getpeername(self): return self.__socket.getpeername() - def getsockopt(self, level, optname, optlen): - return self.__socket.getsockopt(level, optname, optlen) + def getsockopt(self, level: int, optname: int, buflen: int): + return self.__socket.getsockopt(level, optname, buflen) - def setsockopt(self, level, optname, optval, optlen): - if optval is not None: - return self.__socket.setsockopt(level, optname, optval) + def setsockopt(self, level: int, optname: int, value: Union[int, bytes, None], optlen: int = 0) -> None: + if value is None: + self.__socket.setsockopt(level, optname, None, optlen) else: - return self.__socket.setsockopt(level, optname, None, optval) - + self.__socket.setsockopt(level, optname, value) + def accept(self): try: con, addr = self.__socket.accept() - new_ql_socket = ql_socket(con) except BlockingIOError: # For support non-blocking sockets - return None, None + addr = None + new_ql_socket = None + else: + new_ql_socket = ql_socket(con) + return new_ql_socket, addr - - def recv(self, recv_len, recv_flags): - return self.__socket.recv(recv_len, recv_flags) - - def send(self, send_buf, send_flags): - return self.__socket.send(send_buf, send_flags) - - def recvmsg(self, bufsize, ancbufsize, flags): + + def recv(self, bufsize: int, flags: int) -> bytes: + return self.__socket.recv(bufsize, flags) + + def send(self, data, flags: int) -> int: + return self.__socket.send(data, flags) + + def recvmsg(self, bufsize: int, ancbufsize: int, flags: int): return self.__socket.recvmsg(bufsize, ancbufsize, flags) - def recvfrom(self, recvfrom_len, recvfrom_flags): - return self.__socket.recvfrom(recvfrom_len, recvfrom_flags) + def recvfrom(self, bufsize: int, flags: int): + return self.__socket.recvfrom(bufsize, flags) def sendto(self, sendto_buf, sendto_flags, sendto_addr): return self.__socket.sendto(sendto_buf, sendto_flags, sendto_addr) @property - def family(self): + def family(self) -> AddressFamily: return self.__socket.family @property - def socktype(self): + def socktype(self) -> SocketKind: return self.__socket.type @property - def socket(self): + def socket(self) -> socket: return self.__socket # def __getattr__(self,name): @@ -139,41 +142,40 @@ def socket(self): # raise AttributeError("A instance has no attribute '%s'" % name) class ql_pipe: - def __init__(self, fd): + def __init__(self, fd: int): self.__fd = fd @classmethod - def open(self): + def open(cls): r, w = os.pipe() - return (self(r), self(w)) - - def read(self, read_len): - return os.read(self.__fd, read_len) - - def write(self, write_buf): - return os.write(self.__fd, write_buf) - - def fileno(self): + + return (cls(r), cls(w)) + + def read(self, length: int) -> bytes: + return os.read(self.__fd, length) + + def write(self, data: bytes) -> int: + return os.write(self.__fd, data) + + def fileno(self) -> int: return self.__fd - def close(self): - return os.close(self.__fd) - - def fcntl(self, fcntl_cmd, fcntl_arg): + def close(self) -> None: + os.close(self.__fd) + + def fcntl(self, cmd, arg): try: - return fcntl.fcntl(self.__fd, fcntl_cmd, fcntl_arg) + return fcntl.fcntl(self.__fd, cmd, arg) except Exception: pass - def ioctl(self, ioctl_cmd, ioctl_arg): + def ioctl(self, cmd, arg): try: - return fcntl.ioctl(self.__fd, ioctl_cmd, ioctl_arg) + return fcntl.ioctl(self.__fd, cmd, arg) except Exception: - pass - - def dup(self): - new_fd = os.dup(self.__fd) - new_ql_pipe = ql_pipe(new_fd) - return new_ql_pipe + pass + def dup(self) -> 'ql_pipe': + new_fd = os.dup(self.__fd) + return ql_pipe(new_fd) diff --git a/qiling/os/posix/syscall/socket.py b/qiling/os/posix/syscall/socket.py index a80c7b998..3eb4deefe 100644 --- a/qiling/os/posix/syscall/socket.py +++ b/qiling/os/posix/syscall/socket.py @@ -69,38 +69,44 @@ def ql_bin_to_ip(ip): def ql_syscall_socket(ql: Qiling, socket_domain, socket_type, socket_protocol): idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] is None), -1) - regreturn = idx if idx != -1: + emu_socket_value = socket_type + # ql_socket.open should use host platform based socket_type. try: - emu_socket_value = socket_type emu_socket_type = socket_type_mapping(socket_type, ql.arch.type, ql.os.type) - socket_type = getattr(socket, emu_socket_type) - ql.log.debug(f'Convert emu_socket_type {emu_socket_type}:{emu_socket_value} to host platform based socket_type {emu_socket_type}:{socket_type}') + except KeyError: + ql.log.error(f'Cannot convert emu_socket_type {emu_socket_value} to host platform based socket_type') + raise + try: + socket_type = getattr(socket, emu_socket_type) except AttributeError: ql.log.error(f'Cannot convert emu_socket_type {emu_socket_type}:{emu_socket_value} to host platform based socket_type') raise - except Exception: - ql.log.error(f'Cannot convert emu_socket_type {emu_socket_value} to host platform based socket_type') - raise + ql.log.debug(f'Convert emu_socket_type {emu_socket_type}:{emu_socket_value} to host platform based socket_type {emu_socket_type}:{socket_type}') try: - if ql.verbose >= QL_VERBOSE.DEBUG: # set REUSEADDR options under debug mode - ql.os.fd[idx] = ql_socket.open(socket_domain, socket_type, socket_protocol, (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)) - else: - ql.os.fd[idx] = ql_socket.open(socket_domain, socket_type, socket_protocol) - except OSError as e: # May raise error: Protocol not supported + sock = ql_socket.open(socket_domain, socket_type, socket_protocol) + + # set REUSEADDR options under debug mode + if ql.verbose >= QL_VERBOSE.DEBUG: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + ql.os.fd[idx] = sock + + # May raise error: Protocol not supported + except OSError as e: ql.log.debug(f'{e}: {socket_domain=}, {socket_type=}, {socket_protocol=}') - regreturn = -1 + idx = -1 socket_type = socket_type_mapping(socket_type, ql.arch.type, ql.os.type) socket_domain = socket_domain_mapping(socket_domain, ql.arch.type, ql.os.type) - ql.log.debug("socket(%s, %s, %s) = %d" % (socket_domain, socket_type, socket_protocol, regreturn)) + ql.log.debug("socket(%s, %s, %s) = %d" % (socket_domain, socket_type, socket_protocol, idx)) - return regreturn + return idx def ql_syscall_connect(ql: Qiling, connect_sockfd, connect_addr, connect_addrlen): @@ -146,29 +152,31 @@ def ql_syscall_getsockopt(ql: Qiling, sockfd, level, optname, optval_addr, optle try: optlen = min(ql.unpack32s(ql.mem.read(optlen_addr, 4)), 1024) + if optlen < 0: return -EINVAL + emu_level = level + try: - emu_level = level emu_level_name = socket_level_mapping(emu_level, ql.arch.type, ql.os.type) - level = getattr(socket, emu_level_name) - ql.log.debug("Convert emu_level {}:{} to host platform based level {}:{}".format( - emu_level_name, emu_level, emu_level_name, level)) + except KeyError: + ql.log.error(f"Can't convert emu_level {emu_level} to host platform based level") + raise + try: + level = getattr(socket, emu_level_name) except AttributeError: - ql.log.error("Can't convert emu_level {}:{} to host platform based emu_level".format( - emu_level_name, emu_level)) + ql.log.error(f"Can't convert emu_level {emu_level_name}:{emu_level} to host platform based emu_level") raise - except Exception: - ql.log.error("Can't convert emu_level {} to host platform based level".format(emu_level)) - raise + ql.log.debug(f"Convert emu_level {emu_level_name}:{emu_level} to host platform based level {emu_level_name}:{level}") - try: - emu_opt = optname + emu_opt = optname + try: emu_level_name = socket_level_mapping(emu_level, ql.arch.type, ql.os.type) + # emu_opt_name is based on level if emu_level_name == "IPPROTO_IP": emu_opt_name = socket_ip_option_mapping(emu_opt, ql.arch.type, ql.os.type) @@ -180,18 +188,17 @@ def ql_syscall_getsockopt(ql: Qiling, sockfd, level, optname, optval_addr, optle if emu_opt_name.endswith("_NEW") or emu_opt_name.endswith("_OLD"): emu_opt_name = emu_opt_name[:-4] - optname = getattr(socket, emu_opt_name) - ql.log.debug("Convert emu_optname {}:{} to host platform based optname {}:{}".format( - emu_opt_name, emu_opt, emu_opt_name, optname)) + except KeyError: + ql.log.error(f"Can't convert emu_optname {emu_opt} to host platform based optname") + raise + try: + optname = getattr(socket, emu_opt_name) except AttributeError: - ql.log.error("Can't convert emu_optname {}:{} to host platform based emu_optname".format( - emu_opt_name, emu_opt)) + ql.log.error(f"Can't convert emu_optname {emu_opt_name}:{emu_opt} to host platform based emu_optname") raise - except Exception: - ql.log.error("Can't convert emu_optname {} to host platform based optname".format(emu_opt)) - raise + ql.log.debug(f"Convert emu_optname {emu_opt_name}:{emu_opt} to host platform based optname {emu_opt_name}:{optname}") optval = ql.os.fd[sockfd].getsockopt(level, optname, optlen) ql.mem.write(optval_addr, optval) @@ -210,26 +217,27 @@ def ql_syscall_setsockopt(ql: Qiling, sockfd, level, optname, optval_addr, optle ql.os.fd[sockfd].setsockopt(level, optname, None, optlen) else: try: + emu_level = level + try: - emu_level = level emu_level_name = socket_level_mapping(emu_level, ql.arch.type, ql.os.type) - level = getattr(socket, emu_level_name) - ql.log.debug("Convert emu_level {}:{} to host platform based level {}:{}".format( - emu_level_name, emu_level, emu_level_name, level)) + except KeyError: + ql.log.error(f"Can't convert emu_level {emu_level} to host platform based level") + raise + try: + level = getattr(socket, emu_level_name) except AttributeError: - ql.log.error("Can't convert emu_level {}:{} to host platform based emu_level".format( - emu_level_name, emu_level)) + ql.log.error(f"Can't convert emu_level {emu_level_name}:{emu_level} to host platform based emu_level") raise - except Exception: - ql.log.error("Can't convert emu_level {} to host platform based level".format(emu_level)) - raise + ql.log.debug(f"Convert emu_level {emu_level_name}:{emu_level} to host platform based level {emu_level_name}:{level}") - try: - emu_opt = optname + emu_opt = optname + try: emu_level_name = socket_level_mapping(emu_level, ql.arch.type, ql.os.type) + # emu_opt_name is based on level if emu_level_name == "IPPROTO_IP": emu_opt_name = socket_ip_option_mapping(emu_opt, ql.arch.type, ql.os.type) @@ -241,18 +249,17 @@ def ql_syscall_setsockopt(ql: Qiling, sockfd, level, optname, optval_addr, optle if emu_opt_name.endswith("_NEW") or emu_opt_name.endswith("_OLD"): emu_opt_name = emu_opt_name[:-4] - optname = getattr(socket, emu_opt_name) - ql.log.debug("Convert emu_optname {}:{} to host platform based optname {}:{}".format( - emu_opt_name, emu_opt, emu_opt_name, optname)) + except KeyError: + ql.log.error(f"Can't convert emu_optname {emu_opt} to host platform based optname") + raise + try: + optname = getattr(socket, emu_opt_name) except AttributeError: - ql.log.error("Can't convert emu_optname {}:{} to host platform based emu_optname".format( - emu_opt_name, emu_opt)) + ql.log.error(f"Can't convert emu_optname {emu_opt_name}:{emu_opt} to host platform based emu_optname") raise - except Exception: - ql.log.error("Can't convert emu_optname {} to host platform based optname".format(emu_opt)) - raise + ql.log.debug(f"Convert emu_optname {emu_opt_name}:{emu_opt} to host platform based optname {emu_opt_name}:{optname}") optval = ql.mem.read(optval_addr, optlen) ql.os.fd[sockfd].setsockopt(level, optname, optval, None) @@ -304,13 +311,13 @@ def ql_syscall_bind(ql: Qiling, bind_fd, bind_addr, bind_addrlen): # need a proper fix, for now ipv4 comes first elif sin_family == 2 and ql.os.bindtolocalhost == True: - ql.os.fd[bind_fd].bind(('127.0.0.1', port)) host = "127.0.0.1" + ql.os.fd[bind_fd].bind((host, port)) # IPv4 should comes first elif ql.os.ipv6 == True and sin_family == 10 and ql.os.bindtolocalhost == True: - ql.os.fd[bind_fd].bind(('::1', port)) host = "::1" + ql.os.fd[bind_fd].bind((host, port)) elif ql.os.bindtolocalhost == False: ql.os.fd[bind_fd].bind((host, port)) diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py index 046446f63..9c1f6fcdc 100644 --- a/qiling/os/posix/syscall/unistd.py +++ b/qiling/os/posix/syscall/unistd.py @@ -338,23 +338,25 @@ def ql_syscall_write(ql: Qiling, fd: int, buf: int, count: int): def ql_syscall_readlink(ql: Qiling, path_name: int, path_buff: int, path_buffsize: int): pathname = ql.os.utils.read_cstring(path_name) # pathname = str(pathname, 'utf-8', errors="ignore") - real_path = ql.os.path.transform_to_link_path(pathname) - relative_path = ql.os.path.transform_to_relative_path(pathname) + host_path = ql.os.path.virtual_to_host_path(pathname) + virt_path = ql.os.path.virtual_abspath(pathname) - if not os.path.exists(real_path): - regreturn = -1 + # cover procfs psaudo files first + # TODO: /proc/self/root, /proc/self/cwd + if virt_path == r'/proc/self/exe': + p = ql.os.path.host_to_virtual_path(ql.path) + p = p.encode('utf-8') - elif relative_path == r'/proc/self/exe': - localpath = os.path.abspath(ql.path) - localpath = bytes(localpath, 'utf-8') + b'\x00' + ql.mem.write(path_buff, p + b'\x00') + regreturn = len(p) - ql.mem.write(path_buff, localpath) - regreturn = len(localpath) - 1 + elif os.path.exists(host_path): + regreturn = 0 else: - regreturn = 0 + regreturn = -1 - ql.log.debug("readlink(%s, 0x%x, 0x%x) = %d" % (relative_path, path_buff, path_buffsize, regreturn)) + ql.log.debug('readlink("%s", 0x%x, 0x%x) = %d' % (virt_path, path_buff, path_buffsize, regreturn)) return regreturn @@ -376,16 +378,17 @@ def ql_syscall_getcwd(ql: Qiling, path_buff: int, path_buffsize: int): def ql_syscall_chdir(ql: Qiling, path_name: int): pathname = ql.os.utils.read_cstring(path_name) - real_path = ql.os.path.transform_to_real_path(pathname) - relative_path = ql.os.path.transform_to_relative_path(pathname) + host_path = ql.os.path.virtual_to_host_path(pathname) + virt_path = ql.os.path.virtual_abspath(pathname) + + if os.path.exists(host_path) and os.path.isdir(host_path): + ql.os.path.cwd = virt_path - if os.path.exists(real_path) and os.path.isdir(real_path): - ql.os.path.cwd = relative_path regreturn = 0 - ql.log.debug("chdir(%s) = %d"% (relative_path, regreturn)) + ql.log.debug("chdir(%s) = %d"% (virt_path, regreturn)) else: regreturn = -1 - ql.log.warning("chdir(%s) = %d : not found" % (relative_path, regreturn)) + ql.log.warning("chdir(%s) = %d : not found" % (virt_path, regreturn)) return regreturn @@ -393,21 +396,26 @@ def ql_syscall_chdir(ql: Qiling, path_name: int): def ql_syscall_readlinkat(ql: Qiling, dfd: int, path: int, buf: int, bufsize: int): pathname = ql.os.utils.read_cstring(path) # pathname = str(pathname, 'utf-8', errors="ignore") - real_path = ql.os.path.transform_to_link_path(pathname) - relative_path = ql.os.path.transform_to_relative_path(pathname) + host_path = ql.os.path.virtual_to_host_path(pathname) + virt_path = ql.os.path.virtual_abspath(pathname) - if not os.path.exists(real_path): - regreturn = -1 + # cover procfs psaudo files first + # TODO: /proc/self/root, /proc/self/cwd + if virt_path == r'/proc/self/exe': + p = ql.os.path.host_to_virtual_path(ql.path) + p = p.encode('utf-8') - elif relative_path == r'/proc/self/exe': - localpath = os.path.abspath(ql.path) - localpath = bytes(localpath, 'utf-8') + b'\x00' + ql.mem.write(buf, p + b'\x00') + regreturn = len(p) - ql.mem.write(buf, localpath) - regreturn = len(localpath) -1 - else: + elif os.path.exists(host_path): regreturn = 0 + else: + regreturn = -1 + + ql.log.debug('readlinkat(%d, "%s", 0x%x, 0x%x) = %d' % (dfd, virt_path, buf, bufsize, regreturn)) + return regreturn diff --git a/qiling/os/thread.py b/qiling/os/thread.py index 28adec686..dc1ccfa20 100644 --- a/qiling/os/thread.py +++ b/qiling/os/thread.py @@ -3,20 +3,20 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -from abc import ABCMeta, abstractmethod +from abc import abstractmethod from gevent import Greenlet -from qiling.const import * -from .const import * + +from qiling import Qiling class QlThread(Greenlet): - __metaclass__=ABCMeta - def __init__(self, ql): - super(QlThread, self).__init__() + def __init__(self, ql: Qiling): + super().__init__() + self.ql = ql self.log_file_fd = None - def __str__(self): + def __str__(self) -> str: return f"QlThread {self.get_id()}" # the common functions which are used in qiling core. diff --git a/qiling/os/utils.py b/qiling/os/utils.py index 1228df029..fa4bacca0 100644 --- a/qiling/os/utils.py +++ b/qiling/os/utils.py @@ -7,12 +7,16 @@ This module is intended for general purpose functions that are only used in qiling.os """ -from typing import MutableMapping, Union, Sequence, MutableSequence, Tuple +import ctypes +from typing import Callable, Iterable, Iterator, List, MutableMapping, Sequence, Tuple, TypeVar, Union from uuid import UUID from qiling import Qiling from qiling.const import QL_VERBOSE +# TODO: separate windows-specific implementation +from qiling.os.windows.structs import make_unicode_string + class QlOsUtils: ELLIPSIS_PREF = r'__qlva_' @@ -101,39 +105,119 @@ def __assign_arg(name: str, value: str) -> str: else: self.ql.log.info(log) - def __common_printf(self, format: str, args: MutableSequence, wstring: bool): - fmtstr = format.split("%")[1:] + def __common_printf(self, format: str, va_args: Iterator[int], wstring: bool) -> Tuple[str, Sequence[int]]: + import re + + # https://docs.microsoft.com/en-us/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions + # %[flags][width][.precision][size]type + fmtstr = re.compile(r'''% + (?P%| + (?P[-+0 #]+)? + (?P[*]|[0-9]+)? + (?:.(?P[*]|[0-9]+))? + (?Phh|ll|I32|I64|[hjltwzIL])? + (?P[diopuaAcCeEfFgGsSxXZ]) + ) + ''', re.VERBOSE) + + T = TypeVar('T') + + def __dup(iterator: Iterator[T], out: List[T]) -> Iterator[T]: + """A wrapper iterator to record iterator elements as they are being yielded. + """ + + for elem in iterator: + out.append(elem) + yield elem + + repl_args = [] # processed arguments + orig_args = [] # original arguments + + va_list = __dup(va_args, orig_args) + read_string = self.read_wstring if wstring else self.read_cstring - for i, f in enumerate(fmtstr): - if f.startswith("s"): - args[i] = read_string(args[i]) + def __repl(m: re.Match) -> str: + """Convert printf format string tokens into Python's. + """ + + if m['follows'] == '%': + return '%%' + + else: + flags = m['flags'] or '' + + fill = ' ' if ' ' in flags else '' + align = '<' if '-' in flags else '' + sign = '+' if '+' in flags else '' + pound = '#' if '#' in flags else '' + zeros = '0' if '0' in flags else '' + + width = m['width'] or '' - out = format.replace(r'%llx', r'%x') - out = out.replace(r'%p', r'%#x') + if width == '*': + width = f'{next(va_list)}' - return out % tuple(args) + prec = m['precision'] or '' - def va_list(self, format: str, ptr: int) -> MutableSequence[int]: - count = format.count("%") + if prec == '*': + prec = f'{next(va_list)}' - return [self.ql.mem.read_ptr(ptr + i * self.ql.arch.pointersize) for i in range(count)] + if prec: + prec = f'.{prec}' - def sprintf(self, buff: int, format: str, args: MutableSequence, wstring: bool = False) -> int: - out = self.__common_printf(format, args, wstring) + typ = m['type'] + arg = next(va_list) + + if typ in 'sS': + typ = 's' + arg = read_string(arg) + + elif typ == 'Z': + # note: ANSI_STRING and UNICODE_STRING have identical layout + strcls = make_unicode_string(self.ql.arch.bits).__class__ + data = self.ql.mem.read(arg, ctypes.sizeof(strcls)) + strobj = strcls.from_buffer_copy(data) + + typ = 's' + arg = read_string(strobj.Buffer) + + elif typ == 'p': + pound = '#' + typ = 'x' + + repl_args.append(arg) + + return f'%{fill}{align}{sign}{pound}{zeros}{width}{prec}{typ}' + + out = fmtstr.sub(__repl, format) + + return out % tuple(repl_args), orig_args + + def va_list(self, ptr: int) -> Iterator[int]: + while True: + yield self.ql.mem.read_ptr(ptr) + + ptr += self.ql.arch.pointersize + + def sprintf(self, buff: int, format: str, va_args: Iterator[int], wstring: bool = False) -> Tuple[int, Callable]: + out, args = self.__common_printf(format, va_args, wstring) enc = 'utf-16le' if wstring else 'utf-8' self.ql.mem.write(buff, (out + '\x00').encode(enc)) - return len(out) + return len(out), self.__update_ellipsis(args) - def printf(self, format: str, args: MutableSequence, wstring: bool = False) -> int: - out = self.__common_printf(format, args, wstring) + def printf(self, format: str, va_args: Iterator[int], wstring: bool = False) -> Tuple[int, Callable]: + out, args = self.__common_printf(format, va_args, wstring) enc = 'utf-8' self.ql.os.stdout.write(out.encode(enc)) - return len(out) + return len(out), self.__update_ellipsis(args) + + def __update_ellipsis(self, args: Iterable[int]) -> Callable[[MutableMapping], None]: + def __do_update(params: MutableMapping) -> None: + params.update((f'{QlOsUtils.ELLIPSIS_PREF}{i}', a) for i, a in enumerate(args)) - def update_ellipsis(self, params: MutableMapping, args: Sequence) -> None: - params.update((f'{QlOsUtils.ELLIPSIS_PREF}{i}', a) for i, a in enumerate(args)) + return __do_update \ No newline at end of file diff --git a/qiling/os/windows/api.py b/qiling/os/windows/api.py index bd001c381..12a8cca3d 100644 --- a/qiling/os/windows/api.py +++ b/qiling/os/windows/api.py @@ -111,6 +111,7 @@ LPSYSTEMTIME = POINTER LPSYSTEM_INFO = POINTER LPTHREAD_START_ROUTINE = POINTER +LPTIME_ZONE_INFORMATION = POINTER LPTOP_LEVEL_EXCEPTION_FILTER= POINTER LPUNKNOWN = POINTER LPVOID = POINTER diff --git a/qiling/os/windows/clipboard.py b/qiling/os/windows/clipboard.py index f7fecad9d..0d57e51d7 100644 --- a/qiling/os/windows/clipboard.py +++ b/qiling/os/windows/clipboard.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # # A Simple Windows Clipboard Simulation from typing import TYPE_CHECKING, Optional +from qiling.os.windows.const import * + if TYPE_CHECKING: from qiling.os.windows.windows import QlOsWindows NOT_LOCKED = -1 -ERROR_CLIPBOARD_NOT_OPEN = 0x58a class Clipboard: @@ -19,9 +20,12 @@ def __init__(self, os: 'QlOsWindows'): self.data = b"Default Clipboard Data" self.os = os - # Valid formats taken from https://doxygen.reactos.org/d8/dd6/base_ - # 2applications_2mstsc_2constants_8h_source.html - self.formats = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 128, 129, 130, 131, 142, 512, 767, 768, 1023] + self.formats = [ + CF_TEXT, CF_BITMAP, CF_METAFILEPICT, CF_SYLK, CF_DIF, CF_TIFF, CF_OEMTEXT, CF_DIB, + CF_PALETTE, CF_PENDATA, CF_RIFF, CF_WAVE, CF_UNICODETEXT, CF_ENHMETAFILE, CF_HDROP, + CF_LOCALE, CF_MAX, CF_OWNERDISPLAY, CF_DSPTEXT, CF_DSPBITMAP, CF_DSPMETAFILEPICT, + CF_DSPENHMETAFILE, CF_PRIVATEFIRST, CF_PRIVATELAST, CF_GDIOBJFIRST, CF_GDIOBJLAST + ] def open(self, h_wnd: int) -> bool: """Lock clipboard to hWnd if not already locked. @@ -50,23 +54,35 @@ def close(self) -> bool: def format_available(self, fmt: int) -> bool: return fmt in self.formats - def set_data(self, fmt: int, data: bytes) -> int: - if fmt not in self.formats: - return 0 - + def set_data(self, format: int, address: int) -> int: hWnd = self.os.thread_manager.cur_thread.id if self.locked_by != hWnd: self.os.last_error = ERROR_CLIPBOARD_NOT_OPEN return 0 - self.data = data + def __handle_text(a: int) -> bytes: + return self.os.utils.read_cstring(a).encode() + + def __handle_uctext(a: int) -> bytes: + return self.os.utils.read_wstring(a).encode() + + # TODO: support more clipboard formats + format_handlers = { + CF_TEXT : __handle_text, + CF_UNICODETEXT : __handle_uctext + } + + if format not in format_handlers: + return 0 + + self.data = format_handlers[format](address) - # BUG: this should be the handle of the clipboard object - return 1 + # TODO: should create a handle for the clipboard object? + return address - def get_data(self, fmt: int) -> Optional[bytes]: - if fmt not in self.formats: + def get_data(self, format: int) -> Optional[bytes]: + if format not in self.formats: return None hWnd = self.os.thread_manager.cur_thread.id diff --git a/qiling/os/windows/const.py b/qiling/os/windows/const.py index 13884a299..e7c6d8fe4 100644 --- a/qiling/os/windows/const.py +++ b/qiling/os/windows/const.py @@ -19,7 +19,9 @@ ERROR_MORE_DATA = 0xEA ERROR_NO_MORE_ITEMS = 0x103 ERROR_NOT_OWNER = 0x120 -ERROR_OLD_WIN_VERSION = 0X47E +ERROR_NO_UNICODE_TRANSLATION = 0x459 +ERROR_OLD_WIN_VERSION = 0x47E +ERROR_CLIPBOARD_NOT_OPEN = 0x58a # ... # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 @@ -35,25 +37,12 @@ INVALID_HANDLE_VALUE = -1 -STD_INPUT_HANDLE = 0xfffffff6 -STD_OUTPUT_HANDLE = 0xfffffff5 -STD_ERROR_HANDLE = 0xfffffff4 +STD_INPUT_HANDLE = 0xfffffff6 # -10 +STD_OUTPUT_HANDLE = 0xfffffff5 # -11 +STD_ERROR_HANDLE = 0xfffffff4 # -12 # Registry Type -# Predefined Keys -REG_KEYS = { - 0x80000000: "HKEY_CLASSES_ROOT", - 0x80000005: "HKEY_CURRENT_CONFIG", - 0x80000001: "HKEY_CURRENT_USER", - 0x80000007: "HKEY_CURRENT_USER_LOCAL_SETTINGS", - 0x80000002: "HKEY_LOCAL_MACHINE", - 0x80000004: "HKEY_PERFORMANCE_DATA", - 0x80000060: "HKEY_PERFORMANCE_NLSTEXT", - 0x80000050: "HKEY_PERFORMANCE_TEXT", - 0x80000003: "HKEY_USERS" -} - REG_TYPES = { "REG_NONE": Registry.RegNone, "REG_SZ": Registry.RegSZ, @@ -611,17 +600,23 @@ # ... +# documented PROCESSINFOCLASS values # https://docs.microsoft.com/en-us/windows/win32/procthread/zwqueryinformationprocess -# https://www.pinvoke.net/default.aspx/ntdll/PROCESSINFOCLASS.html ProcessBasicInformation = 0 ProcessDebugPort = 7 -ProcessExecuteFlags = 0x22 ProcessWow64Information = 26 ProcessImageFileName = 27 ProcessBreakOnTermination = 29 ProcessProtectionInformation = 61 -ProcessDebugObjectHandle = 0x1E -ProcessDebugFlags = 0x1F + +# more PROCESSINFOCLASS values +# https://www.pinvoke.net/default.aspx/ntdll/PROCESSINFOCLASS.html +ProcessDebugObjectHandle = 30 +ProcessDebugFlags = 31 +ProcessExecuteFlags = 34 +ProcessImageInformation = 37 +ProcessMitigationPolicy = 52 +ProcessFaultInformation = 63 # https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ps/psquery/class.htm ThreadBasicInformation = 0x0 @@ -632,26 +627,79 @@ # ... +# message box types: # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw -MB_ABORTRETRYIGNORE = 0x000000020 -MB_CANCELTRYCONTINUE = 0x000000060 -MB_HELP = 0x000040000 -MB_OK = 0x000000000 -MB_OKCANCEL = 0x000000010 -MB_RETRYCANCEL = 0x000000050 -MB_YESNO = 0x000000040 -MB_YESNOCANCEL = 0x000000030 - -IDABORT = 3 -IDCANCEL = 2 -IDCONTINUE = 11 -IDIGNORE = 5 -IDNO = 7 -IDOK = 1 -IDRETRY = 4 +MB_OK = 0x00000000 +MB_OKCANCEL = 0x00000001 +MB_ABORTRETRYIGNORE = 0x00000002 +MB_YESNOCANCEL = 0x00000003 +MB_YESNO = 0x00000004 +MB_RETRYCANCEL = 0x00000005 +MB_CANCELTRYCONTINUE = 0x00000006 + +MB_ICONERROR = 0x00000010 +MB_ICONQUESTION = 0x00000020 +MB_ICONWARNING = 0x00000030 +MB_ICONINFORMATION = 0x00000040 + +MB_DEFBUTTON1 = 0x00000000 +MB_DEFBUTTON2 = 0x00000100 +MB_DEFBUTTON3 = 0x00000200 +MB_DEFBUTTON4 = 0x00000300 + +MB_APPLMODAL = 0x00000000 +MB_SYSTEMMODAL = 0x00001000 +MB_TASKMODAL = 0x00002000 +MB_HELP = 0x00004000 + +MB_SETFOREGROUND = 0x00010000 +MB_DEFAULT_DESKTOP_ONLY = 0x00020000 +MB_TOPMOST = 0x00040000 +MB_RIGHT = 0x00080000 +MB_RTLREADING = 0x00100000 +MB_SERVICE_NOTIFICATION = 0x00200000 + +# message box responses: +IDOK = 1 +IDCANCEL = 2 +IDABORT = 3 +IDRETRY = 4 +IDIGNORE = 5 +IDYES = 6 +IDNO = 7 IDTRYAGAIN = 10 -IDYES = 6 +IDCONTINUE = 11 + +# clipboard data types +# https://doxygen.reactos.org/d8/dd6/base_2applications_2mstsc_2constants_8h_source.html + +CF_TEXT = 1 +CF_BITMAP = 2 +CF_METAFILEPICT = 3 +CF_SYLK = 4 +CF_DIF = 5 +CF_TIFF = 6 +CF_OEMTEXT = 7 +CF_DIB = 8 +CF_PALETTE = 9 +CF_PENDATA = 10 +CF_RIFF = 11 +CF_WAVE = 12 +CF_UNICODETEXT = 13 +CF_ENHMETAFILE = 14 +CF_HDROP = 15 +CF_LOCALE = 16 +CF_MAX = 17 +CF_OWNERDISPLAY = 128 +CF_DSPTEXT = 129 +CF_DSPBITMAP = 130 +CF_DSPMETAFILEPICT = 131 +CF_DSPENHMETAFILE = 142 +CF_PRIVATEFIRST = 512 +CF_PRIVATELAST = 767 +CF_GDIOBJFIRST = 768 +CF_GDIOBJLAST = 1023 # https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories @@ -709,3 +757,26 @@ OF_SHARE_DENY_WRITE = 0x00000020 OF_SHARE_DENY_READ = 0x00000030 OF_SHARE_DENY_NONE = 0x00000040 + +# code pages identifiers +CP_ACP = 0 +CP_OEMCP = 1 +CP_THREAD_ACP = 3 +CP_UTF16 = 1200 +CP_UTF16BE = 1201 +CP_ASCII = 20127 +CP_UTF7 = 65000 +CP_UTF8 = 65001 + +# conversion types +WC_DISCARDNS = 0x0010 +WC_SEPCHARS = 0x0020 +WC_DEFAULTCHAR = 0x0040 +WC_ERR_INVALID_CHARS = 0x0080 +WC_COMPOSITECHECK = 0x0200 +WC_NO_BEST_FIT_CHARS = 0x0400 + +TIME_ZONE_ID_INVALID = -1 +TIME_ZONE_ID_UNKNOWN = 0 +TIME_ZONE_ID_STANDARD = 1 +TIME_ZONE_ID_DAYLIGHT = 2 diff --git a/qiling/os/windows/dlls/advapi32.py b/qiling/os/windows/dlls/advapi32.py index e98779897..153e08a4b 100644 --- a/qiling/os/windows/dlls/advapi32.py +++ b/qiling/os/windows/dlls/advapi32.py @@ -14,27 +14,35 @@ def __RegOpenKey(ql: Qiling, address: int, params): lpSubKey = params["lpSubKey"] phkResult = params["phkResult"] - if hKey not in REG_KEYS: + handle = ql.os.handle_manager.get(hKey) + + if handle is None or not handle.name.startswith('HKEY'): return ERROR_FILE_NOT_FOUND - s_hKey = REG_KEYS[hKey] - key = s_hKey + "\\" + lpSubKey + params["hKey"] = handle.name - # Keys in the profile are saved as KEY\PARAM = VALUE, so i just want to check that the key is the same - keys_profile = [key.rsplit("\\", 1)[0] for key in ql.os.profile["REGISTRY"].keys()] - if key.lower() in keys_profile: - ql.log.debug("Using profile for key of %s" % key) - ql.os.registry_manager.access(key) - else: - if not ql.os.registry_manager.exists(key): - ql.log.debug("Value key %s not present" % key) - return ERROR_FILE_NOT_FOUND + if lpSubKey: + key = f'{handle.name}\\{lpSubKey}' + + # Keys in the profile are saved as KEY\PARAM = VALUE, so i just want to check that the key is the same + keys_profile = [entry.casefold() for entry in ql.os.profile["REGISTRY"].keys()] + + if key.casefold() in keys_profile: + ql.log.debug("Using profile for key of %s" % key) + ql.os.registry_manager.access(key) + + else: + if not ql.os.registry_manager.exists(key): + ql.log.debug("Value key %s not present" % key) + return ERROR_FILE_NOT_FOUND + + # new handle + handle = Handle(obj=key) + ql.os.handle_manager.append(handle) + + if phkResult: + ql.mem.write_ptr(phkResult, handle.id) - # new handle - new_handle = Handle(obj=key) - ql.os.handle_manager.append(new_handle) - if phkResult != 0: - ql.mem.write_ptr(phkResult, new_handle.id) return ERROR_SUCCESS def __RegQueryValue(ql: Qiling, address: int, params, wstring: bool): @@ -84,35 +92,34 @@ def __RegQueryValue(ql: Qiling, address: int, params, wstring: bool): return ret def __RegCreateKey(ql: Qiling, address: int, params): - ret = ERROR_SUCCESS - hKey = params["hKey"] lpSubKey = params["lpSubKey"] phkResult = params["phkResult"] - if hKey not in REG_KEYS: + handle = ql.os.handle_manager.get(hKey) + + if handle is None or not handle.name.startswith('HKEY'): return ERROR_FILE_NOT_FOUND - s_hKey = REG_KEYS[hKey] - params["hKey"] = s_hKey + params["hKey"] = handle.name - keyname = f'{s_hKey}\\{lpSubKey}' + if lpSubKey: + keyname = f'{handle.name}\\{lpSubKey}' - if not ql.os.registry_manager.exists(keyname): - ql.os.registry_manager.create(keyname) - ret = ERROR_SUCCESS + if not ql.os.registry_manager.exists(keyname): + ql.os.registry_manager.create(keyname) - # new handle - if ret == ERROR_SUCCESS: - new_handle = Handle(obj=keyname) - ql.os.handle_manager.append(new_handle) - if phkResult != 0: - ql.mem.write_ptr(phkResult, new_handle.id) - else: - # elicn: is this even reachable? - new_handle = 0 + handle = ql.os.handle_manager.search_by_obj(keyname) - return ret + # make sure we have a handle for this keyname + if handle is None: + handle = Handle(obj=keyname) + ql.os.handle_manager.append(handle) + + if phkResult: + ql.mem.write_ptr(phkResult, handle.id) + + return ERROR_SUCCESS def __RegSetValue(ql: Qiling, address: int, params, wstring: bool): hKey = params["hKey"] @@ -458,11 +465,17 @@ def hook_GetTokenInformation(ql: Qiling, address: int, params): TokenInformationLength = params["TokenInformationLength"] ReturnLength = params["ReturnLength"] - token = ql.os.handle_manager.get(TokenHandle).obj + handle = ql.os.handle_manager.get(TokenHandle) + + if handle is None: + ql.os.last_error = ERROR_INVALID_HANDLE + return 0 + + token = handle.obj information_value = token.get(TokenInformationClass) - ql.mem.write_ptr(ReturnLength, len(information_value), 4) - return_size = ql.mem.read_ptr(ReturnLength, 4) + return_size = len(information_value) + ql.mem.write_ptr(ReturnLength, return_size, 4) ql.log.debug("The target is checking for its permissions") diff --git a/qiling/os/windows/dlls/kernel32/__init__.py b/qiling/os/windows/dlls/kernel32/__init__.py index ca9762783..e07d5685a 100644 --- a/qiling/os/windows/dlls/kernel32/__init__.py +++ b/qiling/os/windows/dlls/kernel32/__init__.py @@ -23,3 +23,4 @@ from .consoleapi3 import * from .debugapi import * from .psapi import * +from .timezoneapi import * diff --git a/qiling/os/windows/dlls/kernel32/errhandlingapi.py b/qiling/os/windows/dlls/kernel32/errhandlingapi.py index 5dbc32d7a..4dba7efb6 100644 --- a/qiling/os/windows/dlls/kernel32/errhandlingapi.py +++ b/qiling/os/windows/dlls/kernel32/errhandlingapi.py @@ -22,10 +22,13 @@ def hook_SetUnhandledExceptionFilter(ql: Qiling, address: int, params): if handle is None: handle = Handle(name="TopLevelExceptionHandler", obj=addr) ql.os.handle_manager.append(handle) + prev_filter = 0 + else: + prev_filter = handle.obj handle.obj = addr - return 0 + return prev_filter # _Post_equals_last_error_ DWORD GetLastError(); @winsdkapi(cc=STDCALL, params={}) @@ -73,11 +76,19 @@ def hook_SetErrorMode(ql: Qiling, address: int, params): 'lpArguments' : POINTER }) def hook_RaiseException(ql: Qiling, address: int, params): - func_addr = ql.os.handle_manager.search("TopLevelExceptionHandler").obj + nNumberOfArguments = params['nNumberOfArguments'] + lpArguments = params['lpArguments'] - ql.os.fcall.call_native(func_addr, [], None) + handle = ql.os.handle_manager.search("TopLevelExceptionHandler") - return 0 + if handle is None: + ql.log.warning(f'RaiseException: top level exception handler not found') + return + + exception_handler = handle.obj + args = [(PARAM_INTN, ql.mem.read_ptr(lpArguments + i * ql.arch.pointersize)) for i in range(nNumberOfArguments)] if lpArguments else [] + + ql.os.fcall.call_native(exception_handler, args, None) # PVOID AddVectoredExceptionHandler( # ULONG First, diff --git a/qiling/os/windows/dlls/kernel32/handleapi.py b/qiling/os/windows/dlls/kernel32/handleapi.py index 11e7291b7..bbc4debec 100644 --- a/qiling/os/windows/dlls/kernel32/handleapi.py +++ b/qiling/os/windows/dlls/kernel32/handleapi.py @@ -48,12 +48,12 @@ def hook_CloseHandle(ql: Qiling, address: int, params): if handle is None: ql.os.last_error = ERROR_INVALID_HANDLE return 0 - else: - if handle.permissions is not None and handle.permissions & HANDLE_FLAG_PROTECT_FROM_CLOSE >= 1: - # FIXME: add error - return 0 - else: - ql.os.handle_manager.delete(value) + + if handle.permissions is not None and handle.permissions & HANDLE_FLAG_PROTECT_FROM_CLOSE: + # FIXME: add error + return 0 + + ql.os.handle_manager.delete(value) return 1 diff --git a/qiling/os/windows/dlls/kernel32/libloaderapi.py b/qiling/os/windows/dlls/kernel32/libloaderapi.py index 658107135..2f0370c22 100644 --- a/qiling/os/windows/dlls/kernel32/libloaderapi.py +++ b/qiling/os/windows/dlls/kernel32/libloaderapi.py @@ -18,12 +18,10 @@ def _GetModuleHandle(ql: Qiling, address: int, params): if lpModuleName == 0: ret = ql.loader.pe_image_address else: - lpModuleName = lpModuleName.lower() - if not has_lib_ext(lpModuleName): lpModuleName = f'{lpModuleName}.dll' - image = ql.loader.get_image_by_name(lpModuleName) + image = ql.loader.get_image_by_name(lpModuleName, casefold=True) if image: ret = image.base @@ -69,6 +67,35 @@ def hook_GetModuleHandleExW(ql: Qiling, address: int, params): return res +def __GetModuleFileName(ql: Qiling, address: int, params, *, wide: bool): + hModule = params["hModule"] + lpFilename = params["lpFilename"] + nSize = params["nSize"] + + if not hModule: + if ql.code: + raise QlErrorNotImplemented('cannot retrieve module file name in shellcode mode') + + hModule = ql.loader.pe_image_address + + hpath = next((image.path for image in ql.loader.images if image.base == hModule), None) + + if hpath is None: + ql.os.last_error = ERROR_INVALID_HANDLE + return 0 + + encname = 'utf-16le' if wide else 'latin' + vpath = ql.os.path.host_to_virtual_path(hpath) + truncated = vpath[:nSize - 1] + '\x00' + encoded = truncated.encode(encname) + + if len(vpath) + 1 > nSize: + ql.os.last_error = ERROR_INSUFFICIENT_BUFFER + + ql.mem.write(lpFilename, encoded) + + return min(len(vpath), nSize) + # DWORD GetModuleFileNameA( # HMODULE hModule, # LPSTR lpFilename, @@ -80,30 +107,7 @@ def hook_GetModuleHandleExW(ql: Qiling, address: int, params): 'nSize' : DWORD }) def hook_GetModuleFileNameA(ql: Qiling, address: int, params): - hModule = params["hModule"] - lpFilename = params["lpFilename"] - nSize = params["nSize"] - ret = 0 - - # GetModuleHandle can return pe_image_address as handle, and GetModuleFileName will try to retrieve it. - # Pretty much 0 and pe_image_address value should do the same operations - if not ql.code and (hModule == 0 or hModule == ql.loader.pe_image_address): - filename = ql.loader.filepath - filename_len = len(filename) - - if filename_len > nSize - 1: - filename = ql.loader.filepath[:nSize - 1] - ret = nSize - else: - ret = filename_len - - ql.mem.write(lpFilename, filename + b"\x00") - - else: - ql.log.debug("hModule %x" % hModule) - raise QlErrorNotImplemented("API not implemented") - - return ret + return __GetModuleFileName(ql, address, params, wide=False) # DWORD GetModuleFileNameW( # HMODULE hModule, @@ -116,30 +120,7 @@ def hook_GetModuleFileNameA(ql: Qiling, address: int, params): 'nSize' : DWORD }) def hook_GetModuleFileNameW(ql: Qiling, address: int, params): - hModule = params["hModule"] - lpFilename = params["lpFilename"] - nSize = params["nSize"] - ret = 0 - - # GetModuleHandle can return pe_image_address as handle, and GetModuleFileName will try to retrieve it. - # Pretty much 0 and pe_image_address value should do the same operations - if not ql.code and (hModule == 0 or hModule == ql.loader.pe_image_address): - filename = ql.loader.filepath.decode('ascii').encode('utf-16le') - filename_len = len(filename) - - if filename_len > nSize - 1: - filename = ql.loader.filepath[:nSize - 1] - ret = nSize - else: - ret = filename_len - - ql.mem.write(lpFilename, filename + b"\x00") - - else: - ql.log.debug("hModule %x" % hModule) - raise QlErrorNotImplemented("API not implemented") - - return ret + return __GetModuleFileName(ql, address, params, wide=True) # FARPROC GetProcAddress( # HMODULE hModule, @@ -188,9 +169,11 @@ def hook_GetProcAddress(ql: Qiling, address: int, params): def _LoadLibrary(ql: Qiling, address: int, params): lpLibFileName = params["lpLibFileName"] - if not ql.code and lpLibFileName == ql.loader.filepath.decode(): - # Loading self - return ql.loader.pe_image_address + # TODO: this searches only by basename; do we need to search by full path as well? + dll = ql.loader.get_image_by_name(lpLibFileName, casefold=True) + + if dll is not None: + return dll.base return ql.loader.load_dll(lpLibFileName) diff --git a/qiling/os/windows/dlls/kernel32/processthreadsapi.py b/qiling/os/windows/dlls/kernel32/processthreadsapi.py index b01aa1822..27937f0fc 100644 --- a/qiling/os/windows/dlls/kernel32/processthreadsapi.py +++ b/qiling/os/windows/dlls/kernel32/processthreadsapi.py @@ -8,7 +8,7 @@ from qiling.os.windows.const import * from qiling.os.windows.fncc import * -from qiling.os.windows.thread import QlWindowsThread +from qiling.os.windows.thread import QlWindowsThread, THREAD_STATUS from qiling.os.windows.handle import Handle from qiling.os.windows.structs import Token, StartupInfo @@ -164,20 +164,13 @@ def hook_CreateThread(ql: Qiling, address: int, params): dwCreationFlags = params["dwCreationFlags"] lpThreadId = params["lpThreadId"] - # new thread obj - new_thread = QlWindowsThread(ql) - - if dwCreationFlags & CREATE_SUSPENDED == CREATE_SUSPENDED: - thread_status = QlWindowsThread.READY + if dwCreationFlags & CREATE_SUSPENDED: + thread_status = THREAD_STATUS.READY else: - thread_status = QlWindowsThread.RUNNING + thread_status = THREAD_STATUS.RUNNING # create new thread - thread_id = new_thread.create( - lpStartAddress, - lpParameter, - thread_status - ) + new_thread = QlWindowsThread.create(ql, dwStackSize, lpStartAddress, lpParameter, thread_status) # append the new thread to ThreadManager ql.os.thread_manager.append(new_thread) diff --git a/qiling/os/windows/dlls/kernel32/stringapiset.py b/qiling/os/windows/dlls/kernel32/stringapiset.py index 2b9ce4e62..157a1e815 100644 --- a/qiling/os/windows/dlls/kernel32/stringapiset.py +++ b/qiling/os/windows/dlls/kernel32/stringapiset.py @@ -7,6 +7,7 @@ from qiling.os.windows.api import * from qiling.os.windows.fncc import * from qiling.os.windows.utils import cmp +from qiling.os.windows.const import * # BOOL GetStringTypeW( # DWORD dwInfoType, @@ -43,6 +44,42 @@ def hook_GetStringTypeExA(ql: Qiling, address: int, params): # TODO: implement return 1 +def __encoding_name(codepage: int) -> str: + '''Get python encoding name from codepage value. + + @see: https://docs.python.org/3.8/library/codecs.html#standard-encodings + ''' + + # mapping of special codepage values to encodings + encodings = { + # available only on windows hosts + CP_ACP : 'mbcs', + CP_OEMCP : 'oem', + CP_THREAD_ACP : 'mbcs', + + # avaiable everywhere + CP_UTF16 : 'utf-16', + CP_UTF16BE : 'utf-16be', + CP_ASCII : 'ascii', + CP_UTF7 : 'utf-7', + CP_UTF8 : 'utf-8' + } + + if codepage in encodings: + encname = encodings[codepage] + + else: + encname = f'cp{codepage}' + + # encoding might break on the hosting system; test it and + # fallback to windows-1252 ('western') if it fails + try: + _ = '\x00'.encode(encname) + except LookupError: + encname = 'cp1252' + + return encname + # int WideCharToMultiByte( # UINT CodePage, # DWORD dwFlags, @@ -56,7 +93,7 @@ def hook_GetStringTypeExA(ql: Qiling, address: int, params): @winsdkapi(cc=STDCALL, params={ 'CodePage' : UINT, 'dwFlags' : DWORD, - 'lpWideCharStr' : WSTRING, # LPCWCH + 'lpWideCharStr' : POINTER, # WSTRING 'cchWideChar' : INT, 'lpMultiByteStr' : LPSTR, 'cbMultiByte' : INT, @@ -64,16 +101,54 @@ def hook_GetStringTypeExA(ql: Qiling, address: int, params): 'lpUsedDefaultChar' : LPBOOL }) def hook_WideCharToMultiByte(ql: Qiling, address: int, params): + CodePage = params['CodePage'] + dwFlags = params['dwFlags'] + lpWideCharStr = params['lpWideCharStr'] + cchWideChar = params['cchWideChar'] + lpMultiByteStr = params['lpMultiByteStr'] cbMultiByte = params["cbMultiByte"] - s_lpWideCharStr = params["lpWideCharStr"] - lpMultiByteStr = params["lpMultiByteStr"] - s = (s_lpWideCharStr + "\x00").encode("utf-16le") + if not cchWideChar: + # TODO: set last error + return 0 + + # -1 indicates the string is null-terminated. the string is + # read along with its null-terminator + elif cchWideChar == 0xffffffff: + wcstr = bytearray() + ch = 1 + + while ch != b'\x00\x00': + ch = ql.mem.read(lpWideCharStr, 2) + wcstr.extend(ch) + + lpWideCharStr += 2 + + # read exactly cbMultiByte bytes. that may or may not include + # a null-terminator + else: + wcstr = ql.mem.read(lpWideCharStr, cchWideChar * 2) + + decoded = wcstr.decode('utf-16le') - if cbMultiByte != 0 and lpMultiByteStr != 0: - ql.mem.write(lpMultiByteStr, s) + encname = __encoding_name(CodePage) + errors = 'strict' if dwFlags & WC_ERR_INVALID_CHARS else 'replace' - return len(s) + try: + encoded = decoded.encode(encname, errors) + except UnicodeEncodeError: + ql.os.last_error = ERROR_NO_UNICODE_TRANSLATION + return 0 + + if not cbMultiByte: + return len(encoded) + + if not lpMultiByteStr: + return 0 + + ql.mem.write(lpMultiByteStr, encoded[:cbMultiByte]) + + return min(len(encoded), cbMultiByte) # int MultiByteToWideChar( # UINT CodePage, @@ -86,18 +161,53 @@ def hook_WideCharToMultiByte(ql: Qiling, address: int, params): @winsdkapi(cc=STDCALL, params={ 'CodePage' : UINT, 'dwFlags' : DWORD, - 'lpMultiByteStr' : WSTRING, # LPCCH + 'lpMultiByteStr' : POINTER, # STRING, 'cbMultiByte' : INT, 'lpWideCharStr' : LPWSTR, 'cchWideChar' : INT }) def hook_MultiByteToWideChar(ql: Qiling, address: int, params): - wide_str = (params['lpMultiByteStr'] + "\x00").encode('utf-16le') + CodePage = params['CodePage'] + lpMultiByteStr = params['lpMultiByteStr'] + cbMultiByte = params['cbMultiByte'] + lpWideCharStr = params['lpWideCharStr'] + cchWideChar = params['cchWideChar'] + + if not cbMultiByte: + # TODO: set last error + return 0 + + # -1 indicates the string is null-terminated. the string is + # read along with its null-terminator + elif cbMultiByte == 0xffffffff: + mbstr = bytearray() + ch = 1 + + while ch != b'\x00': + ch = ql.mem.read(lpMultiByteStr, 1) + mbstr.extend(ch) + + lpMultiByteStr += 1 + + # read exactly cbMultiByte bytes. that may or may not include + # a null-terminator + else: + mbstr = ql.mem.read(lpMultiByteStr, cbMultiByte) + + # use specified code page to translate bytes into string + encname = __encoding_name(CodePage) + decoded = mbstr.decode(encname) + + # this is a dry-run; just return the amount of chars + if not cchWideChar: + return len(decoded) + + if not lpWideCharStr: + return 0 - if params['cchWideChar'] != 0: - ql.mem.write(params['lpWideCharStr'], wide_str) + ql.mem.write(lpWideCharStr, decoded[:cchWideChar].encode('utf-16le')) - return len(wide_str) + return min(len(decoded), cchWideChar) def __CompareString(ql: Qiling, address: int, params) -> int: lpString1 = params["lpString1"] diff --git a/qiling/os/windows/dlls/kernel32/synchapi.py b/qiling/os/windows/dlls/kernel32/synchapi.py index 50ccd5e01..0ce5c65ff 100644 --- a/qiling/os/windows/dlls/kernel32/synchapi.py +++ b/qiling/os/windows/dlls/kernel32/synchapi.py @@ -145,15 +145,17 @@ def hook_WaitForMultipleObjects(ql: Qiling, address: int, params): return 0 def __OpenMutex(ql: Qiling, address: int, params): + lpName = params["lpName"] + + if not lpName: + return 0 + # The name can have a "Global" or "Local" prefix to explicitly open an object in the global or session namespace. # It can also have no prefix - try: - _type, name = params["lpName"].split("\\") - except ValueError: - name = params["lpName"] - _type = "" + _type, name = lpName.split("\\") if '\\' in lpName else ('', lpName) handle = ql.os.handle_manager.search(name) + if _type == "Global": # if is global is a Windows lock. We always return a valid handle because we have no way to emulate them # example sample: Gandcrab e42431d37561cc695de03b85e8e99c9e31321742 @@ -175,11 +177,13 @@ def __OpenMutex(ql: Qiling, address: int, params): raise QlErrorNotImplemented("API not implemented") def __CreateMutex(ql: Qiling, address: int, params): - try: - _type, name = params["lpName"].split("\\") - except ValueError: - name = params["lpName"] - _type = "" + lpName = params["lpName"] + owning = params["bInitialOwner"] + + if not lpName: + return 0 + + _type, name = lpName.split("\\") if '\\' in lpName else ('', lpName) handle = ql.os.handle_manager.search(name) @@ -187,7 +191,6 @@ def __CreateMutex(ql: Qiling, address: int, params): # ql.os.last_error = ERROR_ALREADY_EXISTS return 0 - owning = params["bInitialOwner"] mutex = Mutex(name, _type) if owning: @@ -279,30 +282,30 @@ def hook_ReleaseMutex(ql: Qiling, address: int, params): def __CreateEvent(ql: Qiling, address: int, params): # Implementation seems similar enough to Mutex to just use it + lpName = params["lpName"] - try: - namespace, name = params["lpName"].split("\\") - except ValueError: - name = params["lpName"] - namespace = "" + if lpName: + namespace, name = lpName.split('\\') if '\\' in lpName else ('', lpName) + handle = ql.os.handle_manager.search(name) - handle = ql.os.handle_manager.search(name) + if handle is not None: + ql.os.last_error = ERROR_ALREADY_EXISTS + return handle.id - if handle is not None: - ql.os.last_error = ERROR_ALREADY_EXISTS - # FIXME: fail with a nullptr? - # return 0 else: - mutex = Mutex(name, namespace) + # TODO: should be None instead of empty strings? + namespace = '' + name = '' - if params['bInitialState']: - mutex.lock() + mutex = Mutex(name, namespace) + + if params['bInitialState']: + mutex.lock() - handle = Handle(obj=mutex, name=name) - ql.os.handle_manager.append(handle) + handle = Handle(obj=mutex, name=name) + ql.os.handle_manager.append(handle) - # FIXME: shouldn't it be 'id' instead of 'ID'? - return handle.ID + return handle.id # HANDLE CreateEventA( # LPSECURITY_ATTRIBUTES lpEventAttributes, diff --git a/qiling/os/windows/dlls/kernel32/sysinfoapi.py b/qiling/os/windows/dlls/kernel32/sysinfoapi.py index fc2073eb4..8a5f6aa87 100644 --- a/qiling/os/windows/dlls/kernel32/sysinfoapi.py +++ b/qiling/os/windows/dlls/kernel32/sysinfoapi.py @@ -10,7 +10,7 @@ from qiling.os.windows.api import * from qiling.os.windows.const import * from qiling.os.windows.fncc import * -from qiling.os.windows.structs import SystemInfo, SystemTime +from qiling.os.windows.structs import FILETIME, SystemInfo, SystemTime # NOT_BUILD_WINDOWS_DEPRECATE DWORD GetVersion( # ); @@ -81,8 +81,20 @@ def hook_GetLocalTime(ql: Qiling, address: int, params): 'lpSystemTimeAsFileTime' : LPFILETIME }) def hook_GetSystemTimeAsFileTime(ql: Qiling, address: int, params): - # TODO - pass + ptr = params['lpSystemTimeAsFileTime'] + + epoch = datetime(1601, 1, 1) + elapsed = datetime.now() - epoch + + # number of 100-nanosecond intervals since Jan 1, 1601 utc + # where: (10 ** 9) / 100 -> (10 ** 7) + hnano = int(elapsed.total_seconds() * (10 ** 7)) + + mask = (1 << 32) - 1 + hi = (hnano >> 32) & mask + lo = (hnano >> 0) & mask + + ql.mem.write(ptr, bytes(FILETIME(lo, hi))) # DWORD GetTickCount( # ); diff --git a/qiling/os/windows/dlls/kernel32/timezoneapi.py b/qiling/os/windows/dlls/kernel32/timezoneapi.py new file mode 100644 index 000000000..b5af67ded --- /dev/null +++ b/qiling/os/windows/dlls/kernel32/timezoneapi.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +from qiling import Qiling +from qiling.os.windows.api import * +from qiling.os.windows.const import * +from qiling.os.windows.fncc import * + +# DWORD GetTimeZoneInformation( +# [out] LPTIME_ZONE_INFORMATION lpTimeZoneInformation +# ); +@winsdkapi(cc=STDCALL, params={ + 'lpTimeZoneInformation' : LPTIME_ZONE_INFORMATION +}) +def hook_GetTimeZoneInformation(ql: Qiling, address: int, params): + # TODO: implement this later. fail for now + return TIME_ZONE_ID_INVALID diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index 73b84b3c4..c75e06d6a 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -4,6 +4,7 @@ # import time +from typing import Sequence from qiling import Qiling from qiling.exception import QlErrorNotImplemented @@ -21,6 +22,49 @@ def hook___set_app_type(ql: Qiling, address: int, params): pass +def __alloc_strings_array(ql: Qiling, items: Sequence[str], *, wide: bool) -> int: + '''Allocate and populate an array of strings and return its address. + ''' + + enc = 'utf-16le' if wide else 'utf-8' + + nitems = len(items) + + # allocate room for pointers to items and a trailing null pointer + p_array = ql.os.heap.alloc((nitems + 1) * ql.arch.pointersize) + + # encode all arguments into bytes + items_bytes = [f'{item}\x00'.encode(enc) for item in items] + + # allocate room for the items + p_items_bytes = ql.os.heap.alloc(sum(len(a) for a in items_bytes)) + + for i, item in enumerate(items_bytes): + # write argument data + ql.mem.write(p_items_bytes, item) + + # write pointer to argument data into the argv array + ql.mem.write_ptr(p_array + (i * ql.arch.pointersize), p_items_bytes) + + p_items_bytes += len(item) + + # write trailing null pointer + ql.mem.write_ptr(p_array + (nitems * ql.arch.pointersize), 0) + + return p_array + +def __getmainargs(ql: Qiling, params, wide: bool) -> int: + argc = len(ql.argv) + argv = __alloc_strings_array(ql, ql.argv, wide=wide) + env = __alloc_strings_array(ql, [f'{k}={v}' for k, v in ql.env], wide=wide) + + # write out paramters + ql.mem.write_ptr(params['_Argc'], argc, 4) + ql.mem.write_ptr(params['_Argv'], argv) + ql.mem.write_ptr(params['_Env'], env) + + return 0 + # int __getmainargs( # int * _Argc, # char *** _Argv, @@ -36,7 +80,17 @@ def hook___set_app_type(ql: Qiling, address: int, params): '_StartInfo' : POINTER }) def hook___getmainargs(ql: Qiling, address: int, params): - return 0 + return __getmainargs(ql, params, wide=False) + +@winsdkapi(cc=CDECL, params={ + '_Argc' : POINTER, + '_Argv' : POINTER, + '_Env' : POINTER, + '_DoWildCard' : INT, + '_StartInfo' : POINTER +}) +def hook___wgetmainargs(ql: Qiling, address: int, params): + return __getmainargs(ql, params, wide=True) # int* __p__fmode(); @winsdkapi(cc=CDECL, params={}) @@ -53,12 +107,14 @@ def hook___p__commode(ql: Qiling, address: int, params): # char** __p__acmdln(); @winsdkapi(cc=CDECL, params={}) def hook___p__acmdln(ql: Qiling, address: int, params): + # TODO: use values calculated at __getmainargs addr = ql.loader.import_address_table['msvcrt.dll'][b'_acmdln'] return addr # wchar_t ** __p__wcmdln(); @winsdkapi(cc=CDECL, params={}) def hook___p__wcmdln(ql: Qiling, address: int, params): + # TODO: use values calculated at __getmainargs addr = ql.loader.import_address_table['msvcrt.dll'][b'_wcmdln'] return addr @@ -127,7 +183,7 @@ def hook__cexit(ql: Qiling, address: int, params): 'pfend' : POINTER }) def hook__initterm(ql: Qiling, address: int, params): - pass + return 0 # void exit( # int const status @@ -207,9 +263,10 @@ def hook_sprintf(ql: Qiling, address: int, params): if format == 0: format = "(null)" - args = ql.os.utils.va_list(format, arglist) - count = ql.os.utils.sprintf(buff, format, args, wstring=False) - ql.os.utils.update_ellipsis(params, args) + args = ql.os.utils.va_list(arglist) + + count, upd_args = ql.os.utils.sprintf(buff, format, args, wstring=False) + upd_args(params) return count @@ -223,12 +280,10 @@ def hook_printf(ql: Qiling, address: int, params): if format == 0: format = "(null)" - nargs = format.count("%") - ptypes = (POINTER, ) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[1:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.printf(format, args, wstring=False) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.printf(format, args, wstring=False) + upd_args(params) return count @@ -242,12 +297,10 @@ def hook_wprintf(ql: Qiling, address: int, params): if format == 0: format = "(null)" - nargs = format.count("%") - ptypes = (POINTER, ) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[1:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.printf(format, args, wstring=True) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.printf(format, args, wstring=True) + upd_args(params) return count @@ -264,9 +317,10 @@ def __stdio_common_vfprintf(ql: Qiling, address: int, params, wstring: bool): # TODO: take _Stream into account - args = ql.os.utils.va_list(format, arglist) - count = ql.os.utils.printf(format, args, wstring) - ql.os.utils.update_ellipsis(params, args) + args = ql.os.utils.va_list(arglist) + + count, upd_args = ql.os.utils.printf(format, args, wstring) + upd_args(params) return count @@ -297,9 +351,10 @@ def __stdio_common_vsprintf(ql: Qiling, address: int, params, wstring: bool): # TODO: take _BufferCount into account - args = ql.os.utils.va_list(format, arglist) - count = ql.os.utils.sprintf(buff, format, args, wstring) - ql.os.utils.update_ellipsis(params, args) + args = ql.os.utils.va_list(arglist) + + count, upd_args = ql.os.utils.sprintf(buff, format, args, wstring) + upd_args(params) return count @@ -373,6 +428,19 @@ def hook___stdio_common_vswprintf_s(ql: Qiling, address: int, params): def hook___lconv_init(ql: Qiling, address: int, params): return 0 +@winsdkapi(cc=CDECL, params={ + 'str' : POINTER, + 'maxsize' : SIZE_T +}) +def hook___strncnt(ql: Qiling, address: int, params): + s = params["str"] + maxsize = params["maxsize"] + + data = ql.mem.read(s, maxsize) + + # a simple hack to make sure a null terminator is found at most at 'maxsize' + return (data + b'\x00').find(b'\00') + # size_t strlen( # const char *str # ); diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 5f424f777..ab9b4ae52 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -66,9 +66,12 @@ def _QueryInformationProcess(ql: Qiling, address: int, params): addr = ql.os.heap.alloc(pbi.size) pbi.write(addr) value = ql.pack(addr) + else: - ql.log.debug(str(flag)) - raise QlErrorNotImplemented("API not implemented") + # TODO: support more info class ("flag") values + ql.log.info(f'SetInformationProcess: no implementation for info class {flag:#04x}') + + return STATUS_UNSUCCESSFUL ql.log.debug("The target is checking the debugger via QueryInformationProcess ") ql.mem.write(dst, value) @@ -289,7 +292,7 @@ def _SetInformationProcess(ql: Qiling, address: int, params): process = params["ProcessHandle"] flag = params["ProcessInformationClass"] dst = params["ProcessInformation"] - pt_res = params["ReturnLength"] + dst_size = params["ProcessInformationLength"] if flag == ProcessDebugFlags: value = b"\x01" * 0x4 @@ -324,9 +327,12 @@ def _SetInformationProcess(ql: Qiling, address: int, params): addr = ql.os.heap.alloc(pbi.size) pbi.write(addr) value = ql.pack(addr) + else: - ql.log.debug(str(flag)) - raise QlErrorNotImplemented("API not implemented") + # TODO: support more info class ("flag") values + ql.log.info(f'SetInformationProcess: no implementation for info class {flag:#04x}') + + return STATUS_UNSUCCESSFUL # TODO: value is never used after assignment diff --git a/qiling/os/windows/dlls/ntoskrnl.py b/qiling/os/windows/dlls/ntoskrnl.py index c54d274e8..dcdaf2eb7 100644 --- a/qiling/os/windows/dlls/ntoskrnl.py +++ b/qiling/os/windows/dlls/ntoskrnl.py @@ -124,12 +124,10 @@ def hook_DbgPrintEx(ql: Qiling, address: int, params): if Format == 0: Format = "(null)" - nargs = Format.count("%") - ptypes = (ULONG, ULONG, POINTER) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[3:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.printf(Format, args, wstring=False) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.printf(Format, args, wstring=False) + upd_args(params) return count @@ -147,12 +145,10 @@ def hook_DbgPrint(ql: Qiling, address: int, params): if Format == 0: Format = "(null)" - nargs = Format.count("%") - ptypes = (POINTER, ) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[1:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.printf(Format, args, wstring=False) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.printf(Format, args, wstring=False) + upd_args(params) return count diff --git a/qiling/os/windows/dlls/shell32.py b/qiling/os/windows/dlls/shell32.py index 18bcc56c1..7c6e4dd96 100644 --- a/qiling/os/windows/dlls/shell32.py +++ b/qiling/os/windows/dlls/shell32.py @@ -13,7 +13,7 @@ from qiling.os.windows.fncc import * from qiling.os.windows.handle import Handle -from qiling.os.windows.thread import QlWindowsThread +from qiling.os.windows.thread import QlWindowsThread, THREAD_STATUS from qiling.exception import QlErrorNotImplemented from qiling.os.windows.structs import ShellExecuteInfoA @@ -74,7 +74,7 @@ def __wstr(shellex: Sequence): if obj.show[0] == SW_HIDE: ql.log.debug(" | With an hidden window") - process = QlWindowsThread(ql, status=0, isFake=True) + process = QlWindowsThread(ql, status=THREAD_STATUS.READY) handle = Handle(obj=process) ql.os.handle_manager.append(handle) diff --git a/qiling/os/windows/dlls/user32.py b/qiling/os/windows/dlls/user32.py index a761ded42..ef8af547a 100644 --- a/qiling/os/windows/dlls/user32.py +++ b/qiling/os/windows/dlls/user32.py @@ -156,13 +156,15 @@ def hook_CloseClipboard(ql: Qiling, address: int, params): 'hMem' : HANDLE }) def hook_SetClipboardData(ql: Qiling, address: int, params): - try: - data = bytes(params['hMem'], 'ascii', 'ignore') - except (UnicodeEncodeError, TypeError): - data = b"" - ql.log.debug('Failed to set clipboard data') + uFormat = params['uFormat'] + hMem = params['hMem'] - return ql.os.clipboard.set_data(params['uFormat'], data) + handle = ql.os.clipboard.set_data(uFormat, hMem) + + if not handle: + ql.log.debug(f'Failed to set clipboard data (format = {uFormat})') + + return handle # HANDLE GetClipboardData( @@ -172,14 +174,17 @@ def hook_SetClipboardData(ql: Qiling, address: int, params): 'uFormat' : UINT }) def hook_GetClipboardData(ql: Qiling, address: int, params): - data = ql.os.clipboard.get_data(params['uFormat']) + uFormat = params['uFormat'] + + data = ql.os.clipboard.get_data(uFormat) if data: addr = ql.os.heap.alloc(len(data)) ql.mem.write(addr, data) + else: + ql.log.debug(f'Failed to get clipboard data (format = {uFormat})') addr = 0 - ql.log.debug('Failed to get clipboard data') return addr @@ -690,12 +695,10 @@ def hook_wsprintfW(ql: Qiling, address: int, params): if Format == 0: Format = "(null)" - nargs = Format.count("%") - ptypes = (POINTER, POINTER) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[2:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.sprintf(Buffer, Format, args, wstring=True) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.sprintf(Buffer, Format, args, wstring=True) + upd_args(params) return count @@ -771,12 +774,10 @@ def hook_wsprintfA(ql: Qiling, address: int, params): if Format == 0: Format = "(null)" - nargs = Format.count("%") - ptypes = (POINTER, POINTER) + (PARAM_INTN, ) * nargs - args = ql.os.fcall.readParams(ptypes)[2:] + args = ql.os.fcall.readEllipsis(params.values()) - count = ql.os.utils.sprintf(Buffer, Format, args, wstring=False) - ql.os.utils.update_ellipsis(params, args) + count, upd_args = ql.os.utils.sprintf(Buffer, Format, args, wstring=False) + upd_args(params) return count @@ -793,17 +794,27 @@ def hook_wsprintfA(ql: Qiling, address: int, params): 'uType' : UINT }) def hook_MessageBoxW(ql: Qiling, address: int, params): - # We always return a positive result - type_box = params["uType"] - - if type_box in (MB_OK, MB_OKCANCEL): - return IDOK - - if type_box in (MB_YESNO, MB_YESNOCANCEL): - return IDYES - - ql.log.debug(type_box) - raise QlErrorNotImplemented("API not implemented") + uType = params["uType"] + + buttons = uType & 0x0000000f + # icon = uType & 0x000000f0 + # default = uType & 0x00000f00 + # modal = uType & 0x0000f000 + # order = uType & 0x00ff0000 + + # we strive to return a positive result when possible. + # if there is an "ok", "yes" or "continue" button, press it + press = { + MB_OK : IDOK, + MB_OKCANCEL : IDOK, + MB_ABORTRETRYIGNORE : IDABORT, + MB_YESNOCANCEL : IDYES, + MB_YESNO : IDYES, + MB_RETRYCANCEL : IDCANCEL, + MB_CANCELTRYCONTINUE : IDCONTINUE + } + + return press[buttons] # int MessageBoxA( # HWND hWnd, diff --git a/qiling/os/windows/handle.py b/qiling/os/windows/handle.py index b5776c961..8e52d976c 100644 --- a/qiling/os/windows/handle.py +++ b/qiling/os/windows/handle.py @@ -32,24 +32,25 @@ class HandleManager: STDOUT = Handle(id=STD_OUTPUT_HANDLE) STDERR = Handle(id=STD_ERROR_HANDLE) - # Register - HKEY_CLASSES_ROOT = Handle(id=0x80000000) - HKEY_CURRENT_CONFIG = Handle(id=0x80000005) - HKEY_CURRENT_USER = Handle(id=0x80000001) - HKEY_CURRENT_USER_LOCAL_SETTINGS = Handle(id=0x80000007) - HKEY_LOCAL_MACHINE = Handle(id=0x80000002) - HKEY_PERFORMANCE_DATA = Handle(id=0x80000004) - HKEY_PERFORMANCE_NLSTEXT = Handle(id=0x80000060) - HKEY_PERFORMANCE_TEXT = Handle(id=0x80000050) - HKEY_USERS = Handle(id=0x80000003) - def __init__(self): self.handles: MutableMapping[int, Handle] = {} + # standard io streams self.append(HandleManager.STDIN) self.append(HandleManager.STDOUT) self.append(HandleManager.STDERR) + # registry hives + self.append(Handle(id=0x80000000, name='HKEY_CLASSES_ROOT')) + self.append(Handle(id=0x80000001, name='HKEY_CURRENT_USER')) + self.append(Handle(id=0x80000002, name='HKEY_LOCAL_MACHINE')) + self.append(Handle(id=0x80000003, name='HKEY_USERS')) + self.append(Handle(id=0x80000004, name='HKEY_PERFORMANCE_DATA')) + self.append(Handle(id=0x80000005, name='HKEY_CURRENT_CONFIG')) + self.append(Handle(id=0x80000007, name='HKEY_CURRENT_USER_LOCAL_SETTINGS')) + self.append(Handle(id=0x80000060, name='HKEY_PERFORMANCE_NLSTEXT')) + self.append(Handle(id=0x80000050, name='HKEY_PERFORMANCE_TEXT')) + def append(self, handle: Handle) -> None: self.handles[handle.id] = handle diff --git a/qiling/os/windows/registry.py b/qiling/os/windows/registry.py index 70dbff3cc..5cd0a6a68 100644 --- a/qiling/os/windows/registry.py +++ b/qiling/os/windows/registry.py @@ -74,7 +74,7 @@ def write(self, key: str, subkey: str, reg_type: int, data: Union[str, bytes, in def save(self, fname: str): if self.conf: with open(fname, 'wb') as ofile: - data = json.dumps(self.conf) + data = json.dumps(self.conf, indent=4) ofile.write(data.encode('utf-8')) diff --git a/qiling/os/windows/structs.py b/qiling/os/windows/structs.py index 84412a571..fd30b4b46 100644 --- a/qiling/os/windows/structs.py +++ b/qiling/os/windows/structs.py @@ -1072,6 +1072,13 @@ class LdrDataTableEntry(Struct): return LdrDataTableEntry() +class FILETIME(ctypes.LittleEndianStructure): + _fields_ = ( + ('dwLowDateTime', ctypes.c_uint32), + ('dwHighDateTime', ctypes.c_int32) + ) + + class WindowsStruct: def __init__(self, ql): @@ -1102,14 +1109,14 @@ def generic_write(self, addr: int, attributes: list): (val, size, endianness, typ) = elem if typ == int: value = val.to_bytes(size, endianness) - self.ql.log.debug("Writing to %d with value %s" % (addr + already_written, value)) + self.ql.log.debug("Writing to %#x with value %s" % (addr + already_written, value)) self.ql.mem.write(addr + already_written, value) elif typ == bytes: if isinstance(val, bytearray): value = bytes(val) else: value = val - self.ql.log.debug("Writing at addr %d value %s" % (addr + already_written, value)) + self.ql.log.debug("Writing at addr %#x value %s" % (addr + already_written, value)) self.ql.mem.write(addr + already_written, value) elif issubclass(typ, WindowsStruct): @@ -1126,7 +1133,7 @@ def generic_read(self, addr: int, attributes: list): for elem in attributes: (val, size, endianness, type) = elem value = self.ql.mem.read(addr + already_read, size) - self.ql.log.debug("Reading from %d value %s" % (addr + already_read, value)) + self.ql.log.debug("Reading from %#x value %s" % (addr + already_read, value)) if type == int: elem[0] = int.from_bytes(value, endianness) elif type == bytes: @@ -1731,9 +1738,9 @@ def __init__(self, ql, length=None, maxLength=None, buffer=None): else: self.size = self.USHORT_SIZE * 2 + 4 + self.POINTER_SIZE - self.length = [length, self.USHORT_SIZE, "little", int, self.USHORT_SIZE] - self.maxLength = [maxLength, self.USHORT_SIZE, "little", int, self.USHORT_SIZE] - self.buffer = [buffer, self.POINTER_SIZE, "little", int, self.POINTER_SIZE] + self.length = [length, self.USHORT_SIZE, "little", int] + self.maxLength = [maxLength, self.USHORT_SIZE, "little", int] + self.buffer = [buffer, self.POINTER_SIZE, "little", int] # typedef struct _OBJECT_TYPE_INFORMATION { diff --git a/qiling/os/windows/thread.py b/qiling/os/windows/thread.py index ca36a4772..3c0d3bb5b 100644 --- a/qiling/os/windows/thread.py +++ b/qiling/os/windows/thread.py @@ -3,23 +3,26 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +from enum import Enum from typing import TYPE_CHECKING, cast from qiling import Qiling -from qiling.const import QL_ARCH +from qiling.const import QL_ARCH, QL_HOOK_BLOCK from qiling.os.thread import QlThread if TYPE_CHECKING: from qiling.os.windows.windows import QlOsWindows -class QlWindowsThread(QlThread): - # static var - ID = 0 +class THREAD_STATUS(Enum): READY = 0 RUNNING = 1 TERMINATED = 2 - def __init__(self, ql: Qiling, status: int = 1, isFake: bool = False): +class QlWindowsThread(QlThread): + # static var + ID = 0 + + def __init__(self, ql: Qiling, status: THREAD_STATUS = THREAD_STATUS.RUNNING): super().__init__(ql) self.ql = ql @@ -29,56 +32,56 @@ def __init__(self, ql: Qiling, status: int = 1, isFake: bool = False): self.waitforthreads = [] self.tls = {} self.tls_index = 0 - self.fake = isFake - # create new thread - def create(self, func_addr: int, func_params: int, status: int) -> int: - os = cast('QlOsWindows', self.ql.os) + # create a new thread with context + @classmethod + def create(cls, ql: Qiling, stack_size: int, func_addr: int, func_params: int, status: THREAD_STATUS) -> 'QlWindowsThread': + os = cast('QlOsWindows', ql.os) + + thread = cls(ql, status) # create new stack - stack_size = 1024 new_stack = os.heap.alloc(stack_size) + stack_size - asize = self.ql.arch.pointersize - context = self.ql.arch.regs.save() + asize = ql.arch.pointersize + context = ql.arch.regs.save() # set return address - self.ql.mem.write_ptr(new_stack - asize, os.thread_manager.thread_ret_addr) + ql.mem.write_ptr(new_stack - asize, os.thread_manager.thread_ret_addr) # set parameters - if self.ql.arch.type == QL_ARCH.X86: - self.ql.mem.write_ptr(new_stack, func_params) - elif self.ql.arch.type == QL_ARCH.X8664: + if ql.arch.type == QL_ARCH.X86: + ql.mem.write_ptr(new_stack, func_params) + elif ql.arch.type == QL_ARCH.X8664: context["rcx"] = func_params # set eip/rip, ebp/rbp, esp/rsp - if self.ql.arch.type == QL_ARCH.X86: + if ql.arch.type == QL_ARCH.X86: context["eip"] = func_addr context["ebp"] = new_stack - asize context["esp"] = new_stack - asize - elif self.ql.arch.type == QL_ARCH.X8664: + elif ql.arch.type == QL_ARCH.X8664: context["rip"] = func_addr context["rbp"] = new_stack - asize context["rsp"] = new_stack - asize - self.saved_context = context - self.status = status + thread.saved_context = context - return self.id + return thread def suspend(self) -> None: self.saved_context = self.ql.arch.regs.save() def resume(self) -> None: self.ql.arch.regs.restore(self.saved_context) - self.status = QlWindowsThread.RUNNING + self.status = THREAD_STATUS.RUNNING def stop(self) -> None: - self.status = QlWindowsThread.TERMINATED + self.status = THREAD_STATUS.TERMINATED def is_stop(self) -> bool: - return self.status == QlWindowsThread.TERMINATED + return self.status == THREAD_STATUS.TERMINATED def waitfor(self, thread: 'QlWindowsThread') -> None: self.waitforthreads.append(thread) @@ -88,13 +91,12 @@ def has_waitfor(self) -> bool: # Simple Thread Manager -class QlWindowsThreadManagement(QlThread): +class QlWindowsThreadManagement: TIME_SLICE = 10 def __init__(self, ql: Qiling, os: 'QlOsWindows', cur_thread: QlWindowsThread): - super().__init__(ql) - self.ql = ql + # main thread self.cur_thread = cur_thread self.threads = [self.cur_thread] @@ -110,29 +112,37 @@ def __thread_scheduler(ql: Qiling, address: int, size: int): else: self.icount += 1 - self.do_schedule() + switched = self.do_schedule() + + # in case another thread was resumed, all remaining hooks should be skipped to prevent them + # from running with the new thread's context. + + return QL_HOOK_BLOCK if switched else 0 ql.hook_code(__thread_scheduler) def append(self, thread: QlWindowsThread): self.threads.append(thread) - def need_schedule(self): - return self.cur_thread.is_stop() or (self.icount % QlWindowsThreadManagement.TIME_SLICE) == 0 + def do_schedule(self) -> bool: + need_schedule = self.cur_thread.is_stop() or (self.icount % QlWindowsThreadManagement.TIME_SLICE) == 0 + switched = False - def do_schedule(self) -> None: - if self.need_schedule(): + if need_schedule: # if there is less than one thread, this loop won't run for i in range(1, len(self.threads)): next_id = (self.cur_thread.id + i) % len(self.threads) next_thread = self.threads[next_id] # find next thread - if next_thread.status == QlWindowsThread.RUNNING and not next_thread.has_waitfor(): + if next_thread.status == THREAD_STATUS.RUNNING and not next_thread.has_waitfor(): if not self.cur_thread.is_stop(): self.cur_thread.suspend() next_thread.resume() self.cur_thread = next_thread + switched = True break + + return switched diff --git a/qiling/profiles/windows.ql b/qiling/profiles/windows.ql index 17488330a..8dbf184ba 100644 --- a/qiling/profiles/windows.ql +++ b/qiling/profiles/windows.ql @@ -6,7 +6,8 @@ stack_size = 0x40000 image_address = 0x400000 dll_address = 0x7ffff0000000 entry_point = 0x140000000 -KI_USER_SHARED_DATA = 0xfffff78000000000 +# KI_USER_SHARED_DATA = 0xfffff78000000000 +KI_USER_SHARED_DATA = 0x7ffe0000 [OS32] heap_address = 0x5000000 @@ -16,7 +17,8 @@ stack_size = 0x21000 image_address = 0x400000 dll_address = 0x10000000 entry_point = 0x40000 -KI_USER_SHARED_DATA = 0xffdf0000 +# KI_USER_SHARED_DATA = 0xffdf0000 +KI_USER_SHARED_DATA = 0x7ffe0000 [CODE] # ram_size 0xa00000 is 10MB diff --git a/tests/test_pe.py b/tests/test_pe.py index 176bb894b..f1cb73fb2 100644 --- a/tests/test_pe.py +++ b/tests/test_pe.py @@ -218,22 +218,21 @@ def __rand_name(minlen: int, maxlen: int) -> str: def test_pe_win_x86_multithread(self): def _t(): - thread_id = None - def ThreadId_onEnter(ql, address, params): + thread_id = -1 + + def ThreadId_onEnter(ql: Qiling, address: int, params): nonlocal thread_id + thread_id = ql.os.thread_manager.cur_thread.id - return address, params ql = Qiling(["../examples/rootfs/x86_windows/bin/MultiThread.exe"], "../examples/rootfs/x86_windows") ql.os.set_api("GetCurrentThreadId", ThreadId_onEnter, QL_INTERCEPT.ENTER) ql.run() - - if not ( 1<= thread_id < 255): - return False - + del ql - return True - + + return (1 <= thread_id < 255) + self.assertTrue(QLWinSingleTest(_t).run())