工作中遇到个问题:需要在没有代码的情况下禁用某软件的一个功能。该软件为Windows软件。
接到任务后,先理下思路:
这几个过程里最耗时的是步骤2,如果目标模块没有符号,那任务将更加艰巨。
好了,开始干活。
1. 由于要禁用的功能是从界面事件激发的,所以考虑先从界面模块入手。但是经过分析发现目标软件的界面库是一套自绘界面库,并没用到任何Windows系统控件,所以放弃,转为从功能模块的dll入手。
2. 用IDA打开目标模块,如果目标模块包含了符号,那么在 Functions Window中看到的是真实的函数名,如图:
并且在IDA View窗口,会看到很多友好的注释和函数名,帮助你分析汇编代码,如图:
很悲催,我看到的是这样的:
表明目标模块中没有符号。
3. 无奈只能想办法缩小范围了。通过以下几种方法分析软件的行为
使用ProcessMonitor分析软件的行为。这个软件可以监测软件读写注册表、文件、网络等信息。是微软官方的工具。
使用ProcessExplorer动态分析线程和堆栈信息,这也是微软官方的工具。如果直接用IDA调试的话,可以不用这个。
使用IDA不断调试来验证自己的想法。
经过长时间的分析,最终发现在该软件设置界面启用目标功能时,该软件会向系统的AppData目录下写一个配置文件。在IDA里搜索该配置文件的文件名,找到了几处可疑的地方。这下就简单了,在可疑的地方打上断点,用IDA调试。最终确定的函数如下:
push ebp
mov ebp, esp
mov ecx, [ecx+7Ch]
push [ebp+8]
add ecx, 0B10h
push ecx
mov eax, [ecx]
mov edx, [eax+24h]
mov eax, esp
push 1
mov dword ptr [eax], 7A98DDFCh
call edx
pop ebp
retn 4
该函数的字节码如下:
55 8B EC 8B 49 7C FF 75 08 81 C1 10 0B 00 00 51 8B 01 8B 50 24 8B C4 6A 01 C7 00 FC DD 98 7A FF D2 5D C2 04
4. 接下来,在自己的模块中实现搜索字节码的功能。一个开源库的简单实现:
/* Scan for the signature in memory then return the starting position's address */
void* CSigScan::FindSignature(void) noexcept {
const unsigned char *pBasePtr = base_addr;
const unsigned char *pEndPtr = base_addr+base_len;
size_t i = 0;
while(pBasePtr < pEndPtr) {
for(i = 0;i < sig_len;i++) {
if((sig_mask[i] != '?') && (sig_str[i] != pBasePtr[i]))
break;
}
// If 'i' reached the end, we know we have a match!
if(i == sig_len)
return (void*)pBasePtr;
pBasePtr++;
}
return nullptr;
}
其中的sig_str就是字节码,sig_mask为占位符,类似这样 "xxxxxxxxxx???xxxxx", 其中 x 为要搜索的字节,? 为忽略字节。
本以为会很顺利,结果打脸,没搜到。反复检查发现,由于目标进程加载了我的注入模块,改变了后续模块的加载地址,这导致模块中一些常量字串的地址发生了偏移,所以我们需要忽略这函数中引用的常量地址。就是这句
mov dword ptr [eax], 7A98DDFCh
这句中的 7A98DDFCh 是常量地址,对应字节码 倒数第9 ~ 倒数第6 字节。
FC DD 98 7A
我们在搜索时忽略这4个字节就成了。
5. 讲下注入。注入的方法有很多。比如:CreateRemoteThreadEx,SetWindowsHookEx 等。而我采用了 DetourCreateProcessWithDllExW 方案。这条函数虽然会启动目标进程,但这样做有个好处,可以在所有其它模块执行前加载你的模块。
6. 替换目标函数。使用Detour很简单就实现了。
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(m_fnOld, m_fnReplacement);
DetourTransactionCommit();
在替换时,如果目标函数是一个类的成员函数,还需要些小技巧。类成员的调用方式为 __thiscall。而vc不允许我们定义__thiscall,否则编译会报错:
error C3865: '__thiscall' : can only be used on native member functions
我们可以定义一个 __fastcall 调用方式的函数 来替换 __thiscall 。由于__thiscall 默认用 ECX传递this指针作为第一个参数,而__fastcall默认用 ECX, EDX传递前两个参数,所以我们在定义时要多定义一个没用的参数,像下面这样:
typedef int (__thiscall *myFooPtr)( void* , unsigned int, unsigned int);
char* __fastcall myFoo( void* This, void* notUsed , unsigned int firstParam,
unsigned int secondParam) { }
可以看到我们定义的 __fastcall 函数,比声明的原函数类型多一个 notUsed参数。我的函数的实现很简单,将传入的int类型参数直接改为 0 并传入原始函数就成了。
终于实现了既定目标,看了看键盘上掉落的头发,我心里些许有些安慰 。。。