行业新闻

2021WMCTF中的Re1&Re2

2021WMCTF中的Re1&Re2

 

Re1

首先ida反编译main函数会报错,这个一般是程序中有花指令导致的。

因为main函数比较大,用提示成功字符串定位到最后的汇编代码,向上翻翻便看见出问题的代码。

双击该地址,可以发现ida将这段数据解析成了代码且最上面有一个设置的条件绝对跳转跳过了执行下面的错误带代码,这里可以直接把jnb改成jmp,并把下面垃圾代码nop掉。

继续向上翻又可以看见如下的花指令:不断跳转到下一条指令,统统nop掉即可。

from ida_bytes import *

addr = 0x140002DEC
while addr <= 0x140002DFF:
    patch_byte(addr, 0x90)
    addr += 1
print('*'*100)

然后我们就可以反编译了。

先看到对输入的处理:

开始判断了flag长度范围[12, 45],然后判断格式是否是WMCTF{}

接着申请了576字节大小的空间block,并把输入的除去格式(WMCTF{}外)的前4个字节以如下方式填入block

再把输入的除去格式(WMCTF{}外)的4-20字节填入block+530开始的位置。

最后就是将剩下的输入以_@#?!&-$+为区分,分别进行不同的处理。其中输入是hex形式,先把每4个hex转化两字节数据后,再用第一字节作为index,第二字节作为数据对block进行操作。

下面再看加密部分:

首先sub_7FF79BD33960()函数也是加了上面所说的花指令,去除后看到伪代码,用CRC算法生成256个4字节数据:

__int64 sub_7FF79BD33960()
{
  __int64 result; // rax
  unsigned int j; // [rsp+4h] [rbp-Ch]
  unsigned int i; // [rsp+8h] [rbp-8h]
  unsigned int v3; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i < 0x100; ++i )
  {
    v3 = i;
    for ( j = 0; j < 8; ++j )
    {
      if ( (v3 & 1) != 0 )
        v3 = (v3 >> 1) ^ 0x8320EDB8;
      else
        v3 >>= 1;
    }
    dword_7FF79BD57A70[i] = v3;
    result = i + 1;
  }
  return result;
}

然后对前4字节填充的block,用CRC生成的256个4字节数据,经过移位,异或运算生成4个4字节数据后与硬编码的数据比较:

最后使用最开始在block填充的0xDEAD改变vars88,vaes84, vaes80, v58后作为密钥对除去WMCTF{}格式外输入的4-20字节进行2个xtea加密。

下面开始解密:

首先用z3将vars88,vaes84, vaes80, v58四个值就求出来:

from z3 import *

s = Solver()

key = [BitVec('x%d'%i, 32) for i in range(4)]
s.add((key[0]+key[1]) == 0x11AB7A7A)
s.add(key[1]-key[2] == 0x1CD4F222)
s.add(key[2]+key[3] == 0xC940F021)
s.add(key[0]+key[2]-key[3] == 0x7C7D68D1)

if s.check() == sat:
    m = s.model()
    m = [m[key[i]].as_long() for i in range(4)]
    print(m)
else: 
    print('Not Found!')
#[2750330814, 1841087164, 1357369498, 2019106695]

再用上面4个数据依次爆破出对应的4字节明文数据:

#include <stdio.h>

unsigned int box[256];
char res[5];
int number[] = {0x100, 0x100, 0xf, 0x1c};
unsigned enc[] = {2750330814, 1841087164, 1357369498, 2019106695};

void gen_box()
{
  unsigned int j; // [rsp+4h] [rbp-Ch]
  unsigned int i; // [rsp+8h] [rbp-8h]
  unsigned int v3; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i < 0x100; ++i )
  {
    v3 = i;
    for ( j = 0; j < 8; ++j )
    {
      if ( (v3 & 1) != 0 )
        v3 = (v3 >> 1) ^ 0x8320EDB8;
      else
        v3 >>= 1;
    }
    box[i] = v3;
  }
}

unsigned int fun1(unsigned int a1, unsigned char a2[256], unsigned int a3)
{
    unsigned int v4; // [rsp+4h] [rbp-1Ch]
    unsigned int v5; // [rsp+8h] [rbp-18h]

    v5 = 0;
    v4 = a1;
    while ( v5 < a3 )
        v4 = (v4 >> 8) ^ box[(unsigned char)(a2[v5++] ^ v4)];
    return a1 ^ v4;
}

