From 2e2fef95a2fa8f88e283da334ce21f1b29e3735c Mon Sep 17 00:00:00 2001 From: xlatbx59 Date: Tue, 25 Nov 2025 12:01:09 +0100 Subject: [PATCH 1/3] Fix elf stack, pe stack and pe sections memory protection --- qiling/loader/elf.py | 2 +- qiling/loader/pe.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/qiling/loader/elf.py b/qiling/loader/elf.py index a5a8ac6fe..9c8c22d64 100644 --- a/qiling/loader/elf.py +++ b/qiling/loader/elf.py @@ -88,7 +88,7 @@ def run(self): stack_address = self.profile.getint('stack_address') stack_size = self.profile.getint('stack_size') top_of_stack = stack_address + stack_size - self.ql.mem.map(stack_address, stack_size, info='[stack]') + self.ql.mem.map(stack_address, stack_size, UC_PROT_READ | UC_PROT_WRITE, info='[stack]') self.path = self.ql.path diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 30959d0f8..59e37031f 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -15,6 +15,7 @@ from unicorn import UcError from unicorn.x86_const import UC_X86_REG_CR4, UC_X86_REG_CR8 +from unicorn.unicorn_const import UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC, UC_PROT_NONE from qiling.arch.x86_const import FS_SEGMENT_ADDR, GS_SEGMENT_ADDR from qiling.const import QL_ARCH, QL_STATE @@ -381,7 +382,8 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: dll_len = image_size self.dll_size += dll_len - self.ql.mem.map(dll_base, dll_len, info=dll_name) + self.ql.mem.map(dll_base, dll_len, UC_PROT_READ, info=dll_name) + self.protect_sections(dll_base, dll) self.ql.mem.write(dll_base, bytes(data)) if dll_base == self.dll_last_address: @@ -879,11 +881,39 @@ def run(self): self.cmdline = bytes(f'{cmdline} {cmdargs}\x00', "utf-8") self.load(pe) + + def protect_sections(self, image_base: int, pe: pefile.PE): + for section in pe.sections: + mem_prot = UC_PROT_NONE + prot_num = 0 + log_str = str(section.Name, 'utf-8') + ' section memory protection: ' + prot_str = [None, None, None] + + if section.Characteristics & pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_EXECUTE']: + mem_prot |= UC_PROT_EXEC + prot_str[prot_num] = 'IMAGE_SCN_MEM_EXECUTE' + prot_num += 1 + if section.Characteristics & pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_READ']: + mem_prot |= UC_PROT_READ + prot_str[prot_num] = 'IMAGE_SCN_MEM_READ' + prot_num += 1 + if section.Characteristics & pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_WRITE']: + mem_prot |= UC_PROT_WRITE + prot_str[prot_num] = 'IMAGE_SCN_MEM_WRITE' + prot_num += 1 + + for i in range(prot_num): + if i != 0: + log_str += ', ' + log_str += prot_str[i] + + self.ql.log.info(log_str) + self.ql.mem.protect(image_base + section.VirtualAddress, (int(section.Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment) + 1) * pe.OPTIONAL_HEADER.SectionAlignment, mem_prot) def load(self, pe: Optional[pefile.PE]): # set stack pointer self.ql.log.info("Initiate stack address at 0x%x " % self.stack_address) - self.ql.mem.map(self.stack_address, self.stack_size, info="[stack]") + self.ql.mem.map(self.stack_address, self.stack_size, UC_PROT_READ | UC_PROT_WRITE, info="[stack]") if pe is not None: image_name = os.path.basename(self.path) @@ -902,7 +932,8 @@ 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, UC_PROT_READ, info=f'{image_name}') + self.protect_sections(image_base, pe) self.images.append(Image(image_base, image_base + pe.NT_HEADERS.OPTIONAL_HEADER.SizeOfImage, os.path.abspath(self.path))) if self.is_driver: From 8b12edefcc2908b23ea783d675b2e980a86e5600 Mon Sep 17 00:00:00 2001 From: xlatbx59 Date: Fri, 28 Nov 2025 13:47:23 +0100 Subject: [PATCH 2/3] Fix elf stack, pe stack and pe sections memory protection according to headers --- qiling/loader/elf.py | 18 ++++++++---- qiling/loader/pe.py | 70 ++++++++++++++++++++++---------------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/qiling/loader/elf.py b/qiling/loader/elf.py index 9c8c22d64..a2601877b 100644 --- a/qiling/loader/elf.py +++ b/qiling/loader/elf.py @@ -84,12 +84,6 @@ def run(self): self.profile = self.ql.os.profile[f'OS{self.ql.arch.bits}'] - # setup program stack - stack_address = self.profile.getint('stack_address') - stack_size = self.profile.getint('stack_size') - top_of_stack = stack_address + stack_size - self.ql.mem.map(stack_address, stack_size, UC_PROT_READ | UC_PROT_WRITE, info='[stack]') - self.path = self.ql.path with open(self.path, 'rb') as infile: @@ -98,6 +92,18 @@ def run(self): elffile = ELFFile(fstream) elftype = elffile['e_type'] + stack_perm = UC_PROT_NONE + for seg in elffile.iter_segments('PT_GNU_STACK'): + stack_perm = QlLoaderELF.seg_perm_to_uc_prot(seg['p_flags']) + + QlLoaderELF.seg_perm_to_uc_prot(stack_perm) + + # setup program stack + stack_address = self.profile.getint('stack_address') + stack_size = self.profile.getint('stack_size') + top_of_stack = stack_address + stack_size + self.ql.mem.map(stack_address, stack_size, stack_perm, info='[stack]') + # is it a driver? if elftype == 'ET_REL': self.load_driver(elffile, top_of_stack, loadbase=0x8000000) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 59e37031f..a9bce32e0 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -382,9 +382,7 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: dll_len = image_size self.dll_size += dll_len - self.ql.mem.map(dll_base, dll_len, UC_PROT_READ, info=dll_name) - self.protect_sections(dll_base, dll) - self.ql.mem.write(dll_base, bytes(data)) + _map_sections_(self.ql, dll, dll_base, dll_len, dll_name) if dll_base == self.dll_last_address: self.dll_last_address = self.ql.mem.align_up(self.dll_last_address + dll_len, 0x10000) @@ -815,6 +813,35 @@ def init_security_cookie(self, pe: pefile.PE, image_base: int): cookie = secrets.randbits(self.ql.arch.bits - 16) self.ql.mem.write_ptr(cookie_rva + image_base, cookie) + +def _map_sections_(ql : Qiling, pe : pefile.PE, image_base: int, image_size : int, image_name : str): + """Load file sections to memory, each in its own memory region protected by + its defined permissions. That allows separation of code and data, which makes + it easier to detect abnomal behavior or memory corruptions. + """ + + # load the header + hdr_base = image_base + hdr_perm = UC_PROT_READ + + ql.mem.map(hdr_base, image_size, hdr_perm, image_name) + + # load sections + for section in pe.sections: + if not section.IMAGE_SCN_MEM_DISCARDABLE: + sec_name = section.Name.rstrip(b'\x00').decode() + sec_data = bytes(section.get_data(ignore_padding=True)) + sec_base = image_base + section.VirtualAddress + sec_size = (int(section.Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment) + 1) * pe.OPTIONAL_HEADER.SectionAlignment + + sec_perm = sum(( + section.IMAGE_SCN_MEM_READ * UC_PROT_READ, + section.IMAGE_SCN_MEM_WRITE * UC_PROT_WRITE, + section.IMAGE_SCN_MEM_EXECUTE * UC_PROT_EXEC + )) + + ql.mem.protect(sec_base, sec_size, sec_perm) + ql.mem.write(image_base, bytes(pe.get_memory_mapped_image())) class QlLoaderPE(QlLoader, Process): def __init__(self, ql: Qiling, libcache: bool): @@ -881,45 +908,20 @@ def run(self): self.cmdline = bytes(f'{cmdline} {cmdargs}\x00', "utf-8") self.load(pe) - - def protect_sections(self, image_base: int, pe: pefile.PE): - for section in pe.sections: - mem_prot = UC_PROT_NONE - prot_num = 0 - log_str = str(section.Name, 'utf-8') + ' section memory protection: ' - prot_str = [None, None, None] - - if section.Characteristics & pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_EXECUTE']: - mem_prot |= UC_PROT_EXEC - prot_str[prot_num] = 'IMAGE_SCN_MEM_EXECUTE' - prot_num += 1 - if section.Characteristics & pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_READ']: - mem_prot |= UC_PROT_READ - prot_str[prot_num] = 'IMAGE_SCN_MEM_READ' - prot_num += 1 - if section.Characteristics & pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_WRITE']: - mem_prot |= UC_PROT_WRITE - prot_str[prot_num] = 'IMAGE_SCN_MEM_WRITE' - prot_num += 1 - - for i in range(prot_num): - if i != 0: - log_str += ', ' - log_str += prot_str[i] - - self.ql.log.info(log_str) - self.ql.mem.protect(image_base + section.VirtualAddress, (int(section.Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment) + 1) * pe.OPTIONAL_HEADER.SectionAlignment, mem_prot) def load(self, pe: Optional[pefile.PE]): # set stack pointer self.ql.log.info("Initiate stack address at 0x%x " % self.stack_address) - self.ql.mem.map(self.stack_address, self.stack_size, UC_PROT_READ | UC_PROT_WRITE, info="[stack]") + self.ql.mem.map(self.stack_address, self.stack_size, info="[stack]") if pe is not None: image_name = os.path.basename(self.path) image_base = pe.OPTIONAL_HEADER.ImageBase image_size = self.ql.mem.align_up(pe.OPTIONAL_HEADER.SizeOfImage) + if pe.OPTIONAL_HEADER.DllCharacteristics & pefile.DLL_CHARACTERISTICS['IMAGE_DLLCHARACTERISTICS_NX_COMPAT']: + self.ql.mem.protect(self.stack_address, self.stack_size, UC_PROT_WRITE | UC_PROT_READ) + # if default base address is taken, use the one specified in profile if not self.ql.mem.is_available(image_base, image_size): image_base = self.image_address @@ -932,8 +934,7 @@ 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, UC_PROT_READ, info=f'{image_name}') - self.protect_sections(image_base, pe) + _map_sections_(self.ql, pe, image_base, image_size, 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: @@ -963,7 +964,6 @@ def load(self, pe: Optional[pefile.PE]): pe.parse_data_directories() # done manipulating pe file; write its contents into memory - self.ql.mem.write(image_base, bytes(pe.get_memory_mapped_image())) if self.is_driver: # security cookie can be written only after image has been loaded to memory From e3c8ef2cbabd25eb2e32559ec00e3295ad51ca1b Mon Sep 17 00:00:00 2001 From: xlatbx59 Date: Sun, 30 Nov 2025 14:22:35 +0100 Subject: [PATCH 3/3] Fix pe section loading --- qiling/loader/elf.py | 8 ++------ qiling/loader/pe.py | 39 +++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/qiling/loader/elf.py b/qiling/loader/elf.py index a2601877b..4725f7d64 100644 --- a/qiling/loader/elf.py +++ b/qiling/loader/elf.py @@ -92,13 +92,9 @@ def run(self): elffile = ELFFile(fstream) elftype = elffile['e_type'] - stack_perm = UC_PROT_NONE - for seg in elffile.iter_segments('PT_GNU_STACK'): - stack_perm = QlLoaderELF.seg_perm_to_uc_prot(seg['p_flags']) - - QlLoaderELF.seg_perm_to_uc_prot(stack_perm) - # setup program stack + stack_seg = elffile.iter_segments('PT_GNU_STACK') + stack_perm = QlLoaderELF.seg_perm_to_uc_prot(next(stack_seg)['p_flags']) stack_address = self.profile.getint('stack_address') stack_size = self.profile.getint('stack_size') top_of_stack = stack_address + stack_size diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index a9bce32e0..7e4f6b422 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -382,7 +382,8 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: dll_len = image_size self.dll_size += dll_len - _map_sections_(self.ql, dll, dll_base, dll_len, dll_name) + _map_pe_(self.ql, dll, dll_base, dll_len, dll_name) + self.ql.mem.write(dll_base, bytes(data)) if dll_base == self.dll_last_address: self.dll_last_address = self.ql.mem.align_up(self.dll_last_address + dll_len, 0x10000) @@ -813,13 +814,19 @@ def init_security_cookie(self, pe: pefile.PE, image_base: int): cookie = secrets.randbits(self.ql.arch.bits - 16) self.ql.mem.write_ptr(cookie_rva + image_base, cookie) - -def _map_sections_(ql : Qiling, pe : pefile.PE, image_base: int, image_size : int, image_name : str): + +def _map_pe_(ql : Qiling, pe : pefile.PE, image_base: int, image_size : int, image_name : str): """Load file sections to memory, each in its own memory region protected by its defined permissions. That allows separation of code and data, which makes it easier to detect abnomal behavior or memory corruptions. """ + # if sections are aligned to page, we can map them separately + sec_alignment = pe.OPTIONAL_HEADER.SectionAlignment + if (sec_alignment % ql.mem.pagesize) != 0: + ql.mem.map(image_base, image_size, info=f'{image_name}') + return + # load the header hdr_base = image_base hdr_perm = UC_PROT_READ @@ -828,20 +835,18 @@ def _map_sections_(ql : Qiling, pe : pefile.PE, image_base: int, image_size : in # load sections for section in pe.sections: - if not section.IMAGE_SCN_MEM_DISCARDABLE: - sec_name = section.Name.rstrip(b'\x00').decode() - sec_data = bytes(section.get_data(ignore_padding=True)) - sec_base = image_base + section.VirtualAddress - sec_size = (int(section.Misc_VirtualSize / pe.OPTIONAL_HEADER.SectionAlignment) + 1) * pe.OPTIONAL_HEADER.SectionAlignment + sec_name = section.Name.rstrip(b'\x00').decode() + sec_base = image_base + section.VirtualAddress + sec_size = ql.mem.align_up(section.Misc_VirtualSize, sec_alignment) - sec_perm = sum(( - section.IMAGE_SCN_MEM_READ * UC_PROT_READ, - section.IMAGE_SCN_MEM_WRITE * UC_PROT_WRITE, - section.IMAGE_SCN_MEM_EXECUTE * UC_PROT_EXEC - )) + sec_perm = sum(( + section.IMAGE_SCN_MEM_READ * UC_PROT_READ, + section.IMAGE_SCN_MEM_WRITE * UC_PROT_WRITE, + section.IMAGE_SCN_MEM_EXECUTE * UC_PROT_EXEC + )) - ql.mem.protect(sec_base, sec_size, sec_perm) - ql.mem.write(image_base, bytes(pe.get_memory_mapped_image())) + ql.mem.protect(sec_base, sec_size, sec_perm) + ql.mem.change_mapinfo(sec_base, sec_base+sec_size, new_perms=sec_perm, new_info=f'{image_name} {sec_name}') class QlLoaderPE(QlLoader, Process): def __init__(self, ql: Qiling, libcache: bool): @@ -934,7 +939,8 @@ 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}') - _map_sections_(self.ql, pe, image_base, image_size, image_name) + _map_pe_(self.ql, pe, image_base, image_size, 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: @@ -964,6 +970,7 @@ def load(self, pe: Optional[pefile.PE]): pe.parse_data_directories() # done manipulating pe file; write its contents into memory + self.ql.mem.write(image_base, bytes(pe.get_memory_mapped_image())) if self.is_driver: # security cookie can be written only after image has been loaded to memory