【非原创】用C++撰写shellcode

用C++撰写shellcode
1 C语言和ShellCode的冲突和解决办法 2
1.1 全局变量与shellcode的冲突和解决方法1 2
1.2 静态变量避免使用 3
1.3 字符串常量的问题和处理 3
1.4 函数指针避免直接使用及处理方法 4
1.5 用函数名字调用函数会出现重定位问题么 5
2 系统API的问题 5
2.1 系统API调用的处理 5
2.2 系统API地址的获取 6
3 C运行时库的问题和办法 9
4 用C++获得更好的编程环境 10
4.1 用C++获得更自然的编程习惯 10
4.1.1 去掉指针的轻松风格 10
4.1.2 用构造函数初始化API函数指针 11
4.2 另一种不传递全局对象指针的思考 12
4.3 继承,更好的对象化 12
4.4 真正的对象化,解决new构造对象的问题 13
4.5 虚函数的问题和解决办法 17
4.6 关于堆代码初始化的的修正和函数入口地址修正 22
5 代码的裁剪和执行 23
5.1 C运行库入口函数CRTMainStartup带来的问题 23
5.2 利用Entry点裁剪代码段 24
5.3执行提取的shellcode 25
5.4 将代码链接到一个自定义的段 26
6解决方案结构和使用描述 26

因为用汇编撰写shellcode比较麻烦。如果用C语言来撰写,则比较好。特别,对于类似病毒这种,远线程注入这种,如果能够用C语言来写,会方便很多。
分析scout(伽利略)这个被称为核武器的重量级分布式后门(400G源码),发现其windows肉鸡上有一些有意思的行为。其中一种工作方式是加载远程传递来的 shellcode执行。这样好奢华!但也换来了极大的好处,硬盘上没有任何有攻击特征的代码,自己运行shellcode,也使得各种检查工具失去了系统加载执行代码时的拦截机会。这也解释了起初看到其肉鸡代码有几十个工程的原因。每个工程都是独立的shellcode的母体嘛。可是,这意味着如果用汇 编编写shellcode将是无法承受的事。
scout采用的是C语言来撰写。网上也有不少c语言写shellcode的文章。不过不系统,比较杂乱。有不少点也讲得不清楚。我花了一天半的时间将这 个命题从C语言上升到了C++。除了虚函数特性用了一种方法进行折中处理,其它面向对象特性都完全支持,是纯 粹的面向对象编写模式。其中关于new操作符的较完善解决方案比较有意思。函数指针的使用的特殊处理。
总之,要理解这些内容你最好具备PE格式,链接器,加载等知识,特别是对重定位的理解。有了它,最终我们可以完成支持继承、封装、虚函数的组件化编程,这对shellcode来说是非常大的福利!
主要的矛盾有避免重定位的编程方式,如何裁剪代码。
的因为用汇编撰写shellcode比较麻烦。如果用C语言来撰写,则比较好。特别,对于类似病毒这种,远线程注入这种,如果能够用C语言来写,会方便很多。

