[2024 古剑山]India Pale Ale
本文最后更新于43 天前,其中的信息可能已经过时,如有错误可以直接在文章下留言

一道IOS逆向,之前忘了哪个比赛遇到过一次,当时没有复现,只是草草看看Writeup,知道这个其实并不一定要依赖IOS环境动态调试才能解题,IDA好像可以硬做。

复现一下,以下是题目描述

#小麦饮料。

这里的描述应该说的就是题目名字,首字母合起来就是IPA

不知道IOS的IPA是个什么概念,这里AI一下

IPA包是iOS应用程序的文件格式,代表“iOS App Store Package”。它是苹果公司用于分发和安装iOS应用的压缩文件,类似于Android的APK文件。IPA文件包含了应用的二进制代码、资源文件(如图像和音频)、以及应用的元数据。

把附件的ipa解压获取其中的EasyiOS文件,然后扔到IDA里面开始分析。

找字符串可以找到这里

很好就找到关键函数了,因为要根据sub_100005C24函数的返回结果来选择输出的字符串。

sub_100005C24内有两个关键函数,sub_1000059A8是一个很明显的Base64算法,但是sub_1000057E0的分析有点困难,但是比赛快结束才看的这一题,并且看不出是什么加密算法,就直接放弃了。

我们先从简单的开始分析,这题有个更坑,在程序执行前,有三个初始化函数会执行

double InitFunc_0()
{
  __int64 v0; // x8
  signed int v1; // w9
  int v2; // w13
  int v3; // w9
  int v4; // w14
  __int64 i; // x9
  double result; // d0
  __int128 v7[2]; // [xsp+0h] [xbp-90h]
  __int128 v8; // [xsp+20h] [xbp-70h]
  __int128 v9; // [xsp+30h] [xbp-60h]
  __int128 v10[2]; // [xsp+40h] [xbp-50h]
  __int128 v11[2]; // [xsp+60h] [xbp-30h] BYREF

  v0 = 0LL;
  v1 = 0;
  v10[0] = xmmword_100007EC0;
  v10[1] = xmmword_100007ED0;
  qmemcpy(v11, " !\"#$%&'()*+,-./0123456789:;<=>?", sizeof(v11));
  do
  {
    v2 = *(v10 + v0);
    v3 = v1 + v2 + byte_100007F00[v0 % 5u];
    v4 = v3 + 63;
    if ( v3 >= 0 )
      v4 = v3;
    v1 = v3 - (v4 & 0xFFFFFFC0);
    *(v10 + v0) = *(v10 + v1);
    *(v10 + v1) = v2;
    ++v0;
  }
  while ( v0 != 64 );
  for ( i = 0LL; i != 64; ++i )
    *(v7 + i) = aAbcdefghijklmn[*(v10 + i)];
  *aAbcdefghijklmn = v7[0];
  *&aAbcdefghijklmn[16] = v7[1];
  result = *&v8;
  *&aAbcdefghijklmn[32] = v8;
  *&aAbcdefghijklmn[48] = v9;
  return result;
}

这个函数估计是对Base64的table的有一个变换

void InitFunc_1()
{
  __int64 i; // x8

  for ( i = 0LL; i != 13; ++i )
    aSimplekeyhere[i] ^= 0xA5u;
}

对密钥的修改

void InitFunc_2()
{
  __int64 i; // x8

  for ( i = 0LL; i != 44; ++i )
    byte_10000D897[i] ^= 0x90u;
}

对密文的异或修改。

这个在主函数前的Init机制和Golang有点像,这种在主函数执行前的初始化,如果我们够细心,能够养成在做题时对关键变量进行一个交叉引用的查看的话,应该是可以发现的。

我们先写脚本来对三者进行一个恢复,脚本如下

byte_10000D897 = [0xF2, 0xD5, 0xE8, 0xF1, 0xA9, 0xE9, 0xBB, 0xC8, 0xFC, 0xD1,
                  0xF2, 0xFC, 0xF5, 0xDA, 0xC0, 0xFC, 0xD2, 0xDA, 0xE9, 0xA5,
                  0xE2, 0xA0, 0xD1, 0xD6, 0xC0, 0xF5, 0xDA, 0xC1, 0xDB, 0xD5,
                  0xDF, 0xD4, 0xD3, 0xC1, 0xA6, 0xD4, 0xA2, 0xA3, 0xFA, 0xDF,
                  0xE0, 0xC2, 0xBB, 0xC8]
for i in range(len(byte_10000D897)):
    byte_10000D897[i] ^= 0x90
