首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第55期->安全文摘
期刊号: 类型: 关键词:
Something about burneye

作者:CoolQ <qufuping@ercist.iscas.ac.cn
出处:http: //www.linuxforum.net/forum/showflat.php?Cat=&Board=security&Number
日期:2005-01-06

|=-----------------------=[ Something about burneye ]=----------------------=|
|=--------------------------------------------------------------------------=|
|=-----------------=[ CoolQ <qufuping@ercist.iscas.ac.cn> ]=----------------=|
|=--------------------------------------------------------------------------=|

--[ 内容

    0 - 前言

    1 - 使用篇
        1.1 下载
        1.2 试用
        1.3 patch

    2 - 分析篇
        2.1 源代码的结构
        2.2 burneye加密的原理
            2.2.1 patch 1
            2.2.2 流程分析
            2.2.3 patch 2
        2.3 burneye的anti-debugger
        2.4 burneye的loader

    3 - 破解篇
        3.1 加密算法
        3.2 对策

    4 - 几个问题

    5 - 参考

--[ 0 - 前言

linuxforum上madsys的ABFrag放了好久了,也没有人感兴趣,看来Burneye还挺很神奇.
ABFrag有人说是一个假的Exploit,但我对Burneye产生了兴趣.
Burneye是Unix/Linux下少见的几个加密ELF的软件之一, 作者是TESO的成员scut.

本文共分三个部分:使用篇,分析篇,破解篇.

我使用的环境是RedHat FC2, kernel 2.6.8.1, gcc 3.3

--[ 1 - 使用篇

--[ 1.1 下载

packetstormsecurity.nl上Burneye的下载共有3个[1]:
    burneye-1.0-linux-static.tar.gz, (静态编译可执行文件)
    burneye-1.0.1-src.tar.bz2,       (源代码包)
    burneye-stripped.tar.gz
其中后两个的差别只是是否有ELF的标准文档,因此我们先把前两个都弄下来.

--[ 1.2 试用

先试着使用静态编译的Burneye
[root@localhost]# ./burneye
[root@localhost]#
[root@localhost]# strace ./burneye
execve("./burneye", ["./burneye"], [/* 20 vars */]) = 0
_exit(73)

看来burneye的二进制程序在我们的系统上不能使用,退出码73为EDOTDOT,并不常见.

接着我们编译一下源代码:
[root@localhost]# tar jvxf burneye-1.0.1-src.tar.bz2
[root@localhost]# cd src && make
....
gcc -Wall  -o burneye burneye.c common.o cipher-glfsr.o cipher-rc4.o
cipher-sha1.o fingerprint.o rs.o

好,编译成功,我们来试用一下

[root@localhost]# ./burneye
usage: ./burneye [options] <program>

banner options
        -b file                display banner from 'file' before start
        -B file                display banner from 'file' on tty before start

password protect options
        -p pass                use password encryption with 'pass' as password
        -P env                first try to read password from environment 'env',
                        will use password from 'env' now, too, if its there
        -i                ignore invalid entered password and execute junk
                        not recommended (default: off)

fingerprinting options
        -S                SEAL mode (options F,f,t are ignored)
        -f file                use fingerprint from 'file' to protect binary
        -F                use fingerprint of current host (do not use -f and -F)
        -t num                tolerate 'num' deviations in fingerprint
        -q                be quiet about wrong fingerprint, just exit
                        (default: 0)
        -E                do tolerance even if erasure warning is given
        -l                list fingerprint tests that can be done
        -e test                enable fingerprint test 'test'
        -d test                disable fingerprint test 'test'

generic options
        -o out                specify another output file name (default: output)
....

burneye中的README给出了几个典型的例子:(详细介绍请参见doc/DIST-README)

burneye -o ls /bin/ls                   # 只作代码的obfuscate,不作加密
burneye -p "secret" -o ls /bin/ls       # 使用密码'secret'加密
burneye -B warning.txt -o ls /bin/ls    # 启动时显示warning.txt的信息
burneye -F -t 1 -B warning.txt -o ls /bin/ls        # 使用本地主机的指纹,启动时
        # 显示warning.txt的信息, 测试时允许有一项失败
fingerprint -d procpartitions -f foohost.fp
burneye -f foohost.fp -t 2 -p "hidden" -o ls /bin/ls
        # 使用fingerprint产生指纹文件,允许两项失败,启用密码保护
burneye -p puke -S -o ls /bin/ls
./ls  # 将会把信息封存并保存到二进制文件中
OLDHOSTNAME=`hostname`
./ls  # 正常运行
hostname moo
./ls  # 运行环境与封存设置不符,失败

下面我们也按照README的例子试用一下:

[root@localhost] ./burneye -p 123 -o ls.new /bin/ls
burneye - TESO ELF Encryption Engine
version 1.0.1
-------------------------------------------------------------------------------

loaded 24768 bytes @ 0x08caf170
doing pre-stub encryption (@ 0x05371000) from offset 0x0000007f
next entry point is 0x05371080
end of segment 1: 0x0000563
bogus bytes at the end, i.e. something between segments end and file end.

并没有加密的ls.new生成,看来编译的源代码也不可用!

在网上搜索了一下,好像没有人遇到过这种情况,奇怪了,难道是系统的问题?

找了一个RedHat 9, kernel 2.4.20-8, gcc 3.2.2
原样不动做了一遍,结果发现静态二进制文件可以运行,而源代码包虽然能编译,
却仍然不能加密。

--[ 1.3 patch

既然我的FC2上不能运行,那就只能自力更生了,通过阅读源代码,发现了原因,自己写了
一个patch, 一切OK。(至于这个Patch是怎么来的,请参考分析篇)

source code patch:
--------------------------- stub.lds.diff ------------------------------------
--- new/src/stub/stub.lds        2001-07-15 06:20:18.000000000 -0400
+++ old/src/stub/stub.lds        2004-12-11 18:11:13.000000000 -0500
@@ -19,6 +19,7 @@
                 *(.text)
                 *(.data)
                 *(.rodata)
+                *(.rodata.str1.1)
                 *(.bss)
                 callgate.o(.text)
----------------------------- stub.diff --------------------------------------
--- new/src/stub/stub.c        2002-04-07 11:13:16.000000000 -0400
+++ old/src/stub/stub.c        2004-12-11 23:04:16.000000000 -0500
@@ -715,12 +715,14 @@

         /* now insertion-sort the given array */
         for (n = 0 ; n < auxc ; ++n) {
-                if (auxv[n].a_type >= 32) {
+                if (auxv[n].a_type >= 42) {
                         be_printf ("FATAL: invalid AT_* entry detected\n");
                         _exit (73);
                 }

                 if (auxv[n].a_type != AT_NULL) {
+                        if(auxv[n].a_type >= 32)
+                                continue;
                         a_copy[auxv[n].a_type].a_type = auxv[n].a_type;
                         a_copy[auxv[n].a_type].a_un.a_val = auxv[n].a_un.a_val;
                         be_printf ("AUXV: 0x%08lx : 0x%08lx\n", auxv[n].a_type

2.6 kernel patch:
------------------------ binfmt_elf.c.diff -----------------------------------
--- /usr/src/linux/fs/binfmt_elf.c        2004-05-08 08:56:42.000000000 -0400
+++ binfmt_elf.c        2004-12-15 20:40:36.145239824 -0500
@@ -171,7 +171,7 @@
          * ARCH_DLINFO must come first so PPC can do its special alignment of
          * AUXV.
          */
-        ARCH_DLINFO;
+        //ARCH_DLINFO;
#endif
         NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
         NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE)

注意:如果你从源代码编译burneye,只需要打源代码的patch即可,如果你手头上有
burneye加密的程序,并且该程序一定要在2.6内核上运行,你需要打上内核源代码的补丁
并重新编译内核。当然,你也可以为二进制的加密程序打patch,不过由于加密程序的
生成环境比较复杂,这种patch也不容易制作。

--[ 2 分析篇

--[ 2.1 源代码的结构

src/burneye.c               命令行主程序
stub/callgate.asm       -+
stub/cgate.c             |
stub/cipher-glfsr-c.c    |  加密解密相关程序,其中glfsr是整个解密程序的入口,
stub/cipher-glfsr.asm    +->由glfsr完成对加密程序的de-obfuscate,之后,转入
stub/cipher-rc4.c        |  init.asm的be_entry处
stub/cipher-sha1.c       |
stub/rs.c               -+
stub/fingerprint.c          生成机器的特征指纹
stub/help.c                 常用函数
stub/init.asm               程序初始化入口,分配好ELF32_auxv_t结构后,转入
                            stub.c的burneye函数
stub/stub.c                 这是加密程序运行的主函数
stub/stub.lds               这是一个连接脚本,以后还要讨论
stub/unlink_stub.asm        删除文件的程序
stub/utils/hdump.c          将二进制文件以十六进制形式打出
stub/utils/sstrip.c         处理ELF文件,使得ELF只保留ELF文件头,PHDR和所有的
                            Segment,其余的无用Section和Section Header一律抛弃

--[ 2.2 burneye加密的原理

burneye类似与Windows下一般的加壳工具,但是又有自己的特色,先来分析一下用
burneye加密后文件的结构:

[ELF头][程序头][Burneye常用例程][stub头][加密后的程序]

其中前三部分,Burneye使用了一个技巧,在编译Burneye命令时,生成了一个很大的数组
stub_bin[],其实就是一个通用的ELF头+程序头+Burneye常用例程,加密程序时,只需要对
数组里的某些字段稍作修改即可.

--[ 2.2.1 patch 1

下面我们来看一下stub_bin[]是怎么生成的,这样我们才能明白为什么原来的源代码无法
使用.

src/stub/Makefile:

stub.bin.h:        stub.bin
        echo "unsigned char        stub_bin[] =" > stub-bin.h
        utils/hdump < stub.bin >> stub-bin.h
        egrep "\.text.+0x.+0x.+cipher-glfsr.o" stub.map | \
                awk '{ printf ("#define BE_LAYER0_START %s\n", $$2); }' \
                >> stub-bin.
...
stub.bin:        buildtools $(OBJS) stub.lds
        sed s/entry_point/$(ENTRY_POINT)/ < stub.lds > stub-bin.lds
        ld -Map stub.map -s -T stub-bin.lds -o stub.bin $(OBJS)
        rm -f stub-bin.lds
ifeq ($(rel),on)
        strip stub.bin
        utils/sstrip stub.bin
endif

可见,stub.bin.h实际上就是stub.bin的十六进制形式,然后添加几个宏构成的。而
stub.bin则是由ld将所有的目标文件按照stub-bin.lds的内容连接而成,并将结果
去除符号表并抛弃多余的Section/Section header。

问题的关键就是这个连接脚本,为什么原来的burneye编译以后无法加密呢?我们先看
一下stub.lds

PHDRS
{
        text PT_LOAD FILEHDR PHDRS FLAGS (0x0007);        /* PF_R | PF_W | PF_X */
        data PT_LOAD FLAGS (0x0006);                        /* PF_R | PF_W */
}

SECTIONS
{
        . = 0x05371000 ;
        .text : {
                cipher-glfsr.o(.text)
                init.o(.text)
                *(.text)
                *(.data)
                *(.rodata)
                *(.bss)
                callgate.o(.text)

                PROVIDE (be_stubhdr_u = ABSOLUTE(.)) ;
                LONG (0x41012345) ;
        } : text


        /* dummy segment to set fixed brk(0) value in kernelspace */
        . = 0x08048000 ;
        .data : {
        } : data

        /* save calls to objcopy, huh */
        /DISCARD/ : {
                *(.eh_frame)
                *(.comment)
                *(.note)
        }
}
用这个脚本生成的stub.bin应该只有两个程序段,第一个段应该把原来所有目标文件
的大部分节都整合在一起,包括所有OBJS的.text, .data, .rodata, .bss.

再看一下原来加密出错时的源代码:
        if (stub_len != phdr[0].p_offset + phdr[0].p_memsz) {
                fprintf (stderr, "bogus bytes at the end, i.e. something "
                        "between segments end and file end.\n");
                exit (EXIT_FAILURE);
        }
其中stub_len就是stub_bin[]的长度,注意到phdr[0]中的0了么?这是指第一个程序段,
也就是说,程序期望stub.bin的第二个程序段的长度是0! 否则应该写成
        if (stub_len != phdr[1].p_offset + phdr[1].p_memsz)

但实际上,我们生成的stub.bin,第二个程序段大小并不为0,我们修改Makefile文件,
将strip和sstrip的步骤省略,然后readelf一下stub.bin,发现了:
[root@localhost]# readelf -l stub.bin
...
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x05370000 0x05370000 0x05630 0x05630 RWE 0x1000
  LOAD           0x006000 0x00000000 0x00000000 0x000c1 0x000c1 RW  0x1000

Section to Segment mapping:
  Segment Sections...
   00     .text
   01     .rodata.str1.1   <---- 看见了吧? 加密不成就是多出来的这个Section.
出现.rodata.str1.1是编译器的原因,当然这是GCC 3.3的结果,我在Redhat 7.2上
还出现了.rodata.str1.32。
因此,我们需要在lds中把.rodata.str1.1也添加进去(当然如果有.32,也需要加上)。

这里还有一个问题,为什么不只用一个段呢?第二个空段是做什么用的呢?
你可以看一下doc/README的内容。

--[ 2.2.2 流程分析

接下来按照Burneye的代码,把整体流程简要描述一下

加密过程:(主要是burneye.c的wrap函数)
burneye首先以加密的文件作为对象,将stub_bin[]作为加密后文件的ELF头+程序头+
Burneye常用例程,然后按照burneye的命令选项,对文件进行逐层的处理,顺序是:
    tag - banner - password - fingerprint - glfsr(obfuscate)
每层处理,都会在stub头中保存相应的标志和信息(例如加密时的随机序列,sha1哈希值)
,然后将结果放到最后一部分中。

解密过程:(主要是cipher-glfsr.asm,init.asm和stub.c中的burneye函数)
加密程序的入口是cipher-glfsr.asm的glent,负责de-obfuscated程序,去掉第一层保护,
接下来由init.asm处理一下运行程序的栈结构,并将系统控制权交给stub.c的burneye。
burneye是最主要的部分,会按照顺序处理Tag并显示Banner(如果有的话),接下来,是
fingerprint的检测,然后会提示你输入加密时设定的密码,如果一切OK,就将加密的
程序解密,然后调用burneye中自带的Loader,将解密后的程序映射到内存中,如过解密
的程序需要interpreter,loader也会负责将interpreter装入到内存中,最后将控制权交
给interpreter或者解密后的ELF。至此,一切恢复正常.

--[ 2.2.3 patch 2

打完了patch1,用burneye加密程序没有错误信息,但是问题依然存在:
[root@localhost]# ./burneye -p 1234 -o ls.new /bin/ls
XXX: stub_len = 0x00005720
phdr 1 @ 0x05370000
phdr 2 @ 0x00000000
burneye - TESO ELF Encryption Engine
version 1.0.1
------------------------------------------------------------------------------

loaded 22256 bytes @ 0x094d2170
doing pre-stub encryption (@ 0x05371000) from offset 0x0000007f
next entry point is 0x05371080
end of segment 1: 0x000056f0
brk(0) to force is 0x0805bff3
obfuscation layer: key = 0x52e8b1d3

------------------------------------------------------------------------------
[root@localhost]# ./ls.new
[root@localhost]#
[root@localhost]# strace ./ls.new
execve("./ls.new", ["./ls.new"], [/* 20 vars */]) = 0
_exit(73)
加密的程序不能运行! 跟前面的结果一样。通过程序的Debug选项,我们找到了出错的
位置:
        for (n = 0 ; n < auxc ; ++n) {
                if (auxv[n].a_type >= 32) {
                        be_printf ("FATAL: invalid AT_* entry detected\n");
                        _exit (73);
                }
这里的auxv是一个ELF32_auxv_t的结构,一般人可能不太接触,这里从[2]上偷一段:

When the userspace receives control, the stack layout has a fixed format.
The rough order is this:

       <arguments> <environ> <auxv> <string data>

The detailed layout, assuming IA32 architecture, is this (Linux kernel
series 2.2/2.4):

  position            content                     size (bytes) + comment
  ------------------------------------------------------------------------
  stack pointer ->  [ argc = number of args ]     4
                    [ argv[0] (pointer) ]         4   (program name)
                    [ argv[1] (pointer) ]         4
                    [ argv[..] (pointer) ]        4 * x
                    [ argv[n - 1] (pointer) ]     4
                    [ argv[n] (pointer) ]         4   (= NULL)

                    [ envp[0] (pointer) ]         4
                    [ envp[1] (pointer) ]         4
                    [ envp[..] (pointer) ]        4
                    [ envp[term] (pointer) ]      4   (= NULL)

                    [ auxv[0] (Elf32_auxv_t) ]    8
                    [ auxv[1] (Elf32_auxv_t) ]    8
                    [ auxv[..] (Elf32_auxv_t) ]   8
                    [ auxv[term] (Elf32_auxv_t) ] 8   (= AT_NULL vector)

                    [ padding ]                   0 - 16

                    [ argument ASCIIZ strings ]   >= 0
                    [ environment ASCIIZ str. ]   >= 0

  (0xbffffffc)      [ end marker ]                4   (= NULL)

  (0xc0000000)      < top of stack >              0   (virtual)
  ------------------------------------------------------------------------
auxv主要是为内核与用户程序(主要是Loader)协同而设置的。2.4内核构造的auxv,
其类型最多不超过32(其实最大也就是23 - AT_SECURE),但是到了2.6内核,又出了
几种新的类型,因此,程序到这一步就出错了。通过GDB调试一个普通的程序,发现
栈中确实有类型为32(AT_SYSINFO)和33(AT_SYSINFO_EHDR)的auxv存在。
解决的方法就是将大于等于32的auxv跳过去(其实Loader只需五种类型的auxv)。
patch 2就这么产生了。

--[ 2.3 burneye的anti-debugger

主要有两种,一种是反编译,一种是反跟踪

反编译是通过对代码进行obfuscate(glfsr),使objdump等工具失效。
反跟踪使用了int3,并在程序里自己截获SIGTRAP,具体的代码是:(参考[3])

        antistrace ();
        
        if (nottraced == 0)
                killme ()

void be_sigtrap (int signum)
{
        nottraced++;
}
static inline int antistrace(void)
{
        long ret;
        
        __asm__ __volatile__ ("int $0x03\n\t"
                             :"=a" (ret)
                             : );
        return (ret);
}
static inline int killme()
{
        long ret;
        
        __asm__ __volatile__ ("xorl        %%eax, %%eax\t\n"
                              "xorl        %%ebx, %%ebx\t\n"
                              "xorl        %%ecx, %%ecx\t\n"
                              "xorl        %%edx, %%edx\t\n"
                              "xorl        %%esi, %%esi\t\n"
                              "xorl        %%edi, %%edi\t\n"
                              "xorl        %%ebp, %%ebp\t\n"
                              "xorl        %%esp, %%esp\t\n"
                              "jmp        %%esi"
                             :"=a" (ret)
                             : );
}        return ret;

--[ 2.4 burneye的loader

由于解密以后的程序与burneye的例程函数是位于同一进程空间的,因此如果想要执行
解密后的程序,例程就需要自己装载程序(普通情况下,程序是由内核来完成装载任务的)
,burneye是通过be_remap来实现的,如果加密之前的程序是动态链接的,还需调用
be_mapinterpreter来装载interpreter。因此,即使你file ls.new结果是statically
linked, 你查看/proc/PID/maps发现多个.so时,也不要惊奇。

--[ 3 破解篇

--[ 3.1 加密算法

burneye的保护主要有三层,最外层的是通过glfsr算法对代码进行的obfuscate,第二层
和第三层以主机的某些特征(fingerprint)和密码作为密钥KEY,首先算出密钥的SHA1哈希
值DIG,然后从随机发生序列器中获得160位的随机数RAN,并与SHA1哈希值异或,得到
KEY2,然后用KEY2对需要程序进行RC4加密。burneye还将加密之前的明文SHA1哈希值保存
到stub头的相应部分,这样做密码验证时,能够判断密码的正确性。

--[ 3.2 对策

最外层的glfsr,虽然比XOR加密要好得多,但是仍然是相对较弱的加密,很容易对付。
第二/三层的加密,算法是比较强的,只能用暴力方法解决,但是如果方法得当,能够大大
提高解密的速度。来看看如何提高解密速度:
原来的加密程序
                SHA1HashLen (pw_pass, strlen (pw_pass), pw_hash);
                for (key_n = 0 ; key_n < sizeof (pw_hash) ; ++key_n)
                        pw_hash[key_n] ^= xor_hash[key_n];
                SHA1HashLen (pw_hash, sizeof (pw_hash), pw_hash);
                rc4_prepare_key (pw_hash, sizeof (pw_hash), &key_r);
                rc4_cipher (exe_data, exe_len, &key_r);
其中,pw_pass就是密码字符串,exe_data就是需要加密的整个文件.
解密的时候使用的还是上边的代码,pw_pass是你输入的密码,而xor_hash保存在加密文件的
stub头中。

是不是需要将所有的exe_data都解密,然后判断密码的正确性呢?不是的,burneye在这一
方面有欠缺。exe_data其实就是待加密的整个ELF文件,因此,前面一部分必定是ELF头,
这样,我们只须解密exe_data的前几个字节,看是不是合法的ELF文件头即可,无须解密
整个exe_data,例如:

static char ELF_HEADER[] = "\x7f\x45\x4c\x46\x01\x01\x01"
                           "\0\0\0\0\0\0\0\0\0\x02\0\x03\0";
...
rc4_cipher (exe_data, sizeof(ELF_HEADER), &key_r);
if(!strncmp(exe_data, ELF_HEADER, sizeof(ELF_HEADER))
        printf("Match!!\n");

互联网上能够找到几个暴力破解Burneye加密的工具包:
[http://www.rootshell.be/~ioc/releases/ioc6-final.tar.gz]
[http://66.230.171.10/releases/Crypto/UNFburninhell/UNFburninhell1.0c.tar.gz]
其中ioc6使用了上面的方法,破解的速度比较快。

--[ 4 几个问题

在研究Burneye的过程中,发现了几个问题,目前还没解决,如果哪位高人知道原因,请
给我回信,不甚感激。

o 问题一
用GDB对加密的程序进行调试,断点设置在ELF头entry point时无效:
根据Fenris的README,Fenris似乎可以设置断点[4]
[root@localhost]# readelf -l ./ls.new | grep entry
  Entry point address:               0x5371035
[root@localhost]# gdb -q ./ls.new
...
(gdb) b *0x5371035
Breakpoint 1 at 0x5371035
(gdb) r
Starting program: /burneye/src/ls.new
warning: shared library handler failed to enable breakpoint

o 问题二
用strace时,结果如下:
execve("./ls.new", ["./ls.new"], [/* 20 vars */]) = 0
signal(SIGTRAP, 0x5371991)              = ? ERESTARTNOINTR (To be restarted)
signal(SIGTRAP, 0x5371991)              = ? ERESTARTNOINTR (To be restarted)
signal(SIGTRAP, 0x5371991)              = ? ERESTARTNOINTR (To be restarted)
signal(SIGTRAP, 0x5371991)              = ? ERESTARTNOINTR (To be restarted)
...
无限循环

这两个问题,用我在Burneye中看到的antistrace都无法解释。

--[ 5 参考

  [1] [http://www.packetstormsecurity.nl/groups/teso/index2.html]
      [http://www.packetstormsecurity.nl/groups/teso/burneye-1.0-linux-static.tar.gz]
      [http://www.packetstormsecurity.nl/groups/teso/burneye-1.0.1-src.tar.bz2]
      [http://www.packetstormsecurity.nl/groups/teso/burneye-stripped.tar.gz]
  [2] [http://www.phrack.org/show.php?p=58&a=5]
  [3] [http://vx.netlux.org/lib/vsc04.html]
  [4] [http://lcamtuf.coredump.cx/fenris/be.txt]
版权所有,未经许可,不得转载