查看原文
其他

cxk壳的流程实现和复盘

上海刘一刀 看雪学院 2019-09-17


1技术背景


加壳语言:C++

目标软件文件格式:PE

加壳平台:Win32 / Win64 (最开始写的时候,只希望在Win 7 32位平台可以运行)



2明确加壳宗旨


软件加壳是为了更好的保护软件,目前世面上各种成熟的商业壳很多。从技术上讲,攻防无绝对,很多很厉害的商业壳被更厉害的大佬脱的干干净净。

目前强度很高,最富盛名的壳子是VMP。当然VMP壳也有对应的脱壳方法,有人做过工作量计算,如果一个大佬一个人全职脱壳(体量不大的情况下),大概半年能完全脱掉。


厉害的壳子并不是完全杜绝破解者破解,因为大佬无穷多,总是有对应的解决办法。厉害的壳子是像VMP壳一样,拉长破解者破解的时间,大概是那种。


我知道各位很厉害,我也知道各位一定能破解的了,我只是希望能多托各位几天。在这种理念前提下,写壳的时候,更多就是计算攻防双方的时间成本。


以下是一个表格,以攻防双方水平差不多为大前提。举例不同手法下,防守方实现防守手段时间,和攻击方成功破解所花费时间。n表示 > 1的未知数。



由上可见,在写壳的过程中:


最理想的情况是:设计出一套高性价比手法的加壳方案(实现起来简单,破解起来麻烦)。


退而求其次的是:设计一些中性价比的方案,争取攻防双方消耗同等时间。


极力要避免的是:低性价比的手法,防守方实现起来无比麻烦,攻击方三两下干掉。



3确定写壳计划


在明确加壳宗旨前的写壳计划:



在经过老师指点,明确加壳宗旨后,修改的工作计划。


对原计划的更改


这里本应该放弃编号2、编号3 两个低性价比的方案,但是在调整方案前,编号2第性价比的方案就已经写完。所以只放弃了编号3性价比低的方案。


新增的工作计划




4具体实现手法


导入表相关:


导入表这里一共做了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算法。



5反调试相关


已经烂大街的用过无数次的反调试, 被调试器各种检测的反调试,这里就不再赘述了。


这里会提到两个至今可以用的、偏冷门一点的反调试。


在提到之前,首先说一下,当前这个壳子在触发反调试后的操作:

1)触发反调试后,退出进程。

2)触发反调试后,继续运行,但是会修改关键数据,输入正确序列号会算出一个错误的值。

3)触发反调试后,对代码段进行异或处理,被修改的代码段执行后一定会崩掉。

之所以设计以上三个流程。是为了让破解者产生一种错觉。类似于 "这次程序没退出,这货的反调试应该被我清干净了"。



可用反调试1 

这个反调试是直接检测多款od的窗口风格,使用GetWindowLongA 获取窗口的 Style 进行对比。这里采集了几款主流调试器。



可以看到上面的 od 窗口风格是 0x16CF0000。



可以看到上面的窗口风格是  0x17CF0000。



x32dbg 这里是 0x97cf0000。


od的窗口风格大概差不多,测试了多款OD后 选取了以上3个固定值进行对比。代码如下:


#define NUM_STYLE_OD1 0x17CF0000
#define NUM_STYLE_OD2 0x16cF0000
#define NUM_STYLE_X32 0x97cf0000

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  



本文由看雪论坛 上海刘一刀 原创

转载请注明来自看雪社区




往期热门回顾

1、一种基于编译器的的JS混淆及反混淆方案

2、C/C++反混淆方法

3、APICloud解密本地资源到逆向APP算法到通用资源解密

4、我的微信数据监控研究发展过程

5、如何实现 Https拦截进行 非常规“抓包”









京华结交尽奇士,意气相期矜豪纵。今夏与君相约看雪,把酒言欢,共话安全技术江湖梦。


10大议题正式公布!第三届看雪安全开发者峰会重磅来袭!



↙点击下方“阅读原文”,查看更多干货

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存