print(byte_10000D897)
# 输出[69, 120, 97, 57, 121, 43, 88, 108, 65, 98, 108, 101, 74, 80, 108, 66, 74, 121, 53, 114, 48, 65, 70, 80, 101, 74, 81, 75, 69, 79, 68, 67, 81, 54, 68, 50, 51, 106, 79, 112, 82, 43, 88]
aSimplekeyhere = list("SimpleKeyHere")
for j in range(len(aSimplekeyhere)):
    aSimplekeyhere[j] = 0xA5 ^ ord(aSimplekeyhere[j])
print(aSimplekeyhere)
# 输出[246, 204, 200, 213, 201, 192, 238, 192, 220, 237, 192, 215, 192]
Base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
v10 = list(range(64))
byte_100007F00 = [0x0D, 0x11, 0x13, 0x17, 0x1D]
v1 = 0
v7 = [0] * 64
for k in range(64):
    v2 = v10[k]
    v3 = v1 + v2 + byte_100007F00[k % 5]
    v4 = v3 + 63
    if v3 >= 0: v4 = v3
    v1 = v3 - (v4 & 0xFFFFFFC0)
    v10[k] = v10[v1]
    v10[v1] = v2
v7 = [Base64_table[v10[i]] for i in range(64)]
Base64_table = ''.join(v7)
print(Base64_table)
# 输出NF01ihUKST9q3lnjEBs47k2w5ad+AVHfPezg/CDyxrMLR6GvomIQJOXcpW8ZbutY

其实就是照着伪代码写一遍。

Base64加密的函数代码如下,看不出哪里有魔改

__int64 __usercall sub_1000059A8@<X0>(__int64 result@<X0>, _QWORD *a2@<X8>)
{
  __int64 v2; // x20
  __int64 v4; // x22
  unsigned __int8 v5; // w8
  __int64 v6; // x9
  unsigned __int64 v7; // x8
  __int64 v8; // x8
  __int64 v9; // x8
  __int64 v10; // x8
  int v11; // w10
  __int64 v12; // x8
  __int64 v13; // x9
  unsigned __int64 v14; // x8
  __int64 v15; // x8
  __int64 v16; // x8
  __int64 v17; // x8

  v2 = result;
  v4 = 0LL;
  *a2 = 0LL;
  a2[1] = 0LL;
  a2[2] = 0LL;
  while ( (*(v2 + 23) & 0x80000000) == 0 )
  {
    v5 = *(v2 + 23) % 3u;
    v6 = v2;
    if ( v4 == *(v2 + 23) - v5 )
    {
      v11 = v5 & 3;
      if ( ((*(v2 + 23) % 3u) & 3) == 0 )
        return result;
      v12 = v2;
      v13 = v2;
      if ( v11 == 1 )
        goto LABEL_23;
LABEL_28:
      std::string::push_back(a2, aAbcdefghijklmn[*(v13 + v4) >> 2]);
      if ( *(v2 + 23) >= 0 )
        v16 = v2;
      else
        v16 = *v2;
      std::string::push_back(a2, aAbcdefghijklmn[(16 * *(v16 + v4)) & 0x30LL | (*(v16 + v4 + 1) >> 4)]);
      if ( *(v2 + 23) >= 0 )
        v17 = v2;
      else
        v17 = *v2;
      std::string::push_back(a2, aAbcdefghijklmn[(4 * *(v17 + v4 + 1)) & 0x7CLL]);
      return std::string::push_back(a2, 61LL);
    }
LABEL_7:
    std::string::push_back(a2, aAbcdefghijklmn[*(v6 + v4) >> 2]);
    if ( *(v2 + 23) >= 0 )
      v8 = v2;
    else
      v8 = *v2;
    std::string::push_back(a2, aAbcdefghijklmn[(16 * *(v8 + v4)) & 0x30LL | (*(v8 + v4 + 1) >> 4)]);
    if ( *(v2 + 23) >= 0 )
      v9 = v2;
    else
      v9 = *v2;
    std::string::push_back(a2, aAbcdefghijklmn[(4 * *(v9 + v4 + 1)) & 0x3CLL | (*(v9 + v4 + 2) >> 6)]);
    if ( *(v2 + 23) >= 0 )
      v10 = v2;
    else
      v10 = *v2;
    result = std::string::push_back(a2, aAbcdefghijklmn[*(v10 + v4 + 2) & 0x3F]);
    v4 += 3LL;
  }
  v7 = *(v2 + 8);
  if ( v4 != 3 * (v7 / 3) )
  {
    v6 = *v2;
    goto LABEL_7;
  }
  v14 = v7 % 3;
  if ( !v14 )
    return result;
  if ( v14 != 1 )
  {
    v13 = *v2;
    goto LABEL_28;
  }
  v12 = *v2;
LABEL_23:
  std::string::push_back(a2, aAbcdefghijklmn[*(v12 + v4) >> 2]);
  if ( *(v2 + 23) >= 0 )
    v15 = v2;
  else
    v15 = *v2;
  std::string::push_back(a2, aAbcdefghijklmn[(16 * *(v15 + v4)) & 0x30LL]);
  std::string::push_back(a2, 61LL);
  return std::string::push_back(a2, 61LL);
}

