某特殊抽取壳的原理简析
2020-05-12 21:42:56 Author: bbs.pediy.com(查看原文) 阅读量:585 收藏

看雪.安恒2020 KCTF春季赛 第十题 沐猴而冠

观察

题目给出的是一个 APK,安装上试一试会发现是一个输入 flag,按一下按钮告诉你对不对的应用。

分析

丢进 JEB 之类的东西看看可以看到 dex 里没有什么逻辑,且看起来没有代码被抽进壳了的迹象。该应用加载了两个 .so,libz****n.1.solibvv.so,然后直接调用 JNI 注册的 native 函数 com.zhuotong.mybox.MainActivity.eq 检查 flag 是否正确。

看 dex 里有的一些空类的包名,再把 libz****n.1.so 丢到 VirusTotal 之类的地方上看第一次提交的时间,可以判断出来这个 so 是原封原样没有修改过的一个某国内通讯应用里带的,太可怕了,我们还是不要看它了。

在此插入声明:本人没有以任何形式逆向工程该文件,以下相关描述如有涉及到的部分,纯属猜测及夜观天象得知。

分析 libvv.so 会发现该 so 带壳,不是常见的壳,看起来应该是作者自己写的。没有 init 函数,是在 JNI_OnLoad 里做了大量的事情。麻烦的一点是这个 so 带一些混淆,看起来跟 这里 描述的混淆类似,看 .comment 里面的内容可以发现是 Android clang version 5.0.300080 (based on LLVM 5.0.300080) 编译出来的,应该是什么小范围流传的基于 LLVM 的混淆器吧。

这个混淆不是很强,动态调试的话很容易就能摸清这个壳的逻辑,但试图调试这个程序会发现它会 crash,并且 crash 点不在 libvv.so 里,通过 夜观天象,可以知道大概是加载的另一个 so 里的反调试捣的鬼,解掉之后可以很容易 dump 出里面实际套的 so,ELF 头里大部分字段被抹了但直接参照 libvv.so 即可修复(因为是同一个 linker 生成的)。

然而由于我们只能 夜观天象,不能去逆向分析另一个 so,按照看雪比赛的规则,并不能写出来怎么过这个反调试,只好纯静态解这个混淆了,真累啊。

如何不夜观天象把程序搞清楚

这个 binary 里的混淆主要有五部分:

  1. 一些涉及 cpsr 的花指令,直接 nop 了就行。
  2. 类似 OLLVM 的 Control Flow Flattening,这个混淆我们不去管它,因为这程序里的函数的控制流都太简单了,直接肉眼就能看懂。
  3. 每个 indirect load / store / branch 之类的指令里的地址都被混淆成了从 .data 上读一个数进来再和一个常数做运算的形式,我们把 .data 上这些常量池所在的部分标成 read-only,然后大部分这种混淆即可被 Hex-Rays 直接解掉,有少量解不掉的,请大佬帮忙写一个 IDAPython 脚本操作 microcode 强行解一下即可。这个混淆还有一个变体是在函数外将某一个寄存器的值设置为固定常数,函数里面依赖其值为这个常数去进行对 offset 的解码,由于 Hex-Rays 显然做不了这么高级的跨过程分析,我们在目标函数里找地方 patch 一下把这个值设进去就行了。
  4. 像是 conditional 的 BranchInst 都被混淆成了一种奇怪的形式,改成了通过 Thumb 里的 If-Then 指令来获取 condition,计算要跳到的位置,然后用 MOV PC, Rx 这样的指令跳过去。If-Then 这部分可能不是作者自己主动写的,而是 LLVM 自己优化出来的。Hex-Rays 不认这种东西,写个脚本挂个模拟器分析一下跳转目标,把它们都 patch 成正常的条件跳转即可。
  5. 每个字符串都被混淆了,保存成一段固定的 xorpad 加上和 xorpad 逐位 xor 过的串的形式,用到的时候再解出来,因为这个程序里的字符串也不是很多,我们写两行代码,遇到一个手动解一个就是了:
    def decrypt(hexdata, keylen):
    of = bytes.fromhex(hexdata)
    return xor(of[:keylen], of[keylen:], cut = 'right')
    

