使用Frida对app进行hook分析的基本方法介绍
导语:Frida是现在Hook实践中最友好的工具。 frida-trace可以实时检测hook函数的所有输入输出和动作,使用Frida可以迅速开发复杂的Hook逻辑,Frida可以分析程序行为,也可以通过Hook改变程序行为。
0x01 概述
我将对使用Frida进行程序检测和在Windows上hook进行基本介绍。在整个帖子中,我将使用frida-trace,因为它提供了方便的实时流,我可以实时检查对函数hook所做的更改。一旦掌握了JavaScript语法,就可以将该知识扩展到各种Frida绑定(Python / C / Node / Swift / .Net / QML)上。
为什么选择Frida?它提供了一个简单的界面,可以在其中快速开发复杂的hook逻辑,并随着需求对其进行更改,与重新部署C ++函数hook的复杂过程比较就方便多了。将Frida用于什么用途?正如我的标题所述的那样,是检查(查看应用程序内部以分析其行为)和hook(更改应用程序的行为)。从安全角度看,Frida是一种研究工具,不适合用于武器部署。话虽如此,Frida可以用于制作攻击性hook的原型,以后可以在其他框架如EasyHook中实施这些hook以进行部署。
0x02 注册表检查
在本节中,我将研究在任意Windows应用程序中被动监视注册表活动。首先,我们将看看RegOpenKeyExW,它最常用于打开注册表项的句柄。下面可以看到C ++函数原型。
LONG WINAPI RegOpenKeyEx( _In_ HKEY hKey, // Handle to the open registry key (commonly the registry hive). _In_opt_ LPCTSTR lpSubKey, // The name of the registry subkey to be opened. _In_ DWORD ulOptions, // REG_OPTION_OPEN_LINK/NULL. _In_ REGSAM samDesired, // A mask that specifies the desired access rights to the key to be opened. _Out_ PHKEY phkResult // A pointer to a variable that receives a handle to the opened key. );
大多数API都有ANSI和Unicode版本。出于我的目的,我应该假定我hook的Windows应用程序将使用Unicode版本,将Frida附加到流程并定义/打印所有这些参数。
Frida使此过程极其容易。当使用trace时,Frida在当前目录中创建一个“ handlers”文件夹,在其中使用onEnter / onLeave原型填充JS文件,以用于用户指定的任何功能。取出函数参数就像在数组中打印参数一样容易。上图所示的JS处理程序如下所示。
/* * Auto-generated by Frida. Please modify to match the signature of RegOpenKeyExW. * This stub is currently auto-generated from manpages when available. * * For full API reference, see: http://www.frida.re/docs/javascript-api/ */ { /** * Called synchronously when about to call RegOpenKeyExW. * * @this {object} - Object allowing you to store state for use in onLeave. * @param {function} log - Call this function with a string to be presented to the user. * @param {array} args - Function arguments represented as an array of NativePointer objects. * For example use Memory.readUtf8String(args[0]) if the first argument is a pointer to a C string encoded as UTF-8. * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. * @param {object} state - Object allowing you to keep state across function calls. * Only one JavaScript function will execute at a time, so do not worry about race-conditions. * However, do not use this to store function arguments across onEnter/onLeave, but instead * use "this" which is an object for keeping state local to an invocation. */ onEnter: function (log, args, state) { log("[+] RegOpenKeyExW"); log("¦- hKey: " + args[0]); log("¦- lpSubKey: " + args[1]); log("¦- ulOptions: " + args[2]); log("¦- samDesired: " + args[3]); log("¦- PHKEY: " + args[4] + "\n"); }, /** * Called synchronously when about to return from RegOpenKeyExW. * * See onEnter for details. * * @this {object} - Object allowing you to access state stored in onEnter. * @param {function} log - Call this function with a string to be presented to the user. * @param {NativePointer} retval - Return value represented as a NativePointer object. * @param {object} state - Object allowing you to keep state across function calls. */ onLeave: function (log, retval, state) { } }
能够提取所有这些函数参数,但是出于快速查看注册表活动的目的,最有用的参数可能是lpSubKey。当然,字符串指针并不是特别有用,但是我们可以轻松地重写onEnter函数来查看unicode字符串,如下所示。
onEnter: function (log, args, state) { log(Memory.readUtf16String(args[1])); }
如果我保存更改并在应用程序中执行一些新活动,将看到该应用程序正在访问完整的子项注册表路径。
如果仔细查看输出,将会发现缺少某些内容。实际上并没有获得注册表配置单元,这是因为该调用使用的是先前打开的要查询的配置单元的句柄。通过在使用过的句柄上进行查找并在调用之间跟踪它们,可以知道配置单元是什么,但这不在本文讨论范围之内。
如果通过COM Hijacking使用某些恶意代码来进行特权提升和持久驻留,其中列出了对包含“ CLSID \”的路径的注册表访问,其中调用的结果是失败的(很可能因为正在HKEY_CURRENT_USER中查询子项)。可以如下快速修改POC来捕获这些调用。
onEnter: function (log, args, state) { this.SubKey = Memory.readUtf16String(args[1]); // @this is available in onLeave if (this.SubKey) { // Make sure the value is not null if (this.SubKey.indexOf("CLSID\\") >= 0) { this.ContainsCLSID = 1; // Bool -> contains substring } } }, onLeave: function (log, retval, state) { if (this.ContainsCLSID) { // Check Bool if (retval != 0){ // If return value is not ERROR_SUCCESS log(this.SubKey); // Print subkey } } }
保存的POC只会返回尝试打开CLSID子项,结果不是ERROR_SUCCESS。
同样,可以跟踪哪些查询成功。如果想知道成功打开子键句柄后正在访问哪些键值,该怎么办?通常是通过使用RegQueryValueEx。如果现在hook这两个函数,可以实现一些简单的逻辑,在其中存储成功调用RegOpenKeyEx返回的句柄,并在调用RegQueryValueEx时将输入句柄与保存的句柄进行比较,如果它们匹配,则可以打印出被查询。可以在下面看到实现此目的的代码。
// The contents of the RegOpenKeyExW.js //--------------------------------------------------------- onEnter: function (log, args, state) { this.SubKey = Memory.readUtf16String(args[1]); // @this is available in onLeave if (this.SubKey) { // Make sure the value is not null if (this.SubKey.indexOf("CLSID\\") >= 0) { this.ContainsCLSID = 1; // Bool -> contains substring this.hSubKey = args[4]; } } }, onLeave: function (log, retval, state) { if (this.ContainsCLSID) { // Check Bool if (retval == 0){ // If return value is ERROR_SUCCESS state.HandleKey = new Array(Memory.readInt(this.hSubKey), this.SubKey); } // @state persists across API calls // We create an array with the handle & path } } // The contents of the RegQueryValueExW.js //--------------------------------------------------------- onEnter: function (log, args, state) { if (state.HandleKey) { // Check our array exists if (state.HandleKey[0] == args[0]) { // Compare stored handle with the new handle if (Memory.readUtf16String(args[1])) { // Make sure the value is not null log("[+] hKey: " + state.HandleKey[0] + "; Path: " + state.HandleKey[1]); log("¦- KeyValue: " + Memory.readUtf16String(args[1]) + "\n"); state.HandleKey = null; // We null here to clear the array } } } }, onLeave: function (log, retval, state) { }
现在,刷新POC仅返回过滤的条目。
这是一个简单的示例,但是可以看到Frida允许轻松地对函数进行测试并使用它们,而无需进行复杂的的Compile-> Test-> Compile循环。
0x03 Hook MessageBox
到目前为止,已经看到了如何进行被动检测,在本节中,将了解如何影响应用程序的行为。作为一个基本示例,我选择使用MessageBox,因为它类似于Windows API的“ Hello World”。为了使我能够动态地测试函数hook,我用C#编写了一个小的Windows MessageBox测试工具,可以在下面看到主要功能。
using System; using System.Runtime.InteropServices; using System.Windows.Forms; namespace msgbox { public partial class Form1 : Form { // Unmanaged MessageBoxA import [DllImport("user32.dll")] public static extern int MessageBox( IntPtr hWnd, String lpText, String lpCaption, UInt32 uType); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // Grab our textbox inputs String lpText = textBox1.Text; String lpCaption = textBox2.Text; UInt32 uType = Convert.ToUInt32(textBox3.Text); MessageBox(IntPtr.Zero, lpText,lpCaption,uType); } } }
使用在上一节中看到的相同技术,可以快速hook住应用程序并编写一些基本JS来转储MessageBox参数。注意使用readAnsiString来匹配MessageBoxA。
onEnter: function (log, args, state) { log("[+] MessageBoxA"); log("¦- hWnd: " + args[0]); log("¦- lpText: " + Memory.readAnsiString(args[1])); log("¦- lpCaption: " + Memory.readAnsiString(args[2])); log("¦- uType: " + args[3] + "\n"); }, onLeave: function (log, retval, state) { }
Frida具有许多用于编辑/分配内存原语的功能,建议读者阅读API文档。在我的简单演示中,我修改了JS以实现两种类型的hook:(1)如果lpText是“ Bob”,则将其更改为“ Alice”;(2)如果uType是6,则将其更改为0。
onEnter: function (log, args, state) { log(""); log("[+] MessageBoxA"); log("¦- hWnd: " + args[0]); log("¦- lpText: " + Memory.readAnsiString(args[1])); log("¦- lpCaption: " + Memory.readAnsiString(args[2])); log("¦- uType: " + args[3] + "\n"); // uType hook if (args[3] == 6) { log("[!] Hooking uType: 6 -> 0"); args[3] = ptr(0); // Overwrite uType with NativePointer(0) } // lpText hook if (Memory.readAnsiString(args[1]) == "Bob") { log("[!] Hooking lpText: Bob -> Alice"); this.lpText = Memory.allocAnsiString("Alice"); // Allocate new heap ANSI string args[1] = this.lpText; // Replace lpText pointer } }, onLeave: function (log, retval, state) { }
可以在下图中观察结果。请注意,这些参数是单独检查的,因此可能会遇到两个hook都没有/都处于活动状态的情况。
0x04 进程隐藏-> SystemProcessInformation
本文的最后一部分,我想简要地展示一个更复杂的hook示例。如果曾经在Windows上使用未公开的API的机会很大,那么已经使用过NtQuerySystemInformation及其一些信息类。这些类之一是SystemProcessInformation类(0x5)。事实证明,SystemProcessInformation是用户级进程,因此,无论通过哪个API获取进程列表的任何应用程序,最终都会过滤到NtQuerySystemInformation(任务管理器/进程浏览器/ Process Hacker / ..... )。
我最近为该函数Get-SystemProcessInformation编写了PowerShell包装器,所以我认为尝试将此函数与Frida Hook以演示用户界面进程隐藏是一个好主意。
https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Get-SystemProcessInformation.ps1
SystemProcessInformation内存布局
要了解hook的工作原理,我们需要知道使用SystemProcessInformation类时NtQuerySystemInformation实际返回的内容。希望下面的布局有助于帮助理解。
NTSTATUS WINAPI NtQuerySystemInformation( _In_ UINT SystemInformationClass, // SYSTEM_INFORMATION_CLASS _Inout_ PVOID SystemInformation, // A pointer to a buffer that receives the requested information _In_ ULONG SystemInformationLength, // Byte count allocated for the request _Out_opt_ PULONG ReturnLength // Pointer to the variable to receives the output size ); SystemInformationClass => SystemProcessInformation = 0x5 SystemInformation => Pointer, eg 0x11223344556 -----------------------| | | | [Points at an array of SYSTEM_PROCESS_INFORMATION Structs] | | |-------------------------------------------------| | |--------------------------------------| | [Int]NextEntryOffset (eg:0x1fb) | --------------------> | | | | | | | | | | ....... | | | | | | | | | | | | [UNICODE_STRING]ImageName | | [2nd Entry = 1st Entry + 0x1fb] | |-> svchost.exe | | | | | | | | | | | | ....... | | | | | | | | | SYSTEM_THREAD_INFORMATION structs | | |--------------------------------------| | | [Int]NextEntryOffset (eg:0x222) | | | | | | | | | | | ....... | | | | | | | | | | | | [UNICODE_STRING]ImageName | | [3rd Entry = 2nd Entry + 0x222] | |-> powershell.exe | | | | | | | | | | | | ....... | | | | | | | | | SYSTEM_THREAD_INFORMATION structs | | |--------------------------------------| | | [Int]NextEntryOffset (eg:0x3a0) | | | | | | | | | | | ....... | | | | | | | | | | | | [UNICODE_STRING]ImageName | | [4th Entry = 3rd Entry + 0x3a0] | |-> notepad.exe | | | | | | | | | | | | ....... | | | | | | | | | SYSTEM_THREAD_INFORMATION structs | | |--------------------------------------| | .....
这种表示方式并不完全准确,因为在数组的开头有一些固定的process。关键是每个blob的大小会有所不同,具体取决于将多少SYSTEM_THREAD_INFORMATION结构附加到SYSTEM_PROCESS_INFORMATION结构。
SystemProcessInformation Hooking 假设要隐藏该列表中的所有PowerShell进程,我要做的就是遍历该列表并在PowerShell之前重写条目,以便NextEntryOffset指向列表中的下一个条目。
|--------------------------------------| | [Int]NextEntryOffset (eg:0x1fb 0x41d)| --------------------> | I | | | I | | | I | | | ....... I==================================> [3nd Entry = 1st Entry + 0x1fb + 0x222 | | | => 1st Entry + 0x41d] | | | | | | | [UNICODE_STRING]ImageName | | | |-> svchost.exe | | | | | | | | | | | | ....... | | | | | | | | | SYSTEM_THREAD_INFORMATION structs | | |--------------------------------------| | | [Int]NextEntryOffset (eg:0x222) | | | | | | | | | | | | ....... | | | | | | | | | | | | [UNICODE_STRING]ImageName | | | |-> powershell.exe | | | | | | | | | | | | ....... | | | | | | | | | SYSTEM_THREAD_INFORMATION structs | | |--------------------------------------| | | [Int]NextEntryOffset (eg:0x3a0) | | | | | | | | | | | ....... | | | | | | | | | | | | [UNICODE_STRING]ImageName | | [4th Entry = 3rd Entry + 0x3a0] | |-> notepad.exe | | | | | | | | | | | | ....... | | | | | | | | | SYSTEM_THREAD_INFORMATION structs | | |--------------------------------------| | .....
这似乎有些复杂,但是我要做的就是截获即将返回的API调用,通过读取偏移量/ unicode字符串遍历列表,并为每个已确定的PowerShell进程覆盖一个整数。可以在下面查看我的Frida 实践。请注意,这些偏移量仅在x64 Win10上进行了测试(尽管它们对于Win7-10 x64应该有效)。
onEnter: function (log, args, state) { if (args[0] == 5) { log("NtQuerySystemInformation:"); log(" --> Class : " + args[0] + " [SystemProcessInformation]"); log(" --> Addr : " + args[1]); log(" --> len : " + args[2]); log(" --> Retlen: " + Memory.readInt(args[3]) + "\n"); this.IsProcInfo = 1; this.Address = args[1]; } }, onLeave: function (log, retval, state) { if (this.IsProcInfo) { while (true) { // Get struct offsets var ImageOffset = ptr(this.Address).add(64); // ImageName->UNICODE_STRING->Buffer var ImageName = Memory.readPointer(ImageOffset); // Cast as ptr var ProcID = ptr(this.Address).add(80); // PID // If PowerShell, rewrite the linked list if (Memory.readUtf16String(ImageName) == "powershell.exe") { log("[!] Hooking to hide PowerShell.."); log(" --> Rewriting linked list\n"); this.PreviousStruct = ptr(this.Address).sub(NextEntryOffset); Memory.writeInt(this.PreviousStruct, (Memory.readInt(this.PreviousStruct)+Memory.readInt(this.Address))) } // Move pointer to next struct var NextEntryOffset = Memory.readInt(this.Address); this.Address = ptr(this.Address).add(NextEntryOffset); if (NextEntryOffset == 0) { // The last struct has a NextEntryOffset of 0 break } } // Null this.IsProcInfo = 0; } }
0x05 实战演示
为该函数Get-SystemProcessInformation编写了PowerShell包装器,尝试将此函数与Frida Hook以演示用户界面进程隐藏。
可以看到现在开启了三个powershell进程。
执行下面的命令,截获即将返回的API调用,通过读取偏移量/ unicode字符串遍历列表,并为每个已确定的PowerShell进程覆盖一个整数。
执行前powershell进程都是存在的。
执行后在任务管理器中隐藏了所有PowerShell进程。
0x06 学习总结
Frida是现在Hook实践中最友好的工具。
frida-trace可以实时检测hook函数的所有输入输出和动作,使用Frida可以迅速开发复杂的Hook逻辑,Frida可以分析程序行为,也可以通过Hook改变程序行为。
Frida-trace -p 7276 -i RegOpenKeyExW -x KERNELBASE.DLL -x KERNEL32.DLL
分析注册表:RegOpenKeyExW用于打开注册表,Frida会捕获到这个API的所有参数,使用trace会创建一个JS文件,可以对JS文件做修改,就会输出不同内容:
onEnter: function (log, args, state) { log("[+] RegOpenKeyExW"); log("¦- hKey: " + args[0]); log("¦- lpSubKey: " + args[1]); log("¦- ulOptions: " + args[2]); log("¦- samDesired: " + args[3]); log("¦- PHKEY: " + args[4] + "\n"); }, onEnter: function (log, args, state) { log(Memory.readUtf16String(args[1])); }
保存后重新执行Frida-trace,就可以看到程序正在访问完整的子项注册表路径
上面是被动监视程序行为,也可以hook改变程序行为,使用相同技术,可以快速hook住应用程序并编写一些基本JS来转储MessageBox参数。
Frida具有许多用于编辑/分配内存原语的功能,修改了JS以实现两种类型的hook:(1)如果lpText是“ Bob”,则将其更改为“ Alice”;(2)如果uType是6,则将其更改为0。
尝试将Get-SystemProcessInformation函数与Frida Hook来演示用户界面进程隐藏,为该函数Get-SystemProcessInformation编写了PowerShell包装器,尝试将此函数与Frida Hook以演示用户界面进程隐藏。
截获即将返回的API调用,通过读取偏移量/ unicode字符串遍历列表,并为每个已确定的PowerShell进程覆盖一个整数。
发表评论