有难度的地方主要在sub_1000057E0函数,代码如下

void __usercall sub_1000057E0(__int64 *a1@<X0>, __int64 *a2@<X1>, _QWORD *a3@<X8>)
{
  _BYTE *v6; // x0
  unsigned __int64 v7; // x10
  _BYTE *v8; // x8
  unsigned __int64 v9; // x9
  unsigned __int64 v10; // x10
  unsigned __int64 v11; // x11
  __int64 v12; // x8
  __int64 v13; // x11
  unsigned __int64 v14; // x12
  int v15; // w13
  __int64 *v16; // x13
  __int64 v17; // x9
  __int64 v18; // x12
  unsigned __int64 v19; // x23
  unsigned __int64 v20; // x22
  __int64 i; // x21
  unsigned __int64 v22; // x8
  __int64 v23; // x9
  char v24; // [xsp+7h] [xbp-49h] BYREF
  void *v25; // [xsp+8h] [xbp-48h] BYREF
  _BYTE *v26; // [xsp+10h] [xbp-40h]

  v24 = 0;
  sub_10000601C(&v25, 250LL, &v24);
  *a3 = 0LL;
  a3[1] = 0LL;
  a3[2] = 0LL;
  v6 = v26;
  if ( v26 == v25 )
  {
    v8 = v26;
  }
  else
  {
    v7 = 0LL;
    v6 = v25;
    do
    {
      v6[v7] = v7;
      ++v7;
      v6 = v25;
      v8 = v26;
      v9 = v26 - v25;
    }
    while ( v7 < v26 - v25 );
    if ( v9 )
    {
      v10 = 0LL;
      v11 = 0LL;
      do
      {
        v12 = v6[v10];
        v13 = v11 + v12;
        v14 = *(a2 + 23);
        v15 = v14;
        if ( (v14 & 0x80u) != 0LL )
          v14 = a2[1];
        if ( v15 >= 0 )
          v16 = a2;
        else
          v16 = *a2;
        v11 = (v13 + *(v16 + v10 % v14)) % v9;
        v6[v10] = v6[v11];
        v6[v11] = v12;
        ++v10;
        v6 = v25;
        v8 = v26;
        v9 = v26 - v25;
      }
      while ( v10 < v26 - v25 );
    }
  }
  v17 = *(a1 + 23);
  v18 = a1[1];
  if ( (v17 & 0x80u) != 0LL )
    a1 = *a1;
  if ( (v17 & 0x80u) != 0LL )
    v17 = v18;
  if ( v17 )
  {
    v19 = 0LL;
    v20 = 0LL;
    for ( i = v17 - 1; ; --i )
    {
      v22 = v8 - v6;
      v19 = (v19 + 1) % v22;
      v23 = v6[v19];
      v20 = (v20 + v23) % v22;
      v6[v19] = v6[v20];
      v6[v20] = v23;
      std::string::push_back(a3, (*a1 ^ *(v25 + (*(v25 + v20) + *(v25 + v19)) % (v26 - v25))));
      if ( !i )
        break;
      a1 = (a1 + 1);
      v6 = v25;
      v8 = v26;
    }
    v6 = v25;
  }
  if ( v6 )
  {
    v26 = v6;
    operator delete(v6);
  }
}

该函数的变量比较多,没什么常量,不仔细看根本看不出来是什么加密算法,如果静态看的话难度应该是有一点的

当时比赛还以为要逆向分析,感觉来不及了。赛后看别人的Writeup说是魔改RC4,看做出来的大部分还是动态调试做出来的,要么就是dump出S盒,要么就是dump出密钥流,参考以下文章

A1natas 2024 古剑山 WriteUp

古剑山WriteUP