处理完之后的 JNI_OnLoad 大概长这样:

JNI_OnLoad

可以看到就是保存了一下传入的 JavaVM*FindClass 找了下 MainActivity 这个 class 并取了一个 ref 存了起来,接下来开了个线程干坏事。
这个线程函数里面调用自己背着的一份代码,查字符串可以看出来似乎是作者把 bionic 里的 linker 代码直接拷过来编译进去了,调用的函数流程大概是 dlopen 自己,又开了一个线程(在 0x1D7FE 处,执行的线程函数是 0x18274),然后 dlsym 找自己的 JNI_OnLoad 的位置,再算了一通偏移,跳到了 0x13708 处的函数里去。

sub_13708

该函数调用 RegisterNatives 注册了 com.zhuotong.mybox.MainActivity.eq 函数在 0x12FC8。进去看看会发现它看起来是个检查函数,然而呃,夜观天象一下(或者你看上面开了个神秘线程点进去看看它干了什么)就可以发现这是个假的检查函数。

看上面又新开的线程的线程函数,0x18274,如下:

sub_18274

跟上面的逻辑一样,又把自己加载了一遍,卸载原来的自己,然后调用 0x18B2C,并把自己的 soinfo 和 dlclose 作为参数穿了进去,看这次调用的函数:

sub_18B2C

又开了一个线程,延迟1秒后再卸载自身,主要是调用进了 0x193B8,看看它干了啥:

sub_193B8_1
sub_193B8_2
sub_193B8_3

显然你不用还原控制流逻辑对着瞎猜也能猜出来,它把之前注册的 native 函数找出来,把里面的函数指针偷偷换掉了,真实的函数在 0x19FC4。

sub_19FC4

这个函数里调用了一个改过的 gzread (点进去看看里面的 inflate 特征很明显,认出 zlib 再认外面的 gz 就简单辣,看跳过了 10 字节的头和读了一个 flag,很明显),解压了 0x63750 处的共 107279 字节数据。改动点主要是把 magic 检查去掉了,于是对应的压缩过的数据开头的 1F 8B 08 也被调皮的换成了 89 50 4E,我们把它换回去,即可 gzip -d 解压。

解压出来的数据稍微看一看就可以知道是个 ELF,不过头部被搞坏了,参照 libvv.so 开头的内容直接修复即可。找了一下发现文件尾部的 section header 也都被清 0 了,shoff 就只能填 0 辣,不过并不影响分析。

上述函数接下来在内存中加载了这个 so,调用了 0x66C0 处的函数。

吐槽

折腾了这么一长串,其实如果动态调试的话,真的可以直接跳过不看上面那些的。至于为什么不这么做,当然是因为我胆子小不敢去逆奇怪小厂的 so,夜观天象水平又不够高辣。

观察内层 so

readelf 看下 dynamic section 可以看到:

0x0000000e (SONAME)                     Library soname: [libaes_wbox_ori.so]

这提示了我们里面是个什么东西。

(其实在壳的代码里也能看到诸如 D:/code/android_code/MyCtf/myaeswhitebox/src/main/jni/linker/libc_logging.cpp 这样的字符串)

分析内层 so

内层 so 也有混淆,不过逻辑相对简单,并无大碍。
分析 0x66C0 处的函数,这次是真实的对应 com.zhuotong.mybox.MainActivity.eq 的 native 函数了:

int __fastcall RealFinalJNI_eq_handler(JNIEnv *env, jobject activity_class, int cppstrinput)
{
  int start_ret; // r0
  void *table; // r4
  int v7; // r5
  char *input; // r0
  unsigned int i; // r8
  int cb; // r6
  size_t len; // r0
  int ret; // r4
  jclass toast_class; // r10
  _jmethodID *makeText; // r4
  jstring right_str; // r0
  jobject toast; // r9
  _jmethodID *show; // r2
  int v20; // r4
  char blahstr; // [sp+18h] [bp-70h]
  char cbhex[5]; // [sp+27h] [bp-61h]
  char gzf; // [sp+2Ch] [bp-5Ch]
  unsigned __int8 output[16]; // [sp+58h] [bp-30h]
  int v27; // [sp+68h] [bp-20h]

  v27 = *(_DWORD *)off_2ADD4;
  *(_DWORD *)&output[8] = 0;
  *(_DWORD *)&output[12] = 0;
  *(_DWORD *)output = 0;
  *(_DWORD *)&output[4] = 0;
  start_ret = gzmemopen((int)&gzf, kEmbeddedTable, 28946);
  if ( start_ret )
  {
    v20 = start_ret;
    sub_7930(&aError, byte_26A76);              // "Error: \x00"
    fprintf((FILE *)0x32934, (const char *)&aError);
    sub_79A4(aStartFailed, byte_26AB2);         // "start() failed, ret=%d\x00"
    fprintf((FILE *)0x32934, aStartFailed, v20);
    sub_7A98(aFmt_s_d, byte_26AF5);             // ", %s:%d\n"
    sub_7AF8(algn_3252B, byte_26B23);           // "test"
    fprintf((FILE *)0x32934, aFmt_s_d, 0x3252B, 40);// test:40
    ((void (__fastcall *)(int))fflush)(0x32934);
    exit(1);
  }
  table = malloc(28946u);
  if ( gzread(&gzf, (int)table, 28946) <= 0 )
  {
    v7 = 28946;
    do
    {
      ((void (__fastcall *)(void *))free)(table);
      v7 *= 10;
      table = (void *)((int (__fastcall *)(int))malloc)(v7);
    }
    while ( gzread(&gzf, (int)table, v7) < 1 );
  }
  g_table = (int)table;
  input = string::cstr_(cppstrinput);
  TransformBuf(input, output);
  string::string(&blahstr);
  for ( i = 0; i < 0x10; ++i )
  {
    cb = output[i];
    sub_7B48(aFmt_02x, byte_26B4D);             // "%02x"
    sprintf(cbhex, aFmt_02x, cb);
    len = strlen(cbhex);
    string::append_at_end_len(&blahstr, cbhex, len);
  }
  sub_7B98(kExpected, byte_26B74);              // "0b6f04a3427089858041945e70082508"
  if ( string::compare((int)&blahstr, kExpected) )
  {
    if ( !env )
    {
LABEL_10:
      ret = 0;
      goto LABEL_11;
    }
LABEL_9:
    (*env)->ExceptionClear(env);
    goto LABEL_10;
  }
  ret = 1;
  if ( env && activity_class )
  {
    sub_7BD8(&aToast, byte_26BBD);              // "android/widget/Toast"
    toast_class = (*env)->FindClass(env, (const char *)&aToast);
    if ( toast_class )
    {
      sub_7CAC(&aMakeText, byte_26BFA);         // "makeText"
      sub_7D0C(aMakeTextSignature, byte_26C28); // "(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;"
      makeText = (*env)->GetStaticMethodID(env, toast_class, (const char *)&aMakeText, aMakeTextSignature);
      if ( makeText )
      {
        sub_7D58(&aRight, byte_26CAA);          // "right"
        right_str = (*env)->NewStringUTF(env, (const char *)&aRight);
        if ( right_str )
        {
          toast = CallStaticObjectMethodV(env, toast_class, makeText, activity_class, right_str, 1);
          if ( toast )
          {
            sub_7DB8(&aShow, byte_26CE5);       // "show"
            sub_7E0C(&aVoidVoidSignature, byte_26D25);// "()V"
            show = (*env)->GetMethodID(env, toast_class, (const char *)&aShow, (const char *)&aVoidVoidSignature);
            if ( show )
            {
              CallVoidMethodV(env, toast, show);
              (*env)->ExceptionClear(env);
              ret = 1;
              goto LABEL_11;
            }
          }
        }
      }
    }
    goto LABEL_9;
  }
LABEL_11:
  string::destructor(&blahstr);
  return ret;
}

