首页
论坛
专栏
课程

[原创]cxk壳的流程实现和复盘

2019-6-30 21:49 5150

[原创]cxk壳的流程实现和复盘

2019-6-30 21:49
5150

在开始正文之前, 说一些题外话。

1.这是本人在论坛注册后的第二次发帖,如果有格式错误, 发布的版块错误, 请各位明示指出。管理员也可以删帖。

2.因为本人水平有限, 某些地方写的可能是错的, 也请各位大佬指正, 不胜感激。

3.以下文章提到的壳子是本人编写的第二个壳子。编写过程中, 面临毕业找工地搬砖,以及学习新知识,等一系列事情。所以做的并不好。

4.这个壳子并没有起名字, 某些大佬称它为 cxk壳 。壳子中代码 回滚加密 和字符串加密 是我一个同学实现的, 这篇文章只写我自己实现和参与实现的部分。

5.这个帖子是本人对写壳过程的一次复盘。会写一些经验教训, 错误总结, 整体实现思路,和一些没有实现但是觉得很有用的点。

6.为什么要发这个帖子?

       1)本人在学习的时候, 大量阅读了论坛的优秀文章, 受益良多,饮水思源, 自己有一些经验也想分享出来,希望跟我一样的小白可以受益。

7.各位看这个帖子能得到什么?

       1) 在写壳过程中我自己的一些思路(老师指点和个人总结)。   

       2) 两个可用反调试。

       3) 写壳过程中,所有我走过的弯路, 踩过的坑, 大家可以引以为鉴。

       4) 写壳过程中, 一些虽然没有实现, 但是我觉得很棒的思路, 大佬们以后或者可以用以下。

       5) 加壳源码以及shellcode。


正文开始:

加壳语言:                     c++

目标软件文件格式:       PE

加壳平台:                     win32 / win64 (最开始写的时候 只希望在w7 32位平台可以运行)


明确加壳宗旨:

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

    目前强度很高, 最富盛名的壳子是VMP。

    当然VMP壳也有对应的脱壳方法, 有人做过工作量计算, 如果一个大佬一个人全职脱壳(体量不大的情况下), 大概半年能完全脱掉。

    厉害的壳子并不是完全杜绝破解者破解, 因为大佬无穷多, 总是有对应的解决办法。厉害的壳子是像VMP壳一样, 拉长破解者破解的时间。大概是那种,我知道各位很厉害, 我也知道各位一定能破解的了, 我只是希望能多托各位几天。

      在这种理念前提下, 写壳的时候, 更多就是计算攻防双方的时间成本。


以下是一个表格,  以攻防双方水平差不多为大前提.

举例不同手法下, 防守方实现防守手段时间,和攻击方成功破解所花费时间。n表示 > 1的未知数。



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

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

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

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


确定写壳计划:

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



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

1)对原计划的更改


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


2)新增的工作计划



具体实现手法:


导入表相关:

导入表这里一共做了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个固定值进行对比

代码如下

#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的中断门是以w7位平台为目标平台。


ida打开w7 32位的 kernel32.dll 找到 ExitProcess


发现 调用了另一个函数 RtlExitUserProcess 这里 ida打开w7 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好很多, 是我同学的手笔,这里不过多阐述。


后记:

在写这款壳子的时候。有订很多计划,各种原因没完成。最初的设想是有驱动同时保护的。结果后来因为兼容性问题取消(同学写了很久 也很辛苦)。

也有一些本来能完成的计划 ,比如花指令 流程混淆,反IDA调试等等。这些都没有加。

以致于呈现到各位面前的就是一个几乎可以说是裸奔的壳子。。。


这是一个可以写的好一点,但是确没有写好的壳子。

以上。

上海刘一刀 2019.06.30




[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

上传的附件:
打赏 + 5.00
打赏次数 1 金额 + 5.00
收起 
赞赏  天水姜伯约   +5.00 2019/08/19
最新回复 (21)
流星暴雨 2019-7-1 01:15
2
0
支持一下
看场雪 3 2019-7-1 07:46
3
0
果然周末发出来了。多多交流!
htg 1 2019-7-1 11:20
4
0
挺厉害的!
qqsunqiang 2019-7-1 11:27
5
0
mark
tomtory 2019-7-1 11:49
6
0
厉害了
拍拖 2 2019-7-1 11:50
7
0
很不错的思路,不在于技术难度,而在于运用思路上。
coolboyme 2 2019-7-1 20:33
8
0
致敬
nevinhappy 2 2019-7-2 08:18
9
0
666,这是发帖就精啊!
wx_Homer Simpson 2019-7-2 08:40
10
1
 awesome
TopC 2019-7-2 09:59
11
0
kalikaikai 1 2019-7-2 10:06
12
0
666厉害了,学习一波
泪落晨曦 2019-7-2 10:07
13
0
太厉害了
chenjialei 2019-7-2 14:26
14
0
前来捧场
DemonsEllen 2019-7-2 14:49
15
1
杨精华就是牛逼
wx_听海看雪 2019-7-3 09:51
16
1
作者辛苦了,谢谢分享,文章里有个笔误,
正确的姿势如下实例: 

以下代码对应的缓冲区为 sz_code_buf5  吧
notwolf 4 2019-7-3 11:45
17
0
牛逼
上海刘一刀 2 2019-7-3 14:18
18
0
wx_听海看雪 作者辛苦了,谢谢分享,文章里有个笔误, 正确的姿势如下实例: 以下代码对应的缓冲区为 sz_code_buf5 吧
刚看了下  确实是这样  这里写错了 感谢指正
wx_Gen 2019-7-5 14:25
19
0
AperOdry 2019-7-5 15:00
20
0
牛逼 膜拜~
Oday小斯 2019-7-5 15:05
21
0
牛逼,值得学习
天水姜伯约 3 2019-8-19 15:47
22
0
刘京华(精华)tql。
狐白tql。
dalao们带带我!
游客
登录 | 注册 方可回帖
返回