1 C语言和ShellCode的冲突和解决办法
正常的程序借助加载机制,解决了:
(1) 程序中的地址重定位问题,
(2) 系统API定位问题
(3) 程序入口定位问题
关于(1)其实包含多个点
(a) 全局变量类型的内存空间访问问题
(b) 静态变量
(c) 字符串常量
(d) 函数指针
1.1 全局变量与shellcode的冲突和解决方法1
全局变量的指令形式如下
Mov [0x4578392], 18;这时指令中包含了全局变量的地址0x4578392,如果程序加载到非期望的位置,则会产生重定位。而重定位机制是通过可执行模块的重定位表和操作系统加载器协同完成((可参加《老码识途》1.8小节))。
如果,我们的C代码包含了全局变量,而shellcode必然是要被拷贝到任意地方执行的,因此必然发生重定位过程,要修改mov指令中包含地址的部分。这时,shellcode没有重定位表,也不是操作系统加载的,必然无法正常执行。
因此,我们要避免直接定义使用全局变量,而折中的方法。
局部变量的寻址是相对寻址,所以不会发生重定位问题。如果将全局变量转换成局部变量就ok了。
解决方案是定义一个结构体,包含所用要用到的全局变量
struct ShellCode
{
int gVar1;
int gVar2;
...
}
要用的函数,将该结构体的指针作为参数传入,全局只用一份该结构体的变量(这样就相当于全局变量了)
Func1(ShellCode * gVar, …){
gVar->gVar1 = …;//因为这个访问是局部变量其,地址计算是相对于ebp – xxx进行的,不会重定位。
}
下面是作为参数的的结构体指针指向的字段被访问的访问,红色标定的反汇编指令中不会出现重定位
23: void f(Var * v)
24: {
25: v->i1 = 12;
003010D8 8B 45 08 mov eax,dword ptr [ebp+8]
003010DB C7 00 0C 00 00 00 mov dword ptr [eax],0Ch
这个解决方案会导致每个函数对包含这个结构体参数,每个函数都要传递该结构体指针,并且每次访问时都会使用 “结构体参数名->”这样的语句,和我们全局变量的感觉有差异,且比较麻烦。我们后面要使用一种更为自然的方式。
1.2 静态变量避免使用
和1.1相同,静态变量是在数据段分配的,所以本质上是一个全局变量,其寻址是直接在指令里包含了变量地址,因此同样会面临重定位问题。所以要避免
1.3 字符串常量的问题和处理
Printf(“hello”);这样的语句会直接导致在只读数据段存放一个hello串,全局只有一份,这使得hello如同全局变量一样,对它的访问会导致对其地址的引用,因此就会导致重定位。
Printf(“hello”)将会编译成如下指令
push 0x4578332
call printf
push指令中包含了地址,这条指令需要被重定位。
避免的方法,在VS2008,VC6中如下指令是可行的
char buf[3] = {'h', 'e', 0};
printf(buf);
这时buf的初始化被翻译几条赋值语句
类似如下C语言的效果
buf[0] = ‘h’;
buf[1] = ‘e’;
buf[2] = 0;
这三条语句中,没有任何字串地址的引用,每个字符’h’,’e’都被直接编码到赋值指令当中作为一个立即数了。
1.4 函数指针避免直接使用及处理方法
函数指针会导致重定位发生,又不能找到全局变量的折中方案,因此要避免使用。比如,
void (*f)(int, int);
f = func;
这时这条f的赋值语句大概如下
Mov [ebp - 0c], 0x4578384;其中0x4578384就是func的地址。这就导致加载后要重定位的问题。我们如果仿效全局变量将函数指针全部整合到结构体中,成为其成员变量。依然面临一个问题,那就是必须在一开始将结构体中的这些函数指针变量设定初值(指向相关的函数入口)。可是,我们并不知道这些函数的地址是什么。
你会说,我们知道啊
v->func1 = func1;
就这样写嘛。Func1的地址就是它的名字噻。可是,编译成机器码后,就是前面那个地址值了。这个地址值在加载到非期望基址时是不对的,

解决办法1

我们要计算出正确函数入口地址必须获得实际加载地址,然后根据希望加载地址,重新计算入口。这是比较麻烦的。如下:
//设定函数指针变量的值,但这个是在期望加载基址下的入口地址
v.func1 = func1;

//获得实际加载地址和期望加载地址之间的差值loadoffset
_asm mov expected, offset _hereExpected
_asm call _hereExpected
_hereExpected:
_asm{
pop eax
mov actual, eax
}
loadoffset = actual - expected;
//自己完成实际函数入口地址的计算,完成重定位的过程
v.func1 = v.func1 + loadoffset
这样,要使用某个函数指针时,可以从扮演全局变量的结构体上获取到函数的实际入口地址。
v.func1();
这样做不方便的是,要将所有用到的函数的入口地址都记录到那个全局变量中。但每次使用时比较方便,直接取用即可,不做重定位处理。

解决办法2

只是解决办法一的一个变种,我们将办法1中的loadoffset记录到那个全局结构体中,然后提供一个重定位计算的宏函数.

denfine RELOCATE(VAR, FUNC) VAR->loadoffset + FUNC

要用时如下,先重新计算func1的入口地址,再调用:
pFunc1 = RELOCATE(var->loadOffset, func1);
pFunc1(...);

1.5 用函数名字调用函数会出现重定位问题么
从1.4我们的自然提问是,那用函数名字直接调用函数时,会出现函数指针的这种重定位问题么。
func1(...);
我们知道用函数名直接调用时,call指令是一个用相对偏移定位函数入口的。即从call指令结束为起点,以被调函数入口为终点,其差就是偏移(参考老码识途1.3.1)。call的机器码如下
E8 x x x x。后面x代表的4字节就是偏移量。
因为这个代码段包含了所有调用与被调用的函数,且它们之间的相对位置也是不会改变的。我们复制shellcode就是将整个代码段拷贝,那么不论拷贝到哪个地址,函数间的相对位置都不变,所以不影响用相对偏移量寻址函数入口的call指令。
结论,用函数名访问函数时,没有任何影响。
2 系统API的问题
2.1 系统API调用的处理
因为正常的C语言是通过导入表获取到API的入口地址,而shellcode没有导入表。所以必须自己获取api的地址。
为了不用每次都去动态查找api的地址,我们将api地址存放到那个存放全局变量的结构体中
typedef int (_stdcall *GSH)(int i);
struct ShellCode
{
int gVar1;
int gVar2;
GTD GetStdHandle;//存放API GetStdHandle的地址
...
}
要用API时,从ShellCode指针指向的字段获取API地址即可。
Func1(ShellCode * gVar, …){
gVar->GetStdHandle(…);

}

2.2 系统API地址的获取
从原理上,是先获取到dll的基址,再根据导出表获取到函数入口地址。
下面的实现是在win7上通过,遍历module链。注意module名字是unicode。链的结束并非是0或ffffffff代表,而是形成了回环。此函数的字串参数也是unicode。下面代码在win7上测试通过
__declspec(naked) unsigned long _stdcall GetDLLBaseW(void * unicodeStr, int len)
{
_asm{
finddll:;arg1 is ptr to dll name, arg2 is len of dll name; return imagebase in eax or 0 is not found
push ebx ;
mov ebx, fs:[30h]
mov ebx, [ebx+0ch]
mov ebx, [ebx+0ch]
loop_finddll:
push [esp + 12];push len
push [esp + 12];push dll name
push [ebx + 30h];push module name
call strcompare
test eax, eax
jnz found
mov ebx, [ebx]
mov eax, [ebx]
cmp [esp], eax;
jz not_found
jmp loop_finddll
found:
mov eax, [ebx + 18h]
jmp finddll_end
not_found:
xor eax, eax
finddll_end:
pop ebx
ret 8;

strcompare:;arg1 s1, arg2 s2, arg3 is len
push esi
push edi
push ecx
mov esi, [esp + 16]
mov edi, [esp + 20]
mov ecx, [esp + 24]
xor eax, eax
cld
repz cmpsb
jnz strnotequ
inc eax
strnotequ:
pop ecx
pop edi
pop esi
ret 0ch

}

}

函数地址的获取,该函数不完善,对于那种转接的api没处理,比如HeapAlloc在kernel32中并没实现,但却能查到输出的符号。其实函数在ntdll中为RtlHeapCreate
为了解决这个问题,我们先获取到GetProcAddress的地址,让它去处理即可。
__declspec(naked) unsigned long _stdcall getProcEntry(char * base, char * procName, int procNameLen)
{
_asm
{
push ebp
mov ebp, esp
push ebx
push ecx
push edx
push esi
push edi
mov edi, [ebp + 8];
mov ebx, edi
add ebx, OFFSET_NTHDR_START_IN_DOSHDR;
mov ebx, [ebx];
add ebx, edi;get nthdr address
add ebx, OFFSET_NTHDR_EXPORT_DIR
mov ebx, [ebx];get export tb rva
add ebx, edi;get export tb address
mov ecx, ebx
add ecx, OFFSET_NAME_NUM_EXPORT
mov ecx, [ecx];set loop count
mov edx, ebx
add edx, OFFSET_NAME_PTR_EXPORT
mov edx, [edx];get name ptr tb rva
add edx, edi; get name ptr tb address
push 0;allocate i = 0
find_proc_loop:
mov esi, [edx];get name str rva
add esi, edi;get name str address
push [ebp + 10h]
push esi
push [ebp + 0ch]; the proc to find
call strcompare
test eax, eax
jnz find_proc_found
add edx, 4
mov eax, [esp]
inc eax
mov [esp], eax
dec ecx;
jnz find_proc_loop
pop eax;balance stack
xor eax, eax; not found
jmp findproc_end
find_proc_found:
pop eax;//get i
mov edx, ebx;get export tb address
add edx, OFFSET_ORDINAL_EXPORT
mov edx, [edx];get ordinal tb rva
add edx, edi;get ordinal tb address
lea edx, [edx + eax * 2];get the address of i item of ordinal tb
xor eax, eax
mov ax, [edx];get index in proc ptr tb
add ebx, OFFSET_PROC_PTR_EXPORT
mov ebx, [ebx];get proc ptr tb rva
add ebx, edi;get proc ptr tb address
lea eax, [ebx + eax * 4];get proc entry item address
mov eax, [eax];get proc entry rva
add eax, edi;get proc address
findproc_end:
pop edi
pop esi
pop edx
pop ecx;
pop ebx
pop ebp
ret 0ch

strcompare:;arg1 s1, arg2 s2, arg3 is len
push esi
push edi
push ecx
mov esi, [esp + 16]
mov edi, [esp + 20]
mov ecx, [esp + 24]
xor eax, eax
cld
repz cmpsb
jnz strnotequ
inc eax
strnotequ:
pop ecx
pop edi
pop esi
ret 0ch
}
}

使用的效果如下:
OLECHAR s1[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0};
char GStd[] = {'G','e', 't', 'S', 't', 'd', 'H', 'a', 'n', 'd', 'l', 'e', 0};
char WRF[] = {'W','r', 'i', 't', 'e', 'F', 'i', 'l', 'e', 0};

unsigned long pdll = GetDLLBaseW(s1, 26);
GetStdHandle = (GSH)getProcEntry((char *)pdll, GStd, 13);

3 C运行时库的问题和办法
C编写的程序,会调用到printf之类的C库函数。
这些函数有的会调用到系统API。这时,因为我们无法改变原有C库的代码,这样,API的调用会有问题。
我们无法阻止C库使用全局变量、函数指针、静态变量,“...”风格的字串等,所以C库代码无法shellcode化。
解决办法
就是,要不我们自己实现一个C库的子集,替换libc.lib。要不就不用C库的函数。我觉得,一些常用的C库函数,我们反正要用到,其实可以提供一套自己的。
4 用C++获得更好的编程环境
4.1 用C++获得更自然的编程习惯
4.1.1 去掉指针的轻松风格
对于库函数,全局变量,以及我们提供的可复用的库函数是如下方式,比较不符合我们平常的编程风格
struct Prg{
int gx1;
int gx2;
PtrCreateFile CreateFile;
}
func(Prg * prg, ...){
prg->gx1 = 1;
prg->CreateFile(...);
}
...
func(&prg, ...);
这里的问题是,每个要用到API或全局变量的函数都要传入Prg指针,以及都要在程序中用->显式访问这些相关成员变量。我们想更加接近C语言的方式来编写。简单说就是,我们没有显示传递和引用prg指针:
func(...){
gx1 = 1; //没有显式引用prg
CreateFile(...);
}
...
func(..);//没有显式传递prg
这时,我们用面向对象自动隐式传递this指针来解决
1 先定义一个ShellCode类,它代表这个程序,它包含了我们要用到的API指针和我们自己的库函数(特别那些要用到API的库函数,因为他们要用到API指针),以及一个这个程序的入口函数run。
typedef int (_stdcall *GSH)(int i);
typedef int (_stdcall *WF)(int, void *, int, void *, void *);

class ShellCode
{
public:
int gx1;
GSH GetStdHandle;
WF WriteFile;
void run();
};
2 在shellcode开始处,即main函数中构造出ShellCode对象,并初始化好Shellcode中的API指针(GetStdHandle)。
MyShellCode p;
p.GetStdHandle = …;
p.WriteFile = …;
3 在Shellcode中定义自己的函数
void ShellCode::run(){
char buf[10] = {'h', 'e', '\n'};
int i = GetStdHandle(-11);
WriteFile(i, buf, 3, 0, 0);
return;
}
这个run函数要用到GetStdHandle和WriteFile,但我们都没有显式去用->,以及没有传递那个Shellcode的this指针。这样非常巴适。
4 我们的库函数,也成为ShellCode类的函数就好了。
比如我们实现了memcpy
可以这样
Class ShellCode
{
void memcpy(void * des, void * src, int n) ;
}
加入它要用到API,就如3中所示,直接引用好了

最终,我们的做法是,所有自定义的函数都成为Shellcode类的非静态成员函数,这样就ok了。
见工程testCShellcodSimplest
4.1.2 用构造函数初始化API函数指针
我们在构造函数中进行API函数指针的初始化,这样更便于使用
MyShellCode::MyShellCode()
{
OLECHAR s1[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0};
char GStd[] = {'G','e', 't', 'S', 't', 'd', 'H', 'a', 'n', 'd', 'l', 'e', 0};
char WRF[] = {'W','r', 'i', 't', 'e', 'F', 'i', 'l', 'e', 0};

unsigned long pdll = GetDLLBaseW(s1, 26);
GetStdHandle = (GSH)getProcEntry((char *)pdll, GStd, 13);
WriteFile = (WF)getProcEntry((char *)pdll, WRF, 10);

}

这样,只需要在main函数中定义局部变量即可,会自动调用构造函数
MyShellCode p;

工程见testCShellcodeSefGetProc
4.2 另一种不传递全局对象指针的思考
这里可以用结构化异常链
Fs:[0]替换指向成我们的异常块
而其后9-12字节可用一个特殊值代表这是特殊块
再后面就是所有API的入口地址构成的n个字段。
只需要通过fs:[0]回溯找到9-12字节的这个特殊值,第13字节就是全局结构体的入口地址
4.3 继承,更好的对象化
为了更好的封装,我们考虑将API指针的初始化放入ShellCode构造函数中,然后继承ShellCode类成为我们自己的类MyShellCode。当我们需要新的API时,将相关函数指针定义到子类MyShellCode中,在子类的MyShellCode完成相关API初始化工作。
这样,我们可以将常用的自定义库函数放入ShellCode类中,以及常用的API定义到ShellCode类中。编译成一个静态Lib。每次链接时用就可以了。
class MyShellCode : public ShellCode
{
public:
int gx2;
XXX API2;
XXX API3;
MyShellCode();
};

MyShellCode::MyShellCode()
{
API2 = ...;
API3 = ...;
}

对于其他的类,我们也直继承于MyShellCode,则可获得源码上不用传递全局结构体指针和用->访问成员变量的问题。而因为是ShellCode的子类,其构造函数会被自动调用,则API指针会自动初始化!将来即使ShellCode类的api指针表中表项数目变化,只需重新编译一次所有类即可。非常方便。

4.4 真正的对象化,解决new构造对象的问题
如果不能使用new构建对象,则4.3为止的工作在面向对象方面的意义并不显著。本质上仅仅是C风格下的程序。只不过解决了函数访问的便利性问题而已。
我们看看new操作符到底发生了什么事。这是vs2008 release版的反汇编
MyShellCode *ppp = new MyShellCode();
0040105E 6A 08 push 8
00401060 E8 8B 00 00 00 call operator new (4010F0h)
00401065 8B F8 mov edi,eax
00401067 59 pop ecx
00401068 85 FF test edi,edi
0040106A 74 04 je main+27h (401070h)
0040106C 33 C0 xor eax,eax
0040106E AB stos dword ptr es:[edi]
0040106F AB stos dword ptr es:[edi]
这里比较简单,主要调用了new操作符函数。
对于debug版就要复杂些,如下
MyShellCode *pp1 = new MyShellCode();
00DC1075 C7 85 C8 FE FF FF 08 00 00 00 mov dword ptr [ebp-138h],8
00DC107F 8B 85 C8 FE FF FF mov eax,dword ptr [ebp-138h]
00DC1085 50 push eax
00DC1086 E8 C5 00 00 00 call operator new (0DC1150h)
00DC108B 83 C4 04 add esp,4
00DC108E 89 85 CC FE FF FF mov dword ptr [ebp-134h],eax
00DC1094 83 BD CC FE FF FF 00 cmp dword ptr [ebp-134h],0
00DC109B 74 26 je main+73h (0DC10C3h)
00DC109D 8B 8D C8 FE FF FF mov ecx,dword ptr [ebp-138h]
00DC10A3 51 push ecx
00DC10A4 6A 00 push 0
00DC10A6 8B 95 CC FE FF FF mov edx,dword ptr [ebp-134h]
00DC10AC 52 push edx
00DC10AD E8 A4 00 00 00 call memset (0DC1156h)
00DC10B2 83 C4 0C add esp,0Ch

这时,我们看到上面指令中有调用memset函数对分配的内存清零。如果我们不提供自己的shellcode安全c库,是不能成功的。
而如果采用release版,因为只调用了new操作符函数,那么我们重载new操作符提供我们自己的shellcode安全版本的函数是可以解决这个问题的。
但重载new操作符有个问题
Void * operator new(int size)
{
HeapAlloc(gh, …)
}
我们需要一个全局变量来gh来保存堆的句柄。
解决方法一
当然,一个解决办法是GetProcessHeap可以返回一个可用的句柄。但如果是后门加载shellcode,而shellcode中又存在内存泄漏,那么最终将耗尽后门加载器进程的系统堆空间(scout后门采用了加载shellcode的方式)。一个比较安全的办法是,专门为shellcode分配一个堆,在最后强制释放该堆即可。于是我们需要一个全局变量保存堆。这是货真价实的全局变量,而不是前面我们折中时用结构体传入的方法。因为new操作符函数并非我们定义的函数,无法传入堆句柄。
解决方法二
还有一种办法是,我们不用new操作符,而用自定义的相当于new的函数来分配内存也可以。但这时风格比较诡异,且一不小心容易使用new操作符。
解决方法三,提供“全局变量”保存堆句柄
我们定义一个函数,其代码所占空间主要就是拿来做全局变量用的。在运行期这个代码的一部分会被写入数据当全局变量用。如此,我们没有办法在正常的工程中进行调试(这样是没法写程序的,程序是调出来的),因为代码段是不可写的。为了解决这个问题,采用了如下思路:
定义一个函数HeapHandle,它返回一个结构体HeapStuff的指针供被人访问。用条件编译提供两个版本,在debug模式下,我们真的返回一个HeapStuff全局变量的地址,这样就避免了写代码段。在发布模式下,我们返回的是函数HeapHandle自己的一块内存区作为HeapStuff变量供外部读写。
下面代码第一个条件分支是自定位代码,注意其call _here返回后面nop组成的内存的地址作为HeapStuff变量的地址。这个分支是非调试下给ShellCode用的。而#else对应的分支则定义了一个全局变量hpStuff,并将该变量的指针返回去。该分支用于调程序时使用。

ifndef _DEBUG

__declspec(naked) HeapStuff * HeapHandle()
{
/*
the 20 nop are for space of HeapStuff
*/
_asm
{
call _here
nop; //这个nop的总个数就是HeapStuff的大小,这里是20个
nop;

_here:
pop eax;
ret
}
}

else

HeapStuff hpStuff = {(HEAPCREATE)0x90909090, (HEAPALLOC)0x90909090,
HeapStuff hpStuff = {(HEAPCREATE)0x90909090, (HEAPALLOC)0x90909090, (HEAPFREE)0x90909090, (HEAPDESTROY)0x90909090, (void *)0x90909090};
__declspec(naked) HeapStuff * HeapHandle(){
_asm{
push ebp;
mov ebp, esp;
push eax
}
void * p;
p = &hpStuff;
_asm{
mov eax, p;
mov esp, ebp;
pop ebp
ret
}
}

}

endif

HeapStuff如下定义
struct HeapStuff
{
HEAPCREATE HeapCreate;
HEAPALLOC HeapAlloc;
HEAPFREE HeapFree;
HEAPDESTROY HeapDestroy;
void * HeapHandle;
};
因为重载的new操作符将数据写入了代码段中函数HeapHandle所占的内存,调试时代码段是只读的,所以重载的new函数用的是全局变量hpStuff保存api指针和堆句柄。Release版下用一堆nop那个版本,但不能调试运行。
void * operator new(size_t size)
{
HeapStuff * pHeapStuff = HeapHandle();
//第一次因为都是nop则需要初始化堆
if ((unsigned long)pHeapStuff->HeapAlloc == 0x90909090
&& (unsigned long)pHeapStuff->HeapCreate == 0x90909090)//0x90 is nop
{
OLECHAR s1[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0};
OLECHAR s2[] = {'n', 't', 'd', 'l', 'l','.', 'd', 'l', 'l', 0};
char sHC[] = {'H','e', 'a', 'p', 'C', 'r', 'e', 'a', 't', 'e', 0};
char sHA[] = {'R', 't', 'l', 'A', 'l', 'l', 'o', 'c','a', 't', 'e', 'H','e', 'a', 'p', 0};
char sHF[] = {'H','e', 'a', 'p', 'F', 'r', 'e', 'e', 0};
char sHD[] = {'H','e', 'a', 'p', 'D', 'e', 's', 't', 'r', 'o', 'y', 0};

	unsigned long pdll = GetDLLBaseW(s1, 26);
	pHeapStuff->HeapCreate = (HEAPCREATE)getProcEntry((char *)pdll, sHC, ShellCode::strlen(sHC) + 1);		
	pHeapStuff->HeapFree = (HEAPFREE)getProcEntry((char *)pdll, sHF, ShellCode::strlen(sHF) + 1);
	pHeapStuff->HeapDestroy = (HEAPDESTROY)getProcEntry((char *)pdll, sHD, ShellCode::strlen(sHD) + 1);

	pdll = GetDLLBaseW(s2, 20);
	pHeapStuff->HeapAlloc = (HEAPALLOC)getProcEntry((char *)pdll, sHA, ShellCode::strlen(sHA) + 1);
	pHeapStuff->HeapHandle = pHeapStuff->HeapCreate(0, 4096, 0);

}
return pHeapStuff->HeapAlloc(pHeapStuff->HeapHandle, HEAP_ZERO_MEMORY, size);

}

void operator delete(void* p)
{
HeapStuff * pHeapStuff = HeapHandle();
pHeapStuff->HeapFree(pHeapStuff->HeapHandle, 0, p);
}
void ShellCode::MemoryDestroy()
{
HeapStuff * pHeapStuff = HeapHandle();
pHeapStuff->HeapDestroy(pHeapStuff->HeapHandle);
}

本来在new操作符中对引用计数加一,在delete中对引用计数减一,则可自动释放所用的堆。但考虑到我们编程难免有内存泄漏,所以必须提供强制的释放能力。只需要在ShellCode基类上提供一个静态的函数DestroyMemory来强制回收即可。
如下:

MyShellCode * code;
//这里用new的原因是,在release版本下,使得HeapHandle被强制链接进代码段,因为它被new引用。
Code = new MyShellCode();
code->run();
ShellCode::DestroyMemory();

参见工程:
testCShellObjWithoutVirfunc\testCShellcode,
该解决方案使用要得请参考小节6
4.5 虚函数的问题和解决办法
虚函数会用到函数指针,而虚表又会存储到只读全局数据区。这样虚表的访问需要用到绝对地址,虚表中的函数地址也是绝对地址,两者都要涉及重定位,因此,我们在不能动编译器的情况下,只能用C风格的函数指针来模拟虚函数的覆盖,在《老码识途》中我们讨论过。我们尽量用对象的指针来模拟(老码识途中是C语言的模拟,所以必须自己完成this指针的传递,如果用对象的指针,会轻松些)。
总体思想是,一个虚函数对应一个函数指针放入基类中,在要覆盖的子类的构造函数中将子类自己的对应函数指针存入。为了方便使用,做了一些处理。见下。
当我们需要一个虚函数时,必须在第一次声明这个虚函数的类中定义一个类函数指针
比如下面定了了AA类的函数指针类型PtrFunc

class AA;
typedef void (__thiscall AA:😗 PtrFunc)(int i);
然后我们在AA类中定义该类型的函数指针fp,因为子类要用,而外部不想让他们访问,所以定为protected
class AA{
protected:
PtrFunc pf;

};
在下来,我们实现一个满足fp要求的函数p,成为AA的成员函数,并在AA构造函数中将函数指针p赋值给fp
class AA{
void p(int i)
{
printf("A\n");
}
protected:
PtrFunc pf;
public:
AA()
{
pf = &AA::p;//这时调用pf其实是调用的函数p
}
};
由于对象的函数指针语法很不好记所以,为了方便调用虚函数的人感觉不到这一麻烦,我们会定义一个函数virP调用fp,使用fp的人不直接调用fp而是调用virP。同时为了virP中调用对象函数方便,我们又定义了一个宏CALL_OBJ_FUNC,其参数pFunc就是要调用的函数指针的名字,在该例子中是fp。

define CALL_OBJ_FUNC(pfunc) (this->*(pfunc))

class AA{
void p(int i)
{
printf("A\n");
}
protected:
PtrFunc pf;
public:
void virP(int i)
{
CALL_OBJ_FUNC(pf)(i);
}
AA()
{
pf = &AA::p;
}
};
下面定义类C1在其构造函数中覆盖fp,为了阅读方便,实现虚函数的那个函数我们依然叫p,和父类AA是相同的,只是使用时,最好带上C1::p以示区别AA::p。
class C1 : public AA
{
public:
void p(int i)
{
printf("C\n");
}
C1()
{
pf = (PtrFunc)&C1::p;
}
};

最后使用是,是呼叫virP这个函数
C1 * cp = new C1();
cp->virP(1);
这样,虚函数的调用就完全是对象版的了,不需要传递this指针。
因为对于shellcode函数指针涉及到重定位的问题。所以,我们在构造函数中要手动重定位函数指针
void * GetActualAddress(void * expectedAddress)//该参数是编译期计算出的地址,返回是他的真实地址
{
char * actual, * expected;
_asm mov expected, offset _hereExpected
_asm call _hereExpected
_hereExpected:
_asm
{
pop eax
mov actual, eax
}
int loadoffset = actual - expected;
return (char )expectedAddress + loadoffset;
}
由于对象版指针只能在对象版指针类型之间强制转换(哪怕函数原型不匹配),不借助_asm代码难以将其转成void
传递给GetActualAddress。所以我们专门定义了一个假的类MockStub以它的名义定义一个对象的函数指针OBJ_FUNC_PTR(原型无所谓)。然后定义了一个GetObjFuncActualAddr来接收OBJ_FUNC_PTR类型的对象指针(不匹配强转传入即可),它调用GetActualAddress计算出真正的地址
class MockStub
{
};
typedef void (__thiscall MockStub:😗 OBJ_FUNC_PTR )();

OBJ_FUNC_PTR GetObjFuncActualAddr(OBJ_FUNC_PTR funcPtr)
{
void *pXXXX;
_asm{
mov eax, funcPtr;
mov pXXXX, eax
}
pXXXX = GetActualAddress(pXXXX);
_asm{
mov eax, pXXXX;
}
}

修改AA和C1类的构造函数,完成重定位
AA()
{
pf =(PtrFunc)GetObjFuncActualAddr((OBJ_FUNC_PTR)&AA::p);
}

C1()
{
	pf = (PtrFunc)GetObjFuncActualAddr((OBJ_FUNC_PTR)&C1::p);				
}

总结,在shellcode下的虚函数实现和使用原则

1 定义虚函数的类
a 需要定义虚函数指针的类型
b 在保护域定义a所定义的函数指针的变量fp
c 在构造函数对fp赋值,赋值时,要进行重定位
d 在该类中实现一个命名叫VirtualXXXXX的函数,它调用fp,调用时,用CALL_OBJ_FUNC方便调用
2 子类覆盖虚函数
在子类的构造函数中,模仿基类中的1.d条目,覆盖fp
3 使用虚函数
正常构造出对象后,如下使用即可
o->VirtualXXXX(…)即可

工程见解决方案testCShellObjWithoutVirfunc\ testCShellcode,具体工程描述可参加小节6
下面是工程中的代码,有两个类MyClass,和子类MyClassChild。定义了虚函数test。其wrapper是VirtualTest。调用虚函数都调用VirtualTest。
class MyClass;
typedef void (__thiscall MyClass:😗 MYCLASS_TEST)();
class MyClass : public ShellCode
{
protected:
MYCLASS_TEST _testFunc;

public:
MyClass()
{
//because parent class shellcode initialize the api tables, we have it without doing anything!
//if need api, intialize below;
//api3 = (SLEEP)getProcEntry((char *)_hKernel32, ss, strlen(ss) + 1);
//pf = (MYCLASS_TEST)GetObjFuncActualAddr((OBJ_FUNC_PTR)&MyClass::test);
_testFunc = RELCATE_OBJ_FUNC(MYCLASS_TEST, MyClass::test);
}

void VirtualTest()//虚函数的wrapper,调用时用它调用方便
{
	CALL_OBJ_FUNC(_testFunc)();
}

void test()//虚函数test的实现
{
   char buf[10] = {'h', '3', '\n'};
   int i = GetStdHandle(-11);
   WriteFile(i, buf, 3, 0, 0);
   return; 
}

};

class MyClassChild : public MyClass
{
public:
MyClassChild()
{
//because parent class shellcode initialize the api tables, we have it without doing anything!
//if need api, intialize below;
//api3 = (SLEEP)getProcEntry((char *)_hKernel32, ss, strlen(ss) + 1);
_testFunc = RELCATE_OBJ_FUNC(MYCLASS_TEST, MyClassChild::test);//虚函数的重载
}

void test()//虚函数test的实现
{
   char buf[10] = {'h', '4', '\n'};
   int i = GetStdHandle(-11);
   WriteFile(i, buf, 3, 0, 0);
   return; 
}

};
void MyShellCode::run(){
char buf[10] = {'h', 'e', '\n'};
int i = GetStdHandle(-11);
WriteFile(i, buf, 3, 0, 0);
Sleep(2000);
MyClass * p = new MyClass();
p->VirtualTest();//虚函数的调用
delete p;
p = new MyClassChild();
p->VirtualTest();//虚函数的调用
delete p;
return;
}
4.6 关于堆代码初始化的的修正和函数入口地址修正
因为获取入口地址的代码,要用到HeapAlloc,它不是实现在kernel32中。我获取入口地址的代码没有处理这个。所以,我们需要用系统的GetProcAddress来获取。为了方便后面使用,便将GetProcAddress的地址指针变量定义到HeapStuff中,供后面的对象使用。
之前的代码,只有调用了new才会初始化堆,如果跟对象shelcode 不是new,而是直接如下:
Shellcode p;
p.run();
如此,就不会调用new操作符,堆就暂时不会初始化,这本来没什么。但随之HeapStuff中的GetProcAddress字段也没有初始化,将导致程序出问题。于是我们将尝试初始化堆的代码放入tryInitHeap(如果未初始化,它会做初始化动作)中,HeapAlloc地址获取也由系统的GetProcAddress获取。然后在ShellCode构造函数中调用下tryInitHeap即可。这样只要有ShellCode对象,不论堆还是栈上分配的,都会尝试创建堆了。
HeapStuff * tryIntialHeap(){
HeapStuff * pHeapStuff = HeapHandle();
if ((unsigned long)pHeapStuff->HeapAlloc == 0x90909090
&& (unsigned long)pHeapStuff->HeapCreate == 0x90909090)//0x90 is nop
{
OLECHAR s1[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0};
char sHC[] = {'H','e', 'a', 'p', 'C', 'r', 'e', 'a', 't', 'e', 0};
char sHA[] = { 'H','e', 'a', 'p', 'A', 'l', 'l', 'o', 'c', 0};
char sHF[] = {'H','e', 'a', 'p', 'F', 'r', 'e', 'e', 0};
char sHD[] = {'H','e', 'a', 'p', 'D', 'e', 's', 't', 'r', 'o', 'y', 0};
char GProc[] = {'G','e', 't', 'P', 'r', 'o', 'c', 'A', 'd', 'd', 'r', 'e', 's', 's', 0};
unsigned long pdll = GetDLLBaseW(s1, 26);
pHeapStuff->HeapCreate = (HEAPCREATE)getProcEntry((char *)pdll, sHC, ShellCode::strlen(sHC) + 1);
pHeapStuff->HeapFree = (HEAPFREE)getProcEntry((char *)pdll, sHF, ShellCode::strlen(sHF) + 1);
pHeapStuff->HeapDestroy = (HEAPDESTROY)getProcEntry((char *)pdll, sHD, ShellCode::strlen(sHD) + 1);
pHeapStuff->GetProcAddress = (GETPROCADDRESS)getProcEntry((char *)pdll, GProc, strlen(GProc) + 1);

	pHeapStuff->HeapAlloc = (HEAPALLOC)pHeapStuff->GetProcAddress((char *)pdll, sHA);
	pHeapStuff->HeapHandle = pHeapStuff->HeapCreate(0, 4096, 0);
}
return pHeapStuff;

}

ShellCode::ShellCode(void)
{
OLECHAR s1[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0};
char WRF[] = {'W','r', 'i', 't', 'e', 'F', 'i', 'l', 'e', 0};
char GStd[] = {'G','e', 't', 'S', 't', 'd', 'H', 'a', 'n', 'd', 'l', 'e', 0};

tryIntialHeap();
unsigned long pdll = GetDLLBaseW(s1, 26);
_hKernel32 = pdll;

GetStdHandle = (GETSTDHANDLE)GetProcAddress((char *)pdll, GStd);
WriteFile = (WRITEFILE)GetProcAddress((char *)pdll, WRF);
}

测试代码也改成了
MyShellCode p;
p.run();
MyShellCode::MemoryDestroy();
工程参加解决方案testCShellObjWithVirfunc_GetProcOk
5 代码的裁剪和执行
似乎我们只有将代码段拷贝就可以了。但代码的入口点在哪里?是最开始么?不见得。
5.1 C运行库入口函数CRTMainStartup带来的问题
执行程序的入口地址并非我们的main。而是CRTMainstartup。如果我们采用pe格式中Entry点来定位入口地址,则解决办法是。
设定vs的链接选项的高级->入口点设定为main。

这样它就不会链接CRTMainstartup.而直接将entry点指向我们设定的函数,这里是main。

5.2 利用Entry点裁剪代码段
这个方法可以获取到代码入口点。如果这段代码是在后门中,被加载运行,那么可以通过加载代码执行入口点。没有问题。但如果是溢出攻击中的shellcode,无人帮你一开始转跳到入口点。可以在裁剪下来的代码前加入一个相对jmp指令,这样就没问题了。
再配合5.1的方法,将入口点直接设定到我们写的main函数就可以了。
提取shellcode的思路是:
首先找到代码段,拷贝其有效部分的长度(virtualsize)。找到entry点用entry点的值 – 代码段的virtualAddress,就是入口离代码段首的偏移offset。
在代码段前加5个字节 e9 offset。这就是相对jmp指令,offset导致该指令跳到真正的entry点。将jmp + 代码段的内容写入一个文件即可。

include <stdio.h>

include <windows.h>

include <winnt.h>

define OFFSET_OPTHDR_START 0x3c

IMAGE_NT_HEADERS ntHdrs;
void locateNTHdrStart(FILE * fp)
{
int hdrStart;
fseek(fp, OFFSET_OPTHDR_START, SEEK_SET);
fread(&hdrStart, sizeof(hdrStart), 1, fp);
fseek(fp, hdrStart, SEEK_SET);
}

void readHdrs(FILE * fp)
{
locateNTHdrStart(fp);
fread(&ntHdrs, sizeof(ntHdrs), 1, fp);
}

void main(int argC, char ** args)
{
if(argC != 2)
{
printf("please input like this: xxx filename(to get code)\n");
return;
}
FILE * fp;
fp = fopen(args[1], "rb");
if (fp == NULL)
{
printf("file does not exits\n");
return;
}
readHdrs(fp);

int sectionNum = ntHdrs.FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER codeSectionHdr;
bool found = false;
//找到代码段
for(int i = 0; i < sectionNum; i++) {
fread(&codeSectionHdr, sizeof(IMAGE_SECTION_HEADER), 1, fp);
if ((codeSectionHdr.Characteristics & 0x00000020) == 0x00000020)//if code segment
{
found = true;
break;
}
}
if (!found)
{
printf("cannot find code section\n");
return;
}
int codeLen = codeSectionHdr.Misc.VirtualSize + 5;//5是jmp的长度
char * code = (char *)malloc(codeLen);//attach jmp to head which will jmp to actual entry
//build jmp code
code[0] = 0xe9;
*(unsigned long *)(&code[1]) = ntHdrs.OptionalHeader.AddressOfEntryPoint - codeSectionHdr.VirtualAddress;//计算jmp指令的offset
fseek(fp, codeSectionHdr.PointerToRawData, SEEK_SET);
fread(code + 5, codeLen - 5, 1, fp);
fclose(fp);

fp = fopen("shellcode.bin", "wb");
fwrite(code, codeLen, 1, fp);//将合并好的jmp+代码段写入文件
fclose(fp);

5.3执行提取的shellcode
工程参加小节6的描述
非常简单,打开文件,读入内存,call到该段内存即可。只是设定工程属性时要将dep关闭
FILE * pFile;
long size;
pFile = fopen ("shellcode.bin","rb");
if (pFile==NULL)
perror ("Error opening file");
else
{
fseek (pFile, 0, SEEK_END); ///将文件指针移动文件结尾
size=ftell (pFile); ///求出当前文件指针距离文件开始的字节数
fclose (pFile);
//printf ("Size of file.cpp: %ld bytes.\n",size);
}
fclose(pFile);
void * code = malloc(size);
pFile = fopen ("shellcode.bin","rb");
fread(code, size, 1, pFile);
fclose(pFile);
_asm call code
free(code);
return 0;
5.4 将代码链接到一个自定义的段

6解决方案结构和使用描述
解决方案有
testCShellObjWithoutVirfunc
testCShellObjWithVirfunc
testCShellObjWithVirfunc_GetProcOk
是包含了完整shellcode方案的。区别只是在于shellcode是否实现了虚函数。下面描述其相同部分
该解决方案包含了三个工程
1 testCShellcode是ShellCode的工程
2 GetShellCode提取ShellCode,请参见5小节
3 shellloader 该工程从GetShellCode提取的ShellCode文件中读取整个code并调用执行。参加5.3小节

该解决方案的使用,修改shellcode并调试,将testCShellcode设定为启动项目,用debug模式执行。调试完毕,换成release模式,生成对应exe
然后在release模式下,执行GetShellCode即可,将生成一个叫shellcode.bin的执行码文件。然后依然在release模式下执行shellloader可看到效果。
因为GetShellcode要读写文件,shellloader也要读取文件,我设定了项目中相关目录,应该是可移植的(用的是相对路径)。如果有问题根据上述原理,自行调整即可。

posted @ 2023-08-16 17:41  范哥范小飞  阅读(395)  评论(1)    收藏  举报