【2023春节红包】二、三、四、五、六、八WriteUp(其余仅作记录)

查看 162|回复 12
作者:周易   
解题领红包之二 windows 初级题
拿到题目,首先将程序下载到本地,运行后提示 Please input password:,在winhex中可以搜到这个字符串,说明程序没加壳,但用OD加载时却搜不到,经提示应当是方法问题,因现在还不能公开讨论,暂且不管。


4caf23aae7483248b4db43aa724f5c69_0051488a7b2449afad96b5824dd38fb6.png (181.75 KB, 下载次数: 1)
下载附件
2023-2-6 17:05 上传

改用IDA加载本程序,并进行调试


b7fe078c7ef8c813a4db442e86e47706_da3a7181500a4289aaa0bc2880ee1056.png (193.13 KB, 下载次数: 0)
下载附件
2023-2-6 17:06 上传

注意到if ( *(_DWORD *)(v14[0] - 12) == 29 )一句,猜测是判断长度,因为该条件不成立则会提示长度错误,因此输入一段长度为29的任意序列让条件成立。
接下来_ZNSs12_M_leak_hardEv(v14);应当是某种初始化语句,略过。
然后注意到*(_BYTE *)(v14[0] + v13) != (unsigned __int8)(dword_43F000[v13] >> 2)只要成立一次,就会直接跳出循环,并提示Wrong,please try again. ,可见密码应当就是dword_43F000中的前29位右移两位后组成的字符串。
dword_43F000的内容如下:


aea66cdccecda926a3eb164c799b2543_edc037a0b6934d1aaa0a7986e039e905.png (28.91 KB, 下载次数: 0)
下载附件
2023-2-6 17:06 上传

取其前29位,打到js控制台里,并用for循环进行右移处理:


c0bf37974358b3e086d86f2bc93dd565_591f44fb0f2d443ea5f4b16a00c651b7.png (18.63 KB, 下载次数: 0)
下载附件
2023-2-6 17:06 上传

然后,用一个变量将数组中的每一位转为字符拼接上,最终取得flag,提交到论坛,领取奖励。


00f1d0f105c8b8eeba6583bf8e0114d5_5a51c6f9fee243d785e8416b182cdc45.png (14.85 KB, 下载次数: 0)
下载附件
2023-2-6 17:06 上传

解题领红包之三 {Android  初级题}
根据AndroidManifest.xml文件,可知入口为 com.zj.wuaipojie2023_3.MainActivity
反编译这个类,观察逻辑,这里仅摘录关键部分:
public final class MainActivity extends AppCompatActivity {
    private int num = 1;
        public static final void onCreate$lambda-0(MainActivity mainActivity, TextView textView, View view) {
        Context context = (Context) mainActivity;
        mainActivity.jntm(context);
        textView.setText(String.valueOf(mainActivity.num));
        if (mainActivity.check() == 999) {
            Toast.makeText(context, "快去论坛领CB吧!", 1).show();
            textView.setText(mainActivity.decrypt("hnci}|jwfclkczkppkcpmwckng\u007f", 2));
        }
    }
    public final int check() {
        this.num++;
        return num;
    }
    public final String decrypt(String str, int i) {
        char[] charArray = str.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char c : charArray) {
            sb.append((char) (c - i));
        }
        String sb2 = sb.toString();
        return sb2;
    }
}
由此至少可以得出以下三种解法:
1.暴力求解
直接用连点器在按钮上点击999次即可得到flag,但要注意一旦超过了999次就得重来了。
2.修改数值
将999改为一个较小的数,例如3;或者将check方法改为如下形式(我采用的是这种,方便整活):
        public final int check() {
        this.num++;
        return 999;
    }
