首页
论坛
专栏
课程

[调试逆向] [原创]从2018强网杯hide题中学习手动脱壳与算法识别的思路

2019-5-16 13:31 854

[调试逆向] [原创]从2018强网杯hide题中学习手动脱壳与算法识别的思路

2019-5-16 13:31
854

环境配置

系统 : Linux kali 4.15.0-kali2-amd64 \ win10 64bit
程序 : hide
要求 : 输入口令
使用工具 :ida

开始分析

粗略观察

使用file命令拿信息:

➜  playground file ./hide
./hide: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped

没什么特殊的,接下来使用strings命令拿信息:

➜  playground strings hide
# 省略大部分字串
UPX!
UPX!

发现upx字样,所以这里应该是加了upx壳保护。

手动脱壳

使用upx命令脱壳:

➜  playground upx -d hide 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2017
UPX 3.94        Markus Oberhumer, Laszlo Molnar & John Reiser   May 12th 2017

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: hide: CantUnpackException: header corrupted 2

Unpacked 0 files.

发现没有办法自动脱壳,所以这里只好采用手动脱壳的方式。具体做法是让程序先运行,然后等到数据自动解压/解密出来后,再dump内存组成elf文件进行分析:

➜  ~ pmap -d 56010
56010:   /root/playground/hide
Address           Kbytes Mode  Offset           Device    Mapping
0000000000400000     808 r-x-- 0000000000000000 000:00000   [ anon ]
00000000004ca000    2040 ----- 0000000000000000 000:00000   [ anon ]
00000000006c8000      20 rwx-- 0000000000000000 000:00000   [ anon ]
00000000006cd000     140 rwx-- 0000000000000000 000:00000   [ anon ]
0000000000800000       4 rwx-- 0000000000000000 000:00000   [ anon ]
00007ffff7ffa000      12 r---- 0000000000000000 000:00000   [ anon ]
00007ffff7ffd000       8 r-x-- 0000000000000000 000:00000   [ anon ]
00007ffffffde000     132 rwx-- 0000000000000000 000:00000   [ stack ]
mapped: 3164K    writeable/private: 296K    shared: 0K
➜  ~ dd if=/proc/$(pidof hide)/mem of=hide_dump1 skip=4194304  bs=1c count=827392
dd: /proc/56575/mem: cannot skip to specified offset
827392+0 records in
827392+0 records out
827392 bytes (827 kB, 808 KiB) copied, 4.92887 s, 168 kB/s
➜  ~ file hide_dump1 
hide_dump1: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, missing section headers
➜  ~ dd if=/proc/$(pidof hide)/mem of=hide_dump2 skip=7110656  bs=1c count=1277952
dd: /proc/56575/mem: cannot skip to specified offset
dd: error reading '/proc/56575/mem': Input/output error
20480+0 records in
20480+0 records out
20480 bytes (20 kB, 20 KiB) copied, 0.189007 s, 108 kB/s
➜  ~ cat hide_dump1 hide_dump2 > hide_dump
➜  ~ file hide_dump
hide_dump: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped

静态分析

hide_dump载入ida,按下Shift+F12查看字符串列表并搜索flag:

LOAD:00000000004A0904    00000018    C    qwb{this_is_wrong_flag}
LOAD:00000000004A091C    00000011    C    Enter the flag:\n
LOAD:00000000004A4A48    0000003C    C    \nWARNING: Unsupported flag value(s) of 0x%x in DT_FLAGS_1.\n
LOAD:00000000004B23A0    00000020    C    s->_flags2 & _IO_FLAGS2_FORTIFY
LOAD:00000000004B4D28    00000056    C    version == NULL || (flags & ~(DL_LOOKUP_ADD_DEPENDENCY | DL_LOOKUP_GSCOPE_LOCK)) == 0
LOAD:00000000004B5BC8    00000044    C    imap->l_type == lt_loaded && (imap->l_flags_1 & DF_1_NODELETE) == 0