可以看到就是把输入变换后 hex 编码,再和固定串 "0b6f04a3427089858041945e70082508" 比较,如果正确的话就手动弹个 toast。变换输入的函数特别短,应该是个没什么强度的白盒 AES(假装它是的话),相信作者的人品它真的是 AES 而不是什么仿 AES 之类的东西,直接重新实现一遍上 fault attack。用到的数据是用跟外层压缩 so 的方式一样压缩的(除了这次填的 magic 是 "elf" 了,真调皮),照样用 gzip -d 解压出来即可。

解决

重写的程序见附件 wbox.cc,用 deadpool_dfa.py 可以直接全自动解掉:

import sys
import deadpool_dfa
import pnes

def processinput(iblock, blocksize):
  return b'%0*x' % (2*blocksize, iblock), None

engine = deadpool_dfa.Acquisition(targetbin="./a.out", targetdata="./taowa", goldendata="./taowa.golden", dfa=pnes, processinput=processinput, minleaf=1, minleafnail=1)
tracefiles_sets=engine.run()
print(tracefiles_sets)
for tracefile in tracefiles_sets[0]:
  ans = pnes.crack_file(tracefile, verbose=2)
  if ans:
    print(ans)
    break
  else:
    print("phoenixAES failed")
$ python3 solve.py
Press Ctrl+C to interrupt
Send SIGUSR1 to dump intermediate results file: $ kill -SIGUSR1 3283569
Lvl 016 [0x0001C244-0x0001C245[ xor 0x32 74657374746573747465737474657374 -> 97D1F9E717AE073029006B06C12E082C GoodEncFault Column:0 Logged
Lvl 016 [0x0001C244-0x0001C245[ xor 0xBF 74657374746573747465737474657374 -> C4D1F9E717AE079A29000806C110082C GoodEncFault Column:0 Logged
Lvl 016 [0x0001C244-0x0001C245[ xor 0xD4 74657374746573747465737474657374 -> 8BD1F9E717AE073229002806C1ED082C GoodEncFault Column:0 Logged
Lvl 016 [0x0001C244-0x0001C245[ xor 0x50 74657374746573747465737474657374 -> C5D1F9E717AE072129004806C141082C GoodEncFault Column:0 Logged
Lvl 016 [0x0001C245-0x0001C246[ xor 0x5E 74657374746573747465737474657374 -> C9D1F9C817AE9C6A2979B4063C51082C GoodEncFault Column:3 Logged
Lvl 016 [0x0001C245-0x0001C246[ xor 0x9B 74657374746573747465737474657374 -> C9D1F9E317AE4C6A29FDB4063851082C GoodEncFault Column:3 Logged
Lvl 016 [0x0001C245-0x0001C246[ xor 0xB9 74657374746573747465737474657374 -> C9D1F9E817AEC06A29EFB4061D51082C GoodEncFault Column:3 Logged
Lvl 016 [0x0001C245-0x0001C246[ xor 0xFB 74657374746573747465737474657374 -> C9D1F93917AEB56A291EB406AA51082C GoodEncFault Column:3 Logged
Lvl 016 [0x0001C246-0x0001C247[ xor 0xED 74657374746573747465737474657374 -> C9D124E7176B076A5300B406C1510818 GoodEncFault Column:2 Logged
Lvl 016 [0x0001C246-0x0001C247[ xor 0xC2 74657374746573747465737474657374 -> C9D14AE717D3076A4900B406C15108DF GoodEncFault Column:2 Logged
Lvl 016 [0x0001C246-0x0001C247[ xor 0x31 74657374746573747465737474657374 -> C9D1C1E7174D076A3000B406C15108AF GoodEncFault Column:2 Logged
Lvl 016 [0x0001C246-0x0001C247[ xor 0x72 74657374746573747465737474657374 -> C9D1CBE7172C076A1000B406C151083B GoodEncFault Column:2 Logged
Lvl 016 [0x0001C247-0x0001C248[ xor 0xA5 74657374746573747465737474657374 -> C93DF9E74DAE076A2900B498C1515B2C GoodEncFault Column:1 Logged
Lvl 016 [0x0001C247-0x0001C248[ xor 0xE1 74657374746573747465737474657374 -> C93FF9E7F0AE076A2900B4AEC151762C GoodEncFault Column:1 Logged
Lvl 016 [0x0001C247-0x0001C248[ xor 0x97 74657374746573747465737474657374 -> C935F9E754AE076A2900B41EC151D22C GoodEncFault Column:1 Logged
Lvl 016 [0x0001C247-0x0001C248[ xor 0x07 74657374746573747465737474657374 -> C9B4F9E71CAE076A2900B40CC151A02C GoodEncFault Column:1 Logged
Saving 17 traces in dfa_enc_20200508_084951-084957_17.txt
(['dfa_enc_20200508_084951-084957_17.txt'], [])
97d1f9e717ae073029006b06c12e082c: group 0
c4d1f9e717ae079a29000806c110082c: group 0
Round key bytes recovered:
E6............90....55....4F....
8bd1f9e717ae073229002806c1ed082c: group 0
c5d1f9e717ae072129004806c141082c: group 0
c9d1f9c817ae9c6a2979b4063c51082c: group 3
c9d1f9e317ae4c6a29fdb4063851082c: group 3
Round key bytes recovered:
E6....AE....8B90..B055..714F....
c9d1f9e817aec06a29efb4061d51082c: group 3
c9d1f93917aeb56a291eb406aa51082c: group 3
c9d124e7176b076a5300b406c1510818: group 2
c9d14ae717d3076a4900b406c15108df: group 2
Round key bytes recovered:
E6..18AE..4F8B90D4B055..714F..25
c9d1c1e7174d076a3000b406c15108af: group 2
c9d1cbe7172c076a1000b406c151083b: group 2
c93df9e74dae076a2900b498c1515b2c: group 1
c93ff9e7f0ae076a2900b4aec151762c: group 1
Round key bytes recovered:
E65118AE244F8B90D4B0557F714F6425
Last round key #N found:
E65118AE244F8B90D4B0557F714F6425
$ aes_keyschedule E65118AE244F8B90D4B0557F714F6425 10
K00: 796F752067657420746865206B657921
K01: 35D9885F52BCFC7F26D4995F4DB1E07E
K02: FF387BBCAD8487C38B501E9CC6E1FEE2
K03: 0383E308AE0764CB25577A57E3B684B5
K04: 45DC3619EBDB52D2CE8C28852D3AAC30
K05: D54D32C13E966013F01A4896DD20E4A6
K06: 422416007CB276138CA83E855188DA23
K07: C67330D1BAC146C23669784767E1A264
K08: BE4973540488359632E14DD15500EFB5
K09: C696A6A8C21E933EF0FFDEEFA5FF315A
K10: E65118AE244F8B90D4B0557F714F6425

得到 key 是 you get the key!,接下来直接解密答案即可。

感想

真有意思,不如下次谁出 Windows 上的 CrackMe 的时候加载个某 P 进来反调试,然后让选手们写 writeup 教你过某 P,多好,还可以堂而皇之的说“我算法没用某 P 里面的东西啊,我没调用啊,不用看的啊”,呵呵呵。

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

最后于 11小时前 被kanxue编辑 ,原因:

上传的附件:
  • wbox.cc (1.63kb,5次下载)
  • solve.py (0.50kb,5次下载)
  • taowa.golden (160.00kb,3次下载)

文章来源: https://bbs.pediy.com/thread-259454.htm
如有侵权请联系:admin#unsafe.sh