├── .gitmodules ├── SmhpChunkMetadata.py ├── w10deflate_auto.py ├── StStore.py ├── README.md ├── MiHardwareState.py ├── Smkm.py ├── SmkmStoreMgr.py ├── SmkmStoreMetadata.py ├── Magic.py ├── SmkmStore.py ├── Tools.py ├── StDataMgr.py └── LICENSE.txt /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "flare_emu"] 2 | path = flare_emu 3 | url = https://github.com/fireeye/flare-emu.git 4 | -------------------------------------------------------------------------------- /SmhpChunkMetadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: SmhpChunkMetadata.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | 22 | from Tools import Tools 23 | 24 | 25 | class SmhpChunkMetadata(Tools): 26 | """ 27 | The SmhpChunkMetadata class corresponds to the Windows 10 28 | SMHP_CHUNK_METADATA structure. The file is currently a placeholder on a 29 | lower-priority structure attributed to it not having changed recently. 30 | The structure contains fields used to decode information from the chunk 31 | key identified in ST_DATA_MGR.sLocalTree. The structure is one of the few 32 | involved with page decompression that has not changed through the evolution 33 | of the algorithm. This is not a guarantee of future behavior. 34 | """ 35 | def __init__(self, loglevel=logging.INFO): 36 | self.tools = super(SmhpChunkMetadata, self).__init__() 37 | self.logger = logging.getLogger("SMHP_CHUNK_METADATA") 38 | self.logger.setLevel(loglevel) 39 | self.fe = self.get_flare_emu() 40 | return 41 | 42 | def _dump(self): 43 | """ 44 | Architecture agnostic function used to dump all located fields. 45 | """ 46 | return 47 | 48 | def shcm32_chunkptrarray(self): 49 | return 50 | 51 | def shcm32_bitvalue(self): 52 | return 53 | 54 | def shcm32_pagerecordsperchunkmask(self): 55 | return 56 | 57 | def shcm32_pagerecordsize(self): 58 | return 59 | 60 | def shcm32_chunkpageheadersize(self): 61 | return -------------------------------------------------------------------------------- /w10deflate_auto.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: w10deflate_auto.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | """ 21 | Description: The W10Deflate Auto automation framework is designed to reduce the level of effort 22 | spent analyzing a Windows 10 kernel in search of undocumented structures corresponding to 23 | the Store Manager's RAM-backed Virtual Store. The resolution of these structures enables 24 | end-users to keep the Volatility & Rekall Win10Deflate plugins up-to-date. 25 | 26 | Usage: python w10deflate_auto.py 27 | Environment: IDA Pro 28 | Context: Currently opened database of a Windows 10 ntoskrnl.exe file 29 | """ 30 | import logging 31 | 32 | from Magic import Magic 33 | from SmkmStoreMgr import SmkmStoreMgr 34 | from Smkm import Smkm 35 | from SmkmStoreMetadata import SmkmStoreMetadata 36 | from SmkmStore import SmkmStore 37 | from StStore import StStore 38 | from StDataMgr import StDataMgr 39 | 40 | import idc 41 | 42 | def main(loglevel=logging.INFO): 43 | Magic(loglevel=loglevel)._dump() 44 | SmkmStoreMgr(loglevel=loglevel)._dump() 45 | Smkm(loglevel=loglevel)._dump() 46 | SmkmStoreMetadata(loglevel=loglevel)._dump() 47 | SmkmStore(loglevel=loglevel)._dump() 48 | StStore(loglevel=loglevel)._dump() 49 | StDataMgr(loglevel=loglevel)._dump() 50 | 51 | return 52 | 53 | 54 | if __name__ == "__main__": 55 | logging.basicConfig(level=logging.INFO) 56 | if idc.get_name_ea_simple("_KiSystemStartup@4") == -1: 57 | logging.warning("Launch script from within an ntoskrnl IDB with PDB symbols loaded") 58 | else: 59 | main(loglevel=logging.INFO) 60 | -------------------------------------------------------------------------------- /StStore.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: StStore.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | 22 | from Tools import Tools 23 | 24 | 25 | class StStore(Tools): 26 | """ 27 | The StStore class corresponds to the Windows 10 ST_STORE structure. The 28 | ST_STORE structure is nested within SMKM_STORE and represents a single store. 29 | The nested structure ST_DATA_MGR is the only field of interest in page retrieval. 30 | """ 31 | def __init__(self, loglevel=logging.INFO): 32 | self.tools = super(StStore, self).__init__() 33 | self.logger = logging.getLogger("ST_STORE") 34 | self.logger.setLevel(loglevel) 35 | self.fe = self.get_flare_emu() 36 | return 37 | 38 | def _dump(self): 39 | """ 40 | Architecture agnostic function used to dump all located fields. 41 | """ 42 | arch = 'x64' if self.Info.is_64bit() else 'x86' 43 | self.logger.info("StDataMgr: {0:#x}".format(self.Info.arch_fns[arch]['ss_stdatamgr'](self))) 44 | return 45 | 46 | @Tools.Info.arch32 47 | @Tools.Info.arch64 48 | def ss_stdatamgr(self): 49 | """ 50 | This nested structure contains information used to correlate an SM_PAGE_KEY with a chunk key, 51 | from which a compressed page's location can be derived from within a region of 52 | MemCompression.exe. See ST_DATA_MGR for additional information. This function relies on the 53 | second argument for StDmStart remaining constant. Disassembly snippet from Windows 10 1809 x86 54 | shown below. 55 | 56 | StStart+27A lea edx, [esi+38h] 57 | StStart+27D mov ecx, esi 58 | StStart+27F call ?StDmStart@?$ST_STORE@USM_TRAITS@@@@SGJPAU1@PAU_ST_DATA_MGR@1@... 59 | """ 60 | (startAddr, endAddr) = self.locate_call_in_fn("?StStart", "StDmStart") 61 | self.fe.iterate([endAddr], self.tHook) 62 | reg_dx = 'rdx' if self.Info.is_64bit() else 'edx' 63 | return self.fe.getRegVal(reg_dx) 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Win10Deflate Automated Structure Extraction 2 | `Win10Deflate` currently consists of the FLARE team's Volatility & Rekall plugins designed to support the extraction of compressed pages located in the RAM-backed virtual store. The structures required to decompress these pages are undocumented and ever-changing. The `Win10Deflate Auto` project locates these structures and extracts the corresponding magics & field-offsets of interest for use in FLARE's Volatility & Rekall plugins. The project leverages Tom Bennett's `FLARE-EMU` utility, which provides a series of helper functions to lower the barrier of entry to using the `Unicorn Engine` for emulation. 3 | 4 | ## Setup 5 | 1. Clone repository 6 | 2. If `flare_emu` is installed on your machine, skip to `Usage` 7 | 3. Use `git submodule init` & `git submodule update` to clone the `FLARE-EMU` repository locally 8 | 9 | ## Usage 10 | The `Win10Deflate Auto` script is designed to work in an `IDA Pro 7.x` environment in the context of a Windows 10 `ntoskrnl.exe`. Use `Alt+F7` or `File > Script File` to load `win10deflate_auto.py`. 11 | 12 | ## Output 13 | Expected output will look similar to the output below (`Win10.1809.x64`). 14 | ``` 15 | INFO:Magic:MAGIC.SmGlobals: 0x55a9c0 16 | INFO:Magic:MAGIC.MmPagingFile: 0x43e5e0 17 | INFO:SMKM_STORE_MGR:SMKM_STORE_MGR.sSmKm: 0x0 18 | INFO:SMKM_STORE_MGR:SMKM_STORE_MGR.sGlobalTree: 0x1c0 19 | INFO:SMKM:SMKM.SmkmStoreMetadataArray: 0x0 20 | INFO:SMKM_STORE_METADATA:SMKM_STORE_METADATA.Size: 0x28 21 | INFO:SMKM_STORE_METADATA:SMKM_STORE_METADATA.pSmkmStore: 0x0 22 | INFO:SMKM_STORE:SMKM_STORE.StStore: 0x0 23 | INFO:SMKM_STORE:SMKM_STORE.pCompressedRegionPtrArray: 0x1848 24 | INFO:SMKM_STORE:SMKM_STORE.StoreOwnerProcess: 0x19a8 25 | INFO:ST_STORE:ST_STORE.StDataMgr: 0x50 26 | INFO:ST_STORE:ST_DATA_MGR.sLocalTree: 0x0 27 | INFO:ST_STORE:ST_DATA_MGR.ChunkMetadata: 0xc0 28 | INFO:ST_STORE:ST_DATA_MGR.SmkmStore: 0x320 29 | INFO:ST_STORE:ST_DATA_MGR.RegionSizeMask: 0x328 30 | INFO:ST_STORE:ST_DATA_MGR.RegionLSB: 0x32c 31 | INFO:ST_STORE:ST_DATA_MGR.CompressionFormat: 0x3e0 32 | ``` 33 | 34 | ## Functionality 35 | The `Win10Deflate` automation script relies on known function arguments, callstacks, order of operation, and data manipulation within `ntoskrnl.exe`'s Store Manager functions. By leveraging emulation via `FLARE-EMU`, arguments and structures can be injected into the system, traced, and then located to calculate field offsets in structures of interest. 36 | 37 | ## Additional Reading 38 | 1. [Part 1 - Rekall & Volatility Announcement Blog](https://www.fireeye.com/blog/threat-research/2019/07/finding-evil-in-windows-ten-compressed-memory-part-one.html) 39 | 1. Part 2 - Virtual Store Deep Dive Blog 40 | 1. Part 3 - Automating Undocumented Structure Extraction Blog 41 | 1. BlackHat USA 2019 Whitepaper -------------------------------------------------------------------------------- /MiHardwareState.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: MiHardwareState.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | 22 | import idaapi 23 | 24 | from Tools import Tools 25 | 26 | 27 | class MiHardwareState(Tools): 28 | """ 29 | The _MI_HARDWARE_STATE structure is available as an export in Windows 10. 30 | As of Win10.1803.x64, the derivation of the SM_PAGE_KEY (Store Manager key) 31 | has changed to leverage both the MMPTE_SOFTWARE.SwizzleBit and 32 | MI_HARDWARE_STATE.InvalidPteMask fields. 33 | """ 34 | def __init__(self, loglevel=logging.INFO): 35 | self.tools = super(MiHardwareState, self).__init__() 36 | self.logger = logging.getLogger("MI_HARDWARE_STATE") 37 | self.logger.setLevel(loglevel) 38 | self.fe = self.get_flare_emu() 39 | return 40 | 41 | def _dump(self): 42 | """ 43 | Architecture agnostic function used to dump all located fields. 44 | """ 45 | arch = 'x64' if self.Info.is_64bit() else 'x86' 46 | self.logger.info("InvalidPteMask: {0:#x}".format(self.Info.arch_fns[arch]['mhs_invalidptemask'](self))) 47 | return 48 | 49 | @Tools.Info.arch64 50 | def mhs_invalidptemask(self): 51 | """ 52 | The InvalidPteMask is used in the derivation of the SM_PAGE_KEY in Win10.1803.x64+. 53 | Disassembly snippet from Windows 10 1809 x64 shown below. 54 | 55 | MiSwizzleInvalidPte MiSwizzleInvalidPte proc near ; 56 | MiSwizzleInvalidPte ; 57 | MiSwizzleInvalidPte mov rax, cs:qword_14043A1C0 58 | MiSwizzleInvalidPte+7 test rax, rax 59 | """ 60 | (fn_addr, fn_name) = self.find_ida_name("MiSwizzleInvalidPte") 61 | mHookData = {'offset':None} 62 | 63 | def mHook(uc, accessType, memAccessAddress, memAccessSize, memValue, userData): 64 | if mHookData['offset']: 65 | return 66 | 67 | if accessType == 16: # UC_MEM_READ 68 | self.logger.debug("Mem read @ 0x{0:x}: {1}".format(memAccessAddress, memValue)) 69 | mHookData['offset'] = memAccessAddress 70 | 71 | self.fe.emulateRange(fn_addr, memAccessHook=mHook) 72 | return mHookData['offset'] - idaapi.get_imagebase() 73 | -------------------------------------------------------------------------------- /Smkm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: Smkm.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | import struct 22 | 23 | from Tools import Tools 24 | 25 | 26 | class Smkm(Tools): 27 | """ 28 | The Smkm class corresponds to the Windows 10 SMKM structure.The SMKM 29 | structure is the last global structure used before relying on store-specific 30 | structures to locate the compressed page. 31 | """ 32 | def __init__(self, loglevel=logging.INFO): 33 | self.tools = super(Smkm, self).__init__() 34 | self.logger = logging.getLogger("SMKM") 35 | self.logger.setLevel(loglevel) 36 | self.fe = self.get_flare_emu() 37 | return 38 | 39 | def _dump(self): 40 | """ 41 | Architecture agnostic function used to dump all located fields. 42 | """ 43 | arch = 'x64' if self.Info.is_64bit() else 'x86' 44 | self.logger.info("SmkmStoreMetadataArray: {0:#x}".format(self.Info.arch_fns[arch]['sk_storemetadataarray'](self))) 45 | return 46 | 47 | @Tools.Info.arch32 48 | @Tools.Info.arch64 49 | def sk_storemetadataarray(self): 50 | """ 51 | This is an array of 32 pointers, each of which points to an array of 32 SMKM_STORE_METADATA 52 | structures. The SmKmStoreRefFromStoreIndex function traverses the pointer array. This 53 | signature asks the function to locate Store 0. The value stored in *CX at the end of 54 | function emulation corresponds to the offset of the StoreMetadataArray. Disassembly snippet 55 | from Windows 10 1809 x64 shown below. 56 | 57 | SmKmStoreRefFromStoreIndex SmKmStoreRefFromStoreIndex proc near 58 | SmKmStoreRefFromStoreIndex 59 | SmKmStoreRefFromStoreIndex mov eax, edx 60 | SmKmStoreRefFromStoreIndex+2 shr rax, 5 61 | SmKmStoreRefFromStoreIndex+6 mov r8d, edx 62 | SmKmStoreRefFromStoreIndex+9 mov rdx, [rcx+rax*8] 63 | SmKmStoreRefFromStoreIndex+D test rdx, rdx 64 | SmKmStoreRefFromStoreIndex+10 jnz short loc_140018069 65 | SmKmStoreRefFromStoreIndex+12 xor eax, eax 66 | SmKmStoreRefFromStoreIndex+14 retn 67 | 68 | SmKmStoreRefFromStoreIndex+15 loc_140018069: 69 | SmKmStoreRefFromStoreIndex+15 and r8d, 1Fh 70 | SmKmStoreRefFromStoreIndex+19 lea rax, [r8+r8*4] 71 | SmKmStoreRefFromStoreIndex+1D lea rax, [rdx+rax*8] 72 | SmKmStoreRefFromStoreIndex+21 retn 73 | SmKmStoreRefFromStoreIndex+21 SmKmStoreRefFromStoreIndex endp 74 | """ 75 | (fn_addr, fn_name) = self.find_ida_name("SmKmStoreRefFromStoreIndex") 76 | lp_addr_smkmstoremgr = self.fe.loadBytes(struct.pack(" 5 | Name: SmkmStoreMgr.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | 22 | from Tools import Tools 23 | 24 | 25 | class SmkmStoreMgr(Tools): 26 | """ 27 | The SmkmStoreMgr class corresponds to the Windows 10 SMKM_STORE_MGR 28 | structure. The SMKM_STORE_MGR structure contains information about all the stores 29 | being used by the system. The structure contains a B_TREE of all SM_PAGE_KEYs 30 | being used, as well as a nested structure (SMKM) which is the last global structure 31 | in the path to the compressed page. 32 | """ 33 | def __init__(self, loglevel=logging.INFO): 34 | self.tools = super(SmkmStoreMgr, self).__init__() 35 | self.logger = logging.getLogger("SMKM_STORE_MGR") 36 | self.logger.setLevel(loglevel) 37 | self.fe = self.get_flare_emu() 38 | return 39 | 40 | def _dump(self): 41 | """ 42 | Architecture agnostic function used to dump all located fields. 43 | """ 44 | arch = 'x64' if self.Info.is_64bit() else 'x86' 45 | self.logger.info("sSmKm: {0:#x}".format(self.Info.arch_fns[arch]['sksm_smkm'](self))) 46 | self.logger.info("sGlobalTree: {0:#x}".format(self.Info.arch_fns[arch]['sksm_globaltree'](self))) 47 | return 48 | 49 | @Tools.Info.arch32 50 | @Tools.Info.arch64 51 | def sksm_smkm(self): 52 | """ 53 | This structure is nested within SMKM_STORE_MGR at offset 0. See SMKM for additional 54 | information. Returning offset zero until an update is needed. 55 | """ 56 | return 0 # constant across win10 57 | 58 | @Tools.Info.arch32 59 | @Tools.Info.arch64 60 | def sksm_globaltree(self): 61 | """ 62 | This B+TREE is nested within the SMKM_STORE_MGR and contains leaf nodes of type 63 | SMKM_FRONTEND_ENTRY. The SMKM_FRONTEND_ENTRY structure contains the SM_PAGE_KEY's 64 | store index and creation flags. This function traverses SmFeCheckPresent up until 65 | BTreeSearchKey. It relies on the BTreeSearchKey function being stable in that the 66 | B+TREE is the first argument. Disassembly snippet from Windows 10 1809 x64 shown below. 67 | 68 | SmFeCheckPresent lea rcx, [rdi+1C0h] 69 | SmFeCheckPresent mov eax, [r12] 70 | SmFeCheckPresent lea r8, [rsp+148h+var_108] 71 | SmFeCheckPresent mov r15d, 400h 72 | SmFeCheckPresent mov [rsp+148h+var_128], eax 73 | SmFeCheckPresent mov edx, ebx 74 | SmFeCheckPresent mov [rsp+148h+var_E8], 1 75 | SmFeCheckPresent mov [rsp+148h+var_EC], 8 76 | SmFeCheckPresent xor esi, esi 77 | SmFeCheckPresent mov r14d, r15d 78 | SmFeCheckPresent xor ebp, ebp 79 | SmFeCheckPresent call ?BTreeSearchKey@?$B_TREE@T_SM_PAGE_KEY@@USMKM_FRONTEND_ENTRY... 80 | """ 81 | (startAddr, endAddr) = self.locate_call_in_fn("?SmFeCheckPresent", 82 | "?BTreeSearchKey@?$B_TREE@T_SM_PAGE_KEY@@USMKM_FRONTEND_ENTRY") 83 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 84 | 85 | # @start ecx is SmkmStoreMgr and instantiated to 0. 86 | # @end ecx is the pushlock argument, diff to get struct offset 87 | addr_smkmstoremgr = 0x1000 88 | 89 | def pHook(self, userData, funcStart): 90 | self.logger.debug("pre emulation hook loading ECX") 91 | userData["EmuHelper"].uc.reg_write(userData["EmuHelper"].regs["cx"], addr_smkmstoremgr) 92 | 93 | self.fe.iterate([endAddr], self.tHook, preEmuCallback=pHook) 94 | return self.fe.getRegVal(reg_cx) - addr_smkmstoremgr 95 | -------------------------------------------------------------------------------- /SmkmStoreMetadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: SmkmStoreMetadata.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | 22 | from Tools import Tools 23 | 24 | 25 | class SmkmStoreMetadata(Tools): 26 | """ 27 | The SmkmStoreMetadata class corresponds to the Windows 10 SMKM_STORE_METADATA 28 | structure. Each SMKM_STORE_METADATA structure correlates to a single store, of 29 | the possible 1024 stores (1607+). 30 | """ 31 | def __init__(self, loglevel=logging.INFO): 32 | self.tools = super(SmkmStoreMetadata, self).__init__() 33 | self.logger = logging.getLogger("SMKM_STORE_METADATA") 34 | self.logger.setLevel(loglevel) 35 | self.fe = self.get_flare_emu() 36 | return 37 | 38 | def _dump(self): 39 | """ 40 | Architecture agnostic function used to dump all located fields. 41 | """ 42 | arch = 'x64' if self.Info.is_64bit() else 'x86' 43 | self.logger.info("Size: {0:#x}".format(self.Info.arch_fns[arch]['ssm_sizeof'](self))) 44 | self.logger.info("pSmkmStore: {0:#x}".format(self.Info.arch_fns[arch]['ssm_smkmstore'](self))) 45 | return 46 | 47 | @Tools.Info.arch32 48 | @Tools.Info.arch64 49 | def ssm_sizeof(self): 50 | """ 51 | The size of the SMKM_STORE_METADATA structure is important due to its' presence as an array 52 | of size 32. Traversing the array via index requires you to know the size. The SmKmSToreRefFromStoreIndex 53 | function does this traversal and is the ideal candidate. The emulateRange function is used here 54 | because we are traversing the entire function, not stopping at a certain point. We preset 55 | the Store to 0 and check the value in the *AX register upon completion. Disassembly snippet from 56 | Windows 10 1809 x86 shown below. 57 | 58 | SmKmStoreRefFromStoreIndex _SmKmStoreRefFromStoreIndex@8 proc near ; 59 | SmKmStoreRefFromStoreIndex mov eax, edx 60 | SmKmStoreRefFromStoreIndex+2 shr eax, 5 61 | SmKmStoreRefFromStoreIndex+5 mov ecx, [ecx+eax*4] 62 | SmKmStoreRefFromStoreIndex+8 test ecx, ecx 63 | SmKmStoreRefFromStoreIndex+A jz short loc_4C7D41 64 | SmKmStoreRefFromStoreIndex+C and edx, 1Fh 65 | SmKmStoreRefFromStoreIndex+F imul eax, edx, 14h 66 | SmKmStoreRefFromStoreIndex+12 add eax, ecx 67 | SmKmStoreRefFromStoreIndex+14 retn 68 | """ 69 | (fn_addr, fn_name) = self.find_ida_name("SmKmStoreRefFromStoreIndex") 70 | 71 | addr_smkmstoremgr = 0x1000 72 | lp_addr_smkmstoremgr = self.fe.loadBytes("".ljust(0x1000, "\xFF")) 73 | 74 | # EDX can be checked at the end for the #-of-stores mask 75 | num_store = 0x1 76 | reg_ax = 'rax' if self.Info.is_64bit() else 'eax' 77 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 78 | reg_dx = 'rdx' if self.Info.is_64bit() else 'edx' 79 | regState = {reg_cx:lp_addr_smkmstoremgr, reg_dx:num_store} 80 | self.fe.emulateRange(fn_addr, registers=regState) 81 | return self.fe.getRegVal(reg_ax) + 1 82 | 83 | @Tools.Info.arch32 84 | @Tools.Info.arch64 85 | def ssm_smkmstore(self): 86 | """ 87 | This field is a pointer to an SMKM_STORE structure. See SMKM_STORE for additional information. 88 | This function relies on the first argument to SmStWorkItemQueue remaining constant. Disassembly 89 | snippet from Windows 10 1809 x86 shown below. 90 | 91 | SmIoCtxQueueWork+93 call _SmKmStoreRefFromStoreIndex@8 92 | SmIoCtxQueueWork+98 push 0 ; KIRQL 93 | SmIoCtxQueueWork+9A mov edx, edi 94 | SmIoCtxQueueWork+9C mov ecx, [eax] 95 | SmIoCtxQueueWork+9E call _SmWorkItemQueue@12 96 | """ 97 | (startAddr, endAddr) = self.locate_call_in_fn("SmIoCtxQueueWork", ["SmWorkItemQueue", "SmStWorkItemQueue"]) 98 | self.fe.iterate([endAddr], self.tHook) 99 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 100 | return self.fe.getRegVal(reg_cx) 101 | -------------------------------------------------------------------------------- /Magic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: Magic.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | 22 | import idc 23 | import idautils 24 | import idaapi 25 | 26 | from Tools import Tools 27 | 28 | 29 | class Magic(Tools): 30 | """ 31 | There are two magic offsets on which the extraction of compressed memory 32 | relies on. The pointer to the SM_GLOBALS structure and the MmPagingFile pointer (no 33 | longer exported) containing an array of pointers to nt!_MMPAGING_FILE structures. This 34 | file locates them & extracts them. 35 | """ 36 | cs = None 37 | 38 | def __init__(self, loglevel=logging.INFO): 39 | self.tools = super(Magic, self).__init__() 40 | self.logger = logging.getLogger("Magic") 41 | self.logger.setLevel(loglevel) 42 | return 43 | 44 | def _dump(self): 45 | """ 46 | Architecture agnostic function used to dump all located fields. 47 | """ 48 | if self.Info.is_64bit(): 49 | self.logger.info("SmGlobals: {0:#x}".format(self.Info.arch_fns['x64']['m_smglobals'](self))) 50 | self.logger.info("MmPagingFile: {0:#x}".format(self.Info.arch_fns['x64']['m64_mmpagingfile'](self))) 51 | else: 52 | self.logger.info("SmGlobals: {0:#x}".format(self.Info.arch_fns['x86']['m_smglobals'](self))) 53 | self.logger.info("MmPagingFile: {0:#x}".format(self.Info.arch_fns['x86']['m32_mmpagingfile'](self))) 54 | return 55 | 56 | def _dump64(self): 57 | return 58 | 59 | @Tools.Info.arch32 60 | @Tools.Info.arch64 61 | def m_smglobals(self): 62 | """ 63 | The SM_GLOBALS structure contains information about all stores being used by the system. 64 | It can be located via the nt!SmGlobals symbol. Locating this structure is the fastest way 65 | to begin the page retrieval process. This function searches for the symbol in IDA's namespace. 66 | It is available in the ntoskrnl's PDB. 67 | """ 68 | for va, name in idautils.Names(): 69 | if "?SmGlobals" in name: 70 | return va - idaapi.get_imagebase() 71 | self.logger.error("SmGlobals could not be resolved.") 72 | return None 73 | 74 | @Tools.Info.arch32 75 | def m32_mmpagingfile(self): 76 | """ 77 | The MmPagingFile pointer can be defined as PVOID *MMPAGING_FILE[16]. Support for locating this 78 | pointer is not mandatory, but essential to verify if an MMPAGING_FILE structure corresponds 79 | to a virtual store. Although this pointer was previously exported as nt!MmPagingFile in Windows 80 | 7, the pointer has not been exported by any Windows 10 kernel to date. This function traverses 81 | MiVaIsPageFileHash and stops at the first instance of an memory dereference by index. The 82 | signature appears to be fragile but has worked from 1607-1809. Disassembly snippet from 83 | Windows 10 1809 x86 shown below. 84 | 85 | MiVaIsPageFileHash(x,x) _MiVaIsPageFileHash@8 proc near ; 86 | MiVaIsPageFileHash(x,x) mov edi, edi 87 | MiVaIsPageFileHash(x,x)+2 push ebx 88 | MiVaIsPageFileHash(x,x)+3 push esi 89 | MiVaIsPageFileHash(x,x)+4 push edi 90 | MiVaIsPageFileHash(x,x)+5 mov edi, Count 91 | MiVaIsPageFileHash(x,x)+B xor ecx, ecx 92 | MiVaIsPageFileHash(x,x)+D mov ebx, edx 93 | MiVaIsPageFileHash(x,x)+F test edi, edi 94 | MiVaIsPageFileHash(x,x)+11 jnz short loc_4DC5C7 95 | MiVaIsPageFileHash(x,x)+19 loc_4DC5C7: ; 96 | MiVaIsPageFileHash(x,x)+19 mov esi, dword_6A8614[ecx*4] 97 | """ 98 | (addr, name) = self.find_ida_name("MiVaIsPageFileHash") 99 | 100 | for insn_addr, insn, op0, op1 in self.iter_fn(addr): 101 | if "*4]" in op1: 102 | return idc.get_operand_value(insn_addr, 1) - idaapi.get_imagebase() 103 | 104 | self.logger.error("MmPagingFile could not be resolved.") 105 | return None 106 | 107 | @Tools.Info.arch64 108 | def m64_mmpagingfile(self): 109 | """ 110 | The MmPagingFile pointer can be defined as PVOID *MMPAGING_FILE[16]. Support for locating this 111 | pointer is not mandatory, but essential to verify if an MMPAGING_FILE structure corresponds 112 | to a virtual store. Although this pointer was previously exported as nt!MmPagingFile in Windows 113 | 7, the pointer has not been exported by any Windows 10 kernel to date. This function traverses 114 | MmStorecheckPagefiles. The same signature as x86 could not be used due to compiler optimzations 115 | using the LEA instruction to get the address of the global variable. Disassembly snippet from 116 | Windows 10 1809 x64 shown below. 117 | 118 | MmStoreCheckPagefiles MmStoreCheckPagefiles proc near ; 119 | MmStoreCheckPagefiles mov r9d, cs:Count 120 | MmStoreCheckPagefiles+7 xor r8d, r8d 121 | MmStoreCheckPagefiles+A test r9d, r9d 122 | MmStoreCheckPagefiles+D jz short loc_14072F307 123 | MmStoreCheckPagefiles+F lea eax, [r8+1] 124 | MmStoreCheckPagefiles+13 lea r10, unk_14043E5E0 125 | """ 126 | (addr, name) = self.find_ida_name("MmStoreCheckPagefiles") 127 | 128 | for insn_addr, insn, op0, op1 in self.iter_fn(addr): 129 | if insn == "lea": 130 | if idc.get_operand_type(insn_addr, 1) == idc.o_mem: 131 | return idc.get_operand_value(insn_addr, 1) - idaapi.get_imagebase() 132 | 133 | self.logger.error("MmPagingFile could not be resolved.") 134 | return None -------------------------------------------------------------------------------- /SmkmStore.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019 FireEye, Inc. 3 | 4 | Author: Omar Sardar 5 | Name: SmkmStore.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | import struct 22 | 23 | from Tools import Tools 24 | 25 | 26 | class SmkmStore(Tools): 27 | """ 28 | The SmkmStore class corresponds to the Windows 10 SMKM_STORE structure. 29 | Each SMKM_STORE structure represents a single store. The information in this 30 | structure, and nested structures, is used to locate the specific region containing the 31 | compressed page. 32 | """ 33 | def __init__(self, loglevel=logging.INFO): 34 | self.tools = super(SmkmStore, self).__init__() 35 | self.logger = logging.getLogger("SMKM_STORE") 36 | self.logger.setLevel(loglevel) 37 | self.fe = self.get_flare_emu() 38 | return 39 | 40 | def _dump(self): 41 | """ 42 | Architecture agnostic function used to dump all located fields. 43 | """ 44 | arch = 'x64' if self.Info.is_64bit() else 'x86' 45 | self.logger.info("StStore: {0:#x}".format(self.Info.arch_fns[arch]['sks_ststore'](self))) 46 | self.logger.info("pCompressedRegionPtrArray: {0:#x}".format(self.Info.arch_fns[arch]['sks_compressedregionptrarray'](self))) 47 | self.logger.info("StoreOwnerProcess: {0:#x}".format(self.Info.arch_fns[arch]['sks_storeownerprocess'](self))) 48 | return 49 | 50 | def _dump64(self): 51 | return 52 | 53 | @Tools.Info.arch32 54 | @Tools.Info.arch64 55 | def sks_ststore(self): 56 | """ 57 | This nested structure contains another nested structure (ST_DATA_MGR), typically at a 58 | non-zero offset. See ST_DATA_MGR for additional information. Function should be updated 59 | if this changes in the future. 60 | """ 61 | # Assumption is that the ST_STORE struct is nested @ offset 0 62 | return 0 63 | 64 | @Tools.Info.arch32 65 | @Tools.Info.arch64 66 | def sks_compressedregionptrarray(self): 67 | """ 68 | This field is a pointer to an array of pointers in the MemCompression.exe process. An index 69 | into this array is indirectly derived from the SM_PAGE_KEY. This function loads the SMKM_STORE 70 | structure with a pre-defined pattern. The SmStMapVirtualRegion function is traversed while 71 | a memory hook monitors for read events. This signature is fragile in that we're relying on the 72 | first memory read of the function to be the field of interest. Disassembly snippet from 73 | Windows 10 1809 x86 shown below. 74 | 75 | SmStMapVirtualRegion ?SmStMapVirtualRegion@?$SMKM_STORE@USM_TRAITS... 76 | SmStMapVirtualRegion mov edi, edi 77 | SmStMapVirtualRegion+2 push ebp 78 | SmStMapVirtualRegion+3 mov ebp, esp 79 | SmStMapVirtualRegion+5 and esp, 0FFFFFFF8h 80 | SmStMapVirtualRegion+8 sub esp, 34h 81 | SmStMapVirtualRegion+B push ebx 82 | SmStMapVirtualRegion+C push esi 83 | SmStMapVirtualRegion+D push edi 84 | SmStMapVirtualRegion+E mov edi, ecx 85 | SmStMapVirtualRegion+10 mov [esp+40h+var_28], edx 86 | SmStMapVirtualRegion+14 mov [esp+40h+var_1C], edi 87 | SmStMapVirtualRegion+18 mov eax, [edi+1184h] 88 | """ 89 | (fn_addr, fn_name) = self.find_ida_name("SmStMapVirtualRegion") 90 | pat = self.patgen(8192) 91 | lp_smkmstore = self.fe.loadBytes(pat) 92 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 93 | mHookData = {'offset': None, 'structAddr': lp_smkmstore} 94 | 95 | def mHook(uc, accessType, memAccessAddress, memAccessSize, memValue, userData): 96 | if mHookData['offset']: 97 | return 98 | 99 | if accessType == 16: # UC_MEM_READ 100 | self.logger.debug("Mem read @ 0x{0:x}: {1}".format(memAccessAddress, memValue)) 101 | mHookData['offset'] = memAccessAddress 102 | 103 | regState = {reg_cx: lp_smkmstore} 104 | self.fe.emulateRange(fn_addr, registers=regState, memAccessHook=mHook) 105 | return mHookData['offset'] - mHookData['structAddr'] 106 | 107 | @Tools.Info.arch32 108 | @Tools.Info.arch64 109 | def sks_storeownerprocess(self): 110 | """ 111 | This field contains a pointer to the process being used as a container for compressed memory. As 112 | of Windows 10 1607, this field has pointed to MemCompression.exe. The first argument to KiStackAttachProcess 113 | is the process to which the current kernel thread will attach to. This is the address of the store 114 | owner process in the case of Win10 memory decompression. Disassembly snippet from Windows 10 1809 x86 115 | shown below. 116 | 117 | SmStDirectRead+35 mov ecx, [ebx+1254h] ; BugCheckParameter1 118 | SmStDirectRead+3B lea eax, [ebp+var_1C] 119 | SmStDirectRead+3E push eax ; int 120 | SmStDirectRead+3F xor edx, edx 121 | SmStDirectRead+41 call _KiStackAttachProcess@12 ; KiStackAttachProcess(x,x,x) 122 | """ 123 | (startAddr, endAddr) = self.locate_call_in_fn("?SmStDirectRead@?$SMKM_STORE", "KiStackAttachProcess") 124 | pat = self.patgen(8192) 125 | addr_smkmstore = self.fe.loadBytes(pat) 126 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 127 | struct_fmt = " 5 | Name: StDataMgr.py 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | """ 19 | 20 | import logging 21 | import struct 22 | 23 | import idc 24 | 25 | from Tools import Tools 26 | 27 | 28 | class StDataMgr(Tools): 29 | """ 30 | Description: The StDataMgr class corresponds to the Windows 10 ST_DATA_MGR 31 | structure. The ST_DATA_MGR structure is nested within SMKM_STORE and 32 | contains additional information used to locate the compressed page from a 33 | region within the MemCompression minimal process. 34 | """ 35 | def __init__(self, loglevel=logging.INFO): 36 | self.tools = super(StDataMgr, self).__init__() 37 | self.logger = logging.getLogger("ST_DATA_MGR") 38 | self.logger.setLevel(loglevel) 39 | self.fe = self.get_flare_emu() 40 | return 41 | 42 | def _dump(self): 43 | """ 44 | Architecture agnostic function used to dump all located fields. 45 | """ 46 | arch = 'x64' if self.Info.is_64bit() else 'x86' 47 | self.logger.info("sLocalTree: {0:#x}".format(self.Info.arch_fns[arch]['stdm_localtree'](self))) 48 | self.logger.info("ChunkMetadata: {0:#x}".format(self.Info.arch_fns[arch]['stdm_chunkmetadata'](self))) 49 | self.logger.info("SmkmStore: {0:#x}".format(self.Info.arch_fns[arch]['stdm_smkmstore'](self))) 50 | self.logger.info("RegionSizeMask: {0:#x}".format(self.Info.arch_fns[arch]['stdm_regionsizemask'](self))) 51 | self.logger.info("RegionLSB: {0:#x}".format(self.Info.arch_fns[arch]['stdm_regionlsb'](self))) 52 | self.logger.info("CompressionFormat: {0:#x}".format(self.Info.arch_fns[arch]['stdm_compressionformat'](self))) 53 | return 54 | 55 | @Tools.Info.arch32 56 | @Tools.Info.arch64 57 | def stdm_localtree(self): 58 | """ 59 | This B+TREE is nested within the ST_DATA_MGR and contains leaf nodes of type ST_PAGE_ENTRY. 60 | The ST_PAGE_ENTRY structure contains two fields- the SM_PAGE_KEY and a 32-bit chunk key. The 61 | chunk key is encoded with region information used to ultimately locate an ST_PAGE_RECORD 62 | structure (see ST_PAGE_RECORD), from which a compressed page can be found. This structure 63 | has historically been at offset 0. Function can be updated if this changes in the future. 64 | """ 65 | return 0 66 | 67 | @Tools.Info.arch32 68 | @Tools.Info.arch64 69 | def stdm_chunkmetadata(self): 70 | """ 71 | The SMHP_CHUNK_METADATA contains information used to locate the compressed page's corresponding 72 | ST_PAGE_RECORD, using information derived from the chunk key. See SMHP_CHUNK_METADATA for 73 | additional information. This function relies on the first argument to SmhHpChunkAlloc remaining 74 | constant. Disassembly snippet from Windows 10 1809 x86 shown below. 75 | 76 | StDmpSinglePageAdd+2B0 lea ecx, [ebx+6Ch] 77 | StDmpSinglePageAdd+2B3 call _SmHpChunkAlloc@4 ; SmHpChunkAlloc(x) 78 | """ 79 | (startAddr, endAddr) = self.locate_call_in_fn("?StDmpSinglePageAdd", "SmHpChunkAlloc") 80 | self.fe.iterate([endAddr], self.tHook) 81 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 82 | return self.fe.getRegVal(reg_cx) 83 | 84 | @Tools.Info.arch32 85 | @Tools.Info.arch64 86 | def stdm_smkmstore(self): 87 | """ 88 | This function relies on the first argument to SmStReleaseVirtualRegion remaining constant. 89 | Disassembly snippet from Windows 10 1809 x86 shown below. 90 | 91 | StReleaseRegion+26 mov edi, [ebx+1C0h] 92 | StReleaseRegion+2C test byte ptr [edi+10F5h], 4 93 | StReleaseRegion+33 jz loc_5B4984 94 | StReleaseRegion+39 push 0 95 | StReleaseRegion+3B mov ecx, edi 96 | StReleaseRegion+3D call ?SmStReleaseVirtualRegion@?$SMKM_STORE@USM_TRAITS@@@@SGJPAU1@KK@Z 97 | """ 98 | (startAddr, endAddr) = self.locate_call_in_fn("?StReleaseRegion", "?SmStReleaseVirtualRegion") 99 | pat = self.patgen(8192) 100 | lp_stdatamgr = self.fe.loadBytes(pat) 101 | reg_cx = 'rcx' if self.Info.is_64bit() else 'ecx' 102 | struct_fmt = "