双击跟踪第二个字符串:

LOAD:00000000004A0904 aQwbThisIsWrong db 'qwb{this_is_wrong_flag}',0
LOAD:00000000004A0904                                         ; DATA XREF: sub_4009AE+10↑o
LOAD:00000000004A091C ; char aEnterTheFlag[]
LOAD:00000000004A091C aEnterTheFlag   db 'Enter the flag:',0Ah,0
LOAD:00000000004A091C                                         ; DATA XREF: sub_4009EF+4F↑o
LOAD:00000000004A091C                                         ; LOAD:00000000004C8EC2↓o
LOAD:00000000004A092D ; char aYouAreRight[]
LOAD:00000000004A092D aYouAreRight    db 'You are right',0Ah,0
LOAD:00000000004A092D                                         ; DATA XREF: sub_4009EF+8E↑o
LOAD:00000000004A092D                                         ; sub_4C8EF4:loc_4C8FB2↓o

对敏感字符串aYouAreRight,查看其交叉参考:

Direction    Type    Address    Text
Up    o    sub_4009EF+8E    mov     esi, offset aYouAreRight; "You are right\n"
Down    o    sub_4C8EF4:loc_4C8FB2    mov     rsi, offset aYouAreRight; "You are right\n"

错误分支

先进入第一个函数看看:

__int64 sub_4009EF()
{
  const char *v0; // rsi
  __int64 v1; // rdx
  __int64 result; // rax
  __int64 v3; // rcx
  unsigned __int64 v4; // rt1
  char v5; // [rsp+0h] [rbp-80h]
  char v6; // [rsp+10h] [rbp-70h]
  unsigned __int64 v7; // [rsp+78h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  if ( sub_43F380(0LL, v5) )
    sub_40EAD0(0LL, 0LL);
  print_43E9B0(1LL, "Enter the flag:\n");
  sub_43E950();
  if ( sub_4009AE(&v6) != 0 )
  {
    v0 = "You are right\n";
    print_43E9B0(1LL, "You are right\n");
  }
  else
  {
    v0 = "You are wrong\n";
    print_43E9B0(1LL, "You are wrong\n");
  }
  result = 0LL;
  v4 = __readfsqword(0x28u);
  v3 = v4 ^ v7;
  if ( v4 != v7 )
    sub_442480(1LL, v0, v1, v3);
  return result;
}

进入sub_4009AE函数看看:

_BOOL8 __fastcall sub_4009AE(__int64 a1)
{
  return sub_400360(a1, "qwb{this_is_wrong_flag}") == 0;
}

所以这就是那个误导人的分支啰?pass掉。

还原函数

进入第二个地址,发现ida没有将其识别称为一个函数,这里,我们往上搜索,发现0x4C8EF4这个位置不错,将其设定为函数头。光标定位到0x4C8EF4,然后右击Create Functionsub_4C8EF4函数就出现了:

signed __int64 sub_4C8EF4()
{
  _BYTE *v0; // rdi
  int *v1; // rsi
  unsigned __int64 v2; // rdx
  signed __int64 result; // rax

  if ( strlen(my_str_6CCDB0) == 21
    && my_str_6CCDB0[1] == 'w'
    && my_str_6CCDB0[2] == 'b'
    && my_str_6CCDB0[3] == '{'
    && my_str_6CCDB0[20] == '}' )
  {
    sub_4C8CC0(&content_6CCDB4);
    easy_xor_4C8E50(&content_6CCDB4);
    sub_4C8CC0(&content_6CCDB4);
    easy_xor_4C8E50(&content_6CCDB4);
    sub_4C8CC0(&content_6CCDB4);
    v0 = &content_6CCDB4;
    easy_xor_4C8E50(&content_6CCDB4);
    v1 = &str_y_4C8CB0;
    v2 = 0LL;
    while ( v2 < 0x10 && *v0 == *v1 )
    {
      ++v2;
      ++v0;
      v1 = (v1 + 1);
    }
  }
  __asm { syscall; LINUX - sys_write }
  result = 60LL;
  __asm { syscall; LINUX - sys_exit }
  return result;
}