unsigned int bp(int up, int number, unsigned int pre, unsigned int next)
{
    for(int i = 0; i < 127; i++)
    {
        unsigned char block[256];
        for(int j = 0; j < number; j++)
        {
            block[j] = i+j+up;
        }

        if(fun1(pre, block, number) == next)
            return i;    
    }    
}

int main(void)
{
    gen_box();

    for(int i = 0; i < 4; i++)
    {
        if(i == 0)
            res[i] = bp(i, number[i], -2, enc[i]);
        else
            res[i] = bp(i, number[i], enc[i-1], enc[i]);
    }

    puts(res);    
}

//Hah4

用满足前4字节的测试输入WMCTF{Hah41111111111111111}输入程序,然后在xtea加密前取出密钥:

但用这个密钥解密密文怎么都不正确。。还测试了自己的xtea解密好几遍,这里卡了好一会。

后面确定肯定是密钥的问题,但输入的前4字节是满足要求的,密钥是通过前4字节明文算出来的。但注意这里的密钥还用开始在block填充的0xDEAD的经过了变换的。这让我想到我忽略了输入的(WMCTF{}格式外)20字节后处理,开始闲麻烦懒得看直接跳过了。

所以问题现在应该就出在了有两个字节数据对密钥的影响。

爆破这2个字节,从解密结果中看像是flag的片段的:

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

unsigned int get_delat()
{
    int i = 0;
    unsigned int ans = 0, delat = 0x667E5433;

    for(i = 0; i < 32; i++)
        ans -= delat;

    return ans;
} 

void decrypt1(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 = 0x667E5433, sum = get_delat();
    //printf("%x", sum);  
    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;  
}

int check(unsigned a)
{
    for(int i = 0; i < 4; i++)
    {
        if(((char *)&a)[i] < 32 || ((char *)&a)[i] > 127)
            return 0;
    }

    return 1;
}

int main(void)
{
    //['a3eeb7be', '6dbcc2bc', '50e7d09a', '78591f87']

    uint32_t k[4]={0x78591FAD, 0x6DBCC2BC, 0xA3EEB7BE, 0x50E7DE9A};
    for(int i = 10; i < 0xff; i++)
    {
        for(int j = 0; j < 0xff; j++)
        {
            uint32_t v[2]={0x1989FB2B, 0x83F5A243};
            k[3] &= 0xFFFF00FF;
            k[3] |= i << 8;
            k[0] &= 0xFFFFFF00;
            k[0] |= j;

            unsigned int r=32;
            decrypt1(r, v, k);

            if(check(v[0]) && check(v[1]))
            {
                for(int k = 0; k < 8; k++)
                {
                    printf("%c", ((char *)v)[k]);
                }
                printf(" %x %x", i, j);
                putchar(10);
            }
        }

    }


    return 0;  
}
/*
pWRTPO{> 13 9f
<<R|CJA< 24 c7
\o{2%lSf 28 7f
t<o.:RMY 2d 69
b%AGkVTt 36 2d
e.xQVP!| 53 0
0bOMoJI8 54 b1
"pWU3*@+ 73 d2
>]zSE>?d 81 d7
(sqF m# 8a 6b
Z,wRg8T_ 92 76
yOu_L1kE b7 ad
!vta&K]M ba d3
K?Gl@~Rw bf b5
1C ="`~p c3 71
?&bqWg]_ cd b1
SX|6u|v f4 43
+zWv6`!C fb a2
*/

可以看到yOu_L1kE,满足要求的两个字节是0xb7 0xad

然后解密2段密文再按一定顺序拼接一下得到:_D0_yOu_L1kE_It!

现在就是去求_@#?!&-$+对应的处理函数怎么才能将((_WORD )Block + 273)的0xDEAD的改为0xB7AD

输入为hex,4字节为一组转化为2个byte,第一个byte是index,第二个byte是data

根据要求推算出这样一个顺序是满足b要求的:

首先@对应的处理函数将block[256] = 0xFE。注意下面是char a2,所以传入0xFF就是-1了,因此满足输入为:@FFFE

