当前位置:   article > 正文

从0带你写插件之微信防撤回,保姆级教学代码一行一行解读_vx防撤回分析

vx防撤回分析

前几天逛看雪的时候,发现fickle大佬的这个思路很是不错,正好我也挺感兴趣的,就去学了一下inlinehook,嗯,不太难,所以就有了这篇文章,我从0开始讲这个代码框架,我很尽量的把这篇博客写的详细了,应该会一点点C基础就能看懂,想看原文这里https://bbs.pediy.com/thread-258054.htm

dll注入技术

就是把我们的代码加在目标进程然后执行,可以理解为一个变种的exe文件,只不过main函数变成了DLLMain,

DLLMain

MSDN定义DLLMain原型

BOOL APIENTRY DllMain( HMODULE hModule, //指向自身的句柄
                       DWORD  ul_reason_for_call, //调用原因
                       LPVOID lpReserved//隐式加载和显式加载
	//LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候在转换回来
                     )
  • 1
  • 2
  • 3
  • 4
  • 5

三个参数都是_In_类型
第二个参数

系统是在什么时候调用DllMain函数的呢?静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是::
DLL_PROCESS_ATTACH、
DLL_PROCESS_DETACH、
DLL_THREAD_ATTACH、
DLL_THREAD_DETACH。
以下从这四种情况来分析系统何时调用了DllMain。
进程映射
DLL_PROCESS_ATTACH
大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。
当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH,这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
可参考DllMainTest的DLL_PROCESS_ATTACH_Test函数。
进程卸载
DLL_PROCESS_DETACH
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。
那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:
FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)
注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。
可参考DllMainTest的DLL_PROCESS_DETACH_Test函数。
线程映射
DLL_THREAD_ATTACH
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。
注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
线程卸载
DLL_THREAD_DETACH
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

简单说

ul_reason_for_call的值	代表的状态
DLL_PROCESS_ATTACH	Dll刚刚映射到进程空间中
DLL_THREAD_ATTACH	进程中有新线程创建
DLL_THREAD_DETACH	进程中有新线程销毁
DLL_PROCESS_DETACH    Dll从进程空间中接触映射
  • 1
  • 2
  • 3
  • 4
  • 5

一般只用DLL_PROCESS_ATTACH(Dll刚刚映射到进程空间的时候就执行相关的代码)就够了,
DLLMain函数怎么写