_BYTE *__fastcall sub_4C8E50(__int64 my_str)
{
  _BYTE *result; // rax
  signed int i; // [rsp+14h] [rbp-4h]

  for ( i = 0; i <= 15; ++i )
  {
    result = (i + my_str);
    *result ^= i;
  }
  return result;
}

由伪代码可知,字符串格式为qwb{x*16},内容经过一系列的加密和异或,最终与str_y_4C8CB0对比,一致则成功。

算法识别

首先进入sub_4C8CC0函数:

unsigned __int64 __fastcall sub_4C8CC0(__int64 my_str)
{
  unsigned __int64 result; // rax
  unsigned __int64 v2; // rt1
  unsigned int v0; // [rsp+18h] [rbp-48h]
  __int64 v1; // [rsp+1Ch] [rbp-44h]
  signed int index_i; // [rsp+24h] [rbp-3Ch]
  signed int index_j; // [rsp+28h] [rbp-38h]
  int key_1; // [rsp+40h] [rbp-20h]
  int key_2; // [rsp+44h] [rbp-1Ch]
  int key_3; // [rsp+48h] [rbp-18h]
  int key_4; // [rsp+4Ch] [rbp-14h]
  unsigned __int64 v11; // [rsp+58h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  key_1 = 0x70493173;
  key_2 = 0x45723350;
  key_3 = 0x79523376;
  key_4 = 0x33593464;
  for ( index_i = 0; index_i <= 1; ++index_i )
  {
    v0 = *(8 * index_i + my_str);
    v1 = *(my_str + 4 + 8 * index_i);
    for ( index_j = 0; index_j <= 7; ++index_j )
    {
      v0 += (*(&key_1 + (BYTE4(v1) & 3)) + HIDWORD(v1)) ^ (((v1 >> 5) ^ 16 * v1) + v1);
      HIDWORD(v1) += 0x676E696C;
      LODWORD(v1) = ((*(&key_1 + ((HIDWORD(v1) >> 11) & 3)) + HIDWORD(v1)) ^ (((v0 >> 5) ^ 16 * v0) + v0)) + v1;
    }
    *(my_str + 8 * index_i) = v0;
    *(my_str + 4 + 8 * index_i) = v1;
  }
  v2 = __readfsqword(0x28u);
  result = v2 ^ v11;
  if ( v2 != v11 )
    result = (loc_4C8B9A)();
  return result;
}

搜索算法常数key1、2、3、4没有发现有价值的线索,搜索0x676E696C也是如此。

彩蛋

出于对十六进制的敏感,试了一下算法常数的字符串形式是什么样子:

➜  ~ python
Python 2.7.14+ (default, Mar 13 2018, 15:23:44) 
[GCC 7.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from libnum import n2s
>>> n2s(0x676E696C)
'gnil'

百度搜索ctf gnil,发现此人,也就是本题作者,应该是FlappyPig战队的成员。

搜索结构

根据结构可以知道,这是tea算法,但细节处也有很多不同。不妨搜索>> 11) & 3 tea加密这样由知名加密算法+特殊计算的关键词,很简单知道了这里利用的是xtea算法。

算法区别

我们对比标准的xtea算法和程序中的算法可知有三点不同

  1. 算法常数不同
  2. key和迭代轮次不同
  3. sum变量被设置成了v1的高位

关于第三点,我们在ida中双击v1查看函数栈视图:

-0000000000000048 v0              dd ?
-0000000000000044 v1              dq ?
-000000000000003C index_i         dd ?
-0000000000000038 index_j         dd ?

v0 占了四个字节,v1占了八个字节,即 sum == HIDWORD(v1)

逻辑推理