但是其实静态仔细看的话是完全能看的,根据这里频繁出现取模运算,并且还大多是两个变量相加后再取模某个值,其实是完全可以猜测出是RC4加密算法的。我将函数中的变量重命名一下,就更加豁然开朗了

void __usercall sub_1000057E0(__int64 *input@<X0>, __int64 *key@<X1>, _QWORD *a3@<X8>)
{
  _BYTE *sbox_copy; // x0
  unsigned __int64 i_0; // x10
  _BYTE *v8; // x8
  unsigned __int64 v9; // x9
  unsigned __int64 i_1; // x10
  unsigned __int64 j_1; // x11
  __int64 tmp_0; // x8
  __int64 v13; // x11
  unsigned __int64 keylen; // x12
  int v15; // w13
  __int64 *keycopy; // x13
  __int64 v17; // x9
  __int64 v18; // x12
  unsigned __int64 i_2; // x23
  unsigned __int64 j_2; // x22
  __int64 i; // x21
  unsigned __int64 v22; // x8
  __int64 tmp; // x9
  char v24; // [xsp+7h] [xbp-49h] BYREF
  void *sbox; // [xsp+8h] [xbp-48h] BYREF
  _BYTE *v26; // [xsp+10h] [xbp-40h]

  v24 = 0;
  sub_10000601C(&sbox, 250LL, &v24);
//猜测函数作用是为S盒开辟大小为250的空间,这里也就是RC4的魔改
  *a3 = 0LL;
  a3[1] = 0LL;
  a3[2] = 0LL;
  sbox_copy = v26;
  if ( v26 == sbox )
  {
    v8 = v26;
  }
  else
  {
    i_0 = 0LL;
    sbox_copy = sbox;
    do
    {
      sbox_copy[i_0] = i_0;
      ++i_0;
//S盒的初始化
      sbox_copy = sbox;
      v8 = v26;
      v9 = v26 - sbox;
    }
    while ( i_0 < v26 - sbox );
    if ( v9 )
    {
      i_1 = 0LL;
      j_1 = 0LL;
      do
      {
        tmp_0 = sbox_copy[i_1];
        v13 = j_1 + tmp_0;
        keylen = *(key + 23);
        v15 = keylen;
        if ( (keylen & 0x80u) != 0LL )
          keylen = key[1];
        if ( v15 >= 0 )
          keycopy = key;
        else
          keycopy = *key;
        j_1 = (v13 + *(keycopy + i_1 % keylen)) % v9;
        sbox_copy[i_1] = sbox_copy[j_1];
        sbox_copy[j_1] = tmp_0;
        ++i_1;
        sbox_copy = sbox;
//根据密钥打乱S盒
        v8 = v26;
        v9 = v26 - sbox;
      }
      while ( i_1 < v26 - sbox );
    }
  }
  v17 = *(input + 23);
  v18 = input[1];
  if ( (v17 & 0x80u) != 0LL )
    input = *input;
  if ( (v17 & 0x80u) != 0LL )
    v17 = v18;
  if ( v17 )
  {
    i_2 = 0LL;
    j_2 = 0LL;
    for ( i = v17 - 1; ; --i )
    {
      v22 = v8 - sbox_copy;
      i_2 = (i_2 + 1) % v22;
      tmp = sbox_copy[i_2];
      j_2 = (j_2 + tmp) % v22;
      sbox_copy[i_2] = sbox_copy[j_2];
      sbox_copy[j_2] = tmp;
      std::string::push_back(a3, (*input ^ *(sbox + (*(sbox + j_2) + *(sbox + i_2)) % (v26 - sbox))));
//生成密钥流,并与我们的输入进行异或
      if ( !i )
        break;
      input = (input + 1);
      sbox_copy = sbox;
      v8 = v26;
    }
    sbox_copy = sbox;
  }
  if ( sbox_copy )
  {
    v26 = sbox_copy;
    operator delete(sbox_copy);
  }
}

可见算法将S盒的大小从256魔改成250,那么后续的轮数和取模的值,我们猜测也是修改成250,因为它这里没有什么常量,连取模的值都是用变量来表示,不动态调试的情况下,只能猜了。

那么我们的想法当然就是按正常流程修改RC4解密脚本,都将常量256改成250就可以解出答案了,但是实际并不能得到flag,通过运行代码,和别人正确的S盒比较,我发现也是不一样的。

这里感谢A1natas战队的carbofish师傅,这里的问题我也是询问了他,他还帮我用静态分析的方法复现了一下,并告诉了我原因,他这里应该是动态调试看了看哪里出现了问题,出问题的原因还真是之前群友提过的。😭

