摘要:利用反调试,虚拟机保护,关键函数和字符串隐藏,SMC加密技术,一机一码,花指令等技术实现一个软件保护的demo。
关键字:反调试,SMC,花指令,VMProtect,一机一码。
目录
一. 相关技术介绍和实现原理... 5
1. 什么是软件保护?如何做软件保护... 5
2. 花指令... 6
3. SMC加密技术... 6
4. 用户名和序列号加密算法设计... 7
5. 隐藏GetProcAddress 7
6. VMProtectSDK 保护... 8
7. 反调试原理... 8
1) IsDebuggerPresent. 8
2) CheckRemoteDebuggerPresent. 9
3) NtSetInformationThread(). 9
4) PebIsDebugged(). 9
5) HeapFlags(). 9
6) NtQueryInformationProcess(). 10
7) 异常处理... 10
8) 前台窗口... 11
9) 进程快照方式... 11
10) 硬件断点... 11
11) 软件断点... 11
二.程序一实现... 11
1. MFC实现一个CreakMe. 11
2. SMC加密技术... 12
3. SMC外部加密程序.. 14
4. 花指令... 16
1. 花指令1,函数跳转.. 16
2. 花指令2. 18
5. 用户名一机一码实现... 19
6. 序列号加密算法... 20
7. 隐藏GetProcAddress函数.. 21
8. VMProtectSDK 保护... 23
9. 反调试实现(代码和注释,原理在第一部分).. 25
10. KeyGen. 31
三.程序二实现... 32
1. 基于花指令或SMC技术,实现程序的静态反汇编逆向分析保护。... 32
2. 基于反动态调试技术,实现程序的反动态调试保护,要求实现多重反动态调试保护技术。... 36
3. 基于序列号保护技术,实现程序的软件版权保护。要求序列号实现和用户名相关并一机一码,不能使用硬编码序列号。... 40
四.功能测试... 43
1. 程序正常执行流程:... 43
1) 第一个程序... 43
2) 第二个程序... 45
2. 简单逆向程序一... 54
五.总结... 57
六.附录... 59
随着计算机技术的迅猛发展以及软件技术的快速成长,计算机软件技术不仅对国民经济建 设、信息安全起着重要作用,而且还是提高科技创新能力,增强技术竞争实力的重要组成部 分。由于软件的复制、拷贝是件很容易的事,所以导致非法复制、盗版软件之风的泛滥。
在这种形势下,为了防止软件的非法复制、盗版,保护软件开发商的利益,研制者和销售商对自己的软件进行技术保护和数据加密,以此来保护计算机软件著作权人的权益,鼓励计算机软件的 开发与应用,促进软件产业和国民经济信息化的发展。软件保护技术是软件开发者寻找各种有效方 法和技术来维护软件版权,增加其盗版的难度, 或延长软件破解的时间,尽可能防止软件被非法使用。
软件技术上分为很多不同的分支,主要包括:加密、防篡改、软件水印、软件多样化、反逆向技术、虚拟机、基于网络的保护和基于硬件的保护等。
1.加密:软件的代码以加密形式保护,在执行代码前进行解密操作,是一种应用最广的软件保护技术,对代码进行加密,并在软件运行前解密就是所谓的加壳。
2.加壳技术:源于加密技术但是后来由于其使用的广泛性,逐渐自成一派,而且综合使用了其它各种软件保护技术,可认为是软件保护技术的一种应用。
3.软件防篡改:在软件中加入一些特殊的机制,使得其他人试图修改软件时,软件做出拒绝执行、随机崩溃或者删除自身文件等保护软件的行为。防篡改算法要完成两个基本任务:
第一个是检查程序是否被修改,第二个是在发现代码被修改时执行相应的反制措施。
4.软件水印:在软件中嵌入唯一的标识以证明开发者对软件版权的所有权,从而防止因软件被盗版损害开发者利益。
5.软件多样化:一个软件可以生成不同的副本,让每个副本都各不相同以至于攻击者破解了软件的一个副本,不能用于其它副本,防止利用已知的漏洞进行攻击或者通过注册机进行盗版。
6.代码混淆:主要的目的是保护软件中的一些重要信息不被轻易获得,通过一系列的混淆方法,使非软件开发方通过逆向工程获得软件源代码的难度增大、时间增长,从而达到保护软件结构和数据的目的。代码混淆对逆向工程的抵抗作用明显,作为加密技术的补充和发展,受到越来越多的关注。它的出现使得攻击者难以通过IDA等工具反汇编、反编译逆向分析出程序的源码或中间码,从而获得程序的逻辑和算法。
7.反调试:在软件中加入各种调试器和虚拟机的探测器,一旦发现程序被调试或者在虚拟机中运行离开采取退出或自毁等防护措施,避免程序被分析。
8.反逆向技术:通过各种方式使攻击者无法获取和逆向程序的代码,又可进一步细分为反调试、代码混淆、自修改代码、代码分离等。
9.自修改代码( self-modifying code, SMC ):程序运行期间修改或产生代码的一种机制。自修改保护机制是有效抵御静态逆向分析的代码保护技术之一,广泛应用于软件保护和恶意代码等领域。计算机病毒等恶意代码的作者通常采用该技术动态修改内存中的指令来达到对代码加密或变形的目的,从而躲过杀毒软件的检测与查杀,或者增加恶意代码逆向分析人员对代码进行分析的难度。
10. 虚拟机软件保护:属于自修改代码的一种,近年来逐渐发展为一条独立的软件保护方法分支。虚拟机保护就是将某段程序编译成具有特定意义的一段代码,这段代码不能在目标机器上直接执行,要通过解释器模拟执行。虚拟机代码在可执行文件中只是一块数据,反汇编工具是不能反编译虚拟机代码的,因为虚拟机代码是在运行过程中解释执行的。虚拟机保护方法的局限性在于其设计机制复杂,开发成本较高,而且经过此方法保护的程序容量会大大增加,造成的时间和空间开销都很大。
在原程序中添加一些汇编指令,添加后不影响原程序的正常功能,但是能使反汇编工具反汇编错误。花指令是一段汇编指令,对于程序而言,花指令是无用的汇编代码,花指令的有无不影响程序的运行。
原理:根据反汇编工具的反汇编算法,构造代码和数据,在指令流中插入很多“数据垃圾”,干扰反汇编软件的判断,从而使得它错误地确定指令的起始位置,杜绝了先把程序代码列出来再慢慢分析的做法。
编写花指令的基本原则:保持堆栈平衡以及花指令不会影响程序的功能。堆栈平衡是指执行花指令前和执行后esp保持不变。
(注:原理介绍使用其他博客文章内容,会在最后给出博客地址)
所谓SMC(Self Modifying Code)技术,就是一种将可执行文件中的代码或数据进行加密,防止别人使用逆向工程工具(比如一些常见的反汇编工具)对程序进行静态分析的方法,只有程序运行时才对代码和数据进行解密,从而正常运行程序和访问数据。计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。现在,很多加密软件(或者称为“壳”程序)为了防止Cracker(破解者)跟踪自己的代码,也采用了动态代码修改技术对自身代码进行保护。以下的伪代码演示了一种SMC技术的典型应用:
proc main:
............
IF .运行条件满足
CALL DecryptProc (Address of MyProc);对某个函数代码解密
........
CALL MyProc ;调用这个函数
........
CALL EncryptProc (Address of MyProc);再对代码进行加密,防止程序被Dump
......
end main
然后写外部函数对区段进行解密操作。
对用户名的唯一性获取,先获得CPU的制造商,这里需要借助cpuid的汇编指令。CPUID操作码是一个面向x86架构的处理器补充指令,它的名称派生自CPU识别,作用是允许软件发现处理器的详细信息。Win32k 平台上,获取CPUID的办法主要有两种,一种是利用 WMI 另一种是利用 x86 汇编的 cpuid 指令,而最快的办法就是通过汇编了,而且 WMI 与汇编之间效率上的差距的确有点让人难以忍受,WMI 获取 CPUID 的效率几乎接近了一秒钟,而利用 cpuid 指令的办法,大概是几个 us 时间的问题,调用代码在第二部分实现。
然后根据得到的字符串和进程PID进行异或,这里这个异或虽然在输入的时候会麻烦一点,但是在调试的时候利用GetCurrentID()函数得到的进程id是调试器的,会对反调试有作用。
序列号的处理是利用得到的用户名和CPU制造商,然后先解密字符串“20171120051”,再根据这个字符串的数值取出CPU制造商的字符串对应位置的字符。再把得到的字符根据学号数值插入到用户名对应的位置形成序列号。实现代码见第二部分。
隐藏方式就是根据它在kernel32里面的作用,自己写一个函数去实现它。函数获得DLL句柄(也就是DLL的内存映射基地址),和需要找的函数字符串。根据导出表去寻找这个函数的函数地址,导出表结构如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Base 函数以序号导出的时候的序号基数,从这个数开始递增
NumberOfFunctions 本dll一共有多少个导出函数,不管是以序号还是以函数名导出
NumberOfFunctions 本dll中以能够以函数名称导出的函数个数(注意,说一下,其实函数里的 每一个函数都能通过序号导出,但是为了兼容性等等,也给一些函数提供用函数名称来导出) AddressOfFunctions 指向一个DWORD数组首地址,共有NumberOfFunctions 个元素,每一个元素都是一个函数地址 AddressOfNames 指向一个DWORD数组首地址,共有NumberOfNames个元素,每一个元素都是一个字符串(函数名字符串)首地址 AddressOfNameOrdinals指向一个WORD数组首地址,共有NumberOfNames个元素,每一个元素都是一个函数序号 我们说的最后俩数组,其实是一种一一对应的关系,假如分别叫 dwNames[] 和 dwNamesOrdinals[], 假如dwNames[5]的值(这个指是一个地址,前面都说了)指向的字符串等于“GetValue”,那么dwNamesOrdinals[5]的值 (这个指是一个序号,前面都说了),就是GetValue导出函数的序号啦,这时候就需要用到第一个数组了,假如名字叫dwFuncAddress[], GetValue的导出地址就是 dwFuncAddress[dwNamesOrdinals[5]] + 模块基址 。
代码实现部分在第二部分。借助了Stack Overflow的部分,后面会给出我参考的博客地址。
虚拟机保护技术,是指将代码翻译为机器和人都无法识别的一串伪代码字节流;在具体执行时再对这些伪代码进行一一翻译解释,逐步还原为原始代码并执行。
这段用于翻译伪代码并负责具体执行的子程序就叫作虚拟机VM(好似一个抽象的CPU)。它以一个函数的形式存在,函数的参数就是字节码的内存地址。大作业当中调用了最新的VMProtect3.3版本的SDK进行代码虚拟或者变异。实现过程和使用方法见第二部分实现部分。在被加密之后的函数位置不是原来的指令了,而是一些垃圾跳转和指令,逆向的时候需要寻找bytecode解密出跳转表来找正常的代码逻辑。
(注:这里原理介绍借用了看雪帖子的文章)
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。
这个也是NtDLL里面的结构体,你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger)
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。进程运行时,位置fs:[30h]指向PEB的基地址。为了实现反调试技术,恶意代码通过这个位置检查BeingDebugged标志,这个标志标识进程是否正在被调试。
Reserved数组中一个未公开的位置叫作ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫作ForceFlags和Flags。Win10偏移地址是0x44。下面是一个win10堆栈结构表:
这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。大作业中使用的是debugPort。
进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。SetUnhandledExceptionFilter函数可以修改系统最后的异常处理器。下面的代码先触发异常,然后在新注册的最后的异常处理器内部判断进程正常运行还是调试运行。进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其能够继续运行;进程调试运行时产生无效的内存访问异常,从而无法继续调试。这里是设置的异常时除数是0。但是需要在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。不然没效果。
可以使用FindWindowA或者GetWindowTextA获得窗口句柄来比较窗口名字,可以自己设置一个黑名单进行比较。通配符暂时实现不了。
通过CreateToolhelp32Snapshot()函数获得运行程序的京城快照,然后定义一个PROCESSENTRY32结构体,根据句柄返回的文件名进行循环比较黑名单的文件名。
在OllyDbg的寄存器窗口按下右键,点击View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7这几个寄存器。DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。DR4、DR5由系统保留。 DR6、DR7用于记录Dr0-Dr3中断点的相关属性。如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。
软件断点是通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。这里以普通断点和函数断点分别举例。
这部分是参考C++中文网的demo和它的消息机制,具体代码见后面附件或者打包文件,不做过多阐述。
(注:这部分只展示实现的情况和相关代码,测试阶段会比较调试的时候区别)
首先把需要保护的加密算法保护起来:
#pragma code_seg(".JIANG")
void fun()
{
MessageBox(NULL, TEXT("wrong"), TEXT("错误"), MB_ICONINFORMATION);
}
string Fun1(string userkey, string cpu) //处理之后的用户名,和CPU型号 处理序列号函数
{
string name = "kihnhhkiilh"; //20171120051
char a[50];
char CPU[50];
strcpy_s(CPU, cpu.c_str());
for (int i = 0; i < name.length(); i++)
{
char k = name[i] ^ 'Y';
a[i] = CPU[hex2int(k)];
}
for (int i = 0; i < name.length(); i++)
{
char k = name[i] ^ 'Y';
string s(1, a[i]);
userkey.insert(hex2int(k), s);
}
return userkey;
}
#pragma code_seg()
#pragma comment(linker, "/SECTION:.JIANG,ERW")
面对之前的问题,就是try_except里面不能放置类,所以这里在后面解密阶段直接进行解密,不进行try结构来检查函数是否解密完成。解密(加密)操作就是遍历找到区块,然后进行先解密一个字符串,再和这个字符串进行循环异或,再进行移位运算。
遍历找区段名字:
void SMC_De(char* pBuf, char* key) //SMC解密函数
{
const char* szSecName = ".JIANG";
short nSec;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeader;
PIMAGE_SECTION_HEADER pSec;
pDosHeader = (PIMAGE_DOS_HEADER)pBuf;
pNtHeader = (PIMAGE_NT_HEADERS)& pBuf[pDosHeader->e_lfanew];
nSec = pNtHeader->FileHeader.NumberOfSections;
pSec = (PIMAGE_SECTION_HEADER)& pBuf[sizeof(IMAGE_NT_HEADERS) + pDosHeader->e_lfanew];
for (int i = 0; i < nSec; i++)
{
if (strcmp((char*)& pSec->Name, szSecName) == 0)
{
int pack_size;
char* packStart;
pack_size = pSec->SizeOfRawData;
packStart = &pBuf[pSec->VirtualAddress];
xorPlus(packStart, pack_size, key, strlen(key));
return;
}
pSec++;
}
}
然后调用异或和移位加密算法:
void xorPlus(char* soure, int dLen, char* Key, int Klen) //异或之后再移位
{
for (int i = 0; i < dLen;)
{
for (int j = 0; (j < Klen) && (i < dLen); j++, i++)
{
soure[i] = soure[i] ^ Key[j];
soure[i] = ~soure[i];
}
}
for (int i = 0; i < dLen; i++)
{
char m = soure[i];
soure[i] = soure[dLen - i - 1];
soure[dLen - i - 1] = m;
}
}
加密的字符串要先进行解密操纵再待到后面运算:
string KeyBuffer = "86y6+4y ,787y 70/<+*0- y,4kihnhhkiilh";
for (int i = 0; i <KeyBuffer.length(); i++)
{
KeyBuffer[i] = KeyBuffer[i] ^ 'Y';
}
首先选择需要进行加密的文件,这部分代码和之前实验的一样:
//取得文件路径部分
TCHAR szFilePath[MAX_PATH];
OPENFILENAME ofn = { 0 };
memset(szFilePath, 0, MAX_PATH);
memset(&ofn, 0, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL;
ofn.hInstance = GetModuleHandle(NULL);
ofn.nMaxFile = MAX_PATH;
ofn.lpstrInitialDir = ".";
ofn.lpstrFile = szFilePath;
ofn.lpstrTitle = "选择PE文件";
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
ofn.lpstrFilter = "(*.*)\0*.exe;*.dll\0";
GetOpenFileName(&ofn);
if (szFilePath == NULL)
{
MessageBox(NULL, "打开文件错误", NULL, NULL);
//return 0;
}
//创建文件句柄
HANDLE hFile;
char KeyBuffer[MAX_PATH] = "ao Form yunNan University Num:20171120051";
hFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, TEXT("打开文件失败"), NULL, NULL);
return;
}
打开程序之后就跟解密的思路是一样的了,先找到需要进行加密的区段,然后对区段进行异或和移位加密:
void xorPlus(char* soure, int dLen, char* Key, int Klen)
{
for (int i = 0; i < dLen;)
{
for (int j = 0; (j < Klen) && (i < dLen); j++, i++)
{
soure[i] = soure[i] ^ Key[j];
soure[i] = ~soure[i];
}
}
for (int i = 0; i < dLen; i++)
{
char m = soure[i];
soure[i] = soure[dLen - i - 1];
soure[dLen - i - 1] = m;
}
}
void SMC(HANDLE hFile, char* key)
{
// SMC 加密XX区段
HANDLE hMap;
const char* szSecName = ".JIANG";
char* pBuf;
int size;
short nSec;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeader;
PIMAGE_SECTION_HEADER pSec;
size = GetFileSize(hFile, 0);
hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, size, NULL);
if (hMap == INVALID_HANDLE_VALUE)
{
_viewf:
MessageBox(NULL, TEXT("映射失败"), NULL, NULL);
return;
}
pBuf = (char*)MapViewOfFile(hMap, FILE_MAP_WRITE | FILE_MAP_READ, 0, 0, size);
if (!pBuf) goto _viewf;
pDosHeader = (PIMAGE_DOS_HEADER)pBuf;
pNtHeader = (PIMAGE_NT_HEADERS)& pBuf[pDosHeader->e_lfanew];
if (pNtHeader->Signature != IMAGE_NT_SIGNATURE)
{
MessageBox(NULL, TEXT("不是有效的win32 可执行文件"), NULL, NULL);
goto _clean;
}
nSec = pNtHeader->FileHeader.NumberOfSections;
pSec = (PIMAGE_SECTION_HEADER)& pBuf[sizeof(IMAGE_NT_HEADERS) + pDosHeader->e_lfanew];
for (int i = 0; i < nSec; i++)
{
if (strcmp((char*)& pSec->Name, szSecName) == 0)
{
int pack_size;
char* packStart;
pack_size = pSec->SizeOfRawData;
packStart = &pBuf[pSec->PointerToRawData];
xorPlus(packStart, pack_size, key, strlen(key));
MessageBox(NULL, TEXT("加密成功"), NULL, NULL);
goto _clean;
}
pSec++;
}
MessageBox(NULL, TEXT("未找到JIANG区段,加密失败"), NULL, NULL);
_clean:
UnmapViewOfFile(pBuf);
CloseHandle(hMap);
return;
}
这个只能加密自己需要保护的程序,保护不了加密不了其他程序。
有很多花指令的设计思路是用垃圾参数和字节来填充空间,然后利用反汇编工具的工作模式不能正确解读造成干扰。我设计的思路是去干扰调试者,能让反汇编工具正常工作而且函数是正常,但是函数没有实际意义,而是一堆循环。
首先是定义三个基本函数,这三个基本函数的出口都是一个具体的数值,但是出口数据可以影响另一个函数的运行,不管怎么相互调用,最后的结果都是1,平坦化设计思路。三个基本函数如下:
#define FUN0 lo0 //垃圾函数
#define FUN1 lo1
#define FUN2 lo2
#define FUN3 lo3
static inline int FUN0(void)
{
volatile int i = 138, j = 1949;
if ((i++) % 2 > 0) j *= i;
if (j < 0) i *= 2;
else return 0;
i = 1;
while (i++ < 2) { j /= i; j++; i++; }
return i;
}
static inline int FUN1(void)
{
volatile int i = 21, j = 75;
if ((i--) % 3 > 0) j *= i;
if (j > 1) i *= 3;
else return 1;
i = 1;
while (i++ < 3) { j /= i; j--; i++; }
return j;
}
static inline int FUN2(void)
{
volatile int i = 56, j = 17;
if ((i--) % 5 > 0) j *= i;
if (j > 2) i *= 5;
else return 0;
i = 1;
while (i++ < 5) { j *= i; j += 3; i += 3; }
return i;
}
static inline int FUN3(void)
{
volatile int i = 1909, j = 131;
if ((i--) % 7 > 0) j *= i;
if (j > 3) i *= 7;
else return 1;
i = 1;
while (i++ < 7) { j /= i; j -= 5; i += 5; }
return i;
}
然后定义的花指令就可以这样调用:
#define _FLOWER_FUN_0 {if(FUN2())FUN1();if(FUN0()) FUN3();if(FUN1()) FUN2();if(FUN3()) FUN1(); \
if(FUN1())FUN0();if(FUN2()) FUN3();if(FUN3()) FUN1();if(FUN1()) FUN0();}
#define _FLOWER_FUN_1 {if(FUN3())FUN1();if(FUN1()) FUN2();if(FUN2()) FUN0();if(FUN0()) FUN1(); \
if(FUN2())FUN1();if(FUN0()) FUN3();if(FUN1()) FUN2();if(FUN3()) FUN1();}
If语句得到的值不会去影响下面的判断,只要最后return 0就行。
asInvoker:
父进程是什么权限级别,那么此应用程序作为子进程运行时就是什么权限级别。
默认情况下用户启动应用程序都是使用 Windows 资源管理器(explorer.exe)运行的;在开启了 UAC 的情况下,资源管理器是以标准用户权限运行的。于是对于用户点击打开的应用程序,默认就是以标准用户权限运行的。如果已经以管理员权限启动了一个程序,那么这个程序启动的子进程也会是管理员权限。典型的情况是一个应用程序安装包安装的时候使用管理员权限运行,于是这个安装程序在安装完成后启动的这个应用程序进程实例就是管理员权限的。有时候这种设定会出现问题,你可以阅读 在 Windows 系统上降低 UAC 权限运行程序(从管理员权限降权到普通用户权限)。
requireAdministrator
此程序需要以管理员权限运行。在资源管理器中可以看到这样的程序图标的右下角会有一个盾牌图标。
用户在资源管理器中双击启动此程序,或者在程序中使用 Process.Start 启动此程序,会弹出 UAC 提示框。点击“是”会提权,点击“否”则操作取消。
highestAvailable
此程序将以当前用户能获取的最高权限来运行。这个概念可能会跟前面说的 requireAdministrator 弄混淆。
如果你指定为 highestAvailable:当你在管理员账户下运行此程序,就会要求权限提升。资源管理器上会出现盾牌图标,双击或使用 Process.Start 启动此程序会弹出 UAC 提示框。在用户同意后,你的程序将获得完全访问令牌(Full Access Token)。当你在标准账户下运行此程序,此账户的最高权限就是标准账户。受限访问令牌(Limited Access Token)就是当前账户下的最高令牌了,于是 highestAvailable 已经达到了要求。资源管理器上不会出现盾牌图标,双击或使用 Process.Start 启动此程序也不会出现 UAC 提示框,此程序将以受限权限执行。对进程控制块的操作,有管理员权限的程序就能控制。
首先获得CPU制造商,调用函数如下:
string GetManID()//获取制造商信息
{
char ID[25];//存储制造商信息
memset(ID, 0, sizeof(ID));//先清空数组 ID
ExeCPUID(0);//初始化
memcpy(ID + 0, &debx, 4);//制造商信息前四个字符复制到数组
memcpy(ID + 4, &dedx, 4);//中间四个
memcpy(ID + 8, &decx, 4);//最后四个
//如果返回 char * ,会出现乱码;故返回 string 值
return string(ID);
}
然后里面的ExeCPUID(0),是另一个函数,CPUID指令是intel IA32架构下获得CPU信息的汇编指令,可以得到CPU类型,型号,制造商信息,商标信息,序列号,缓存等一系列CPU相关的东西。前面原理部分有介绍。
void ExeCPUID(DWORD veax)//初始化CPU
{
__asm
{
mov eax, veax
cpuid
mov deax, eax
mov debx, ebx
mov decx, ecx
mov dedx, edx
}
}
所以用户名的唯一性就是得到的CPU生产商再和进程pid进行循环异或:(至于为什么还是选择进程pid参与计算,是因为在动态调试的时候,用GetCurrentProcessId()函数返回的可能是调试器的进程id,后面调试会讲。)
processId = GetCurrentProcessId(); //进程pid
string username(string CPU, DWORD pid) //异或拼接
{
string cur_str = to_string(long long(pid));
int j = 0;
for (int i=0; i < CPU.length(); i++)
{
CPU[i] = CPU[i] ^cur_str[j];
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2020-2-13 16:40
被kanxue编辑
,原因:
上传的附件: