虚表指针
虚函数调用过程
内存中虚表结构图
在 VC2008 中编译如下代码并调试
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <iostream>
using namespace std;
class base_class {
private:
int m_base;
public:
virtual void v_func1() {
cout << "This is base_class's v_func1()" << endl;
//printf("This is base_class's v_func1()");
}
virtual void v_func2() {
cout << "This is base_class's v_func2()" << endl;
//printf("This is base_class's v_func2()");
}
virtual void v_func3() {
cout << "This is base_class's v_func3()" << endl;
//printf("This is base_class's v_func3()");
}
};
int main(int argc, char* argv[]) {
base_class MyClass;
base_class v_func1();
//MyClass v_func1();
base_class v_func2();
//MyClass v_func2();
base_class v_func3();
//MyClass v_func3();
return 0;
}
找到程序入口和main函数位置,进入main函数,进入call语句
可以看到虚表指针是通过offset来定义的,相当于是一个全局的地址,由编译器固定
如果继续向下执行,可以推测这句语句执行的效果是将0x00416804这个位置的内容(也就是虚表指针),放到ds:[0012FF70].
在数据窗口中跟随eax,这是之行前后改地址处发生的变化
我们知道,虚表指针指向虚表首地址,在虚表中存放着地址信息(各虚函数的首地址)
在数据区选中指针,右键-在数据窗口中跟随DWORD,找到了我们刚刚定义的几个函数
在反汇编窗口跟随前三个字段表示的地址,
进入v_func1进行查看,可以看到就是实现一个打印的功能
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <iostream>
using namespace std;
class employee {
public:
employee() {
printf("employee()!\n");
}
~employee() {
printf("~employee()!\n");
}
};
class manager : public employee
{
public:
manager() {
printf("manager()!\n");
}
~manager() {
printf("~maneger()!\n");
}
};
int main(int argc, char* argv[]) {
manager My;
getchar();
return 0;
}
在VS2008编译出现如下错误,尝试将运行时库调成“多线程调试(/MTd)”:
error LNK2019: 无法解析的外部符号 \_\_malloc_dbg,该符号在函数 "void * \_\_cdecl operator new(unsigned int,struct std::_DebugHeapTag_t const &,char *,int)"
在OllyICE的mian函数中第一个call指令会获取类的首指针的地址,也就是我们定义的manager类
可以发现指针所指位置的代码对应的就是初始化构造函数的内容。在解析完类之后、程序退出后,会用指针的方式调用析构函数把现在类初始化、堆栈、结构、指针的一些临时信息清空然后释放,让系统去回收这一部分内存。
跟进到该构造函数中,发现ECX指向地址被填充为CC,这是因为它其中没有其他一些成员的函数。
下面第一个call会获取指向其父类也就是employee类的指针,它也是一个构造函数,会根据可访问的内容(public)进行初始化。
再来看第二段代码
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
struct MyStruct {
int x ;
int y ;
};
//函数在结构体外部
void Max(MyStruct* str) {
if (str->x > str->y)
printf("%d",str->x);
else
printf("%d",str->y);
}
int main(int argc, TCHAR* argv[]) {
MyStruct haha ;
haha.x = 1 ;
haha.y = 2 ;
Max(&haha);
printf("%d\n",sizeof(haha));
return 0;
}
可以找到给结构体的两个变量赋值的语句,执行后在内存中跟随到1和2已经被写入。
或者直接在Command:dd eax+4
接下来程序将1和2作为参数压入,并在下面第一个call指令调用Max函数进行比较。
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
class MyTest {
public:
MyTest();
~MyTest();
void SetTest(DWORD dwTest);
DWORD GetTest();
public:
DWORD m_dwTest;
};
MyTest::MyTest() {
printf("1111\r\n");
}
MyTest::~MyTest() {
printf("2222\r\n");
}
void MyTest::SetTest(DWORD dwTest) {
this->m_dwTest = dwTest;
}
DWORD MyTest::GetTest() {
return this->m_dwTest;
}
int main(int argc, char* argv[]) {
MyTest Test;
Test.SetTest(1);
int Number = Test.GetTest(); //添加了Set,Get方法,并调用
getchar();
return 0;
}
这里简单说一下OD断点运作的原理:通过触发软件断点使用CC
填充了这一段,使得本来可以正常执行的程序生成一条系统的SEH异常链,调试器通过捕捉改异常,中断主线程,使新线程执行到断点位置
MyTest Test;
首先运行到MyTest Test
,它会调用printf打印1111。
Test.SetTest(1);
接着是,Test.SetTest(1)
,这里采用硬编码的方法(直接push 1)传参,接着调用GetTest
不难看出,C++的一行指令在汇编中需要四行来完成,从逆向类的角度可以这么看
this->m_dwTest;
##################################
mov dword ptr ss:[ebp-0x8],ecx # i=this
mov eax,dword ptr ss:[ebp-0x8]
mov ecx,dword ptr ss:[ebp+0x8]
mov dword ptr ds:[eax],ecx
执行这四句之前,0x0012FE68处内容为初始化的CC
执行第一句,把this指针的首地址压栈
第二句把该地址赋给eax
第三句,把第一个参数(在ebp+8处)给到ecx
第四句,把该参数写到指针所指向的数据区中去(也就是写到类对象的那个局部变量m_dwTest里去)
int Number = Test.GetTest();
再接着是int Number = Test.GetTest()
,作用是取出类对象中刚刚被赋值的局部变量
查看汇编,一共是三句
int Number = Test.GetTest();
##################################
mov dword ptr ss:[ebp-0x8],ecx
mov eax,dword ptr ss:[ebp-0x8]
mov eax,dword ptr ds:[eax]
执行第一句,将ecx的值作为一个地址指针压栈
执行第二句,将该指针赋给eax
执行第三句,将数据区中eax指向的内容保存到eax,作为函数结果进行返回
跳出GetTest函数可以看到,程序紧接着使用该返回值进行了写回(选中右键-数据窗口跟随-内存地址)
安装Radmin:Radmin是一款远控软件,区别于木马的是,它作用与局域网,也就是说它只能连接静态ip,不具备反弹连接(上线之后主动请求客户端)的功能。
双击受控机口会弹出一个对话框,目的是安全性配置,否则任何人都可以连接我们的被控端,从而对非授限的主机进行操作。
在这一部分我们的目标是:使用远程线程的代码来代替用户的手动输入。
打开x32_dbg,点击文件-附加-双击打开,程序暂停在主线程的代码部分;或者文件-打开程序,F9运行,即可找到程序的入口。
在这里我们选择附加
该弹窗是用mfc写的,我们可以使用一些工具抓到它的句柄。从Spy4Win可以看出,这里使用的是mfc的win32界面,上面有很多控件,绑定着一个标识ID。
现在的窗口设计是独占式线程,也就是说不进行输入主线程会一直暂停直到子线程结束。我们如果要对此进行修改,意味着而我们必须生成一个新线程,于是想到CreateThread。
使用快捷键Ctrl+G找到构造新线程的部分
注意看,在0x7C8106F6处有一个retn 18,这不是返回值,而是Windows下API调用的一个约定,目的是由内部函数来平衡堆栈。
在IDAx86中打开kernel32.dll,在导出表中查找函数CreateThread并跳转,可见内容与x32_dbg中一模一样
对该部分内容F5反编译为伪代码
HANDLE __stdcall CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId)
{
return CreateRemoteThread(
(HANDLE)0xFFFFFFFF,
lpThreadAttributes,
dwStackSize,
lpStartAddress,
lpParameter,
dwCreationFlags,
lpThreadId);
}
第一个参数lpThreadAttributes
是线程的安全属性,用于避免回调函数崩溃;
第二个参数dwStackSize
表示堆栈大小,指的不是CreateThread函数的堆栈大小,而是回调函数里局部堆栈的大小;
第三个参数lpStartAddress
,设置回调函数的指针;
第四个参数lpParameter
,用于传参(强转成指针,什么都可以传);
第五个参数dwCreationFlags
相当于一个flag标志(CreateThread调用CreateRemoteThreadEx_0再调用NTCreateThread,flag保证一致性);
第六个参数lpThreadId
是分配的线程ID。
这就解释了为什么最后要加一个retn 18,因为一共6个参数,每个参数占用4个字节(在内存中以4B对齐),十进制的24就是是十六进制的18,从而做到现场还原。
现在我们在CreateThread的第一句下一个断点,并再次双击受控机图标,发现程序确实被我们断下来了,这说明我们确实找对了位置。
我们现在通过单步+栈回溯的方法找到CreateThread的调用者(或者Alt+F9)
设置断点让程序运行至此
再往外层单步走三层,程序运行到一个较关键位置
通过jnz和jmp可以初步判断这里是一个分支语句
mov esi,dword ptr ss:[esp+83F4]
push eax
push edi
add ebp,3458
push ebp
push esi
call radmin.143F8E0 # 这里调用的CreateThread
add esp,10
test eax,eax
也就是说上面这一部分就是负责弹窗的汇编代码。
那么接下来我们在这段代码的开头设一个断点,重新运行程序,使之中断在该位置。
在内存窗口中跟随地址[esp+83F4],下图错误,应是ESI的值(指针)000B0BF2(指向的内存地址)
经历一系列压栈后,EAX值为00009C49(功能号),EDI值为008C5A74,EBP值为00128B94,ESI值为000B0BF2
跟随EDI指向内存内容如下,这里其实是我们需要连接的被控端的结构体,用于存放配置信息。可以看到存在User等字样,再往下拉可以看到有我们的要连接的IP信息127.0.0.1
知道了调用前的状态,接下来我们就可以伪造寄存器了。这里我们使用Code Injector远程注入汇编代码
pushad
mov esi,0
mov eax,0x9C49
mov edi,0x008C5A74
push eax
push edi
mov ebp, 0x00128B94
push ebp
push esi
mov eax,0x143F8E0
call eax
add esp, 10
popad
选择要注入的目标程序
F9运行,成功调用弹窗