他后面也写了博客

2024 古剑山网络安全大赛 Reverse 方向 | Carbo

就是说RC4的key数组是有符号的,而在打乱S盒的时候,在取key的值的时候,会用到指令LDRSB

而LDRSB指令用于从内存中将一个8位的字节数据读取到指令中的目标寄存器中。并将寄存器的高位设置成该字节数据的符号位的值(即将该8位字节数据进行符号位扩展,生成32位或者64位字数据)。

题目这里是arm64,X开头是arm中64位的寄存器,所有应该将数据字节扩展到64位数据。说实话,如果没学过arm汇编,不动态调试,真的很难发现问题。

也就是说倘若取出key的值是负数,就会在高位进行字节扩展,填充0xff,这里就会填充7个0xff,如果是取模256,那显然是不用在乎这个的,可以用下列脚本验证

data=0xffffff66%256
print(data)
#输出102
data=0x66%256
print(data)
#输出102
data=0xfffffff5%250
print(data)
#输出35
data=0xf5%250
print(data)
#输出245

因为在对256取模的时候,前面扩展无论多少个0xff,最后高位0xff的部分都是256的倍数,我们只用关注低位部分,比如0xffffff00对256取模是0。而我们正常写RC4的解密代码,直接将对256取模改成250,没有字节扩展这一步,就会导致数据处理有错误。

这里直接照搬carbofish师傅的脚本

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "defs.h"


uint8 sbox[250] = {0};

//初始化s表
void init_sbox(char *key, int klen) {
    uint64 i, k;
    int64 tmp;

    for (i = 0; i < 250; i++) {
        sbox[i] = i;
    }

    uint64 j = 0;
    for (i = 0; i < 250; i++) {
        tmp = (uint8) sbox[i];
        int64 v13 = j + tmp;
        char keyVal = key[i % klen];
        int bitFlag = keyVal & 0x80;
        uint64 covertVal = 0;
        if (bitFlag) {
            // 负数
            covertVal = 0xFFFFFFFFFFFFFF00 | keyVal;
        } else {
            covertVal = keyVal;
        }
//处理负数的部分主要在这里,如果是负数就进行字节扩展
        j = (v13 + covertVal) % 250;
        sbox[i] = sbox[j];
        sbox[j] = tmp;
    }
}

//加解密函数
void enc_dec(char *key, unsigned char *data, int slen, int klen) {
    int i, j, k, R, tmp;

    init_sbox(key, klen);

//    for (int l = 0; l < 250; ++l) {
//        printf("0x%x, ", (uint8) sbox[l]);
//    }

    j = k = 0;
    for (i = 0; i < slen; i++) {
        j = (j + 1) % 250;
        k = (k + sbox[j]) % 250;

        tmp = sbox[j];
        sbox[j] = sbox[k];
        sbox[k] = tmp;

        R = sbox[(sbox[j] + sbox[k]) % 250];

        data[i] ^= R;
    }
}

int main() {
    unsigned char key[13] = {0xF6, 0xCC, 0xC8, 0xD5, 0xC9, 0xC0, 0xEE, 0xC0, 0xDC, 0xED, 0xC0, 0xD7, 0xC0};
    unsigned char data[33] = {0xf1,0x0a,0x19,0x2a,0x76,0xf6,0x35,0xcf,0x0d,0x87,0x48,0x0d,0x47,0x49,0xd8,0xa4,0x27,0x01,0x82,0x1d,0x33,0x1d,0x0d,0x66,0x97,0x3b,0x66,0x58,0xc3,0xf5,0xe2,0xc6,0xf6};

    enc_dec((char *)key, data, 33, 13);

    for (int i = 0; i < 33; ++i) {
        printf("%c", data[i]);
    }

    return 0;
}
// flag{45_4_105_r3v3r51n6_b361nn3r}

所有flag就是flag{45_4_105_r3v3r51n6_b361nn3r}

当然这里我一开始怀有疑问,首先按解题脚本的意思,key数组从unsigned char转到char*,这里超出char数据类型的范围,发生了溢出,所以才导致有符号的key数组中的数据全部会变成负数。

但是在正常程序中是从哪里开始溢出的呢

查看key的字符串,数据类型是char,那么在初始化时进行异或运算超出char数据类型的范围,猜测就是在初始化函数当中发生了溢出,然后进入RC4加密的函数将key数组转化成了__int64的类型,依旧是有符号的。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