我们将程序中的xtea算法和异或算法还原出来,按照相反的顺序对给出的密文进行操作,就可以得到flag了。

编写程序

按照以上推理,编写C++程序如下:

#include <stdio.h>
#include <stdint.h>

/* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */

void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], sum=0, delta=0x676E696C;
    for (i=0; i < num_rounds; i++) {
        v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
        sum += delta;
        v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
    }
    v[0]=v0; v[1]=v1;
}

void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], delta=0x676E696C, sum=delta*num_rounds;
    for (i=0; i < num_rounds; i++) {
        v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
        sum -= delta;
        v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
    }
    v[0]=v0; v[1]=v1;
}

void decrypt_128bit_data(unsigned int num_rounds, uint32_t v[4], uint32_t const key[4]) {
    decipher(num_rounds, v, key);
    decipher(num_rounds, &v[2], key);
}

void easy_xor(char* my_str){
    for ( int index = 0 ; index <= 15 ; index++){
    my_str[index] ^= index;
}
}

int main()
{
    uint32_t v[4]={1,2};
    uint32_t const k[4]={0x70493173,0x45723350,0x79523376,0x33593464};
    unsigned int r=8;//num_rounds建议取值为32
    // v为要加密的数据是两个32位无符号整数
    // k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位
//    printf("加密前原始数据:%u %u\n",v[0],v[1]);
//    encipher(r, v, k);
    v[0] = 2131998802;
    v[1] = 468880437;
    v[2] = 3532022772;
    v[3] = 824070003;
    printf("加密后的数据:%u %u %u %u\n",v[0],v[1],v[2],v[3]);
    easy_xor((char*)v);
    decrypt_128bit_data(r, v, k);
    easy_xor((char*)v);
    decrypt_128bit_data(r, v, k);
    easy_xor((char*)v);
    decrypt_128bit_data(r, v, k);
    printf("解密后的数据:0x%x 0x%x 0x%x 0x%x\n",v[0],v[1],v[2],v[3]);
    return 0;
}

运行结果:

➜  playground g++ test.cpp -o test
➜  playground ./test
加密后的数据:2131998802 468880437 3532022772 824070003
解密后的数据:0x644e3166 0x3348545f 0x65646c48 0x45643043

考虑到数据在内存中是以小尾方式存放的,我们使用python脚本转换一下:

from libnum import n2s

flag_data = [0x644e3166, 0x3348545f, 0x65646c48, 0x45643043]
flag = ""

for data in flag_data:
    flag += n2s(data)[::-1]

print flag

运行结果如下:

➜  playground python test.py
f1Nd_TH3HldeC0dE

夺旗成功

使用得出的明文加上flag格式,作为给hide程序的输入,并且以Ctrl+D结束输入:

➜  playground ./hide 
Enter the flag:
qwb{f1Nd_TH3HldeC0dE}You are right

参考链接

  1. XMAN【第二天】hide题解 https://blog.csdn.net/qq_33438733/article/details/81380257
  2. 强网杯qwb 2018 reverse writeup http://www.leadroyal.cn/?p=471
  3. 从做题到出题再到做题三部曲-UPX https://xz.aliyun.com/t/3864#toc-6
  4. TEA、XTEA、XXTEA加密解密算法 https://blog.csdn.net/gsls200808/article/details/48243019
  5. 【强网杯2018】逆向hide https://www.wandouip.com/t5i70150/
  6. xctf FlappyPig https://www.xctf.org.cn/teams/115/


[推荐]看雪企服平台,提供安全分析、定制项目开发、APP等级保护、渗透测试等安全服务!

最后于 2019-5-16 13:35 被顾言庭编辑 ,原因:
上一主题 下一主题
最新回复 (2)
xie风腾 2019-5-16 14:02
2
0

多谢楼主分享哟
Editor 2019-5-16 18:06
3
0
感谢分享~
游客
登录 | 注册 方可回帖
返回