然后#对应的处理函数将block[528] = 0x20,因此满足输入:#0F20

最后对应的处理函数将block[527] = 0xB7,也是我们最后的终点。因此满足输入:-11B7

可以看到上面要能执行最后的(_BYTE )(a1 + 530 + a2) = a3;要求是(*(unsigned __int16 *)(a1 + 528) % 16) == 0,(unsigned int)(*(unsigned __int16 *)(a1 + 528) / 16) < 3

用这2个限制爆破得到:

>>> a = [i for i in range(0xff) if i%16 == 0 and i/16 < 3]
>>> a
[0, 16, 32]

而我们的index为17且index<(unsigned __int16 )(a1 + 528),所以满足要求的就只有最后的32了,故上面#对应的处理函数要将block[528] = 0x20。

最后将我们的所有输入拼接起来得到flag:

WMCTF{Hah4_D0_yOu_L1kE_It!@FFFE#0F20-11B7}

 

Re2

jadx打开app,可以看到关键在native层。

到so文件找到JNI_Onload

其中,上面的JNI_Onload根据sub_7079FF9BBC函数的返回值注册不同的函数。

看到sub_7079FF9BBC:它通过查看/data/local/su是否存在,也就是判断我们的运行环境中有没有root

所以JNI_Onload是根据运行环境是否root注册不同的函数来执行。

接着我把程序在root与非root手机运行来看一下,root下运行随便输入后显示:fake branch,而在非root的手机上运行随便输入后显示:failed,please try again!!!,以此可以得出,我们要分析的非root才注册的函数。

然后也去看了一下root下注册的假流程:经过上面一些加密后最终都是返回同一个字符串。

查看返回的字符串,发现并不是字符串数据,从交叉引用发现.init_array中一些初始化函数对其进行了解密。

并且.init_array中初始化函数动态解密了程序中很多数据:

对上面假流程返回的字符串异或0x6d解密后得到:fake branch

再看到正确分支流程:先异或解密一些数据后注册了如下的函数。

jstring __fastcall sub_7079FF9134(JNIEnv *a1, __int64 a2, __int64 a3)
{
  const char *v5; // x21
  _BYTE *v6; // x20
  char *v7; // x21
  __int64 v8; // x0
  char *v9; // x1
  __int64 v10; // x8
  size_t v11; // w0
  const char *v12; // x1
  const char *v13; // x1
  jstring v14; // x19
  _BYTE v16[56]; // [xsp-30h] [xbp-170h]
  unsigned __int64 v17[2]; // [xsp+8h] [xbp-138h] BYREF
  char *v18; // [xsp+18h] [xbp-128h]
  __int128 v19; // [xsp+20h] [xbp-120h] BYREF
  unsigned __int64 v20[2]; // [xsp+38h] [xbp-108h] BYREF
  char *v21; // [xsp+48h] [xbp-F8h]
  __int64 v22; // [xsp+F8h] [xbp-48h]

  v22 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  strcpy_0(v17, (char *)&xmmword_707A02F0A0);
  v5 = (*a1)->GetStringUTFChars(a1, a3, 0LL);
  if ( (*a1)->GetStringLength(a1, (jstring)a3) == 32 )
  {
    v6 = (_BYTE *)operator new[](0x21uLL);
    __strcpy_chk(v6, v5, 33LL);
    v7 = (char *)operator new[](0x1EuLL);
    sub_7079FF9E80();
    v8 = __strlen_chk(v7, 0x1Eu);
    v7[(int)v8] = 102;
    v7[((v8 << 32) + 0x100000000LL) >> 32] = 108;
    v7[((v8 << 32) + 0x200000000LL) >> 32] = 103;
    v7[((v8 << 32) + 0x300000000LL) >> 32] = 0;
    v19 = xmmword_707A01E5D0;
    sub_7079FFA934((__int64)v20, v7, (long double *)&v19);
    sub_7079FFAE6C(v20, v6, 0x20uLL);
    strcpy_0(v20, (char *)&qword_707A02F058);
    if ( (v20[0] & 1) != 0 )
      v9 = v21;
    else
      v9 = (char *)v20 + 1;
    sub_7079FFA624((int)&v19, v9);
    v10 = 0LL;
    while ( v16[v10] == stru_707A02F000[0].n128_u8[v10] )
    {
      if ( ++v10 == 32 )
      {
        v11 = strlen((const char *)&aQpyl);
        sub_7079FF9670((int)v17, &aQpyl, v11);
        if ( (v17[0] & 1) != 0 )
          v12 = v18;
        else
          v12 = (char *)v17 + 1;
        goto LABEL_18;
      }
    }
    if ( (v17[0] & 1) != 0 )
      v12 = v18;
    else
      v12 = (char *)v17 + 1;
LABEL_18:
    v14 = (*a1)->NewStringUTF(a1, v12);
    if ( (v20[0] & 1) != 0 )
      operator delete(v21);
  }
  else
  {
    if ( (v17[0] & 1) != 0 )
      v13 = v18;
    else
      v13 = (char *)v17 + 1;
    v14 = (*a1)->NewStringUTF(a1, v13);
  }
  if ( (v17[0] & 1) != 0 )
    operator delete(v18);
  return v14;
}

先简单静态分析一下,开始是判断输入的长度是否为32。

然后sub_7B4933FE80函数读取某个文件内容经过对比后返回一串字符串:

后面接着对上面获取到的字符串进行如下赋值:

其实就是在其末尾加上flg

len = strlen(init_key);
init_key[len] = 'f';
init_key[len+1] = 'l';
init_key[len+2] = 'g';
init_key[len+3] = '\x0';

接着sub_7B49340934函数传入两个参数,其中的sub_7B49340820函数用了传入的一个参数串进行aes的密钥扩展:字节替换(但是这里的sbox是替换过的),移位,轮常数异或。44/4 = 11,这也说明了是aes_128,因为密钥11组。

再是将另外一个参数存放在扩展密钥的尾部:

接着的sub_7B49340E6C函数也是很明显的aes_128_cbc加密,sub_7B4934097C中清晰的初始轮(轮密钥加),重复轮(字节替换,行移位,列混合,轮密钥加),最终轮(字节替换,行移位,轮密钥加)结构:

最后sub_7B49340624函数rc4加密,但多异或了0x50:

所以整体上本题的加密就是aes_128_cbc与rc4,麻烦的是数据部分,如aes的密钥,iv,rc4密钥与密文等。因为开始说了在.init_array中进行了很多数据的解密,我在静态分析看到的大多数数据都是没有解密的。那我们现在要么对分析到的数据找到引用修改的.init_array中的函数按照相同的运算逻辑手动patch修改;要么就是把程序调试起来,分析起来会简单很多。

这里我选择了动态调试。

首先将AndroidMannifest.xml中的android:extractNativeLibs=”false“改为true或者删掉,默认为true。因为这个如果为false会让我们在调试时找不到so

然后因为我们调试的断点要断在JNI_OnLoad中(方便把注册的函数修改为正确的分支),那我们必须在程序还没执行System.loadLibrary(“native-lib”);之前就断下来,所以要程序要以调试模式启动。

首先我尝试了ida+jdb的组合:

运行环境中root模式启动好相应的服务程序,转发端口到本地。(停止转发端口:adb forward —remove tcp:端口号adb forward —remove-all)

使用am命令以调试启动app:adb shell am start -D -n come.wmctf.crackme111/.MainActivity

ida在JNI_OnLoad中下好断点,然后找到app对应的进程后附加,接着F9运行

打开ddms,用附加让app运行起来:jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

但是这样做在jdb附加app就报如下的错误。这好像是我手机的原因?

我使用jeb来附加app同样也是报错,这都是在我先用IDA附加了进程的情况下,接着我尝试发现先jdb或jeb附加再IDA附加是可以的,但这样程序已经运行过System.loadLibrary(“native-lib”);了。

而还有一个方法,我们可以使用jeb附加调试断在System.loadLibrary(“native-lib”);之前再用IDA去附加进程呀。

然后成功断在JNI_OnLoad中,在正确分支下好断点,修改检测环境是否root的返回值为false,但是这个在native层运行完JNI_OnLoad函数回到java层的时候app又崩溃了。

最后干脆直接改so得了,就是把根据检测运行环境是否有su的返回值后的条件跳转改一下。

上面修改完后,把app重编译一下,然后普通的附加调试就好了。这也是调试本程序最简单的方法,上面绕了一大圈

关闭