BOOL APIENTRY DllMain( HMODULE hModule, DWORD dwReason,LPVOID lpReserved)
{
switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
		***添加想执行的语句(功能函数)***
    case DLL_THREAD_ATTACH:
		break;
    case DLL_THREAD_DETACH:
		break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

当dll被加载(DLL_PROCESS_ATTACH)时,为了实现功能,因为这次插件要加载其他进程,所以要调用创建进程(CreateThread)调用函数(ThreadProc)
CreateThread函数

MSDN
该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。
原型:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

参数

IpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,NULL使用默认安全性,不可以被子线程继承,否则需要定义一个结构体将它的bInheritHandle成员初始化为TRUE
第一个参数NULL就ok了
wStackSize,设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。
第二个0就行
lpStartAddress,指向线程函数的指针,形式:@函数名,函数名称没有限制,
就是功能函数
lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
dwCreationFlags :线程标志,可取值如下
0就行
lpThreadId:保存新线程的id。```
NULL
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

本次形式:CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(DlgThread), hModule, 0, NULL)
LPTHREAD_START_ROUTINE函数指针

插件思路

如果监控到有被撤回的消息,找到这条消息的发出者(wxid)和内容,然后弹出一个对话框(DialogBox)
先了解一下对话框

DialogBox

对话框
msdn

int DialogBox(
HINSTANCE hInstance,//对话框属于当前进程,HINSTANCE是窗口进程句柄
LPCTSTR lpTemplate,//对话框使用哪个对话框资源
HWND hWndParent,//对话框的父窗口是哪个,NULL表示没有父窗口
DLGPROC lpDialogFunc//窗口有对应的消息处理函数
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, Dlgproc);
最后一个函数是回调函数,继续查MSDN

DLGPROC Dlgproc;	

INT_PTR Dlgproc(
  HWND unnamedParam1,
  UINT unnamedParam2,
  WPARAM unnamedParam3,
  LPARAM unnamedParam4
)
{...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

细看一下

unnamedParam1

Type: HWND

A handle to the dialog box.

unnamedParam2

Type: UINT

The message.

unnamedParam3

Type: WPARAM

Additional message-specific information.

unnamedParam4

Type: LPARAM

Additional message-specific information.

Type: INT_PTR
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

在这里插入图片描述
看这里,第二个参数

unnamedParam2

Type: UINT

The message.
  • 1
  • 2
  • 3
  • 4
  • 5

有好几种类型,点进去看一下WM_INITDIALOG的documentation
在这里插入图片描述
我找到了中文文档,浏览器翻译不太行,一般都是自己翻译。这次竟然有官方中文文档https://docs.microsoft.com/zh-cn/windows/win32/dlgbox/wm-ctlcolordlg
WM _ INITDIALOG 消息:
在对话框显示之前立即发送到对话框过程。 对话框过程通常使用此消息来初始化控件并执行任何其他影响对话框外观的初始化任务。就是表明对话框及其所有子控件都创建完毕了
所以说对话框DialogBox最后一个参数的回调函数应该这样写

INT_PTR CALLBACK Dlgproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_INITDIALOG: {
			m_dialog_hwnd = hWnd;
			wechatWinAddr = GetWxModuleAddress();
			revockCallVA = wechatWinAddr + REVOCK_CALL_RVA;
			revockCallTargetVA = wechatWinAddr + REVOCK_CALL_TARGET_RVA;

			revockCallJmpBackVA = revockCallVA + 5;
			StartHook5(wechatWinAddr+REVOCK_CALL_RVA,backCode,_OnRevock);
		}break;
		case WM_COMMAND:
			break;
		case WM_CLOSE:
			close(hWnd);
		break;
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

RVA是什么呢,看我的这篇笔记
请添加图片描述

请添加图片描述
call的VA = LoadLibraryW(L"WeChatWin.dll") + RVA(0x28c33f)
目标地址(撤回的消息的地址)= LoadLibraryW(L"WeChatWin.dll") + RVA(0x28ccd0)

后面就是inline hook函数了

inline hook

盗个图来解释一下流程
请添加图片描述
但是不能直接把汇编代码插入的,因为内存装载到内存里面以后,如果我们手动强制添加一串代码(指令)进去,那需要给程序重新做地址重定位,这是不可能的事情,所以我们只能在要hook的地方写入一个jmp,跳转到我们写的函数(call)执行功能,然后再跳到原来的函数去,这样内存结构不变,就是个指令替换,如上图,如果还没有看懂,我找到了另外一个图
请添加图片描述
自己也画了个图,按OD的感觉画的
请添加图片描述

inlinehook步骤

1.寻找call

构造一个长度为5字节的jmp指令(jmp的机器码占用1字节,跳转到的地址偏移占用4字节)来替换原来的5字节的call指令。E8,即我们需要寻找长度为5字节的call,来进行替换

2.jmp到指定地址

这次用到的短跳转(近距离地址),E9,如果需要hook的假设需要hook的call的指令的内存地址为:0x1000,我们想要它执行后跳转到我们的函数(假设函数在内存中的地址:0x5000),那么,构造jmp指令时,指令应为:

jmp (0x5000-(0x1000 + 5))
jmp 0x3FFB
  • 1
  • 2

因为jmp指令占5字节,所以要加5

hook代码框架

在这里插入图片描述
Write(Read)ProcessMemory太简单了,但是正好有好久好久之前的笔记,那就放出来吧,省得你们去查MSDN了请添加图片描述请添加图片描述
okk,对应Write(Read)ProcessMemory的文档就很明确了,hookAddr是要被替换的call指令的内存地址(0x1000);backCode是长度为5字节的数组缓冲区,写的是call指令(E9 0x3FF8);FuncBeCall是0x5000,用OpenProcess打开当前进程,然后读取当前进程hookAddr处的指令,写入backCode数组,再将构造好的jmp指令写入当前进程hookAddr处。
StartHook函数第3个参数接收一个函数地址,这个函数地址指向的函数应该是这样的:

DWORD tEsp = 0;
_declspec(naked) void _OnRevock() {
	__asm {
		mov tEsp, esp
		pushad
	}
	OnRevock(tEsp);
	__asm {
		popad
		call revockCallTargetVA
		jmp revockCallJmpBackVA
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

OnRevock(tEsp)就是功能函数

void OnRevock(DWORD esp) {
	wchar_t *tips = *(wchar_t **)(esp + 0xC);
	wchar_t *msg = *(wchar_t **)(esp + 0x10);
	if (NULL != tips) {
		WCHAR buffer[8192];
		wchar_t* pos = wcsstr(tips, L"撤回了一条消息");
		if (pos!= NULL && NULL != msg) {
			swprintf_s(buffer, L"* %s,内容:%s \r\n\r\n",tips, msg);
			HWND hTextBox = GetDlgItem(m_dialog_hwnd, IDC_EDIT1);
			//EM_SETSEL消息,wParam代表开始,lParam代表结束
			SendMessage(hTextBox, EM_SETSEL, -1, -1);
			//EM_REPLACESEL消息,wParam代表是否可以撤销替换,lParam代表替换成的文本
			SendMessage(hTextBox, EM_REPLACESEL, 0, (LPARAM)buffer);
		}
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

裸函数

_OnRevock函数用_declspec(naked)修饰,被它修饰的函数我们常称它为裸函数,编译器不对裸函数做任何操作,也就是说裸函数就是我们自己写的汇编代码,裸函数的特点是在编译生成的时候不会产生过多用于平衡堆栈的指令,这意味着在裸函数中我们要编写内联汇编控制堆栈平衡。一个比较简单写法是备份所有的寄存器,做完其他操作后再把寄存器的值还原回去,

笨代码示例

DWORD tEax = 0,tEcx = 0,tEdx = 0,tEbx = 0,tEsp = 0,tEbp = 0,tEsi = 0,tEdi = 0;
_declspec(naked) void OnCall() {
	__asm {
		mov tEax, eax
		mov tEcx, ecx
		mov tEdx, edx
		mov tEbx, ebx
		mov tEsp, esp
		mov tEbp, ebp
		mov tEsi, esi
		mov tEdi, edi
	}
	//do something
	__asm {
		mov eax, tEax
		mov ecx, tEcx
		mov edx, tEdx
		mov ebx, tEbx
		mov esp, tEsp
		mov ebp, tEbp
		mov esi, tEsi
		mov edi, tEdi
		call ...
		jmp ...
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

裸函数规则

The following rules and limitations apply to naked functions(摘自MSDN)
在这里插入图片描述
官方示例代码

// nkdfastcl.cpp
// compile with: /c
// processor: x86
__declspec(naked) int __fastcall  power(int i, int j) {
   // calculates i^j, assumes that j >= 0

   // prolog
   __asm {
      push ebp
      mov ebp, esp
      sub esp, __LOCAL_SIZE
     // store ECX and EDX into stack locations allocated for i and j
     mov i, ecx
     mov j, edx
   }

   {
      int k = 1;   // return value
      while (j-- > 0)
         k *= i;
      __asm {
         mov eax, k
      };
   }

   // epilog
   __asm {
      mov esp, ebp
      pop ebp
      ret
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

改良裸函数
因为tEax = 0,tEcx = 0,tEdx = 0,tEbx = 0,tEsp = 0,tEbp = 0,tEsi = 0,tEdi = 0;
寄存器入栈可以改变一下

改前
push EAX
push ECX
push EDX
push EBX
push ESP
push EBP
push ESI
push EDI
改后
pushad
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

同理,mov 8个通用寄存器, tEax改为popad
所以,改后就成为了下面这种模板

_declspec(naked) void OnCall() {
	__asm {
	    pushad
	}
	//do something 功能函数
	__asm {
	    popad
	    call ...
	    jmp ...
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

卸载inline hook

卸载hook的流程比较简单,也是打开当前进程,把hook时备份的call指令写回原来的位置

int Unhook(DWORD hookAddr, BYTE backCode[5]) {
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
	if (WriteProcessMemory(hProcess, (LPVOID)hookAddr, backCode, 5, NULL) == 0) {
		return -1;
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

参数hookAddr是原来的call的内存地址,backCode是之前备份下来的call指令(EB XXXX0000)

将dll注入程序

怎么说,正好手头有例子,看下恶意代码例子12-01吧,xp虚拟机启动,IDA启动
在这里插入图片描述

在这里插入图片描述
每分钟都会有个讨厌的弹窗,而且我虚拟机里的这个IDA没用反汇编插件,反正汇编也不难,直接分析吧
在这里插入图片描述

LoadLibraryA和GetProcAddress将psapi.dll中的三个函数的地址分别保存在dword_408714,dword_40870c,dword_408710
我继续向下看,到这我发现有点不太对在这里插入图片描述
很明显了,用lab12-01.dll来调用LoadLibraryA然后注入Lab12-01.dll,和我们这次要说的没啥关系
浪费我五分钟时间,不说这个了,感兴趣的看这个博客https://blog.csdn.net/wangtiankuo/article/details/77097536,短小精干,我下次遇到inlinehook恶意代码我再补充
在这里插入图片描述
注入器来着吾爱大神,帖子https://www.52pojie.cn/thread-429548-1-1.html
注入器放网盘了
链接:https://pan.baidu.com/s/1UkIb-ODMQgzjNTBdxNjixQ
提取码:hlll
说实话,这个老哥把注入器的源码发出来了,真想今天晚上就搞明白,给这篇文章再加一个从0带你写注入器,MD,两点了熬不动了,明天周五和周末搞
源码放网盘了,带了点配置文件,懒
链接:https://pan.baidu.com/s/19Yn1L7guj-p5NjPBGpe2ag
提取码:hlll

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/421001
推荐阅读
相关标签
  

闽ICP备14008679号