StealC Malware Analysis Part 1

blog.lexfo.fr · Lexfo · 1 year ago · research
quality 7/10 · good
0 net
Lexfo's security blog - StealC Malware Analysis Part 1 01 - Introduction This series of blog posts is aimed at a technical audience interested in reverse engineering and, more specifically, malware analysis. In this article, we'll take a look at the analysis of a malicious sample for Windows from the StealC family, from the packed sample to the recovery of C2. We'll automate our analysis steps with a view to integrating them into an automated pipeline for extracting indicators of compromise. In the second article we'll retrieve C2 from the loader, get the third stage sample and unpack it. The third article will focus on the last stage (StealC malware) and C2 recovery using static analysis. Requirements The prerequisites for this articles are : Knowledge of reverse engineering on malicious programs Basic knowledge of x86 assembler Knowledge of C/Cpp is strongly recommended Knowledge of Python is required for automation Knowledge of a disassembler (Binary Ninja, IDA, Ghidra) Experience of opening malware in a disassembler Experience with a debugger (x64dbg/WinDBG...) Motivation and a few liters of coffee 02 - Reminders and preparation of the analysis environment Some definitions In order to follow this article, you need to be familiar with the vocabulary of malware analysis. A sandbox A sandbox is a solution for detonating (executing) a malicious program in a controlled environment. The purpose of this solution is to help you understand how a malicious program works. I invite you to find out for yourself about this fascinating subject, as it can save you precious time when you're under time pressure. It's worth noting that some malware programs have features that can detect whether they are running in this type of environment, so as not to execute the malicious payload and thwart detection. Open-source ( CapeSandbox , Drakfuf Sanbox ...) and proprietary ( AnyRun , JoeSandbox ...) sandboxes are available. Your choice of sandbox depends on the level of discretion you require when analyzing malware. In fact, some sandboxes collect the samples you send them and make them available to a more or less restricted audience, as is the case with some free online sandboxes. If you need to maintain a high level of discretion, you should opt for a sandbox that can be hosted on your premises, and disable all telemetry options or, even more radically, cut off access to the Internet. A command and control server A command and control server, also known as C2 or CnC (Command and Control), is a server belonging to a malicious actor, enabling them to collect and even interact with an agent (malware) installed on an infected workstation. The C2 of a malicious program is very often requested from a malware analyst with the aim of adding it in detection and network flow equipments (e.g. firewalls, SIEMs, etc.). The C2 of a malicious program can, for example, be identified as a domain or an IP address. A packer A packer is generally the name given to a tool that compresses and potentially obfuscates an original program. A packer can be used legitimately by software publishers to protect intellectual property. Malicious programs are regularly packed to bypass static detection systems. A packer can incorporate obfuscation, anti-emulation, anti-VM and anti-debugging techniques. The packer studied in this blog post will contain only anti-emulation and obfuscation methods. There are open-source packers and obfuscators available on Github, and other proprietary ones (e.g. Tigress , VMProtect ). An emulator (symbolic execution) In the context of reverse engineering, an emulator simulates the behavior of a program or a sequence of instructions by reproducing the execution context (CPU register, memory, etc.). Emulation does not deliver the same performance as a virtual machine, because the program instructions are not executed directly by the processor, but simulated. Among other things, emulation makes it possible to bypass specific anti-debug or anti-VM mechanisms that may be implemented in programs. On the other hand, some programs use anti-emulation. Anti-emulation can be characterized by the presence of dead code (useless code) that requires a quite a few of execution time, e.g. a loop that calls a useless function. A normal CPU would take a fraction of a second to execute, whereas an emulator can take several minutes. Emulators are present in detection systems such as antivirus software. The latter analyzes the behavior of programs by emulating them. If anti-emulation techniques are present in the program and are not thwarted by the emulator, the emulation may then terminate in timeout. Some open-source emulators allow symbolic execution of Windows PEs, such as MIASM , Qiling , Triton or SpeakEasy . Analysis tools For malware analysis, we strongly recommend using a virtual environment. You can use the hypervisor of your choice (e.g. VMWare, Virtualbox, KVM). It can be useful to use different systems, as some malware only work on specific systems. For example, you may have a Windows 7 machine and a Windows 10 machine. Once your machine is installed, you can install analysis tools, a disassembler, a debugger, dynamic analysis tools, and a development and compilation environment. Below is a non-exhaustive list of tools that may be useful for your analyses: Sysinternal Suite (procmon, processexplorer, autoruns, tcpview...) Detect It Easy PEStudio DNSpy (if you come across .NET) PE-Bear Floss Isolate your machine from the network to prevent any connection to the outside world. Unless you're using a system such as inetsim , disable network interface completely . Make a snapshot of your virtual machine in a healthy state once all your analysis tools are installed and configured, so that you can return to a healthy state at any time. Set up a system enabling you to transfer files between your host and your virtual system. Be careful to restrict access to an empty directory or one with no critical data (some ransomware encrypts file shares) . If in doubt, disable file sharing completely once you've transferred the malicious sample to the virtual machine. Make regular snapshots as you progress through the analyses, so that you can keep a record of your progress. A disassembler There are several disassemblers on the market, but the one that seems to be the most widely used in professional environments today is IDA Pro . It costs a quite a few of money, especially if you need a decompiler for a specific architecture. It has a built-in debugger and an API to automate your analysis, as well as a number of open-source plugins available on Github. In this article we use Binary Ninja , which has the same features as the IDA Pro tool, but at a much more affordable price. Binary Ninja has an active community and responsive bug-fixing support. Its integrated plugin manager lets you quickly install and configure plugins. If you want to stay in the free world, there are disassemblers such as: Radare with its graphical interface iaito (or the fork Rizin with Cutter GUI) Ghidra , a disassembler initially developed by the NSA using the Java language A debugger When you want to debug a program on Windows, you can use a debugger. Windows provides its own debugger, WinDBG . There's also a relief for OllyDBG (for the oldest among you) called x64dbg . It has a large number of useful plugins for your reverse sessions. Some useful plugins for malware analysis: https://github.com/x64dbg/ScyllaHide https://github.com/buzzer-re/x64dbg-ASLR-Removal https://github.com/therealdreg/DbgChild Initial sample (Stage 1) Open source research Where to start The aim of the open-source research phase is to gather the information you need for your analyses. When you're analyzing a malicious sample, it's possible that some people have already studied the sample, or at least the malware family. If you're dealing with a totally unknown sample, grab your knowledge, some tools, a few liters of coffee and get started! The malicious actor who developed the malicious sample aims to discourage you, waste your time and make you give up. Take the time to read blog articles on malicious sample analysis, attend or watch replays of conferences on the subject (e.g. Botconf , Virus Bulletin ), monitor social networks such as Twitter/X. Another very useful platform for your malware research is Malpedia . It categorizes malware families by actor and provides samples, Yara rules and links to external articles. Sample recovery In this article, we've chosen an arbitrarily selected sample for you. The sample was part of one of the last submissions on Malware Bazaar (malicious sample sharing platform) by user zbetcheckin . This sample has been categorized as part of the StealC family. The program appears to be detected by 58 antivirus programs. A search in Malpedia tells us that StealC : is a Malware-as-a-Service has existed since january 2023 was written in C collects information on: web browsers cryptographic wallets instant message software and emails communicates with its C2 using HTTP POST requests Two Yara rules are supplied: win_stealc_auto and win_stealc_w0 . These date from 2023. We can put them aside for later, as they'll come in handy. Blog posts are also available - feel free to have a look if you want to understand some of the grey areas. Here's some information about the sample we'll be looking at in this article: Type Data SHA256 c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82 SHA1 87e5501bbc72be1d0b763acec9bf08c9db26a8d1 MD5 51e5979460e5a9dc941c03bc76cc3855 File size 455'681 bytes First seen 2024-04-08 07:13:32 UTC (Malware Bazaar) MIME type application/x-dosexec imphash 9ee9346826f4cfd6b39a524a25cdc5de ssdeep 12288:f9zyluCg7RvcQ7tZRsuPE16N0N9k9ptHMF:PCg7RvcKKnitHMF User zbetcheckin indicates the origin of the malware in a comment on Malware Bazaar: Comment on Malware Bazaar The URL in the comment can be used to extract the following indicators of compromise: Type Data IPv4 185.172.128[.]59 Filename ISetup8.exe URL hxxp://185.172[.]128.59/ISetup8.exe By digging deeper into these indicators of compromise, it would be possible to retrieve more information on the infrastructure of the malicious actor. For example, we could retrieve the AS (autonomous system) associated with the IP address and pivot using this information. But this is beyond the scope of this article. We can retrieve the malicious sample via open sources such as Malware Bazaar, VirusTotal (requires an appropriate license) or directly from the infrastructure of the malicious actor if the latter has not shut down its service. If you plan to retrieve a sample from the infrastructure of a malicious actor, we strongly recommend that you use at least one bounce to connect to it (e.g. Tor, VPN, even a proxy). The files can be recovered here . Malicious samples are contained in password-protected ZIP archives. The password used for these archives is the one most commonly used when exchanging malicious files: infected Packer identification If you have not yet configured your machine, please refer to the chapter Reminders and preparation of the analysis environment in this article. Now that you have the sample on your analysis system, make sure you have shut down the network. Entropy With Binary Ninja If you want to save time and you own Binary Ninja, you can open our .bndb file . If we look at the binary data graph, we can see that a large part of the program contains data that cannot be understood by the disassembler: Stage 1 - Pixel map You can get an entropy score with the get_entropy method of the Binary Ninja API: >>> for section in bv . sections : ... sec = bv . get_section_by_name ( section ) ... print ( f "Section ' { section } ': { bv . get_entropy ( sec . start , sec . start ) } " ) ... Section '.data' : [ 0.446795254945755 ] Section '.extern' : [ 0.0 ] Section '.rdata' : [ 0.6443907618522644 ] Section '.rsrc' : [ 0.5072780251502991 ] Section '.synthetic_builtins' : [ 0.0 ] Section '.text' : [ 0.839885950088501 ] With Detect It Easy You can also check the entropy of the binary with another tool such as Detect It Easy . With this tool, you can see that the .data and .text sections have a rather high entropy level: Detect It Easy - Stage 1's Entropy With Yara Another less precise way of identifying whether a program is packed is to use Yara's math.entropy module. import "math" rule PE_High_entropy { condition : uint16 ( 0 ) == 0x5A4D and uint32 ( uint32 ( 0x3C )) == 0x00004550 and math . entropy ( 0 , filesize ) > 6.5 } Unpack sample manually If you're faced with this type of packer and you're in a hurry, it may be necessary to unpack it manually if no automated solution exists. It is common for packers to use memory allocation methods such as VirtualAlloc , then add execution permissions to this memory area with VirtualProtect . Once the memory area has been allocated, the packers retrieve the encrypted second stage. The second stage may, for example, be present in the program resources. The packer then uses decryption or decoding alrogithms to decrypt the second stage and execute it. One way of extracting the packed program is to set write hardware breakpoints on memory areas freshly allocated by methods such as VirtualAlloc and then identify the decryption routine. Once the decryption loop has passed, you can extract the memory area. Note that this type of case does not always work. The pkr_ce1a packer uses the VirtualAlloc method, decrypts the second layer and adds a few modifications to it, then releases the memory area with VirtualFree . Memory allocation To get started, set up your favorite debugger in a virtual machine isolated from the network and any file sharing. Set a breakpoint on the VirtualAlloc and VirtualFree methods of the Kernel32.dll library. Each packer has its own way of extracting. Adapt the various steps to your technical environment. If you're using Binary Ninja or a WinDBG base, you can set a breakpoint on specific methods once you've reached the entry point of your program. The command is bu kernel32.dll!VirtualAlloc : Binary Ninja - Breakpoint in debugger console Continue program execution ( F9 ) until the breakpoint is triggered. Get the size of the VirtualAlloc argument. Here the argument sent to the function is pushed onto the stack and then stored in the eax register ( 0x6ae00 ) in the VirtualAlloc function. Binary Ninja - Debugging view To get the return address, continue to the function return ( CTRL + F9 in binary ninja). This will give us the address of the memory area allocated by VirtualAlloc in 0x21a0000 . This address will not necessarily be the same on your environment: Binary Ninja - Debugging view Save the starting address of the memory area, and its size for later usage. Continue executing the program until you call the VirtualFree ( F9 ) method: Binary Ninja - VirtualFree Bp hit PE extraction Before extracting the PE, you can retrieve the first two bytes and make sure it's a Windows executable and has the MZ header: Binary Ninja - MZ header in memory region You can then dump the memory area and save it on your filesystem. Below is the code you can adapt to the memory address allocated by your system and the path where you wish to save the unpacked program: import base64 pe_data = dbg . read_memory ( 0x21a0000 , 0x6ae00 ) . base64_encode () f = open ( r "C:\Users\user\Desktop\pe_unpacked.bin" , "wb" ) f . write ( base64 . b64decode ( pe_data )) f . close () Once done, you can open it in a tool such as Detect It Easy to check its validity: Detect it Easy on unpacked sample Bravo, you've just unpacked the first Stage! \o/ Identifying elements in the packer Cryptographic functions A packer can use cryptographic functions to hide/unhide code. You can use Yara or plug-ins built into your disassembler to identify cryptographic constants. For IDA, you can use Findcrypt-yara . With Binary Ninja, you can use CryptoScan : CryptoScan - TEA algorithm match A cryptographic constant from the TEA algorithm seems to be used in 0x4014c3 , so let's take a closer look. We can start by renaming the variables and removing the dead code (garbage) present in this function: 0040146 e int32_t __fastcall xx_decode_shellcode_TEA ( int32_t * memory_area ) 00401477 int32_t blob_length2_1 = blob_length2 004014 85 int32_t data_1 = * memory_area 004014 87 int32_t data_2 = memory_area [ 1 ] 004014 8 a int32_t previous_data_1 = data_1 004014 93 if ( blob_length2_1 == 0x594 ) 004014 a0 void lpFilename // Garbage 004014 a0 GetModuleFileNameW ( hModule : nullptr , lpFilename : & lpFilename , nSize : 0 ) 004014 a6 blob_length2_1 = blob_length2 004014 ac int32_t key1_1 = key1 004014 b4 int32_t key_1 = 0 004014 bb int32_t key2_1 = key2 004014 c0 int32_t delta = 0x9e3779b9 // Crypto constant identified by CryptoScan 004014 ca add_ecx_content_0xc6ef34e1 ( & key_1 ) // Second crypto constant calculated 004014 cf key_1 += 0x23f 004014 d9 void lpszVolumeName 004014 d9 if ( blob_length2_1 == 0x14 ) 004014e6 // Garbage 004014e6 FindNextVolumeA ( hFindVolume : nullptr , lpszVolumeName : & lpszVolumeName , cchBufferLength : 0 ) 004014 ec int32_t data_3 = key3 004014f 2 int32_t data_6 = key4 004014f 9 int32_t data = data_3 004014f f int32_t loop_counter = 0x20 00401643 int32_t loop_index 00401643 do 00401500 int32_t var_18_1 = 2 00401507 char shift_amount_2 = 5 00401521 int32_t temp_key = key_1 00401527 int32_t temp_data_7 = data_4c1b48 0040152 c if ( blob_length2 == 0xfa9 ) // Garbage 0040152 c temp_data_7 = 0xedeb2e40 00401531 bool cond : 2 _1 = blob_length2 == 0x3eb 0040153 b data_4c1b48 = temp_data_7 00401540 int32_t temp_data_8 = data_4c1aa0 00401545 if ( cond : 2 _1 ) 00401545 temp_data_8 = 0 0040154 a data_4c1aa0 = temp_data_8 0040155 e int32_t temp_data_9 = (( data_1 << 4 ) + data_3 ) ^ ( temp_key + data_1 ) 00401565 data_4c1b44 = 0xf4ea3dee 0040157 9 int32_t shift_result = temp_data_9 0040157f if ( blob_length2 == 0x213 ) // Garbage 004015 86 CreateHardLinkW ( lpFileName : nullptr , lpExistingFileName : nullptr , lpSecurityAttributes : nullptr ) 004015 8 e IsValidCodePage ( CodePage : 0 ) 004015 9 a uint8_t * var_28 004015 9 a GetCompressedFileSizeW ( lpFileName : nullptr , lpFileSizeHigh : & var_28 ) 004015 ad void lpBaseAddress 004015 ad WriteProcessMemory ( hProcess : nullptr , lpBaseAddress : & lpBaseAddress , lpBuffer : nullptr , nSize : 0 , lpNumberOfBytesWritten : nullptr ) 004015 b8 GetNumaProcessorNode ( Processor : 0 , NodeNumber : var_28 ) 004015 ca void lpBuffer 004015 ca void lpNumberOfEventsRead 004015 ca ReadConsoleInputA ( hConsoleInput : nullptr , lpBuffer : & lpBuffer , nLength : 0 , lpNumberOfEventsRead : & lpNumberOfEventsRead ) 004015 d5 DeleteTimerQueueTimer ( TimerQueue : nullptr , Timer : nullptr , CompletionEvent : nullptr ) 004015 de data_2 -= (( previous_data_1 u >> 5 ) + data_6 ) ^ temp_data_9 004015f 8 int32_t xor_result_1 = ( data_2 u >> shift_amount_2 ) + key2_1 00401606 int32_t temp_data_11 = (( data_2 << 4 ) + key1_1 ) ^ ( key_1 + data_2 ) 00401615 int32_t xor_result // Garbage 00401615 if ( blob_length2 != 0xb03 ) 00401625 xor_result = xor_result_1 00401615 else // Garbage 00401617 GetConsoleAliasExesLengthA () 00401620 xor_result = xor_result_1 0040162 8 int32_t temp_data_12 = temp_data_11 ^ xor_result 0040162 c int32_t result_1 = temp_data_12 00401634 data_1 = add ( data_1 , temp_data_12 ) 00401636 previous_data_1 = data_1 0040163 c key_1 -= delta 0040163f data_3 = data 00401642 loop_index = loop_counter 00401642 loop_counter -= 1 00401643 while ( loop_index != 1 ) 0040164 c * memory_area = data_1 0040164f memory_area [ 1 ] = data_2 00401657 return delta We can see that the encryption keys are directly integrated into the function: TEA algorithm keys Starting from the following C code: void decrypt ( uint32_t v [ 2 ], const uint32_t k [ 4 ]) { uint32_t v0 = v [ 0 ], v1 = v [ 1 ], sum = 0xC6EF3720 , i ; /* set up; sum is (delta << 5) & 0xFFFFFFFF */ uint32_t delta = 0x9E3779B9 ; /* a key schedule constant */ uint32_t k0 = k [ 0 ], k1 = k [ 1 ], k2 = k [ 2 ], k3 = k [ 3 ]; /* cache key */ for ( i = 0 ; i < 32 ; i ++ ) { /* basic cycle start */ v1 -= (( v0 << 4 ) + k2 ) ^ ( v0 + sum ) ^ (( v0 >> 5 ) + k3 ); v0 -= (( v1 << 4 ) + k0 ) ^ ( v1 + sum ) ^ (( v1 >> 5 ) + k1 ); sum -= delta ; } /* end cycle */ v [ 0 ] = v0 ; v [ 1 ] = v1 ; } It can be transcribed into python and the encryption keys added: KEY = [ 0x1ed822c8 , 0x1eda8d45 , 0x6fd0bf39 , 0x19ea64a5 ] def tea_decipher ( v ): y = c_uint32 ( v [ 0 ]) z = c_uint32 ( v [ 1 ]) sum = c_uint32 ( 0xc6ef3720 ) delta = 0x9e3779b9 n = 32 w = [ 0 , 0 ] while ( n > 0 ): z . value -= ( y . value << 4 ) + KEY [ 2 ] ^ y . value + sum . value ^ ( y . value >> 5 ) + KEY [ 3 ] y . value -= ( z . value << 4 ) + KEY [ 0 ] ^ z . value + sum . value ^ ( z . value >> 5 ) + KEY [ 1 ] sum . value -= delta n -= 1 w [ 0 ] = y . value w [ 1 ] = z . value return w The function that calls our xx_decode_TEA function (renamed by us) seems to decode an entire buffer: Buffer decoding with TEA A search on a search engine allows us to put a name to our packer thanks to an article from Elemental X : Google search Identify the shellcode call If we go back to the Calltree of our xx_decode_shellcode_TEA decryption function, we can see that the function is called by xx_decode_shellcode , which decrypts a buffer, and then by a xxx_call_stage2 function in 0x004016ca : Binary Ninja - Calltree If you look at the code in the xxx_call_stage2 function, you'll see a lot of unnecessary code and anti-emulation. For example, the code below shows a loop that performs 0x29156e rounds by calling the WinAPI method SelectObject . NULL values are sent to it, and the return of the function is not even checked. This type of call considerably slows down program execution in an emulator. Anti-Emulation loop Writing a Yara rule for the packer Now that we have a name for our pkr_ce1a packer, we can search the Internet for existing Yara rules. Malwarology has wrote a very good blog post on analyzing the packer. They also provide two Yara rules, one that retrieves the size of a shellcode and the other that retrieves the shellcode address. strings: $shellcode_size = { 00699AF974[4]96AACB4600 } $shellcode_addr = { 0094488D6A[4]F2160B6800 } We can search for these sequences of bytes in our binary and rename the variables accordingly in our disassembler. In this sample, the shellcode address is 0x41f0e8 and the size pointer is 0x41eef3 . In addition to the two strings of the Malwarology rules, we're adding other parameters that we've identified from the start to reduce the number of false positives: TEA cryptographic constants an entropy check to increase the likelihood of it being a packed program This can give us the following rule: import "math" import "pe" rule Packer_pkr_ce1a_generic { meta : date = "2024-04-25" description = "Detect pkr_ce1a packer" sharing = "TLP:CLEAR" example = "c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82" packer = "pkr_ce1a" strings : // First stage shellcode size: 0x41ef1f $shellcode_size = { 006 99 AF974 [ 4 ] 96 AACB4600 } // First stage shellcode addr: 0x41eef3 $shellcode_addr = { 00 94488 D6A [ 4 ] F2160B6800 } // delta TEA algorithm : 0x004014c0 $tea_const_delta = { B979379E } // sum TRA algorithm : Not found in this sample $tea_const_sum = { 2037 EFC6 } /* // The tea sum is calculated; perhaps some samples don't have this implementation. 004014b1 8d4df8 lea ecx, [ebp-0x8 {key_1}] 00401462 8101e134efc6 add dword [ecx], 0xc6ef34e1 004014cf 8145f83f020000 add dword [ebp-0x8 {key_1}], 0x23f */ $tea_sum_calculated1 = { 8101E134 EFC6 } $tea_sum_calculated2 = { 8145F83F 020000 } condition : uint16 ( 0 ) == 0x5A4D and uint32 ( uint32 ( 0x3C )) == 0x00004550 and $shellcode_size and $shellcode_addr and ( 1 of ( $tea_const_ * ) or 2 of ( $tea_sum_calculated * )) and math . entropy ( 0 , filesize ) > 6.5 } This Yara rule can be improved by you to reduce the number of false positives or increase the match ratio. You can now hunt (search for new samples) on platforms such as VirusTotal (if you have a license), MalwareBazaar , UnpacMe , or in your own database using a tool such as mquery (open-source and developed by CERT.pl). Extract shellcode (layer 2) To extract the shellcode from the binary, we can use our disassembler API to automate decryption and extraction. We know the address of the shellcode, its size and the encryption algorithm. from binaryninja import * import sys from ctypes import * output_shellcode_path = "/tmp/shellcode" encrypted_blob_shellcode_address = 0x41f0e8 encrypted_blob_shellcode_length = 0x37718 #Â Crypto key found in Tea function @0x0040146e KEY = [ 0x1ed822c8 , 0x1eda8d45 , 0x6fd0bf39 , 0x19ea64a5 ] def tea_decipher ( v ): y = c_uint32 ( v [ 0 ]) z = c_uint32 ( v [ 1 ]) sum = c_uint32 ( 0xc6ef3720 ) delta = 0x9e3779b9 n = 32 w = [ 0 , 0 ] while ( n > 0 ): z . value -= ( y . value << 4 ) + KEY [ 2 ] ^ y . value + sum . value ^ ( y . value >> 5 ) + KEY [ 3 ] y . value -= ( z . value << 4 ) + KEY [ 0 ] ^ z . value + sum . value ^ ( z . value >> 5 ) + KEY [ 1 ] sum . value -= delta n -= 1 w [ 0 ] = y . value w [ 1 ] = z . value return w def bytes_to_int32_array ( byte_data ): num_int32 = len ( byte_data ) // 4 int32_array = struct . unpack ( "< {} I" . format ( num_int32 ), byte_data ) return int32_array def int32_array_to_bytes ( int32_array ): byte_data = struct . pack ( "< {} I" . format ( len ( int32_array )), * int32_array ) return byte_data def main (): encrypted_blob_shellcode = bv . read ( encrypted_blob_shellcode_address , encrypted_blob_shellcode_length ) encrypted_blob_shellcode_int32_array = bytes_to_int32_array ( encrypted_blob_shellcode ) i = 0 decrypted_shellcode_int32_array = [] while i < encrypted_blob_shellcode_length / 4 : memory_area = [ encrypted_blob_shellcode_int32_array [ i ], encrypted_blob_shellcode_int32_array [ i + 1 ] ] memory_area = tea_decipher ( memory_area ) decrypted_shellcode_int32_array . append ( memory_area [ 0 ]) decrypted_shellcode_int32_array . append ( memory_area [ 1 ]) i += 2 f = open ( output_shellcode_path , "wb+" ) f . write ( int32_array_to_bytes ( decrypted_shellcode_int32_array )) f . close () print ( f "Stage2 dumped : { output_shellcode_path } " ) return main () Shellcode is then extracted, but this is not the originally packed program. This shellcode continues loading the various elements and calls up allocation and decryption methods. You can continue its analysis to go step by step through the decryption of the sample. Shellcode analysis will not be covered in this article. We chose to take a more traditional approach to extraction, either using manual extraction or, to save time, automated extraction with an emulator. Unpack the sample automatically with MIASM When you encounter the same type of packer several times, or when you want to scale up your analyses, it may be a good idea to automate your actions. You can use sandboxes to extract samples. The drawback is that some packer programs or malware use anti-VM techniques, and the execution (VM restoration and analysis time) can be more or less long and require a lot of machine resources. Using a sandbox can be complicated, especially if you have a very large number of samples to extract. One possible solution (which also has its drawbacks) is to use an emulation solution. In this article, we chose the jitter of MIASM to emulate our sample. We chose MIASM because it meets our needs very well. But there are also emulators with a sandbox, such as Qiling . Preparation of the environment We will use the Jitter module of MIASM and its sandbox example here . To run the basic script, you need to install the MIASM environment: git clone https://github.com/cea-sec/miasm.git python3 -m venv ./venv/ source venv/bin/activate cd miasm python3 setup.py install You can display script help with the --help option: Help menu from MIASM jitter Throughout the development of the unpacker, you will be confronted with errors of various kinds, non-existent memory zones, WinAPI methods not implemented in MIASM, etc... Example: MIASM jitter stacktrace example New libraries You should encounter a problem: the loading of new DLLs that are not present in your project by default. Below is the error this generates, where msimg32.dll is missing: [ INFO ] : kernel32_LoadLibrary ( dllname = 0x4192f8 ) ret addr: 0x401871 [ WARNING ] : Create dummy entry for 'msimg32.dll' WARNING: address 0x341EDC is not mapped in virtual memory: Traceback ( most recent call last ) : File "/home/user/Dev/test_stealc/sandbox_pe_x86_32.py" , line 143 , in sb.run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 529 , in run super ( Sandbox_Win_x86_32, self ) .run ( addr ) File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 136 , in run self.jitter.continue_run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 430 , in continue_run return next ( self.run_iterator ) ^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 395 , in runiter_once raise JitterException ( exception_flag ) miasm.jitter.jitload.JitterException: A jitter exception occurred: DO_NOT_UPDATE_PC & ACCESS_VIOL ( 0x2004000 ) You can create a win_dll folder, which must contain standard Windows libraries (DLL). Add missing DLLs you may find on a healthy Windows system. Once you've added the directory, you should be able to continue running your program. MIASM and Windows API By default, the sandbox does not use system environment options, such as memory segments, Windows structures, etc... You can add them by specifying them as arguments to the -o -i -s -y script. As you can see from the execution below, we take this a step further by crashing a Windows function ( FlsAlloc from the kernel32.dll library): $ python3 sandbox_pe_x86_32.py -o -i -s -y c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82.exe cannot find crypto, skipping [ ERROR ] : Cannot open win_dll/kernel32.dll [ ERROR ] : Cannot open win_dll/gdi32.dll c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82.exe kernel32.dll gdi32.dll [ WARNING ] : Create dummy entry for 'kernel32.dll' [ WARNING ] : Create dummy entry for 'gdi32.dll' [ INFO ] : Add module 400000 'c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82.exe' [ WARNING ] : Unknown module: omitted from link list ( 'kernel32.dll' ) [ WARNING ] : Unknown module: omitted from link list ( 'gdi32.dll' ) [ WARNING ] : No main pe, ldr data will be unconsistant [ , None, None ] [ WARNING ] : No main pe, ldr data will be unconsistant [ WARNING ] : No main pe, ldr data will be unconsistant [ INFO ] : kernel32_GetSystemTimeAsFileTime ( lpSystemTimeAsFileTime = 0x13ffdc ) ret addr: 0x409c40 [ INFO ] : kernel32_GetCurrentThreadId () ret addr: 0x409c4f [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x409c58 [ INFO ] : kernel32_QueryPerformanceCounter ( lpPerformanceCount = 0x13ffd4 ) ret addr: 0x409c65 [ INFO ] : kernel32_GetStartupInfo ( ptr = 0x13ff6c ) ret addr: 0x405194 [ INFO ] : kernel32_GetProcessHeap () ret addr: 0x406d4e [ INFO ] : kernel32_EncodePointer ( 0x0 ) ret addr: 0x403ff1 [ INFO ] : kernel32_EncodePointer ( 0x403b32 ) ret addr: 0x403b84 [ INFO ] : kernel32_GetModuleHandle ( dllname = 0x413ab8 ) ret addr: 0x405222 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413ad4 ) ret addr: 0x405232 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413ae0 ) ret addr: 0x405245 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413ae8 ) ret addr: 0x405258 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413af4 ) ret addr: 0x40526b [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b00 ) ret addr: 0x40527e [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b1c ) ret addr: 0x405291 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b2c ) ret addr: 0x4052a4 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b40 ) ret addr: 0x4052b7 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b58 ) ret addr: 0x4052ca [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b70 ) ret addr: 0x4052dd [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413b84 ) ret addr: 0x4052f0 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413ba4 ) ret addr: 0x405303 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413bbc ) ret addr: 0x405316 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413bd4 ) ret addr: 0x405329 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413be8 ) ret addr: 0x40533c [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413bfc ) ret addr: 0x40534f [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413c18 ) ret addr: 0x405362 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413c38 ) ret addr: 0x405375 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413c54 ) ret addr: 0x405388 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413c74 ) ret addr: 0x40539b [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413c88 ) ret addr: 0x4053ae [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413ca4 ) ret addr: 0x4053c1 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413cb8 ) ret addr: 0x4053d4 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413cc8 ) ret addr: 0x4053e7 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413cd8 ) ret addr: 0x4053fa [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413ce8 ) ret addr: 0x40540d [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413cf8 ) ret addr: 0x405420 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413d14 ) ret addr: 0x405433 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413d28 ) ret addr: 0x405446 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413d38 ) ret addr: 0x405459 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413d4c ) ret addr: 0x40546c [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413d5c ) ret addr: 0x40547f [ INFO ] : kernel32_GetProcAddress ( libbase = 0x71112000, fname = 0x413d7c ) ret addr: 0x405492 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457b90, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457ba8, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457bc0, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457bd8, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457bf0, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c08, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c20, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c38, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c50, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c68, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c80, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457c98, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457cb0, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 [ INFO ] : kernel32_InitializeCriticalSectionEx ( lpCriticalSection = 0x457cc8, dwSpinCount = 0xfa0, Flags = 0x0 ) ret addr: 0x4051c2 Traceback ( most recent call last ) : File "/home/user/Dev/test_stealc/sandbox_pe_x86_32.py" , line 16 , in sb.run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 529 , in run super ( Sandbox_Win_x86_32, self ) .run ( addr ) File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 136 , in run self.jitter.continue_run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 430 , in continue_run return next ( self.run_iterator ) ^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 369 , in runiter_once for res in self.breakpoints_handler.call_callbacks ( self.pc, self ) : File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 129 , in call_callbacks res = c ( *args ) ^^^^^^^^ File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 517 , in handle_lib raise ValueError ( 'unknown api' , hex ( jitter.pc ) , repr ( fname )) ValueError: ( 'unknown api' , '0x71112624' , "'kernel32_FlsAlloc'" ) If you don't want to specify the above-mentioned script options, you can force them into your script: options = parser . parse_args () options . usesegm = True options . use_windows_structs = True options . load_hdr = True options . dependencies = True The FLS methods have been implemented in MIASM , we will use them in our script: from miasm.os_dep.win_api_x86_32 import FLS [ ... ] fls = FLS () kernel32_FlsAlloc = fls . kernel32_FlsAlloc kernel32_FlsSetValue = fls . kernel32_FlsSetValue kernel32_FlsGetValue = fls . kernel32_FlsGetValue Functions not implemented Adding these lines allows us to go further in executing the program, until a similar error appears, indicating that the GetEnvironmentStringsW method in kernel32.dll is not implemented: ValueError: ('unknown api', '0x71112564', "'kernel32_GetEnvironmentStringsW'") Not all methods are implemented in MIASM, and sometimes you'll have to write them yourself in order to achieve your goal in program execution. One of MIASM's contributors (Commial) published on his GitHub a list of WinAPI methods with input parameters from several libraries. You can use this as a basis for implementing methods that are not present in MIASM. In some cases, you don't necessarily have to simulate the actual behavior of a method. Some malware only checks the return value of the function, so you can force the value to avoid triggering an error or being directed to an unwanted branch. According to MSDN, the GetEnvironmentStringsW method takes no arguments and returns a pointer to a string of Wild characters. We can implement the method as follows: from miasm.jitter.csts import * from miasm.os_dep.common import set_win_str_w from miasm.os_dep.win_api_x86_32 import FLS , winobjs [ ... ] GetEnvironmentStringsW_addr = None def kernel32_GetEnvironmentStringsW ( jitter ): """ LPWCH GetEnvironmentStringsW() """ global GetEnvironmentStringsW_addr # Get return address and function arguments ret_ad , args = jitter . func_args_stdcall ([]) #Â If GetEnvironmentStrings isn't allocated yet if not GetEnvironmentStringsW_addr : # Get next allocation address on heap alloc_addr = winobjs . heap . next_addr ( 50 ) # Allocate memory on heap winobjs . allocated_pages [ alloc_addr ] = ( alloc_addr , 50 ) # Allocate memory page with READ/WRITE permission and write 50 bytes of nullbytes jitter . vm . add_memory_page ( alloc_addr , PAGE_READ | PAGE_WRITE , b " \x00 " * 50 ) # Put env variable in allocated memory set_win_str_w ( jitter , alloc_addr , "HelloWorld" ) # Store allocated address for next call of GetEnviromentStringsW GetEnvironmentStringsW_addr = alloc_addr # Return address of allocated memory jitter . func_ret_stdcall ( ret_ad , GetEnvironmentStringsW_addr ) [ ... ] The next execution causes problems for the FreeEnvironmentStringsW method. As mentioned earlier, you can return a value expected by the program without having to actually implement the method: def kernel32_FreeEnvironmentStringsW ( jitter ): ret_ad , args = jitter . func_args_stdcall ([ "lpszEnvironmentBlock" ]) jitter . func_ret_stdcall ( ret_ad , 1 ) jitter . running = True return True Continue implementing the various methods on your own, as the logic remains very similar. Anti-emulation At some point, you'll come to a repetitive call to the GetCurrentProcess and GetCurrentProcessId methods: [ ... ] [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ ... SAME LINES ... ] [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ ... SAME LINES ... ] [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 [ INFO ] : kernel32_GetCurrentProcess () ret addr: 0x401b76 [ INFO ] : kernel32_GetCurrentProcessId () ret addr: 0x401b70 ^C KeyboardInterrupt The return addresses for these methods are 0x401b70 and 0x401b76 . Open the program in your disassembler and go to these addresses. You should find the following code: Stage 1 - Anti-Emulation loops As you can see, you've just encountered the loop in the first red frame. The GetCurrentProcessId and GetCurrentProcess methods are called a very large number of times (without the return values of these functions being checked) as long as the value of the ESI register is less than 0x4f672 . This is an anti-emulation method. On a real environment, the execution of this loop is very fast, but when the environment is emulated, execution is much longer. Some security systems, such as antivirus software, stop their analysis when they encounter this type of situation. You have several options: Wait until the end (beware, a second loop will follow) Bypass this loop by adding a breakpoint in MIASM Implement an anti-anti-emulation bypass code First, we'll opt for the second method: bypass this loop in the hope that the packer doesn't evolve too much over time and that the anti-emulation mechanisms are fixed (spoiler: the packer evolves, constants, WinAPI methods, loops required for unpacking or not...). def jmp_0x00401bda ( jitter ): jitter . pc = 0x00401bda jitter . running = True return True [ ... ] sb . jitter . add_breakpoint ( 0x401b68 , jmp_0x00401bda ) # anti-emu GetCurrentProcess & GetCurrentProcessId Continue execution and you'll encounter anti-emulation again. Here, the GetLastError method is called without checking its return: Anti-Emulation loop - GetLastError [ ... ] def set_esi_0x674db ( jitter ): jitter . cpu . ESI = 0x674db jitter . running = True return True [ ... ] sb . jitter . add_breakpoint ( 0x401829 , set_esi_0x674db ) # anti-emu GetLastError sb . jitter . add_breakpoint ( 0x40184a , set_esi_0x674db ) # anti-emu GetLastError SEH The next step you should encounter after implementing the non-existent Module32First method in MIASM is access to a non-existent memory area ( 0x7FFFF000 ): [ INFO ] : kernel32_CreateToolhelp32Snapshot ( dwflags = 0x8, th32processid = 0x0 ) ret addr: 0x2000f7d1 [ INFO ] : kernel32_Module32First ( hSnapshot = 0xaaaa00, lpme = 0x13d49c ) ret addr: 0x2000f7f1 [ INFO ] : kernel32_VirtualAlloc ( lpvoid = 0x0, dwsize = 0x6bc67, alloc_type = 0x1000, flprotect = 0x40 ) ret addr: 0x2000f4b9 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13c438 ) ret addr: 0x20047a22 WARNING: address 0x7FFFF000 is not mapped in virtual memory: Traceback ( most recent call last ) : File "/home/user/Dev/test_stealc/sandbox_pe_x86_32.py" , line 151 , in sb.run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 529 , in run super ( Sandbox_Win_x86_32, self ) .run ( addr ) File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 136 , in run self.jitter.continue_run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 430 , in continue_run return next ( self.run_iterator ) ^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 395 , in runiter_once raise JitterException ( exception_flag ) miasm.jitter.jitload.JitterException: A jitter exception occurred: DO_NOT_UPDATE_PC & ACCESS_VIOL ( 0x2004000 ) To understand in more detail what's happening at this point, you can display the new instruction blocks in MIASM using the -b option. You can also debug instruction by instruction using the -z option. You can trigger these verbosity modes directly from your code with the following snippet: jitter . set_trace_log ( trace_instr = False , #Â Display each instructions trace_regs = False , # Display registers values trace_new_blocks = True #Â Display new instructions block ) This produces the following code: loc_2004704c CALL loc_20047a3f -> c_next:loc_20047051 loc_20047a3f PUSH EBP MOV EBP, ESP PUSH ECX MOV EAX, DWORD PTR FS:[0x0] ; Get TIB ptr MOV DWORD PTR [EBP + 0xFFFFFFFC], EAX ; Store TIB ptr in [EBP-0x4] MOV EAX, DWORD PTR [EBP + 0xFFFFFFFC] ; Get TIB TIB which was stored in stack JMP loc_20047a56 -> c_to:loc_20047a56 loc_20047a56 CMP DWORD PTR [EAX], 0xFFFFFFFF ; compare deref TIB ptr with 0xFFFFFFFF JNZ loc_20047a51 -> c_next:loc_20047a5b c_to:loc_20047a51 loc_20047a51 MOV EAX, DWORD PTR [EAX] MOV DWORD PTR [EBP + 0xFFFFFFFC], EAX CMP DWORD PTR [EAX], 0xFFFFFFFF JNZ loc_20047a51 -> c_next:loc_20047a5b c_to:loc_20047a51 As you can see, the packer code retrieves the TIB (Thread Information Block) pointer and compares its contents. The address contained in FS:[0x0] is 0x7FFFF000 . We can allocate memory at this address and write 0xFFFFFFFF to it to validate the comparison: sb . jitter . vm . add_memory_page ( 0x7FFFF000 , PAGE_READ | PAGE_WRITE , b " \xff " * 4 ) # SEH PE extraction You should then arrive at the phase where the program exits with the atexit method of msvcr100.dll . Don't bother implementing it, as what we're interested in happens before that. As seen in the manual unpacking phase, we have two VirtualAlloc and one VirtualFree : [ INFO ] : kernel32_Module32First ( hSnapshot = 0xaaaa00, lpme = 0x13d49c ) ret addr: 0x2000f7f1 [ INFO ] : kernel32_VirtualAlloc ( lpvoid = 0x0, dwsize = 0x6bc67, alloc_type = 0x1000, flprotect = 0x40 ) ret addr: 0x2000f4b9 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13c438 ) ret addr: 0x20047a22 [ INFO ] : kernel32_LoadLibrary ( dllname = 0x13d3e4 ) ret addr: 0x20047099 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x200470ce [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x20047106 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x20047134 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x2004716c [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x200471a8 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x200471e0 [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7c800000, fname = 0x13d3e4 ) ret addr: 0x20047215 [ INFO ] : kernel32_SetErrorMode ( uMode = 0x400 ) ret addr: 0x20047e1c [ INFO ] : kernel32_SetErrorMode ( uMode = 0x0 ) ret addr: 0x20047e21 [ INFO ] : kernel32_GetVersionEx ( ptr_struct = 0x13c3cc ) ret addr: 0x20047dad [ INFO ] : kernel32_VirtualAlloc ( lpvoid = 0x0, dwsize = 0x6ae00, alloc_type = 0x1000, flprotect = 0x4 ) ret addr: 0x20047250 [ INFO ] : kernel32_VirtualProtect ( lpvoid = 0x400000, dwsize = 0x6e000, flnewprotect = 0x40, lpfloldprotect = 0x13d454 ) ret addr: 0x200472e6 [ WARNING ] : set page 400000 7 [ WARNING ] : set page 401000 7 [ WARNING ] : set page 412000 7 [ WARNING ] : create page 41b000 7 [ WARNING ] : create page 46e000 7 [ INFO ] : kernel32_VirtualFree ( lpvoid = 0x200b3000, dwsize = 0x0, alloc_type = 0x8000 ) ret addr: 0x20047446 [ INFO ] : kernel32_LoadLibrary ( dllname = 0x43851c ) ret addr: 0x200474f6 [ ... ] [ INFO ] : kernel32_LoadLibrary ( dllname = 0x13d3e4 ) ret addr: 0x2004789d [ WARNING ] : Create dummy entry for 'msvcr100.dll' [ INFO ] : kernel32_GetProcAddress ( libbase = 0x7111a000, fname = 0x13d3e4 ) ret addr: 0x200478c7 Traceback ( most recent call last ) : File "/home/user/Dev/test_stealc/sandbox_pe_x86_32.py" , line 168 , in sb.run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 529 , in run super ( Sandbox_Win_x86_32, self ) .run ( addr ) File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/analysis/sandbox.py" , line 136 , in run self.jitter.continue_run () File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 430 , in continue_run return next ( self.run_iterator ) ^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 369 , in runiter_once for res in self.breakpoints_handler.call_callbacks ( self.pc, self ) : File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 129 , in call_callbacks res = c ( *args ) ^^^^^^^^ File "/home/user/Dev/test_stealc/venv/lib/python3.11/site-packages/miasm-0.1.5.dev47-py3.11-linux-x86_64.egg/miasm/jitter/jitload.py" , line 517 , in handle_lib raise ValueError ( 'unknown api' , hex ( jitter.pc ) , repr ( fname )) ValueError: ( 'unknown api' , '0x7111a004' , "'msvcr100_atexit'" ) We'll modify MIASM's VirtualFree method and register the memory zone starting with MZ like this: import os [ ... ] def kernel32_VirtualFree ( jitter ): ret_ad , args = jitter . func_args_stdcall ([ "lpvoid" , "dwsize" , "alloc_type" ]) all_mem = jitter . vm . get_all_memory () for region in all_mem : if region != args . lpvoid : continue region_data = all_mem [ region ][ "data" ] if region_data [: 2 ] == b "MZ" : print ( f "PE unpacked : ' { out_fname } ' (Region : 0x { region : x } )" ) f = open ( out_fname , "wb" ) f . write ( region_data ) f . close () exit () [ ... ] bname , fname = os . path . split ( options . filename ) fname = os . path . join ( bname , fname . replace ( '.' , '_' )) out_fname = fname + '_miasm_unpacked.bin' This generates the following result after execution: [ ... ] [ INFO ] : kernel32_VirtualFree ( lpvoid = 0x200b3000, dwsize = 0x0, alloc_type = 0x8000 ) ret addr: 0x20047446 PE unpacked : 'c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82_exe_miasm_unpacked.bin' ( Region : 0x200b3000 ) You can compare the cryptographic fingerprint of the manually unpacked PE with the one you've just generated with MIASM: $ sha256sum c173cfcb0adfa3013a398638789bf4350601cce0e1c55a456d98311543062f82_exe_miasm_unpacked.bin 9874c7bd9d008c8a7105c8e402813204d5c3ddc3fb8d1aaddbb0e19d65062dfb Congratulations! You've just unpacked Stage 1 with MIASM! \o/ You'll find the complete code for extraction with MIASM here . You can go on to the next article to analyze this unpacked sample Loader analysis (Stage 2) . Content #binary ninja #malware #miasm #packer #pkr_ce1a #stealc Check our offensive & continuous web security assessment service