cxk壳的流程实现和复盘
加壳语言:C++
目标软件文件格式:PE
加壳平台:Win32 / Win64 (最开始写的时候,只希望在Win 7 32位平台可以运行)
软件加壳是为了更好的保护软件,目前世面上各种成熟的商业壳很多。从技术上讲,攻防无绝对,很多很厉害的商业壳被更厉害的大佬脱的干干净净。
目前强度很高,最富盛名的壳子是VMP。当然VMP壳也有对应的脱壳方法,有人做过工作量计算,如果一个大佬一个人全职脱壳(体量不大的情况下),大概半年能完全脱掉。
厉害的壳子并不是完全杜绝破解者破解,因为大佬无穷多,总是有对应的解决办法。厉害的壳子是像VMP壳一样,拉长破解者破解的时间,大概是那种。
我知道各位很厉害,我也知道各位一定能破解的了,我只是希望能多托各位几天。在这种理念前提下,写壳的时候,更多就是计算攻防双方的时间成本。
以下是一个表格,以攻防双方水平差不多为大前提。举例不同手法下,防守方实现防守手段时间,和攻击方成功破解所花费时间。n表示 > 1的未知数。
由上可见,在写壳的过程中:
最理想的情况是:设计出一套高性价比手法的加壳方案(实现起来简单,破解起来麻烦)。
退而求其次的是:设计一些中性价比的方案,争取攻防双方消耗同等时间。
极力要避免的是:低性价比的手法,防守方实现起来无比麻烦,攻击方三两下干掉。
在明确加壳宗旨前的写壳计划:
在经过老师指点,明确加壳宗旨后,修改的工作计划。
对原计划的更改
这里本应该放弃编号2、编号3 两个低性价比的方案,但是在调整方案前,编号2第性价比的方案就已经写完。所以只放弃了编号3性价比低的方案。
新增的工作计划
导入表相关:
导入表这里一共做了3个操作:
1、 目标文件原导入表被清0,运行时在堆区申请空间,填写API地址,把堆区地址写入IAT,完成IAT混淆。
2、 堆区填写IAT地址的时候,为防止破解直接跳转到关键点,写了6段垃圾代码,随机选用填充。
3 、在填写API时候,并未直接出现函数名的字符串对比,而是对函数名算了个hash,通过Hash对比填写API地址。
把上面的操作一步一步展开说:
1)原文件导入表清空填0,代码如下:
//这里清空原文件导入表的操作 封装成了一个函数
bool Pe_Imp::clear_imp()
{
if (0 == this->m_imp_lst.size())
{
this->get_imp_data();
}
DWORD dw_iat_foa = 0;
DWORD dw_int_foa = 0;
DWORD dw_name_foa = 0;
DWORD dw_byname_foa = 0;
bool b_ret = false;
char* p_name = 0;
St_Pe_Int* p_iat = 0;
St_Pe_Int* p_int = 0;
St_Int_Name* p_int_name = 0;
std::list<St_Imp_Data>::iterator it_imp_begin = m_imp_lst.begin();
std::list<St_Imp_Data>::iterator it_imp_cur = m_imp_lst.begin();
std::list<St_Imp_Data>::iterator it_imp_end = m_imp_lst.end();
//清理iat byname
for (; it_imp_cur != it_imp_end; it_imp_cur++)
{
//清空dll名
dw_name_foa = it_imp_cur->dw_name_foa;
p_name = this->m_p_data_buf + dw_name_foa;
memset(p_name, 0, strlen(p_name));
//获取 int iat表的 foa
dw_int_foa = it_imp_cur->dw_int_foa;
dw_iat_foa = it_imp_cur->dw_iat_foa;
std::list<St_Int_Data>::iterator it_int_cur = it_imp_cur->int_lst.begin();
std::list<St_Int_Data>::iterator it_int_end = it_imp_cur->int_lst.end();
for (; it_int_cur != it_int_end; it_int_cur++)
{
p_int = (St_Pe_Int*)(this->m_p_data_buf + dw_int_foa);
p_iat = (St_Pe_Int*)(this->m_p_data_buf + dw_iat_foa);
//int清0 iat清0
memset(p_int, 0, sizeof(St_Pe_Int));
memset(p_iat, 0, sizeof(St_Pe_Int));
dw_iat_foa += sizeof(St_Pe_Int);
dw_int_foa += sizeof(St_Pe_Int);
//如果是序号 就没啥事了
if (true == it_int_cur->is_ords)
{
continue;
}
//定位到名称表 全部填0
p_int_name = (St_Int_Name*)(this->m_p_data_buf
+ it_int_cur->dw_byname_foa);
p_int_name->Hint = 0;
memset(&(p_int_name->Name), 0, it_int_cur->n_name_len);
}
}
St_Pe_Imp* p_cur_imp = 0;
p_cur_imp = this->m_p_imp_table;
for (int n_index = 0; n_index < this->m_n_imp_count; n_index++)
{
p_cur_imp->OriginalFirstThunk = 0; //int填0
p_cur_imp++;
}
return 0;
}
2)垃圾代码填充,申请堆区空间,完成IAT混淆,代码如下:
//填充代码1 0xffffffff 的位置是将要填入真正API地址的位置
unsigned char sz_code_buf1[11] = { 0x51, 0x53, 0x5b, 0x59, 0x68, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xc3 };
//填充代码2 0xffffffff 的位置是将要填入真正API地址的位置
unsigned char sz_code_buf2[11] = { 0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0x50, 0x53, 0x5B, 0x58, 0xc3, 0xc3 };
//填充代码3 0xffffffff 的位置是将要填入真正API地址的位置
unsigned char sz_code_buf3[65] = {
0x9C, 0x50, 0x50, 0x8B, 0xC2, 0x05, 0x56, 0x05, 0x00, 0x00, 0xEB, 0x05, 0x42, 0x40, 0xEB, 0x02,
0x42, 0x83, 0xC4, 0x02, 0x83, 0xC4, 0x02, 0x58, 0x68, 0x66, 0x33, 0x22, 0x55, 0x50, 0x68, 0x66,
0x33, 0x55, 0x22, 0x83, 0xC4, 0x08, 0xEB, 0x00, 0x83, 0xC4, 0x03, 0x83, 0xC4, 0x01, 0x9D, 0x68,
0xFF, 0xFF, 0xFF, 0xFF, 0xEB, 0x06, 0x90, 0x90, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3
};
//填充代码4 0xffffffff 的位置是将要填入真正API地址的位置
unsigned char sz_code_buf4[64] = {
0x60, 0x9C, 0xB8, 0x01, 0x00, 0x00, 0x00, 0x41, 0xB9, 0x45, 0x87, 0x12, 0x00, 0x42, 0x90, 0x8B,
0xCA, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0xEB, 0x02, 0xF7, 0xE8, 0x52,
0x51, 0x8B, 0xC8, 0x41, 0x90, 0x83, 0xC4, 0x08, 0x9D, 0x61, 0x51, 0x50, 0x58, 0x59, 0x90, 0x90,
0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0xEB, 0x06, 0x68, 0x33, 0x88, 0x55, 0x22, 0x90, 0xC3, 0xC3, 0x48
};
//填充代码5 0xffffffff 的位置是将要填入真正API地址的位置
unsigned char sz_code_buf5[81] = {
0x9C, 0x50, 0x52, 0xB8, 0x33, 0x23, 0x62, 0x56, 0x40, 0x03, 0xD1, 0x03, 0xC3, 0xF7, 0xEA, 0x51,
0x53, 0x8B, 0xC3, 0x8B, 0xD9, 0x0F, 0xAF, 0xD9, 0xF7, 0xEB, 0xF7, 0xE9, 0x40, 0x4B, 0x4B, 0x5B,
0x59, 0x5A, 0x58, 0xEB, 0x11, 0x68, 0x50, 0x20, 0x87, 0x77, 0x68, 0x30, 0x66, 0x88, 0x77, 0x90,
0x6A, 0x00, 0xFF, 0x75, 0x08, 0x90, 0x9D, 0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0x51, 0x53, 0x52, 0x5A,
0x5B, 0x59, 0xEB, 0x06, 0xC3, 0x68, 0x30, 0x25, 0x63, 0x77, 0xC3, 0x90, 0xC3, 0x90, 0x20, 0x40,
0x00
};
//填充代码6 0xffffffff 的位置是将要填入真正API地址的位置
unsigned char sz_code_buf6[63] = {
0xEB, 0x09, 0x68, 0x30, 0x61, 0x89, 0x77, 0xC3, 0xC3, 0x60, 0x9C, 0x9C, 0x90, 0x50, 0x51, 0xB8,
0x66, 0x33, 0x22, 0x55, 0xEB, 0x03, 0x40, 0x49, 0xC3, 0x48, 0x59, 0x58, 0x51, 0x81, 0xC1, 0x77,
0x66, 0x55, 0x00, 0x8B, 0xC8, 0xEB, 0x07, 0x68, 0x20, 0x33, 0x96, 0x78, 0xC3, 0x9D, 0x59, 0x9D,
0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0x90, 0xC3, 0xC3, 0x9D, 0x68, 0x30, 0x22, 0x65, 0x77, 0xC3
};
//6段垃圾代码 随机选用一个
unsigned char* sz_code_buf_ary[NUM_CODE_CNT] = {sz_code_buf1, sz_code_buf2, sz_code_buf3,
sz_code_buf4, sz_code_buf5, sz_code_buf6};
//数组保存了几段垃圾代码的长度
unsigned int n_code_len_ary[NUM_CODE_CNT] = {11, 11, 65, 64, 81, 63};
//受限于篇幅 这里省略部分代码
//......................................
//......................................
//申请空间 里面放真正api的地址
char* p_iat_addr = (char*)pfnVirtualAlloc(NULL,
0x100,
MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
//根据时间随机选用一段垃圾代码填充
DWORD dw_tick = pfnGetTickCount();
//产生dw_tick == 0 这种极端情况也是有的 如果电脑长时间不关机的话
if (0 == dw_tick)
{
dw_tick = 3;
}
else
{
dw_tick %= NUM_CODE_CNT;
}
//全部填充成nop指令
MyMemset(p_iat_addr, 0x90, 0x100);
unsigned char* p_cur_code_buf = 0;
p_cur_code_buf = sz_code_buf_ary[dw_tick];
unsigned int n_cur_code_len = n_code_len_ary[dw_tick];
//获取 0xFFFFFFF 的地址 这里要替换成函数地址
int n_ret_idx = GetApiAddrPos((char*)p_cur_code_buf, n_cur_code_len);
if (-1 == n_ret_idx)
{
continue;
}
//拷贝垃圾代码到缓冲区
mymemcpy(p_iat_addr + NUM_REFUSE_CODE_BASE, p_cur_code_buf, n_cur_code_len);
//修改 0xfffffff 为 真正的函数地址
DWORD* p_dw_func_oft = (DWORD*)((DWORD)p_iat_addr + NUM_REFUSE_CODE_BASE + n_ret_idx);
*p_dw_func_oft = dw_func_addr;
//把申请空间写入iat
*(DWORD*)(dw_iat_rva) = (DWORD)p_iat_addr;
这里注意!
这里注意!
这里注意!
填充的垃圾代码有注意事项:
1、不能修改寄存器的值,如果要修改寄存器的值,一定要事先保存,然后恢复。
2、也最好不要修改 eflag寄存器的值, 因为执行一些API的时候,会用寄存器做参数传递,这里切记不要破坏环境。
因为之前犯了这个错误,调bug调了几个小时。
正确的姿势如下实例:
以下代码对应的缓冲区为 sz_code_buf3:
当前方案的缺点:
当前的垃圾代码只有6段,细心一点的破解者完全可以识别一下6段垃圾代码特征,针对不同的代码跳到不同的位置,秒破。
未实现的更好的思路:
比起手写垃圾代码,随机选用不同的填充。还有一个更好的思路。但是这个壳子并没有实现。
这个思路就是随机生成垃圾代码。随机生成的好处: 破解者修正的时候,无法定位到关键的地址。此思路要结合其他条件,比如不能让破解者定位到返回随机数的关键点,不然每次返回一样的随机数就凉了。
3、使用hash对比获取函数地址。
为何要这样做: 如果直接用字符串进行对比的话, 很容易被攻击者定位还原
实现思路: 读取原程序的导入表信息,对函数名计算一个hash值,存到外壳程序。获取函数地址的时候,从外壳中取出 hash 进行对比。
以下为hash算法源码,加壳器和外壳hash算法一致。
//所有函数名经过计算后 都是一个 4个字节的 DWORD类型的 hash值
DWORD GetHash(LPCSTR sz)
{
DWORD dwVal, dwHash = 0;
while (*sz) {
dwVal = (DWORD)* sz++;
dwHash = (dwHash >> 13) | (dwHash << 19);
dwHash += dwVal;
}
return dwHash;
}
原文件导入表读取函数名,获取hash后序列化到文件的代码。
for (it_imp_cur = it_imp_begin; it_imp_cur != it_imp_end; it_imp_cur++)
{
//遍历存储导入表信息的数据结构
auto it_int_cur = it_imp_cur->int_lst.begin();
auto it_int_end = it_imp_cur->int_lst.end();
for (; it_int_cur != it_int_end; it_int_cur++)
{
St_Func_Info st_func_info = { 0 };
//如果是序号
if (it_int_cur->is_ords)
{
st_func_info.is_name = false;
st_func_info.dw_ords = it_int_cur->dw_ords;
fs.write((char*)&st_func_info, sizeof(St_Func_Info));
continue;
}
//如果是名字
st_func_info.is_name = true;
st_func_info.dw_name_oft = n_name_oft;
st_func_info.c_name_len = NUM_LEN_HASH; //hash加密后固定4个字节
//写入func_info
fs.write((char*)&st_func_info, sizeof(St_Func_Info));
//获取流 写完func_info 的位置
dw_func_oft = fs.tellp();
//移动流指针 写入函数名
fs.seekp(st_func_info.dw_name_oft, std::ios::beg);
//获取函数名的hash值 修正的时候 用hash值进行对比
DWORD dw_func_hash = GetHash(it_int_cur->p_func_name);
PBYTE p_func_hash = (PBYTE)&dw_func_hash;
// Hash 后异或
for (int n_idx = 0; n_idx < NUM_LEN_HASH; n_idx++)
{
p_func_hash[n_idx] ^= ENC_VALUE;
}
//hash后函数名序列化到文件
fs.write((char*)p_func_hash,
NUM_LEN_HASH);
//恢复流指针
fs.seekp(dw_func_oft);
n_name_oft += NUM_LEN_HASH;
}
外壳shellcode处对比dll导出表函数名hash,获取函数地址的代码。
//检查传入参数是函数名还是序号
//函数名地址的情况
if (hash > 0xffff)
{
//遍历函数名表,比较字符串
DWORD dwCountFunc = 0;
while (dwCountFunc < dwNumOfNames)
{
//获取当前函数名
LPCSTR pName = (LPCSTR)(AryFuncNames[dwCountFunc] + (BYTE *)lpDosHeader);
//获得函数名的HASH
DWORD dwCurHash = GetHash(pName);
//比较HASH是否相等
if (dwCurHash == hash)
{
break;
}
........
}
//以此为下标,访问AddressOfFunctions
FARPROC lpExportFunc = (FARPROC)(AryExportFuncs[wOrdinal] + (BYTE *)lpDosHeader);
在这一步踩过的坑,需要注意的点:
这里注意 !
这里注意!
这里注意!
千万不能用MD5 !或者是我用MD5的姿势不对。
因为用了MD5 算hash的话特别卡。实测一个1m多一点的程序使用MD5 对比获取函数地址的时候,双击运行,大概10分钟之后才跑起来。所以这里大家可以自己写一个简单的hash算法。
已经烂大街的用过无数次的反调试, 被调试器各种检测的反调试,这里就不再赘述了。
这里会提到两个至今可以用的、偏冷门一点的反调试。
在提到之前,首先说一下,当前这个壳子在触发反调试后的操作:
1)触发反调试后,退出进程。
2)触发反调试后,继续运行,但是会修改关键数据,输入正确序列号会算出一个错误的值。
3)触发反调试后,对代码段进行异或处理,被修改的代码段执行后一定会崩掉。
之所以设计以上三个流程。是为了让破解者产生一种错觉。类似于 "这次程序没退出,这货的反调试应该被我清干净了"。
可用反调试1
这个反调试是直接检测多款od的窗口风格,使用GetWindowLongA 获取窗口的 Style 进行对比。这里采集了几款主流调试器。
可以看到上面的 od 窗口风格是 0x16CF0000。
可以看到上面的窗口风格是 0x17CF0000。
x32dbg 这里是 0x97cf0000。
od的窗口风格大概差不多,测试了多款OD后 选取了以上3个固定值进行对比。代码如下:
bool Anti::wnd_long_debug()
{
HWND h_debug_wnd = 0;
h_debug_wnd = GetForegroundWindow();
if (0 == h_debug_wnd)
{
return false;
}
LONG l_style = GetWindowLongA(h_debug_wnd, GWL_STYLE);
if (NUM_STYLE_OD1 == l_style || NUM_STYLE_OD2 == l_style
|| NUM_STYLE_X32 == l_style)
{
return true;
}
return false;
}
经过实测,x32dbg只有在全屏的时候能够检测到,别的OD调试器检测率很高。
可用反调试2
这里是检测PEB的两个标志位。正常运行时,两个标志位的值都是1。但是调试状态下会被修改。
x32dbg 调试运行
普通od 调试运行
以下代码应该是"原创" ,因为我并没有在别的地方看到类似的反调试。
bool Anti::flag_debug()
{
bool b_ret = false;
__asm
{
mov eax, fs:[0x30]
mov eax, dword ptr[eax + 0x10]
cmp byte ptr[eax + 0x68], 0x81
je anti_addr
cmp byte ptr[eax + 0x68], 0x0
je anti_addr
cmp byte ptr[eax + 0x6c], 0xa
je anti_addr
cmp byte ptr[eax + 0x6c], 0x0
je anti_addr
ok_addr :
mov b_ret, 0
jmp ret_addr
anti_addr :
mov b_ret, 1
ret_addr:
mov eax, 1
}
return b_ret;
}
主流调试器在上面的反调试中都会被检测到。
以上反调试相关 实名感谢一个 高中生大佬 @狐白小刺客 的思路和指点。
耦合相关
我们写代码最好做到高内聚低耦合。但是写壳子就不一样了,最好做到高耦合性。也就是说,外壳与原程序做到紧密联结。脱完这个壳子,程序就不能跑了。
当前的壳子实现了两种比较简单的方法,有些没实现但是很好用的方法我也会写出来。
1) 互斥体检测实现耦合
具体思路: 外壳shellcode 创建互斥体对象。源程序进行检测这个内核对象是否存在,不存在则表示被脱壳。
//这里在 shellcode处创建了一个互斥体对象
void coup_event(PFN_CreateMutexA pfn_cre_mutex)
{
char sz_event_name[] = { 'l', 'y', 'd', '\0' };
pfn_cre_mutex(NULL, FALSE, sz_event_name); //创建互斥体
}
源程序的检测代码和检测到的处理修改代码。
//源程序中的检测互斥体是否存在的代码
//true 存在互斥体 false壳子被脱
bool Coup::coup_mutex_func()
{
HANDLE h_mutex = 0;
h_mutex = CreateMutexA(NULL, TRUE, STR_EVENT_NAME); //创建互斥体
DWORD dw_error_code = GetLastError();
//如果存在 不做处理
if (ERROR_ALREADY_EXISTS == dw_error_code)
{
CloseHandle(h_mutex);
return true;
}
return false;
}
//检测内核对象是否存在的耦合性处理代码 不存在则修改数据
//---------耦合相关---------------
bool b_ret_mutex = Coup::coup_mutex_func();
if (!b_ret_mutex)
{
if (0 != g_p_diy)
{
//被拖壳修改数据
//puts("壳子已经被脱, 修改数据中");
g_p_diy->Diy_Upd_Data();
}
}
//---------耦合相关---------------
2)独占方式创建文件实现耦合性检测
实现思路外壳shelloce端创建一个独占形式打开的文件,源程序里面能正常打开则表示壳子被脱,此刻就可以修改数据或者退出进程。
//外壳shelloce端创建一个独占形式打开的文件
void coup_file(PFN_CreateFileA pfn_createfileA, PFN_DeleteFileA pfn_DeleteFileA)
{
HANDLE h_file = 0;
char sz_file_path[] = { '-','\0' };
//存在就删除
pfn_DeleteFileA(sz_file_path);
//以独占打开一个文件 不存在则创建
h_file = pfn_createfileA(sz_file_path, // open One.txt
0x80000000L, // open for reading GENERIC_READ
0, // do not share
0, // no security
1, // 不存在则创建
0x00000080, // normal file 0x00000080 FILE_ATTRIBUTE_NORMAL
NULL);
DWORD dw_ww = 0x226688;
}
源程序的检测代码和检测到后的处理代码。
//true 存在文件 false壳子被脱 检测代码
bool Coup::coup_file_func()
{
HANDLE h_file = 0;
h_file = CreateFileA("-", // open 0.txt
GENERIC_READ, // open for reading
0, // do not share
NULL, // no security
OPEN_EXISTING, // existing file only
FILE_ATTRIBUTE_NORMAL, // normal file
NULL);
//DWORD dw_xx = 0x221166;
DWORD dw_error_code = GetLastError();
//如果成功打开 壳子已经被脱
if (INVALID_HANDLE_VALUE != h_file)
{
return false;
}
//如果报共享文件冲突 表示壳子存在
if (ERROR_SHARING_VIOLATION == dw_error_code)
{
DeleteFileA("-");
return true;
}
return false;
}
//检测耦合性的处理相关代码
//---------耦合相关---------------
//耦合性代码等 壳子定性后再添加
bool b_ret_file = Coup::coup_file_func();
if (!b_ret_file)
{
if (0 != g_p_diy)
{
//被拖壳修改数据
//puts("壳子已经被脱, 修改数据中");
g_p_diy->Diy_Upd_Data();
}
}
//---------耦合相关---------------
以上是两种比较简单的思路。有一些思路有同样的作用但是并未实现,比如:
外壳shellcode 往本机的端口发送一个数据包。源程序在代码里面接收数据包。如果没有收到,就表示壳子被脱了。
中断门相关
为了防止API下断点,可以用windows的中断门实现 API。这里如果要用到中断,就必须绑定平台。
最开始设计壳子的时候,设定要运行的环境是w7 32位,所以这里API的中断门是以Win 7位平台为目标平台。
IDA打开Win 7 32位的 kernel32.dll 找到 ExitProcess。
发现调用了另一个函数 RtlExitUserProcess 这里IDA打开Win 7 32位ntdll.dll。发现调用了 ZwTerminateProcess。
查看ZwTerminateProcess函数。
所以中断门实现 ExitProcess 的代码如下:
__declspec(naked) void exit_proc()
{
__asm
{
push 0 //退出码
push - 1 //表示退出当前进程
mov eax, 0x172 //中断号
mov edx, esp
int 0x2e
add esp, 8
ret
}
}
这里本来想用中断门实现多个 API。但是后面时间和精力不足,所以就写了最简单的一个。
smc相关
加壳程序只是以一个固定值做了代码段的加密,也就是当前解密的次数 * 2进行了异或,shellcode端也以一样的key进行了异或。
加壳器端代码代码:
int Cmps_Shell::enc_code_seg(PBYTE p_code_base, int n_code_size, PBYTE p_next_seg_buf)
{
BYTE c_key = 0;
int n_idx = 0;
int n_dec_oft = 0;
int n_cur_enc_size = n_code_size;
int n_remain_size = n_code_size;
while (true)
{
n_cur_enc_size = n_remain_size;
//如果大于0x2000 一次性加密 0x2000 以外所有数据
//比如代码段 0x9000 一次性加密0x7000
if (n_cur_enc_size > NUM_FIRST_DEC_VALUE)
{
n_cur_enc_size -= NUM_FIRST_DEC_VALUE;
}
//>100 <2000 剩下的每次加密一半
else if (n_cur_enc_size > NUM_NEXT_DEC_VALUE)
{
n_cur_enc_size = n_remain_size / 2;
}
c_key = enc_get_key(n_idx, p_next_seg_buf);
this->enc_code_exec(p_code_base + n_dec_oft,
n_cur_enc_size,
c_key);
n_remain_size -= n_cur_enc_size;
n_dec_oft += n_cur_enc_size;
if (n_remain_size <= 0)
{
break;
}
n_idx++;
}
return 0;
}
shellcode端解密代码:
void dec_code(St_Cmps_Info* p_ci)
{
DWORD dw_xx = 0x225566;
bool b_anti = false;
bool b_anti_falg = false;
//代码分块解密 如果全部解密 返回
if (p_ci->dw_dec_remain_Size <= 0)
{
return;
}
//要解秘代码的起始位置
PBYTE p_dec_offset = 0;
p_dec_offset = p_ci->p_code_seg_start;
int n_dec_size = p_ci->dw_dec_remain_Size;
//-------------反调试相关-----------------
//b_anti_nt = Anti::nt_falg_debug();
b_anti_falg = Anti::flag_debug();
if (b_anti_falg)
{
b_anti = true;
}
//-------------反调试相关-----------------
//如果大于0x2000 一次性解密 0x2000 以外所有数据
//比如代码段 0x9000 一次性解密0x7000
if (n_dec_size > NUM_FIRST_DEC_VALUE)
{
n_dec_size -= NUM_FIRST_DEC_VALUE;
}
//>100 <2000 剩下的每次解密一半
else if (n_dec_size > NUM_NEXT_DEC_VALUE)
{
n_dec_size = p_ci->dw_dec_remain_Size / 2;
}
//从代码段下一个段获取key 每次解密不定长的数据 如果还剩100 全解密
BYTE c_key = get_key(p_ci);
dec_exec(p_dec_offset, n_dec_size, c_key);
p_ci->n_idx++;
p_ci->p_code_seg_start += n_dec_size;
p_ci->dw_dec_remain_Size -= n_dec_size;
//如果有反调试 可以做一些骚操作
if (b_anti)
{
p_ci->n_idx = 0;
p_ci->dw_dec_remain_Size = 0x8000;
}
}
当然,这里的代码段在执行之前是全部加密的,在跳到oep之前,需要手动调用解密函数。
//手动调用解密代码段的函数 执行次数根据当前oep偏移决定
dec_code(p_ci);
dec_code(p_ci);
dec_code(p_ci);
return dwOEP;
上面的 smc 非常初级,不具备普适性,每次oep变化都需要手动调整的代码。
当然这个壳子里面有函数的,回滚加密比当前这个smc好很多,是我同学的手笔,这里不过多阐述。
- End -
看雪ID:上海刘一刀
https://bbs.pediy.com/user-807993.htm
本文由看雪论坛 上海刘一刀 原创
转载请注明来自看雪社区
往期热门回顾
﹀
﹀
﹀
京华结交尽奇士,意气相期矜豪纵。今夏与君相约看雪,把酒言欢,共话安全技术江湖梦。
↙点击下方“阅读原文”,查看更多干货