3.单独分析/运行算法
将decrypt的代码拷到IDE中, 执行如下语句即可
System.out.println(this.decrypt("hnci}|jwfclkczkppkcpmwckng\u007f", 2));
整活
原题目中点击按钮只会发出鸡鸡鸡的声音,太单调了,我们要让它唱鸡你太美
去【(鸡乐盒)[https://ikun.life/audio.html]】中下载前5个音频,按顺序命名为0,1,2,3,4,然后修改`jntm`的代码如下:
        private final void jntm(Context context) {
        try {
            Log.e("zj595", "纯鹿人");
            AssetManager assets = context.getAssets();
            AssetFileDescriptor openFd = assets.openFd((this.num % 5) + ".mp3");
            MediaPlayer mediaPlayer = new MediaPlayer();
            mediaPlayer.setDataSource(openFd.getFileDescriptor(), openFd.getStartOffset(), openFd.getLength());
            mediaPlayer.prepare();
            mediaPlayer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
将着5个文件放入apk的assert目录,重新打包运行即可。
效果见视频,flag已打码: https://www.bilibili.com/video/BV1DY411X7a3
解题领红包之四 {Android 初级题}
观察AndroidManifest.xml可知入口在com.zj.wuaipojie2023_1.MainActivity中,进入该类查看代码,其中点击验证按钮后执行的部分如下(部分字段命名经过调整):
public static final void m25onCreate$lambda0(MainActivity this$0, View view) {
    Intrinsics.checkNotNullParameter(this$0, "this$0");
    A a = A.INSTANCE;
    EditText editText = this$0.edit_uid;
    EditText editText2 = null;
    if (editText == null) {
        Intrinsics.throwUninitializedPropertyAccessException("edit_uid");
        editText = null;
    }
    String uid = StringsKt.trim((CharSequence) editText.getText().toString()).toString();
    EditText editText3 = this$0.edit_flag;
    if (editText3 == null) {
        Intrinsics.throwUninitializedPropertyAccessException("edit_flag");
    } else {
        editText2 = editText3;
    }
    if (a.B(uid, StringsKt.trim((CharSequence) editText2.getText().toString()).toString())) {
        Toast.makeText(this$0, "恭喜你,flag正确!", 1).show();
    } else {
        Toast.makeText(this$0, "flag错误哦,再想想!", 1).show();
    }
}
可见关键判断为:
        a.B(uid, StringsKt.trim((CharSequence) editText2.getText().toString()).toString())
其代码如下(部分字段命名经过调整):
    public final boolean B(String uid, String flag) {
        Intrinsics.checkNotNullParameter(uid, "str");
        Intrinsics.checkNotNullParameter(flag, "str2");
        if (!(uid.length() == 0 && flag.length() == 0) && StringsKt.startsWith$default(flag, "flag{", false, 2, (Object) null) && StringsKt.endsWith$default(flag, "}", false, 2, (Object) null)) {
            String flag_content = flag.substring(5, flag.length() - 1); // flag_content 里面存放的是去掉了 flag{ } 后的内容
            Intrinsics.checkNotNullExpressionValue(flag_content, "this as java.lang.String…ing(startIndex, endIndex)");
            C c = C.INSTANCE;
            MD5Utils mD5Utils = MD5Utils.INSTANCE;
            Base64Utils base64Utils = Base64Utils.INSTANCE;
            String _52pj_uid_enc = B.encode(uid + "Wuaipojie2023");
            Intrinsics.checkNotNullExpressionValue(_52pj_uid_enc, "encode(str3)");
            byte[] _52pj_uid_bytes = _52pj_uid_enc.getBytes(Charsets.UTF_8);
            Intrinsics.checkNotNullExpressionValue(_52pj_uid_bytes, "this as java.lang.String).getBytes(charset)");
            return Intrinsics.areEqual(flag_content, c.cipher(mD5Utils.MD5(base64Utils.encodeToString(_52pj_uid_bytes)), 5));
        }
        return false;
    }
可见flag_content要和c.cipher(mD5Utils.MD5(base64Utils.encodeToString(_52pj_uid_bytes)的内容相同才能通过校验,其中flag_content是去掉了flag{}包装后剩余的内容。
因此,我能想到最简单的办法就是将c.cipher(mD5Utils.MD5(base64Utils.encodeToString(_52pj_uid_bytes)直接打到控制台上,通过增加一行smail汇编实现(愿意多写点代码的话也可以实现直接将正确的flag打到文本框里,不过这里就不弄了)。
    invoke-virtual {v2, p1}, Lcom/zj/wuaipojie2023_1/MD5Utils;->MD5(Ljava/lang/String;)Ljava/lang/String;
    move-result-object p1
    invoke-virtual {v0, p1, v1}, Lcom/zj/wuaipojie2023_1/C;->cipher(Ljava/lang/String;I)Ljava/lang/String;
    move-result-object p1
    # 很明显p1存放的就是运算后得到的flag,因此增加下面两行将其打印出来
    const-string v4, "flag"
    invoke-static {v4, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
    .line 21
    invoke-static {p2, p1}, Lkotlin/jvm/internal/Intrinsics;->areEqual(Ljava/lang/Object;Ljava/lang/Object;)Z
    move-result p1
    return p1
重新打包安装后,发现验证按钮不见了,应该是签名校验,考虑到app中没有lib库,签名校验只会在java层进行,事实上,校验代码就在MainActivity中,如下:
        private final boolean b() {
        String str;
        Signature[] signatureArr;
        try {
            if (Build.VERSION.SDK_INT >= 28) {
                signatureArr = getPackageManager().getPackageInfo(getPackageName(), 134217728).signingInfo.getApkContentsSigners();
            } else {
                signatureArr = getPackageManager().getPackageInfo(getPackageName(), 64).signatures;
            }
            str = MD5Utils.INSTANCE.MD5(Base64Utils.INSTANCE.encodeToString(signatureArr[0].toByteArray()));
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            str = "";
        }
        return Intrinsics.areEqual("12178c3f7f6b734a8a3d0ace3092bd32", str);
    }
直接清空该方法的代码,令它返回true即可(要真找不到也可以直接对原包用去校验,再重新修改smail)。
重新打包运行,输入自己的uid,flag填写flag{cxk},然后在控制台搜索·tag:flag·,发现正确的flag信息已经被打出来了:
2023-01-25    11449-11449    flag    pid-11449    E    4567niganmaaiyou8901
自行加上头尾,组装成flag{4567niganmaaiyou8901},提交到论坛就能领奖励了
解题领红包之五 {Windows  中级题}
本题有各种debuff和陷阱
先介绍下我们这个样本的基本特征: 64位(吾爱破解版OD直接报废)、upx(不脱壳无法静态分析)、动态基址(不关掉很难把函数地址整明白,脱壳后也不能正常运行)、反调试(从调试器运行会退出)
运行
样本大小为52kb,先运行看看:


c135f27c258aafbaf302b8185aeefc32_f562a401cc03437782400a41c69da789.png (23.47 KB, 下载次数: 1)
下载附件
2023-2-6 17:07 上传

输入自己的uid,flag随便填,然后就报错了


3e4ab996b3a002a8aa1109fdeff28564_77b0842adf8b4649a36f0d46125f1783.png (24.67 KB, 下载次数: 0)
下载附件
2023-2-6 17:07 上传

脱壳
DIE查壳结果表明其中有upx壳,且程序是64位的


bf53c94dd977e086c66ce2a55c8ae26b_89b1cc941b704048905571d7d83453a7.png (29.01 KB, 下载次数: 0)
下载附件
2023-2-6 17:07 上传

尝试用upx -d脱壳失败,因此只能用ESP定律手脱了。但在此之前要意识到64位程序是默认开启随机基址的,我们先用Study PE把它关了,点击【固定PE基址】,然后覆盖原文件保存即可。


565ffdd03d4e63ab1eacffd3dc14f39e_312131de61144fd69b8f0b9baf39f756.png (67.17 KB, 下载次数: 0)
下载附件
2023-2-6 17:08 上传

将关掉随机基址的程序载入x64dbg,程序在入口点断下。注意如果入口点的地址和我的不一样就说明随机基址没关闭:


8509a03f6937eb8774f9a1bf0049a631_882a5eb277fc4e03bd84b82a10f7fd80.png (55.47 KB, 下载次数: 0)
下载附件
2023-2-6 17:08 上传

ESP定律中的pushad作用是将所有寄存器状态入栈,这里虽然没有pushad,但仍然有多个push寄存器的操作,且它们执行完毕后同样只有RSP寄存器的数值发生变化,因此这就是我们要找的地方。我们在RSP寄存器所指的内存地址下硬件断点:


cd1c7be854498a527eeec6caa2f5ffca_6f59b75ec0f547028a30cb94e4d6c9ca.png (251.1 KB, 下载次数: 0)
下载附件
2023-2-6 17:08 上传

然后按一次F9让程序跑飞,很快程序断在这里:


390f32efa3accc4c2041b672b41a8bd0_b44da3d2cda7448aab1d0d228be4db87.png (247.68 KB, 下载次数: 0)
下载附件
2023-2-6 17:08 上传

现在可以删除硬件断点了。注意到这个jmp是个大跳转,我们跳转过去,这就是脱壳之后程序真正的入口了:


89705147c84a9f7e193dc4f6af243972_9c2c34573acf49c482d5b1bb1c886694.png (125.3 KB, 下载次数: 0)
下载附件
2023-2-6 17:09 上传

现在让程序停在这里不要动,按照下图步骤完成脱壳即可:


91b6d287dbcd33e7101bb50fd69224e8_db5413fa65754eb3aa463c473d55ba07.png (47.29 KB, 下载次数: 0)
下载附件
2023-2-6 17:09 上传



c581d15efa7e7145492f7cf463f905f1_14d0399881f64201bd25cef03c1854f0.png (52.38 KB, 下载次数: 0)
下载附件
2023-2-6 17:09 上传

点击dump并将文件保存,不要覆盖原文件


915a917b5064e73b2ac0e5ec5c57f400_99c3901eb78044738aed4f81583b8c26.png (42.57 KB, 下载次数: 0)
下载附件
2023-2-6 17:09 上传

获取导入表,并点击Fix Dump,选择刚刚保存的文件


d8facc16351660bb63c3cb756806145d_a2e12462b39b4440aa23404afe3c412c.png (48.28 KB, 下载次数: 0)
下载附件
2023-2-6 17:10 上传

底下提示导入表重建成功:


0a412c867763e60e682205c9d364b19c_fc7a5e25549b4359bf7d5af002cb3e1c.png (21.57 KB, 下载次数: 0)
下载附件
2023-2-6 17:10 上传

找到脱壳后的程序,双击发现可以正常运行(如果没有关掉随机基址现在关也来得及),体积变为138KB:


bb57cb97f48a6164f1959cd858ad9ddb_870aea53d8f64b54913de9b10cef8693.png (23.14 KB, 下载次数: 0)
下载附件
2023-2-6 17:10 上传

至此脱壳完成
反调试
将脱壳后的程序载入x64dbg,程序在我们熟悉的地方断下:


ac7cba1744eb319cb542fc0fb79c3cf4_c3df6f75ed844414b2860ae56a55cce9.png (84.29 KB, 下载次数: 0)
下载附件
2023-2-6 17:10 上传

按下F9,程序直接结束


a95786cb10fa0a60c54757c5c86a293c_0013d03cd306414fa04e129e0f0ef447.png (59.92 KB, 下载次数: 0)
下载附件
2023-2-6 17:10 上传

刚刚已经试过脱壳后的程序是可以双击运行的,说明程序存在反调试。
这个我不知道怎么过掉,有经验的大佬可以说说。
但是,我们可以先把程序跑起来,然后再用x64dbg附加,就能绕开反调试了:


ce17a576cef1e8b48f6105803d881cfb_60a9521d139d4cf496a5eb0b3c86705d.png (82.75 KB, 下载次数: 0)
下载附件
2023-2-6 17:11 上传

动静态分析
在所有模块中搜索字符串,出现以下内容:


5397e48904a93b502e53767569991674_a4ade19e8ab240969eedb51f3a7bbda4.png (260.66 KB, 下载次数: 0)
下载附件
2023-2-6 17:11 上传

现在将所有的ikun,啊不对,所有的"Please input a valid %s"下断点:


48270b33dd365d71d2a2499023c116c3_a3587f08e1ca4386954ca84c5fb019ea.png (227.52 KB, 下载次数: 0)
下载附件
2023-2-6 17:11 上传

再次提交flag,程序断在这里:


aadd434e986068c0719b72497caa70c6_f51bbb4880cd469093b160fa5cb11f23.png (264.6 KB, 下载次数: 0)
下载附件
2023-2-6 17:11 上传

稍微往上翻下,发现这段函数的入口地址为:0x1400011D0(以下简称11D0)


3e4cb07b513c6dbc1ed9266b0017967b_14039cd8615847f0a55a9c1a0e0cd630.png (137.75 KB, 下载次数: 0)
下载附件
2023-2-6 17:12 上传

将程序拖到IDA64中看看逻辑,按G可以快速跳转,输入0x1400011D0按回车:


642f9401bd9cc8ba61a8b97009477507_8f88b8d073a241c0b81e6bcc8edbccbe.png (11.1 KB, 下载次数: 0)
下载附件
2023-2-6 17:12 上传

再按F5查看伪码,内容如下:
__int64 __fastcall sub_1400011D0(__int64 a1, int a2, __int64 a3, __int64 a4)
{
  __int64 v4; // rax
  __int64 result; // rax
  int v6; // eax
  unsigned int v7; // eax
  int i; // [rsp+40h] [rbp-528h]
  wchar_t *String1; // [rsp+48h] [rbp-520h]
  unsigned int v10; // [rsp+50h] [rbp-518h]
  unsigned int v11; // [rsp+58h] [rbp-510h]
  int v12; // [rsp+70h] [rbp-4F8h]
  LPCWSTR v13; // [rsp+78h] [rbp-4F0h]
  int v14[4]; // [rsp+118h] [rbp-450h] BYREF
  int v15[4]; // [rsp+128h] [rbp-440h] BYREF
  int v16[6]; // [rsp+138h] [rbp-430h] BYREF
  char v17[400]; // [rsp+150h] [rbp-418h] BYREF
  char v18[208]; // [rsp+2E0h] [rbp-288h] BYREF
  char v19[208]; // [rsp+3B0h] [rbp-1B8h] BYREF
  WCHAR v20[104]; // [rsp+480h] [rbp-E8h] BYREF
  unsigned int v23; // [rsp+588h] [rbp+20h]
  v23 = a4;
  if ( a2 == 0x110 )
  {
    qword_140017D38 = ((__int64 (__fastcall *)(__int64))qword_140017C90[15])(a1);
    if ( !qword_140017D38 )
      qword_140017D38 = ((__int64 (*)(void))qword_140017C90[14])();
    sub_140001020(a1);
    ((void (__fastcall *)(__int64, int *))qword_140017C90[13])(qword_140017D38, v16);
    ((void (__fastcall *)(__int64, int *))qword_140017C90[13])(a1, v15);
    ((void (__fastcall *)(int *, int *))qword_140017C90[12])(v14, v16);
    Block = (LPCWSTR)malloc(0x384ui64);
    ((void (__fastcall *)(int *, _QWORD, _QWORD))qword_140017C90[11])(v15, (unsigned int)-v15[0], (unsigned int)-v15[1]);
    ((void (__fastcall *)(int *, _QWORD, _QWORD))qword_140017C90[11])(v14, (unsigned int)-v14[0], (unsigned int)-v14[1]);
    ((void (__fastcall *)(int *, _QWORD, _QWORD))qword_140017C90[11])(v14, (unsigned int)-v15[2], (unsigned int)-v15[3]);
    for ( i = 0; i  0 )
          {
            v12 = sub_140002110(v18, v10);
            sub_140002060(a1, 0x111i64, 0x300i64, v12);
            result = 1i64;
          }
          else
          {
            result = 0i64;
          }
        }
        else
        {
          result = 0i64;
        }
        break;
      case 2u:
        ((void (__fastcall *)(__int64, _QWORD))qword_140017C90[6])(a1, (unsigned __int16)a3);
        if ( Block )
          free((void *)Block);
        result = 1i64;
        break;
      case 0x300u:
        switch ( a4 )
        {
          case 1i64:
            String1 = (wchar_t *)(Block + 50);
            *((_WORD *)Block + 53) = 0;
            v6 = wcscmp(String1, &String2);
            break;
          case 2i64:
            String1 = (wchar_t *)(Block + 58);
            v6 = wcscmp(Block + 58, &String2);
            break;
          case 3i64:
            String1 = (wchar_t *)(Block + 50);
            *((_WORD *)Block + 53) = 32;
            v6 = wcscmp(String1, &String2);
            break;
          case 4i64:
            sub_140002E90(a1);
            return 1i64;
          default:
            String1 = (wchar_t *)byte_1400132D0;
            v6 = wcscmp((const wchar_t *)byte_1400132D0, &String2);
            break;
        }
        if ( v6 )
        {
          v13 = Block + 75;
          wsprintfW(v20, Block, String1);
          ((void (__fastcall *)(__int64, WCHAR *, LPCWSTR, __int64))qword_140017C90[5])(a1, v20, v13, 16i64);
          result = 1i64;
        }
        else
        {
          v11 = sub_1400025E0(a1);
          if ( v11 )
          {
            if ( (int)sub_140002840(a1, v19) > 0 )
            {
              sub_140002900(v19, v17);
              v7 = sub_140002A50(v17, v11, v23);
              sub_140002060(a1, 0x111i64, 0x300i64, v7);
              result = 1i64;
            }
            else
            {
              result = 0i64;
            }
          }
          else
          {
            result = 0i64;
          }
        }
        break;
      default:
        return 0i64;
    }
  }
  return result;
}
啊对了,顺便说一下,IDA也是可以动态调试的,甚至可以对伪码下断点,所以给这段伪码定性后我们会转到IDA上调试。
注意到这段伪码对Block的操作
        for ( i = 0; i
看上去像是在解密字符串,事实上我们双击Block可以直接看到它未运行时的内容是一大段零字节:


01d6e9c4b333076bf2ef20afa5c9e54b_7ef8a480072f43ef948874f7f6461778.png (110.73 KB, 下载次数: 1)
下载附件
2023-2-6 17:12 上传

这说明刚刚搜索的字符串是在程序运行时才计算出来的,因此如果想用WinHex直接字符串你是找不到的,这点和初级题很不一样。
另外注意到这个片段:
        if ( v6 )
        {
          v13 = Block + 75;
          wsprintfW(v20, Block, String1);
          ((void (__fastcall *)(__int64, WCHAR *, LPCWSTR, __int64))qword_140017C90[5])(a1, v20, v13, 16i64);
          result = 1i64;
        }
这里打印了一段格式化的字符串,结合x64dbg的内存图和弹框的实际输出可以知道格式化字符串就在下图所指内存地址的开头,内容为"Please input a valid %s":


c898aea5b1aec7995813699ec6b17e9e_cf0bd89b8b5540779b422a17c149660d.png (326.86 KB, 下载次数: 0)
下载附件
2023-2-6 17:12 上传

此外,程序会根据你的输入情况决定后面需要拼接uid还是uid and key,这内存真是够省的。
注意这段代码到上面有个swich块:
switch ( a4 )
        {
          case 1i64:
            String1 = (wchar_t *)(Block + 50);
            *((_WORD *)Block + 53) = 0;
            v6 = wcscmp(String1, &String2);
            break;
          case 2i64:
            String1 = (wchar_t *)(Block + 58);
            v6 = wcscmp(Block + 58, &String2);
            break;
          case 3i64:
            String1 = (wchar_t *)(Block + 50);
            *((_WORD *)Block + 53) = 32;
            v6 = wcscmp(String1, &String2);
            break;
          case 4i64:
            sub_140002E90(a1);
            return 1i64;
          default:
            String1 = (wchar_t *)byte_1400132D0;
            v6 = wcscmp((const wchar_t *)byte_1400132D0, &String2);
            break;
        }
稍微观察下,我们先假定Block被解密后内容就不会再变了(实际上也确实如此),再假定这几个分支里的v6都不为空,那么想让程序提示Success就只能走第四个分支了,因为别的都会被格式化输出提示错误。
但观察代码就会发现a4是从参数传入进来的,尝试在函数头下断点会被频繁断下,显然这里是某周回调函数,还是周期性触发的。那么接下来该怎么办呢?
尝试爆破
稍微往上翻翻,发现如下代码
    switch ( (unsigned __int16)a3 )
    {
      case 1u:
        v10 = sub_140001A20(a1);
        if ( v10 )
        {
          if ( (int)sub_140001FC0(a1, v18) > 0 )
          {
            v12 = sub_140002110(v18, v10);
            sub_140002060(a1, 0x111i64, 0x300i64, v12);
            result = 1i64;
          }
          else
          {
            result = 0i64;
          }
        }
   //...
   }
sub_140002060 中的 0x111 和 0x300 很像进入判断分支的信号量,其中的 a4 由 v12传入,v12由sub_140002110的返回值决定,因此,我们在x64dbg中定位到 0x140002110 ,修改代码让它直接返回4就能实现爆破了:
按Ctrl + G,粘贴函数地址跳转


052d4fab2e458009e6c6c4c64a2acacf_1c975811c0bb495fbc625da956fec24b.png (13.18 KB, 下载次数: 0)
下载附件
2023-2-6 17:13 上传



019d852bb97dcd0ce93b4ca73031a431_12ee75cad1eb45e98240a811156a754b.png (74.15 KB, 下载次数: 0)
下载附件
2023-2-6 17:14 上传

按下空格照下入修改函数头,直接返回4


0c5d99250b2e15ed906cdc2e0b610a4d_3cc7243c4a8d4b8b9406bc52e373a1c0.png (43.46 KB, 下载次数: 0)
下载附件
2023-2-6 17:14 上传

再次提交flag,提示Success


27675067d2f26af66f366da50c3234ac_90920ac4390f445e9fff842bd1dc7a0e.png (23.78 KB, 下载次数: 0)
下载附件
2023-2-6 17:15 上传


那么接下来事情【似乎】就很简单了,我们分析sub_140002110的逻辑,并找出让它返回4的条件就行了。如果你这么想,恭喜你掉坑里了!我们不妨先进来看看这个函数,调试结果表明第一个参数是flag,第二个是转成整数的uid,为了方便阅读这里对部分变量进行了重命名,在IDA中选择对应的变量按N即可实现:
__int64 __fastcall sub_140002110(const wchar_t *flag, int uid)
{
  unsigned __int64 v2; // kr00_8
  int k; // [rsp+20h] [rbp-3A8h]
  int j; // [rsp+24h] [rbp-3A4h]
  int v6; // [rsp+28h] [rbp-3A0h]
  int v7; // [rsp+28h] [rbp-3A0h]
  int map_uid; // [rsp+2Ch] [rbp-39Ch]
  int v9; // [rsp+30h] [rbp-398h]
  int v10; // [rsp+30h] [rbp-398h]
  unsigned int v11; // [rsp+38h] [rbp-390h]
  int len_flag; // [rsp+3Ch] [rbp-38Ch]
  int v13; // [rsp+40h] [rbp-388h]
  int v14; // [rsp+44h] [rbp-384h]
  unsigned int v15; // [rsp+48h] [rbp-380h]
  int map_uid_arr[6]; // [rsp+58h] [rbp-370h] BYREF
  int v17[104]; // [rsp+70h] [rbp-358h] BYREF
  int Destination[104]; // [rsp+210h] [rbp-1B8h] BYREF
  len_flag = wcslen(flag);
  memcpy_s(Destination, 0x38ui64, qword_1400168F0, 0x38ui64);
  flag[len_flag - 1] ^= 0x7Du;
  v6 = -1;
  for ( map_uid = uid - 1; map_uid >= 0; map_uid = 2 * map_uid + 9 )
    ;
  for ( j = 0; j
大家看看这里有几个变量是跟flag有关的,除了一开始煞有介事地取了flag的长度,还做了字符串截取外,后续的 sub_140001D70(&v17[k], map_uid_arr, (unsigned int)map_uid); 似乎也与flag有关,看上去似乎关键点就是在这里了!注意到最终返回的是v15,而v15要么为0,要么为v11,v11是sub_140001D70决定的,只要确保最后一次调用返回的是4就行了。sub_140001D70的伪码如下,看起来已经【非常像】解密函数了:
__int64 __fastcall sub_140001D70(unsigned int *a1, _DWORD *a2, int a3)
{
  unsigned int v4; // [rsp+0h] [rbp-28h]
  unsigned int v5; // [rsp+4h] [rbp-24h]
  unsigned int v6; // [rsp+8h] [rbp-20h]
  unsigned int i; // [rsp+Ch] [rbp-1Ch]
  v4 = *a1;
  v5 = a1[1];
  v6 = 0;
  for ( i = 0; i > 4)) ^ (v6 + v5) ^ (*a2 + 32 * v5);
    v5 += (a2[3] + (v4 >> 4)) ^ (v6 + v4) ^ (a2[2] + 32 * v4);
  }
  *a1 = v4;
  a1[1] = v5;
  return v6;
}
首先还是尝试爆破让这个函数直接返回4,不出意料程序提示Success。
但是仔细看下,这里的返回值完全是由a3(map_uid)决定的,与flag无关!我这边一开始还以为是伪码出了问题,后面经调试和阅读反汇编,发现伪码是正确的,返回值的确与flag无关,排这个坑用了一天多的时间,这里也大概写下过程吧。
首先,怎样让它返回4呢?让v6在0x20(32)次循环累加后值等于4即可!但在整数运算中 4/32 = 0,这明显是不可能的,不过考虑到无符号整数的溢出问题也不是完全没戏。但这些运算看着就头疼,因此我写了代码想从中找到可以直接通过验证的uid,程序跑了几分钟也没给我找出来,跑的范围已经远远超过论坛uid的实际范围了。再加上已经有人成功提交,我这才完全明确程序走的不是这个分支。
一天过去。
再次分析
后面的分析就基本不需要用x64dbg了,因为显然用IDA调试伪码更方便。
同样要靠附加运行,不能直接调试。将程序拖入IDA,同时运行,并在IDA中选择本地Windows调试器:


b46f70489ed1dd73f6a8765558220dbe_9dc3b5db2fd445399d18f9da584511b5.png (34.61 KB, 下载次数: 0)
下载附件
2023-2-6 17:15 上传

然后就可以附加了:


14eb30613acfb18c2de17299ee0cdfd8_17e18feae75541afbb3d3b80683b6feb.png (92.66 KB, 下载次数: 0)
下载附件
2023-2-6 17:15 上传

重新定位到 sub_1400011D0 函数,这次我们直接拿伪码下断点:


0c447042d245d2b7d479a973d13db066_7548dee7677a40b299cff0466b5c8816.png (75.41 KB, 下载次数: 0)
下载附件
2023-2-6 17:15 上传

准备好后,按下F9让程序运行。
可以看到 a4 是一个非常奇怪的数,而且前面的分析也表明了它不可能是4,同样也不可能是1, 2, 3中的任何一个,所以程序只能走default分支


72ee6d8aef3065581b08692b5568b56d_1f2c706593ba4817b49ecd6fe241cac1.png (46.27 KB, 下载次数: 0)
下载附件
2023-2-6 17:16 上传

接下来再看看v6:


506f834d901bc476323e40ea08daa5ec_04ffcf82a02e40a2a643cc4d3e529905.png (19.68 KB, 下载次数: 1)
下载附件
2023-2-6 17:16 上传

v6为空?!这和之前假设的不一样!
接着,程序走到了这里,然后又是熟悉的2060, 0x111, 0x300:


4fdf17155f04c2d926b2326e859a7fcf_4b67d4f7b3784a67863a751226999b5a.png (24.84 KB, 下载次数: 0)
下载附件
2023-2-6 17:16 上传

用x64dbg定位到 sub_140002A50,令它直接返回4,同样弹框提示Success!
另外,经多次调试,我发现程序第一次经过v6时,v6必然为空,而sub_140002A50的逻辑同时与flag和uid有关,也就是说sub_140002A50函数才是真正决定你能否通过验证的函数!
现在让我们重新认识一下上两张图的这段代码,注释内容均为调试得出,这里的map表示映射,uid_map就是说这个数值是只和uid有关的,与flag无关:
        if ( v6 )
        {
                        // 程序会经过两次,第一次 v6 一定是空的
          v13 = Block + 75;
          wsprintfW(v20, Block, String1);
          ((void (__fastcall *)(__int64, WCHAR *, LPCWSTR, __int64))qword_140017C90[5])(hwind, v20, v13, 16 L);
          result = 1 L;
        }
        else
        {
                        // 所以第一次会跑这里再检查一遍, uid_map 应该是与uid有关的值映射,可能与flag无关
                        // 当uid = 835429 时, uid_map = 0xCBF65
          uid_map = sub_1400025E0(hwind);
          if ( uid_map )
          {
                          // 这个函数取了flag的长度,flag_content里存放的是原始的flag(虽然不知道是咋放进去的,全局变量?那没事了)
            if ( (int)sub_140002840(hwind, flag_content) > 0 )
            {
                                //前两个检查都能过,也就是说第一次一定会跑到这里来
                                //这两个函数可能是关键
                                // 本函数调用后,v17内容发生变化,所以v17缓存了按一定规则加密的flag
                                // flag的长度必须是8的整数倍(含flag{}符号),否则 v17 = NULL
                                // 因此实际输入字符的长度可以为 2,10,18,...
              sub_140002900(flag_content, v17);
                          // uid_map2 = a4,第一次a4必然走向default分支,因此此处要动态调试获取uid_map2的值,根据分析uid_map2,a4只和uid有关
                          // 当uid = 835429时, uid_map2,a4 = 0x7ed9fee0
              // 令 v7 = 4 可以直接提示Success
                          // 由上述分析,传入这里的三个参数分别为一个与flag相关的,和两个与uid相关的
                          v7 = sub_140002A50(v17, uid_map, uid_map2);
              winproc(hwind, 0x111 L, 0x300 L, v7);
              result = 1 L;
            }
            else
            {
              result = 0 L;
            }
          }
          else
          {
            result = 0 L;
          }
        }
那么现在重点就只剩两个了,首先弄清楚sub_140002900(flag_content, v17);之后,v17的内容变成了什么,其次,在sub_140002A50(v17, uid_map, uid_map2);之后,怎样让v7的值为4。
sub_140002900 的伪码(已调整)如下,可见flag的长度必须是8的整数倍:
__int64 __fastcall sub_140002900(const wchar_t *flag_content, __int64 flag_enc)
{
  wchar_t v3; // [rsp+20h] [rbp-28h]
  int v4; // [rsp+24h] [rbp-24h]
  unsigned int v5; // [rsp+28h] [rbp-20h]
  __int64 v6; // [rsp+2Ch] [rbp-1Ch]
  HIDWORD(v6) = flag_content == 0 L;
  LODWORD(v6) = flag_enc == 0;
  if ( v6 )
    return 0 L;
  // flag的长度不为8的整数倍,直接返回
  if ( wcslen(flag_content) % 8 )
    return 0 L;
  v4 = 0;
  v5 = 0;
  while ( flag_content[v4] )
  {
    v3 = flag_content[v4 + 8];
    flag_content[v4 + 8] = 0;
    // *(_DWORD *)(flag_enc + 4 * (int)v5) = sub_1400024A0(&flag_content[v4]);
        flag_enc[4*v5] = sub_1400024A0(&flag_content[v4]);
        v5++;
    v4 += 8;
    flag_content[v4] ^= v3;
  }
  return v5;
}
sub_1400024A0的伪码如下:
__int64 __fastcall sub_1400024A0(const wchar_t *flag_content_slice)
{
  int i; // [rsp+20h] [rbp-18h]
  unsigned int result; // [rsp+24h] [rbp-14h]
  result = 0;
  if ( wcslen(flag_content_slice) != 8 )
    return 0;
  for ( i = 0; i  'f' ){
      if ( flag_content_slice  'F' ){
        if ('0'
经观察,可以进一步调整为:
__int64 __fastcall sub_1400024A0(const wchar_t *flag_content_slice)
{
  int i; // [rsp+20h] [rbp-18h]
  unsigned int result; // [rsp+24h] [rbp-14h]
  result = 0;
  if ( wcslen(flag_content_slice) != 8 )
    return 0;
  for ( i = 0; i  0xA-0xF
                result += flag_content_slice - 'W';
        }else if('A'  0xA-0xF
                result += flag_content_slice - '7';
        }else if('0'  0x0-0x9
                result += flag_content_slice - '0';
        }
  }
  return result;
}
这样就清晰多了,这两段函数加起来作用就是将作为宽字符的flag每八个压缩进一个整数里,它们之间的区别就【像】这样子,给大家直观感受下:


a6e76ff1b4c5c943f90da83a9aa21176_f4338038243248edb6e71a555a0d7509.png (13.65 KB, 下载次数: 0)
下载附件
2023-2-6 17:17 上传

sub_140002900的作用清楚了,接下来只要再弄清楚sub_140002A50的作用即可,具体调试过程大家可以根据注释自行尝试:
__int64 __fastcall sub_140002A50(__int64 flag_enc, int uid_map, unsigned int uid_map2)
{
  int i; // [rsp+20h] [rbp-188h]
  int j; // [rsp+20h] [rbp-188h]
  int l; // [rsp+20h] [rbp-188h]
  int m; // [rsp+20h] [rbp-188h]
  int uid_map3; // [rsp+24h] [rbp-184h]
  int v9; // [rsp+28h] [rbp-180h]
  int v10; // [rsp+28h] [rbp-180h]
  int v_MAX_UINT; // [rsp+2Ch] [rbp-17Ch]
  int v12; // [rsp+30h] [rbp-178h]
  unsigned int v13; // [rsp+34h] [rbp-174h]
  int v14; // [rsp+38h] [rbp-170h]
  int v15; // [rsp+3Ch] [rbp-16Ch]
  int v16; // [rsp+40h] [rbp-168h]
  char str1; // [rsp+44h] [rbp-164h]
  char str2; // [rsp+5Fh] [rbp-149h]
  char v19; // [rsp+60h] [rbp-148h]
  int v20[52]; // [rsp+B0h] [rbp-F8h]
  int v21[4]; // [rsp+180h] [rbp-28h] BYREF
  if ( !flag_enc )
    return 0 L;
  v_MAX_UINT = 0x11111111;
  for ( i = 0; i = 0; uid_map3 = 2 * uid_map3 + 9 )
    ;
  //uid = 835429 时, uid_map3 = 0xCBF6CFF7
  // uid不变时,v21的内容也固定
  for ( l = 0; l > 1;
  else
    v13 = 3;
  return v13;
}
其中值得强调的一点是在 v15 = sub_1400026E0( flag_enc + 4*m, v21, (unsigned int)uid_map3, uid_map2); 中 v15总是被赋值为0,与flag无关,这不是巧合,有兴趣的可以自行寻找原因。参考注释的提示,我们需要确保经过处理的 flag_enc 的内容与 v16 的内容完全一致。
sub_1400026E0的伪码如下:
__int64 __fastcall sub_1400026E0(unsigned int *flag_enc_slice, _DWORD *uid_map4, int uid_map3, unsigned int uid_map2)
{
  unsigned int v5; // [rsp+0h] [rbp-28h]
  unsigned int v6; // [rsp+4h] [rbp-24h]
  unsigned int i; // [rsp+8h] [rbp-20h]
  v5 = flag_enc_slice[0];
  v6 = flag_enc_slice[1];
  for ( i = 0; i > 5)) ^ (uid_map2 + v5) ^ (uid_map4[2] + 16 * v5);
    v5 -= (uid_map4[1] + (v6 >> 5)) ^ (uid_map2 + v6) ^ (uid_map4[0] + 16 * v6);
    uid_map2 -= uid_map3;
  }
  flag_enc_slice[0] = v5;
  flag_enc_slice[1] = v6;
  return uid_map2;
}
我们需要写出这个算法的逆运算,然后传入v16,从中推出flag_enc应有的样子,逆运算的实现如下:
uint reverse_sub_1400026E0(unsigned int *flag_enc_slice, const uint32_t *uid_map4, unsigned int uid_map3, unsigned int uid_map2){
    unsigned int slice_0 = flag_enc_slice[0];
    unsigned int slice_1 = flag_enc_slice[1];
    uid_map2 = 0;
    for(int i=0;i > 5)) ^ (uid_map2 + slice_1) ^ (uid_map4[0] + 16 * slice_1);
        //颠倒第一步
        slice_1 += (uid_map4[3] + (slice_0 >> 5)) ^ (uid_map2 + slice_0) ^ (uid_map4[2] + 16 * slice_0);
    }
    flag_enc_slice[0] = slice_0;
    flag_enc_slice[1] = slice_1;
    return uid_map2;
}
在我的uid(835429)下,uid_map4 ={0xCBF6CFF8, 0x97ED9FF0, 0x63E46FE8, 0x2FDB3FE0}, uid_map3 = 0xCBF6CFF7, uid_map2 = 0x7ed9fee0,这些数值与flag无关,可以直接在合适的地方(如sub_1400026E0的入口处)下断点得出,要是谁有兴趣可以进一步分析它们的算法,这比本文的分析要简单多了。
循环调用逆运算函数,
        uint32_t g_uid_map4[4] = {0xCBF6CFF8, 0x97ED9FF0, 0x63E46FE8, 0x2FDB3FE0};
        uint32_t g_uid_map3 = 0xCBF6CFF7; // 1100 1011 1111 0110 1100 1111 1111 0111
        uint32_t g_uid_map2 = 0x7ed9fee0; //       011 1111 0110 1100 1111 1111 0111 0000 0
        uint32_t g_uid_map = 0xCBF65;
        uint32_t g_uid = 835429;
        u_int32_t* flag_target = (u_int32_t *)calloc(8,sizeof(u_int32_t ));
        memcpy(flag_target,"flag{!!!_HAPPY_NEW_YEAR_2023!!!}",32*sizeof(char));
        for(int m=0;m
得到flag_enc的内容应当如下:
        70 7b 7a e9   16 3c 7d 05   92 d5 a6 ba   18 56 37 a7
        02 06 40 1c   0b 8a 0f ec   45 49 3e b3   7b d2 35 04
据此可以直接写出需要提交的flag:
        e97a7b7__57d3c16baa6d592a73756181c4__6_2ec_f8a_bb33e4945_435d27b


1500f42462eadd6ae2abb0363118e622_ab87aa7c21a047cd97ecfa9b5e213005.png (26.03 KB, 下载次数: 0)
下载附件
2023-2-6 17:17 上传

其中 _ 可以用0或任意不表示16进制的ASCII字符代替,但论坛上只认0,所以我们提交
        e97a7b70057d3c16baa6d592a73756181c400602ec0f8a0bb33e49450435d27b


7e8b39e130133b9c979e37637170c8b2_e6ad7b884bd94a62b531f96b86d64c08.png (23.65 KB, 下载次数: 0)
下载附件
2023-2-6 17:17 上传

就可以领取奖励了:


734b2f5e2903eb0d5073a6d27b91c63d_3f10d964f9c245459e99fc1e1372a48a.png (488.14 KB, 下载次数: 0)
下载附件
2023-2-6 17:17 上传

PS:分析时所写的笔记和代码已上传,可以从这里获取。
解题领红包之六 {Android 中级题}
这题的坑非常多,而且不像是crackme。
首先运行APP,并用jadx查看APP的MainActivity代码:
public final class MainActivity extends AppCompatActivity {
    public static final Companion Companion = new Companion(null);
    private TextView automedia;
    private ActivityMainBinding binding;
    private MediaRecorder mediaRecorder;
    private boolean permissionToRecordAccepted;
    private final int REQUEST_RECORD_AUDIO_PERMISSION = ItemTouchHelper.Callback.DEFAULT_DRAG_ANIMATION_DURATION;
    private String[] permissions = {"android.permission.RECORD_AUDIO"};
    public final native String decrypt(String str) throws IOException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException;
    public final native String encrypt(String str) throws IOException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException;
    public final native boolean get_RealKey(String str);
    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_main);
        View findViewById = findViewById(R.id.tv_audio);
        Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(R.id.tv_audio)");
        this.automedia = (TextView) findViewById;
        if (ContextCompat.checkSelfPermission(this, "android.permission.RECORD_AUDIO") != 0) {
            ActivityCompat.requestPermissions(this, this.permissions, this.REQUEST_RECORD_AUDIO_PERMISSION);
        } else {
            startRecording();
        }
    }
    private final void Check_Volume(double d) {
        TextView textView = this.automedia;
        if (textView == null) {
            Intrinsics.throwUninitializedPropertyAccessException("automedia");
            textView = null;
        }
        textView.setText("当前分贝:" + d);
        boolean z = false;
        if (84.0d  0) {
            return 20 * Math.log10(maxAmplitude);
        }
        return 0.0d;
    }
    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, android.app.Activity
    public void onDestroy() {
        super.onDestroy();
        MediaRecorder mediaRecorder = this.mediaRecorder;
        if (mediaRecorder == null) {
            Intrinsics.throwUninitializedPropertyAccessException("mediaRecorder");
            mediaRecorder = null;
        }
        mediaRecorder.release();
    }
    private final void xigou(Context context) {
        AssetFileDescriptor openFd = context.getAssets().openFd("xigou.mp3");
        Intrinsics.checkNotNullExpressionValue(openFd, "context.assets.openFd(fileName)");
        MediaPlayer mediaPlayer = new MediaPlayer();
        mediaPlayer.setDataSource(openFd.getFileDescriptor(), openFd.getStartOffset(), openFd.getLength());
        mediaPlayer.prepare();
        mediaPlayer.start();
    }
    /* compiled from: MainActivity.kt */
    @Metadata(d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002¨\u0006\u0003"}, d2 = {"Lcom/zj/wuaipojie2023_2/MainActivity$Companion;", "", "()V", "app_release"}, k = 1, mv = {1, 7, 1}, xi = 48)
    /* loaded from: classes.dex */
    public static final class Companion {
        public /* synthetic */ Companion(DefaultConstructorMarker defaultConstructorMarker) {
            this();
        }
        private Companion() {
        }
    }
    static {
        System.loadLibrary("52pj");
    }
}
运行过程就不截图了,总之主要逻辑就是先获取录音权限,然后计算你的音量,小于100分贝就播放xigou.mp3,在100~101分贝就将静态资源文件释放到数据目录,并提示你去找flag,大于101分贝则不做处理。
因此Java代码就没必要再理会了,这段代码最大的价值就是告诉我们lib52pj.so里有哪些函数。
运用社会工程学,得知app中没有签名校验,so里除了反调试外没有其他保护。
由于这个so在app中根本没被调用,我们不妨将so拖入IDA分析一下,再决定要做什么。
注意到JNIOnload方法里有个ptrace,这个是让so自己附加自己,从而导致调试器无法附加的代码:


7e7c9e6f6f3c880d9ea7e7f9fa368bf5_eb7d7a4e71ed439da67ce6be8a66a12a.png (74.26 KB, 下载次数: 0)
下载附件
2023-2-6 17:18 上传

按住Ctrl + Alt + K将反汇编中的调用改成NOP即可


d504e254996bdfd3f7eea5bf51406b05_f37ed9db8c354370938425e94ba37ba7.png (29.45 KB, 下载次数: 1)
下载附件
2023-2-6 17:18 上传

下面还有个anti_debug函数,将开头改成RET直接返回即可


fc95dea11106b9c46623d6c69897f518_7e24b8853d7949e0aa6ea0158055efb1.png (53.7 KB, 下载次数: 1)
下载附件
2023-2-6 17:18 上传

(以上修改可能不适用于arm-v7,如有问题请改用 x86_64或arm-v8再试)
接下来再看看这三个在Java中声明了的函数:
get_RealKey:
bool __fastcall get_RealKey(JNIEnv *a1, __int64 a2, void *a3)
{
  int8x16_t *v3; // x19
  int8x16_t v5; // [xsp+0h] [xbp-30h] BYREF
  char v6; // [xsp+10h] [xbp-20h]
  __int64 v7; // [xsp+18h] [xbp-18h]
  v7 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v3 = (int8x16_t *)(*a1)->GetStringUTFChars(a1, a3, 0LL);
  if ( strlen((const char *)v3) != 16 )
    return 0LL;
  v6 = 0;
  // xmmword_3030 = {0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE}
  v5 = vaddq_s8(*v3, (int8x16_t)xmmword_3030);
  return strcmp((const char *)&v5, "thisiskey") != 0;
}
decrypt:
jstring __fastcall Java_com_zj_wuaipojie2023_12_MainActivity_decrypt(JNIEnv *a1, __int64 a2, void *a3)
{
  const char *v5; // x21
  const char *v6; // x22
  v5 = (*a1)->GetStringUTFChars(a1, a3, 0LL);
  v6 = (const char *)AES_ECB_PKCS7_Decrypt((int)v5, "|wfkuqokj4548366");
  (*a1)->ReleaseStringUTFChars(a1, a3, v5);
  return (*a1)->NewStringUTF(a1, v6);
}
encrypt:
jstring __fastcall Java_com_zj_wuaipojie2023_12_MainActivity_encrypt(JNIEnv *a1, __int64 a2, void *a3)
{
  const char *v5; // x21
  const char *v6; // x22
  v5 = (*a1)->GetStringUTFChars(a1, a3, 0LL);
  v6 = (const char *)AES_ECB_PKCS7_Encrypt((int)v5, "|wfkuqokj4548366");
  (*a1)->ReleaseStringUTFChars(a1, a3, v5);
  return (*a1)->NewStringUTF(a1, v6);
}
好了,关键方法都摆到这了。经测试得知写死在加解密方法的密钥是假的,使get_RealKey运算后的字符串等于thisiskey的输入构成的密钥也是假的,猜猜密钥在哪?
答案是将传入加解密方法假密钥放入get_RealKey里参与运算,求和之后的结果是真密钥!
这里的计算结果是写成十六进制密钥形式是77756169706F6A696532303233313134,大家可以自行计算。
解密图片可以用wsl里自带的openssl,不用跑网上找别的工具。不过在解密之前需要用正则表达式将密文加上换行符,否则无法正常解密,具体做法是在表达式中将 (.{64})替换为$1\n,注意将换行符风格设为Unix。


f787204899dff7bb5bc827eec62ffc19_5ce81a5aa3754e2788de6bc7303b690d.png (218.9 KB, 下载次数: 1)
下载附件
2023-2-6 17:18 上传

# 将test.png 解密,保存到decode.png
openssl aes-128-ecb -d -a -in test.png -out decode.png -K 77756169706F6A696532303233313134
decode.png是十六进制文本,用Converter插件转换后发现明显的png文件头:


52820b2b30ee35b855967db7aec769e0_03c88e8506a54a19aeae7b371ca0af4b.png (68.77 KB, 下载次数: 0)
下载附件
2023-2-6 17:19 上传

但文本编辑器不能直接保存二进制数据,因此我们用这个在线工具将它直接转成二进制文件,得到一张多喝岩浆的图片:


d8a0f62f263b4345a56d49ae17fa8140.png (117.66 KB, 下载次数: 0)
下载附件
2023-2-6 17:20 上传

参考下 隐写技巧 | 利用PNG文件格式隐藏Payload - 知乎和从CTF比赛真题中学习压缩包伪加密与图片隐写术,图片里面肯定有鬼。
直接在二进制中搜flag没找到,伪加密或隐写看着也不像,最终用第二篇资料里提到的pngcheck发现了猫腻,这张图片后面有多余的数据,从文件头看是另一张png,在winhex中展示如下(上面那张已经被平台处理过了,所以是看不到的,自己解密app里的吧):


8cbf726d6d4fd281da21af7ddf8fe79f_45c813576ecb424692982afa63d82708.png (41.39 KB, 下载次数: 1)
下载附件
2023-2-6 17:21 上传

在decode.png中搜索文件头,发现只出现了两次,这就好办了,只要把第二个文件头之前的内容全删了,再转成二进制文件就行:


5b6bb2e090003792b7dcb2c538aa40d3_55a3cc853eeb4815814e786b7c3ee8dd.png (35.47 KB, 下载次数: 0)
下载附件
2023-2-6 17:21 上传

这次得到的图片是个二维码,扫码得到flag:
flag{Happy_New_Year_Wuaipojie2023}
解题领红包之七 {Android 高级题}(未解出,仅作记录)
首先查看app的清单文件:


e2969434894d6a38165d7d609c27aa98_4823d83403b3487b847b9cbdf37b9223.png (71.59 KB, 下载次数: 1)
下载附件
2023-2-6 17:21 上传

api 26对应安卓8,由于这个app只提供了armv8的库,不能放模拟器上调试(运行和调试是两回事),而我能root的手机最高安卓版本为7.1.1,所以无法直接安装。
考虑到库文件是armv8架构的,用到的指令集应该都相同,应当可以在旧手机上运行,我重新开发了crackme的ui界面,顺便增加了给uid自动补零的功能,兼容性设置的是安卓5.1


14c32c512bafdb0dd6db171cfc1998d1_1ab6fbfdc397405db9a252812bc2cbe9.png (173.4 KB, 下载次数: 1)
下载附件
2023-2-6 17:21 上传

经测试app确实可以在魅蓝2(64位的安卓5.1手机)上运行了:


e62a6134afed4750bc3fd034d5765217.jpg (47.57 KB, 下载次数: 1)
下载附件
2023-2-6 17:22 上传

自然在我的魅蓝E3也没问题(64位安卓7.1)。这样就可以搭建真机调试环境分析代码了,省下了一笔买新备用机的预算。
搭建真机调试环境
用adb连接手机,进入IDA安装目录下的dbgsrv,输入
adb push android_server64 /data/local/tmp
接下来依次进入shell,切换到root,转到tmp目录,并给server执行权限:
D:\IDA_Pro\dbgsrv>adb shell
MeizuE3:/ $ su
enter main
start command :am start -a android.intent.action.MAIN -n com.android.settings/com.meizu.settings.root.FlymeRootRequestActivity --ei uid 2000 --ei pid 8835 > /dev/null
MeizuE3:/ # cd /data/local/tmp/
MeizuE3:/data/local/tmp # chmod a+x android_server64
MeizuE3:/data/local/tmp # ls -lh
total 1.1M
-rwxr-xr-x 1 shell shell 1.1M 2022-01-18 17:01 android_server64
尝试启动server,成功:
MeizuE3:/data/local/tmp # ./android_server64
IDA Android 64-bit remote debug server(ST) v7.7.27. Hex-Rays (c) 2004-2022
Listening on 0.0.0.0:23946...
再另外开启一个shell进行端口转发:
> adb forward tcp:23946 tcp:23946
23946
同时给app设置按调试模式启动:
> adb shell am set-debug-app -w com.wuaipojie.crackme2023
这样app启动的时候就会提示等待调试器了。
在IDA中选择远程调试器:


a08cd6a2dfdb4f02ea246ec3c6670c1e_122c6b03bbe44757a5f86c741589a365.png (29.5 KB, 下载次数: 0)
下载附件
2023-2-6 17:22 上传

使用默认的网络配置即可。
然后运行应用,此时应该弹窗提示等待调试器,我们用IDA附加app的进程:


020f61aa0c807c69c2b979693295de96_820da1d5dd75491e98a79b881fd73881.png (29.05 KB, 下载次数: 0)
下载附件
2023-2-6 17:22 上传

附加后,app依然处于等待状态,我们先按Shift + F3,打开函数列表,进入JNIOnload给第一条语句下断点,然后按一下F9。之后,app依然处于等待状态。


300b640b98c423f867fdd6e21a4909ad_6c1d792de9204ab0935627ce73684979.png (209.9 KB, 下载次数: 0)
下载附件
2023-2-6 17:23 上传

这时候我们到Android Studio(资料显示DDMS已经被官方弃用,且在Java11上打不开,就别折腾它了)上也附加一下app,就可以进入调试了(如果不需要在一开始就断下,且不需要调试java,可以只用ida附加调试,不启动AS):


9a7382330560271d88582e617c986c71_48b2c614a0294512aac9a5dd8329838b.png (30.37 KB, 下载次数: 1)
下载附件
2023-2-6 17:23 上传

走两步
来张全家福,此时程序断在JNIOnload处,所以界面是白屏:


ace32d3fb91228cd1938a17d418cf95a_62221e0bc54649d5bee5bcf68454bf4e.png (358.24 KB, 下载次数: 0)
下载附件
2023-2-6 17:23 上传

放行后恢复正常,且点击验证按钮程序未闪退,应当不存在反调试:


29dcad374d0885b4c528457cd31e6542_c415aff58df74c32ac8bb44d16f91c62.png (339.91 KB, 下载次数: 0)
下载附件
2023-2-6 17:23 上传

看一眼资源占用,“您的电脑充满活力”:


4d89a67303a8d96be21e1b4ede538575_129c08bfc6444a8ab868c079c9da0ed6.png (48.16 KB, 下载次数: 0)
下载附件
2023-2-6 17:23 上传

checkSn的地址
我们在so文件里没看到checkSn的函数声明,说明此函数是动态注册的,可以考虑hook获取其地址,参考去年的题解,前往github下载frida的server端:


932f90a7d2482e286771c0c568de359d_865425e3e2fc4160a9d145121fd1a298.png (108.32 KB, 下载次数: 1)
下载附件
2023-2-6 17:24 上传

推送并启动服务端:
> adb push frida-server-16.0.8-android-arm64 /data/local/tmp
frida-server-16.0.8-android-arm64: 1 file pushed, 0 skipped. 6.0 MB/s (52210432 bytes in 8.340s)
> adb shell
MeizuE3:/ $ su
enter main
start command :am start -a android.intent.action.MAIN -n com.android.settings/com.meizu.settings.root.FlymeRootRequestActivity --ei uid 2000 --ei pid 11482 > /dev/null
MeizuE3:/ # cd /data/local/tmp
MeizuE3:/data/local/tmp # chmod a+x frida-server-16.0.8-android-arm64
MeizuE3:/data/local/tmp # ./frida-server-16.0.8-android-arm64
将这个脚本保存到script.js中,并增加一行用于打印模块基址:
baseAddress: Module.getBaseAddress(DebugSymbol.fromAddress(fnPtr)['moduleName'])


05d643535579323a0006d43a0889483c_d1f16ca7673b43c5aa75ef92a8cb2559.png (34.1 KB, 下载次数: 1)
下载附件
2023-2-6 17:24 上传

输入命令启动app,即可观察到函数地址:
frida -Uf com.wuaipojie.crackme2023  -l script.js
脚本输出如下:
    / _  |   Frida 16.0.8 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to MEIZU E3 (id=192.168.2.18:5555)
Spawned `com.wuaipojie.crackme2023`. Resuming main thread!
[MEIZU E3::com.wuaipojie.crackme2023 ]->  {"module":"lib52pojie.so","package":"com.wuaipojie.crackme2023","class":"MainActivity","method":"checkSn","signature":"(Ljava/lang/String;Ljava/lang/String;)Z","address":"0x7f7a279830","baseAddress":"0x7f7a26b000"}
{"module":"libqti_performance.so","package":"com.qualcomm.qti","class":"Performance","method":"native_perf_lock_acq","signature":"(II[I)I","address":"0x7f9ea1f104","baseAddress":"0x7f9ea1e000"}
{"module":"libqti_performance.so","package":"com.qualcomm.qti","class":"Performance","method":"native_perf_lock_rel","signature":"(I)I","address":"0x7f9ea1f210","baseAddress":"0x7f9ea1e000"}
{"module":"libqti_performance.so","package":"com.qualcomm.qti","class":"Performance","method":"native_perf_io_prefetch_start","signature":"(ILjava/lang/String;)I","address":"0x7f9ea1f274","baseAddress":"0x7f9ea1e000"}
{"module":"libqti_performance.so","package":"com.qualcomm.qti","class":"Performance","method":"native_perf_io_prefetch_stop","signature":"()I","address":"0x7f9ea1f338","baseAddress":"0x7f9ea1e000"}
{"module":"libqti_performance.so","package":"com.qualcomm.qti","class":"Performance","method":"native_deinit","signature":"()V","address":"0x7f9ea1f394","baseAddress":"0x7f9ea1e000"}
其中只有第一项是我们关心的,其他的应该是厂商分析性能用的,计算可知checkSn的偏移为:
0x7f7a279830 - 0x7f7a26b000 = 0xE830
顺便说下,之后重新用IDA调试时可以在输出窗口看到模块基址,基址会变化,要记住它:


665c585fc6e015546af189f184f377b2_4cc88e631c9342ad9945edac64f144e2.png (32.08 KB, 下载次数: 0)
下载附件
2023-2-6 17:24 上传

修复第一个跳转
这就是我们要调试的函数了,现在让我们来欣赏下它的反汇编和伪码:


8370bbbe7219ebc590ad962272cb1bc5_3958e2a55d6e472691c2c5ed6d02835f.png (88.8 KB, 下载次数: 0)
下载附件
2023-2-6 17:25 上传

是不是特别优美,是不是要被感动哭了,这谁™看得懂啊!
不过根据去年的题解,我们知道X8存放的就是跳转地址,且每次都是固定的,所以只要弄清楚最后一行汇编代码的X8地址是多少,然后替换对应的反汇编就行了,例如,假定我们执行到此后,X8存放的值总是为 0x7F7A27AF1C,模块基址为0x7F7A26B000,则要将BR X8替换为 B 0xFF1C,假如此处是BLR X8则要替换为BL 0xFF1C (只是举例),总之就是把操作码的R去掉,操作数写上偏移量,顺便其他给X8赋值的语句都替换为NOP。
现在我们实战一下,X8在断点处保存的数据是 0x7F991118C4,模块基址是0x7F99103000,因此偏移量为 0x‭E8C4‬,我们修改反汇编如下:


709c6e80947f46f0112841a032f318f3_641498c68efe465c8a8166ecd9da0c33.png (17.91 KB, 下载次数: 0)
下载附件
2023-2-6 17:25 上传

现在进入函数E8C4看看,发现伪码如下:


3a73823e546a6ef279e9bed55fc3b266_7f13fb75a7964a14a602680196e60320.png (14.4 KB, 下载次数: 1)
下载附件
2023-2-6 17:25 上传

之所以这样是因为E8C0上面有句多余的跳转语句,而IDA认为函数从E8C0开始:


94d72879cb87db418a3a5d166937dfe2_45e61a0483e64799acbcc9fe2a011e81.png (59.04 KB, 下载次数: 1)
下载附件
2023-2-6 17:25 上传

将其改为NOP,恢复正常:


44dd4e86c5074cc8cefde8fd81215f3d_7e4c655ebe3a419c89d0b947ade7f814.png (72.52 KB, 下载次数: 0)
下载附件
2023-2-6 17:25 上传

跳转偏移表
考虑到后续有很多这种计算,可以考虑借助Excel完成,这样就不用反复开计算器了,方便不少,计算公式为
=DEC2HEX(SUM(HEX2DEC(A2),-HEX2DEC(B2)))


e6a288623330801e02efcb92becab32b_e603b175154242b28dce349f5b8fe5ea.png (20.5 KB, 下载次数: 0)
下载附件
2023-2-6 17:25 上传

在实际操作中,我让表格隔行着色,深色块代表跳转指令的地址,浅色快代表偏移的地址。
在逐一搜索的排查X8数值过程中,发现这里出现了输入的flag,其中BLR X8指令的偏移量为 0xEC30,X8指向取字符串的函数:


8254314f6dd99dd7574fcc1c43191d38_4369fb1580c540099ea4ecf5a60a5dad.png (50.3 KB, 下载次数: 1)
下载附件
2023-2-6 17:25 上传

在这里发现输入的uid,其中BLR X8指令的偏移量为 0xECD0,X8指向取字符串的函数:


6ab1a32fc1fb4964f886642f20ea4c90_14785b2aa5014f2ba1cae0c75cdefcd2.png (44.51 KB, 下载次数: 0)
下载附件
2023-2-6 17:26 上传

总之,一趟跟踪下来,按照检查所有跳转指令的,但不再进入函数跟踪的原则,我找到了很多被混淆的跳转,其中奇数行是跳转指令本身的偏移,偶数行是跳转目标的偏移,这里仅列举部分如下,完整列表可以在gitee下载查看:
[table]
[tr]
[td]当前地址[/td]
[td]基址[/td]
[td]偏移(计算列)[/td]
[/tr]
[tr]
[td]7F991108BC

下载次数, 下载附件

破竹而入   

完整题解已转为pdf,方便备份,下载地址:
https://gitee.com/kbtxwer/happy_ ... %A2%98%E8%A7%A3.pdf
web题我用golang写了一个单文件exe,运行它即可得到与访问原解题页面相近的体验,flagA和flagC的机制也有体现
https://gitee.com/kbtxwer/happy_ ... 7%BA%A7%E9%A2%98%7D
仿佛_一念成佛   

Patch .bss 可以直接用这个脚本。然后应用到文件就可以了。
import ida_bytes
import ida_idaapi
import ida_xref
import struct
def do_patch(ea):
    if ida_bytes.get_bytes(ea + 3, 1) == b"\xB9":
        reg = ord(ida_bytes.get_bytes(ea, 1)) & 0b00011111
        ida_bytes.patch_bytes(ea, struct.pack("[I]
DEATHTOUCH   



170632weo256e22rka722g.png (20.39 KB, 下载次数: 0)
下载附件
2023-2-7 15:33 上传

    大佬,为什么我直接用C++进行位移得到的值对应不了答案。
        int a,b[5];
        b[0] = 0x198;
        a = b[0]>>2;
        cout <<hex<<a << endl;
得到的16进制是66,可是ASCII表要对应的16进制是35才是对的啊。。是哪个位置还要计算吗?
alcove   

牛哇牛哇
tzblue   

太强啦,那个Windows中级题调了我半天,终于还是没搞出算法,相比去年的难度都可以叫高级题了。
Leland   

太强了,好好学习!
genglezp   

学习了!     
cn2jp   

安卓高级题patch bss段的思路不错,我给个方法:
用frida dump出来内存里面的so,bss段已经被填充成0了,再在ida里面把bss段改成只读就行。
这个方法还可以处理data段的不透明谓词。
这个高级题挺有意思,这种动态跳转我也遇到过,虽然依次patch了跳转的地址,但总感觉ida分析出来不对劲,看不出来逻辑,期待题解。
lhtzty   

学习了,感谢大神分享
您需要登录后才可以回帖 登录 | 立即注册