解题领红包之二 windows 初级题
拿到题目,首先将程序下载到本地,运行后提示 Please input password:
,在winhex中可以搜到这个字符串,说明程序没加壳,但用OD加载时却搜不到,经提示应当是方法问题,因现在还不能公开讨论,暂且不管。
改用IDA加载本程序,并进行调试
注意到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
的内容如下:
取其前29位,打到js控制台里,并用for循环进行右移处理:
然后,用一个变量将数组中的每一位转为字符拼接上,最终取得flag,提交到论坛,领取奖励。
解题领红包之三 {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,先运行看看:
输入自己的uid,flag随便填,然后就报错了
脱壳
DIE查壳结果表明其中有upx壳,且程序是64位的
尝试用upx -d脱壳失败,因此只能用ESP定律手脱了。但在此之前要意识到64位程序是默认开启随机基址的,我们先用Study PE把它关了,点击【固定PE基址】,然后覆盖原文件保存即可。
将关掉随机基址的程序载入x64dbg,程序在入口点断下。注意如果入口点的地址和我的不一样就说明随机基址没关闭:
ESP定律中的pushad作用是将所有寄存器状态入栈,这里虽然没有pushad,但仍然有多个push寄存器的操作,且它们执行完毕后同样只有RSP寄存器的数值发生变化,因此这就是我们要找的地方。我们在RSP寄存器所指的内存地址下硬件断点:
然后按一次F9让程序跑飞,很快程序断在这里:
现在可以删除硬件断点了。注意到这个jmp是个大跳转,我们跳转过去,这就是脱壳之后程序真正的入口了:
现在让程序停在这里不要动,按照下图步骤完成脱壳即可:
点击dump并将文件保存,不要覆盖原文件
获取导入表,并点击Fix Dump,选择刚刚保存的文件
底下提示导入表重建成功:
找到脱壳后的程序,双击发现可以正常运行(如果没有关掉随机基址现在关也来得及),体积变为138KB:
至此脱壳完成
反调试
将脱壳后的程序载入x64dbg,程序在我们熟悉的地方断下:
按下F9,程序直接结束
刚刚已经试过脱壳后的程序是可以双击运行的,说明程序存在反调试。
这个我不知道怎么过掉,有经验的大佬可以说说。
但是,我们可以先把程序跑起来,然后再用x64dbg附加,就能绕开反调试了:
动静态分析
在所有模块中搜索字符串,出现以下内容:
现在将所有的ikun,啊不对,所有的"Please input a valid %s"下断点:
再次提交flag,程序断在这里:
稍微往上翻下,发现这段函数的入口地址为:0x1400011D0(以下简称11D0)
将程序拖到IDA64中看看逻辑,按G可以快速跳转,输入0x1400011D0按回车:
再按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 < 25; ++i )
{
Block[i] = (i + 66) ^ *((_WORD *)qword_1400167F0 + i);
Block[i + 25] = (i + 66) ^ *((_WORD *)&qword_1400167F0[6] + i + 1);
Block[i + 50] = (i + 66) ^ *((_WORD *)&qword_1400167F0[12] + i + 2);
Block[i + 75] = (i + 66) ^ *((_WORD *)&qword_1400167F0[18] + i + 3);
Block[i + 100] = (i + 66) ^ *((_WORD *)&qword_1400167F0[25] + i);
}
((void (__fastcall *)(__int64, _QWORD, _QWORD, _QWORD, _DWORD, _DWORD, int))qword_140017C90[10])(
a1,
0i64,
(unsigned int)(v14[2] / 2 + v16[0]),
(unsigned int)(v14[3] / 2 + v16[1]),
0,
0,
1);
if ( ((unsigned int (__fastcall *)(__int64))qword_140017C90[9])(a3) == 1000 )
{
result = 1i64;
}
else
{
v4 = ((__int64 (__fastcall *)(__int64, __int64))qword_140017C90[7])(a1, 1000i64);
((void (__fastcall *)(__int64))qword_140017C90[8])(v4);
result = 0i64;
}
}
else
{
if ( a2 != 0x111 )
return 0i64;
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;
}
}
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 < 25; ++i )
{
Block[i] = (i + 66) ^ *((_WORD *)qword_1400167F0 + i);
Block[i + 25] = (i + 66) ^ *((_WORD *)&qword_1400167F0[6] + i + 1);
Block[i + 50] = (i + 66) ^ *((_WORD *)&qword_1400167F0[12] + i + 2);
Block[i + 75] = (i + 66) ^ *((_WORD *)&qword_1400167F0[18] + i + 3);
Block[i + 100] = (i + 66) ^ *((_WORD *)&qword_1400167F0[25] + i);
}
看上去像是在解密字符串,事实上我们双击Block可以直接看到它未运行时的内容是一大段零字节:
这说明刚刚搜索的字符串是在程序运行时才计算出来的,因此如果想用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":
此外,程序会根据你的输入情况决定后面需要拼接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,粘贴函数地址跳转
按下空格照下入修改函数头,直接返回4
再次提交flag,提示Success
坑
那么接下来事情【似乎】就很简单了,我们分析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 < 4; ++j )
{
map_uid_arr[j] = j + 1 + (j + 1) * map_uid;
flag[j + 25] = flag[j + 25] + j + 1 - 48;
}
memcpy_s(v17, 0x38ui64, flag + 5, 0x38ui64);
v9 = 3;
v11 = 0;
for ( k = 0; k < 14; k += 2 )
{
v2 = (unsigned __int64)(unsigned int)v17[k] << 16;
v17[k] = v6 ^ (v2 | HIDWORD(v2));
sub_140001D70(&Destination[k], map_uid_arr, (unsigned int)map_uid);
v7 = v6 + 0x11111111;
v17[k + 1] = v7 ^ (HIWORD(v17[k + 1]) | (v17[k + 1] << 16));
v11 = sub_140001D70(&v17[k], map_uid_arr, (unsigned int)map_uid);
v6 = v7 + 0x11111111;
if ( Destination[k] == v17[k] )
v14 = 0;
else
v14 = k + 1;
v10 = v14 + v9;
if ( Destination[k + 1] == v17[k + 1] )
v13 = 0;
else
v13 = k + 1;
v9 = v13 + v10;
}
if ( v9 == 3 )
v15 = 0;
else
v15 = v11;
return v15;
}
大家看看这里有几个变量是跟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 < 0x20; ++i )
{
v6 += a3;
v4 += (a2[1] + (v5 >> 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调试器:
然后就可以附加了:
重新定位到 sub_1400011D0 函数,这次我们直接拿伪码下断点:
准备好后,按下F9让程序运行。
可以看到 a4 是一个非常奇怪的数,而且前面的分析也表明了它不可能是4,同样也不可能是1, 2, 3中的任何一个,所以程序只能走default分支
接下来再看看v6:
v6为空?!这和之前假设的不一样!
接着,程序走到了这里,然后又是熟悉的2060, 0x111, 0x300:
用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 < 8; ++i )
{
result *= 16;
if ( flag_content_slice[i] < 'a' || flag_content_slice[i] > 'f' ){
if ( flag_content_slice[i] < 'A' || flag_content_slice[i] > 'F' ){
if ('0' <= flag_content_slice[i] && flag_content_slice[i] <= '9' ){
result += flag_content_slice[i] - '0';
}
}else {
result += flag_content_slice[i] - '7';
}
}else{
result += flag_content_slice[i] - 'W';
}
}
return result;
}
经观察,可以进一步调整为:
__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 < 8; ++i )
{
result = result << 4;
if ('a' <= flag_content_slice[i] && flag_content_slice[i] <= 'f'){
// 处理后范围在 10~15之间 -> 0xA-0xF
result += flag_content_slice[i] - 'W';
}else if('A' <= flag_content_slice[i] && flag_content_slice[i] <= 'F'){
// 处理后范围在 10~15之间 -> 0xA-0xF
result += flag_content_slice[i] - '7';
}else if('0' <= flag_content_slice[i] && flag_content_slice[i] <= '9'){
// 处理后范围在 0~9之间 -> 0x0-0x9
result += flag_content_slice[i] - '0';
}
}
return result;
}
这样就清晰多了,这两段函数加起来作用就是将作为宽字符的flag每八个压缩进一个整数里,它们之间的区别就【像】这样子,给大家直观感受下:
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 < 14; ++i )
{
v20[i] = v_MAX_UINT ^ *((_DWORD *)qword_1400168F0 + i);
v_MAX_UINT += 0x11111111;
}
// 相当于把 v16的首地址放在了 Block+200 处
LOBYTE(v16) = *((_BYTE *)Block + 200);
BYTE1(v16) = *((_BYTE *)Block + 202);
BYTE2(v16) = *((_BYTE *)Block + 204);
HIBYTE(v16) = *((_BYTE *)Block + 206);
str1 = *((_BYTE *)Block + 208);
for ( j = 1; j < 27; ++j ){
// *((_BYTE *)&v16 + j + 4) = *((_BYTE *)v20 + 2 * j);
// 可以多测试几次看看 v16 是不是固定的
// v16是固定的,值为 flag{!!!_HAPPY_NEW_YEAR_2023!!!}
v16[j+4] = v20[2*j]
}
str2 = *((_BYTE *)Block + 212);
v19 = 0;
for ( uid_map3 = v_MAX_UINT + uid_map; uid_map3 >= 0; uid_map3 = 2 * uid_map3 + 9 )
;
//uid = 835429 时, uid_map3 = 0xCBF6CFF7
// uid不变时,v21的内容也固定
for ( l = 0; l < 4; ++l )
v21[l] = (l + 1) * (uid_map3 + 1);
v9 = 0;
v15 = 0;
// 一轮比较8个字节,共计4轮32字节,因此flag的长度算上头尾应当为32(实际上是被处理后的flag长度为32,原始的flag长度为64)
for ( m = 0; m < 7; m += 2 )
{
// v15 的三个参数第一个由(截断的)flag决定,后面三个由uid决定
// 这个函数对 flag_enc 的切片做了变换,其返回值为 uid_map2 - 0x20*uid_map3,与flag无关
// 经测试,经内部32次循环后,函数的返回值必然为0
// 因此,底下所有的等式都必须成立,从 v16 中可以得知flag_enc 应当具有的形式
// uid = 835429 时, v21 = {0xCBF6CFF8, 0x97ED9FF0, 0x63E46FE8, 0x2FDB3FE0}
v15 = sub_1400026E0( flag_enc + 4*m, v21, (unsigned int)uid_map3, uid_map2);
//if ( *(&v16 + m) == *(_DWORD *)(flag_enc + 4 L * m) )
// 不考虑类型,上句等价于
if ( v16[m] == (_DWORD)flag_enc[4*m] ) // 请注意这里一次性比较了四个字节
v14 = 0;
else
v14 = m + 1;
//if ( *(&v16 + m + 1) == *(_DWORD *)(flag_enc + 4 L * (m + 1)) )
if ( v16[m+1] == (_DWORD)flag_enc[4*m + 1] ) // 请注意这里一次性比较了四个字节
v12 = 0;
else
v12 = m + 1;
// v9可以选择 +0, +(m+1),+2(m+1),其中m为循环计数
v9 += v12 + v14;
}
// 当 v9==v15时, m右移一位,此时m应为8/2 = 4,弹框提示success
//
if ( v9 == v15 )
v13 = m >> 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 < 0x20; ++i )
{
v6 -= (uid_map4[3] + (v5 >> 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 < 0x20;i++){
//颠倒第三步(注意-=换成了+=)
uid_map2 += uid_map3;
//颠倒第二步
slice_0 += (uid_map4[1] + (slice_1 >> 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<7;m+=2){
reverse_sub_1400026E0( &flag_target[m] ,g_uid_map4,g_uid_map3,g_uid_map2);
}
printf("%ls\n",flag_target);
得到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
其中 _ 可以用0或任意不表示16进制的ASCII字符代替,但论坛上只认0,所以我们提交
e97a7b70057d3c16baa6d592a73756181c400602ec0f8a0bb33e49450435d27b
就可以领取奖励了:
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 <= d && d <= 99.0d) {
xigou(this);
return;
}
if (100.0d <= d && d <= 101.0d) {
z = true;
}
if (z) {
Toast.makeText(this, "快去找flag吧", 1).show();
write_img();
}
}
/* JADX DEBUG: Finally have unexpected throw blocks count: 2, expect 1 */
private final void write_img() {
InputStream open = getAssets().open("aes.png");
Intrinsics.checkNotNullExpressionValue(open, "assets.open(\"aes.png\")");
InputStream inputStream = open;
try {
InputStream inputStream2 = inputStream;
FileOutputStream fileOutputStream = new FileOutputStream(new File(getPrivateDirectory(), "aes.png"));
ByteStreamsKt.copyTo$default(inputStream2, fileOutputStream, 0, 2, null);
CloseableKt.closeFinally(fileOutputStream, null);
CloseableKt.closeFinally(inputStream, null);
} finally {
}
}
private final File getPrivateDirectory() {
File file = new File(getExternalFilesDir(null), "images");
if (!file.exists()) {
file.mkdirs();
}
return file;
}
@Override // android.content.ContextWrapper, android.content.Context
public File getExternalFilesDir(String str) {
return getApplicationContext().getExternalFilesDir(str);
}
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, android.app.Activity
public void onRequestPermissionsResult(int i, String[] permissions, int[] grantResults) {
Intrinsics.checkNotNullParameter(permissions, "permissions");
Intrinsics.checkNotNullParameter(grantResults, "grantResults");
super.onRequestPermissionsResult(i, permissions, grantResults);
boolean z = false;
if (i == this.REQUEST_RECORD_AUDIO_PERMISSION && grantResults[0] == 0) {
z = true;
}
this.permissionToRecordAccepted = z;
if (z) {
return;
}
finish();
}
private final void startRecording() {
MediaRecorder mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(1);
mediaRecorder.setOutputFormat(1);
mediaRecorder.setOutputFile("/dev/null");
mediaRecorder.setAudioEncoder(1);
mediaRecorder.prepare();
mediaRecorder.start();
this.mediaRecorder = mediaRecorder;
new Thread(new Runnable() { // from class: com.zj.wuaipojie2023_2.MainActivity$$ExternalSyntheticLambda1
@Override // java.lang.Runnable
public final void run() {
MainActivity.m26startRecording$lambda4(MainActivity.this);
}
}).start();
}
/* JADX INFO: Access modifiers changed from: private */
/* renamed from: startRecording$lambda-4 reason: not valid java name */
public static final void m26startRecording$lambda4(final MainActivity this$0) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
while (true) {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
this$0.runOnUiThread(new Runnable() { // from class: com.zj.wuaipojie2023_2.MainActivity$$ExternalSyntheticLambda0
@Override // java.lang.Runnable
public final void run() {
MainActivity.m27startRecording$lambda4$lambda3(MainActivity.this);
}
});
}
}
/* JADX INFO: Access modifiers changed from: private */
/* renamed from: startRecording$lambda-4$lambda-3 reason: not valid java name */
public static final void m27startRecording$lambda4$lambda3(MainActivity this$0) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.Check_Volume(this$0.getVolume());
}
private final double getVolume() {
MediaRecorder mediaRecorder = this.mediaRecorder;
if (mediaRecorder == null) {
Intrinsics.throwUninitializedPropertyAccessException("mediaRecorder");
mediaRecorder = null;
}
int maxAmplitude = mediaRecorder.getMaxAmplitude();
if (maxAmplitude > 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自己附加自己,从而导致调试器无法附加的代码:
按住Ctrl + Alt + K将反汇编中的调用改成NOP即可
下面还有个anti_debug函数,将开头改成RET直接返回即可
(以上修改可能不适用于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。
# 将test.png 解密,保存到decode.png
openssl aes-128-ecb -d -a -in test.png -out decode.png -K 77756169706F6A696532303233313134
decode.png是十六进制文本,用Converter插件转换后发现明显的png文件头:
但文本编辑器不能直接保存二进制数据,因此我们用这个在线工具将它直接转成二进制文件,得到一张多喝岩浆的图片:
参考下 隐写技巧 | 利用PNG文件格式隐藏Payload - 知乎和从CTF比赛真题中学习压缩包伪加密与图片隐写术,图片里面肯定有鬼。
直接在二进制中搜flag没找到,伪加密或隐写看着也不像,最终用第二篇资料里提到的pngcheck发现了猫腻,这张图片后面有多余的数据,从文件头看是另一张png,在winhex中展示如下(上面那张已经被平台处理过了,所以是看不到的,自己解密app里的吧):
在decode.png中搜索文件头,发现只出现了两次,这就好办了,只要把第二个文件头之前的内容全删了,再转成二进制文件就行:
这次得到的图片是个二维码,扫码得到flag:
flag{Happy_New_Year_Wuaipojie2023}
解题领红包之七 {Android 高级题}(未解出,仅作记录)
首先查看app的清单文件:
api 26对应安卓8,由于这个app只提供了armv8的库,不能放模拟器上调试(运行和调试是两回事),而我能root的手机最高安卓版本为7.1.1,所以无法直接安装。
考虑到库文件是armv8架构的,用到的指令集应该都相同,应当可以在旧手机上运行,我重新开发了crackme的ui界面,顺便增加了给uid自动补零的功能,兼容性设置的是安卓5.1
经测试app确实可以在魅蓝2(64位的安卓5.1手机)上运行了:
自然在我的魅蓝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中选择远程调试器:
使用默认的网络配置即可。
然后运行应用,此时应该弹窗提示等待调试器,我们用IDA附加app的进程:
附加后,app依然处于等待状态,我们先按Shift + F3,打开函数列表,进入JNIOnload给第一条语句下断点,然后按一下F9。之后,app依然处于等待状态。
这时候我们到Android Studio(资料显示DDMS已经被官方弃用,且在Java11上打不开,就别折腾它了)上也附加一下app,就可以进入调试了(如果不需要在一开始就断下,且不需要调试java,可以只用ida附加调试,不启动AS):
走两步
来张全家福,此时程序断在JNIOnload处,所以界面是白屏:
放行后恢复正常,且点击验证按钮程序未闪退,应当不存在反调试:
看一眼资源占用,“您的电脑充满活力”:
checkSn的地址
我们在so文件里没看到checkSn的函数声明,说明此函数是动态注册的,可以考虑hook获取其地址,参考去年的题解,前往github下载frida的server端:
推送并启动服务端:
> 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'])
输入命令启动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调试时可以在输出窗口看到模块基址,基址会变化,要记住它:
修复第一个跳转
这就是我们要调试的函数了,现在让我们来欣赏下它的反汇编和伪码:
是不是特别优美,是不是要被感动哭了,这谁™看得懂啊!
不过根据去年的题解,我们知道X8存放的就是跳转地址,且每次都是固定的,所以只要弄清楚最后一行汇编代码的X8地址是多少,然后替换对应的反汇编就行了,例如,假定我们执行到此后,X8存放的值总是为 0x7F7A27AF1C,模块基址为0x7F7A26B000,则要将BR X8
替换为 B 0xFF1C
,假如此处是BLR X8
则要替换为BL 0xFF1C
(只是举例),总之就是把操作码的R去掉,操作数写上偏移量,顺便其他给X8赋值的语句都替换为NOP。
现在我们实战一下,X8在断点处保存的数据是 0x7F991118C4,模块基址是0x7F99103000,因此偏移量为 0xE8C4,我们修改反汇编如下:
现在进入函数E8C4看看,发现伪码如下:
之所以这样是因为E8C0上面有句多余的跳转语句,而IDA认为函数从E8C0开始:
将其改为NOP,恢复正常:
跳转偏移表
考虑到后续有很多这种计算,可以考虑借助Excel完成,这样就不用反复开计算器了,方便不少,计算公式为
=DEC2HEX(SUM(HEX2DEC(A2),-HEX2DEC(B2)))
在实际操作中,我让表格隔行着色,深色块代表跳转指令的地址,浅色快代表偏移的地址。
在逐一搜索的排查X8数值过程中,发现这里出现了输入的flag,其中BLR X8指令的偏移量为 0xEC30,X8指向取字符串的函数:
在这里发现输入的uid,其中BLR X8指令的偏移量为 0xECD0,X8指向取字符串的函数:
总之,一趟跟踪下来,按照检查所有跳转指令的,但不再进入函数跟踪的原则,我找到了很多被混淆的跳转,其中奇数行是跳转指令本身的偏移,偶数行是跳转目标的偏移,这里仅列举部分如下,完整列表可以在gitee下载查看:
当前地址 |
基址 |
偏移(计算列) |
7F991108BC |
7F99102000 |
E8BC |
7F991108C4 |
7F99102000 |
E8C4 |
7F991108E8 |
7F99102000 |
E8E8 |
7F991115FC |
7F99102000 |
F5FC |
消除不透明谓词
参考此处,所谓不透明谓词,是指在实际运行中一些永远不可能成立或永远成立的条件,例如 x*(x-1) & 1 == 0
,它们的存在纯属多余,就是用来干扰分析的。
下面是checkSn函数的不透明谓词:
选中一个变量双击查看,发现它们都在 .bss 段中
IDA可以自动优化不透明谓词,但前提是它要认为这些数是只读的常量。
按下 Shift+F7,进入段视图,将 .bss段删除,记住它的起止地址:
根据先前的信息重新创建数据段,这次Class选择DATA:
编辑这个段,只给它读权限:
双击段名进来,发现依然是一堆问号:
将当前部分字节patch为0:
将段的末尾也patch为0:
转到 Hex-View,发现段首段尾出现了黄色的0x00:
选中中间的问号,将它们改用NOP填充(只要是定值就行,填什么其实并不重要):
重新回到checkSn,查看伪码,可见不透明谓词已消除:
可惜这种状态无法直接写入so文件,只能借助IDA的数据库保存。
爆破
我在排查地址的时候还发现同一个跳转指令会跳转到不同的目标,这就触及到我的知识盲区了,再加上代码里还有其他类型的垃圾指令,我还没掌握去掉它们的办法,现在就没法继续往下分析了,但至少我已经知道对安卓so的调试虽有门槛,但绝非高不可攀,今年的挑战就先到此为止,来年争取拿下它们。
这里说一个爆破点,在执行到C944处时,将X0寄存器的值改为1再放行,提示成功:
因此,在E924处给W0赋值1,即可无条件提示成功,此处已接近checkSn末尾:
(如果成功复现了他人的题解我会在此用我的理解追加新内容)
解题领红包之八、九、十 {Web 初、中、高级题}
flag1
视频用demo显示了flag1,内容如下
flag1{52pojiehappynewyear}
flag2
扫描视频末尾的二维码,得到flag2
flag2{878a48f2}
flag3
视频第25秒底下会出现这个字符串
i o d j 3 { 0 6 i 9 5 d i g }
696F646A337B30366939356469677D
仅有英文参与凯撒加密,则原文为
f l a g 3 { 0 6 f 9 5 a f d }
666C6167337B30366639356166647D
flag3{06f95afd}
flag4
视频的联合投稿人让我们猜猜 ZmxhZzR7OWNiOTExMTd9 是干啥的,注意到字符串长度为20,可以尝试用base64解码,得到:
flag4{9cb91117}
flagA
访问 https://2023challenge.52pojie.cn/ ,注意到响应头有一段 X-Dynamic-Flag: flagA{Header X-52PoJie-Uid Not Found}。用PostMan在请求头中增加一段 X-52PoJie-Uid: 835429 ,此时响应内容为 flagA{5ced00e9} ExpiredAt: 2023-01-29T15:30:00+08:00
flag5
视频第30秒左右会播放一段摩斯码
我听到的内容为 ..-. .-.. .- --. ..... . .- .. - ,译码为 flag5eait
flag6
视频在拨打电话时,底下出现字幕 flag6{**},对照拨号盘的声音, https://unframework.github.io/dtmf-detect/ 解析到的内容为 56901214(每次都不太一样) 根据停留时间,内容应为590124,即为 flag6{590124}
flag7
网页中存在变量
01 1 001 1 001 1 01 1 0001 1 00001 01 1 001 1 1 001 1 0111 011 1 101100 1 1 0 10 1 011 0 01 0000 1 10000 001 1 01 1 0 011 0 00 10 011 0 010 100 1 1011 000 1 1 0 0 11 01111101
去掉空格按照二进制ASCII处理,得到,flag7{5d06be63}
flag8
未找到,此处仅作记录
访问 这个在线工具 ,将下载的音频提交播放,在约26秒处开始显示flag8:
flag8{c394d7}
flagB
根据提示,在 https://labs.hexingxing.cn/ns/ 查询 2023challenge.52pojie.cn 的DNS信息,观察到如下信息
_52pojie_2023_happy_new_year=
flagB{substr(md5(uid+\"_happy_new_year_\"+floor(timestamp/600)),0,8)}
计算flagB即可,实际操作时,timestamp应该是当前秒数 /600 使有效期不超过10分钟,md5用32位小写生成
例如 2023.1.29 17:17时的字符串应为 835429_happy_new_year_2791639,MD5处理为 68c8b57163b5ebc57992d5770653afa2 ,最终提交 flagB{68c8b571}
flag9
视频片尾中存在一段倒放的flag语音,处理后听译,内容为
flag9{21c5f8}
flag10
未找到,以下为参考其他题解得出:
FLAG_LINE_A_PARTS用正则表达式将FLAG_LINE_A转为字符数组,其中表达式匹配的内容为. ?
也就是任意一个字符+可选的空格。这就使得每个数组元素都有以下几种可能: 0
, 1
, 0
, 1
,其长度分别为 1,1, 2, 2,如果将取得的长度值 -1,则又能构成一个01序列,具体转化过程如下:
FLAG_LINE_A_PARTS.join('')
'|01 1 001 1 001 1 01 1 0001 1 00001 01 1 001 1 1 001 1 0111 011 1 101100 1 1 0 10 1 011 0 01 0000 1 10000 001 1 01 1 0 011 0 00 10 011 0 010 100 1 1011 000 1 1 0 0 11 01111101==========|'
FLAG_LINE_A_PARTS.map(x=>x[0]).join('')
'|01 1 001 1 001 1 01 1 0001 1 00001 01 1 001 1 1 001 1 0111 011 1 101100 1 1 0 10 1 011 0 01 0000 1 10000 001 1 01 1 0 011 0 00 10 011 0 010 100 1 1011 000 1 1 0 0 11 01111101==========|'
FLAG_LINE_A_PARTS.map(x=>x[0]).slice(1,-11).join('')
'01 1 001 1 001 1 01 1 0001 1 00001 01 1 001 1 1 001 1 0111 011 1 101100 1 1 0 10 1 011 0 01 0000 1 10000 001 1 01 1 0 011 0 00 10 011 0 010 100 1 1011 000 1 1 0 0 11 01111101'
FLAG_LINE_A_PARTS.map(x=>x[0]).slice(1,-11).map(x => x.length - 1).join('')
'011001100110110001100001011001110011000100110000011110110011010001100001001101110011010100110010011000100111110100000000'
变换后得到的内容按二进制转为ASCII得到
flag10{4a752b}
flag11
网页中存在变量
++++++++++[>++++++++++>++++++++++>+++++>++++++++++++<<<<-]>++.++++++.>---.<-----.>>-..>+++.<+++++.---.+.---.+++++++.<+++.+.>-.>++.
这是brainf**k
语法,对其进行解读,具体过程如下
++++++++++[ //循环10次
>++++++++++ arr[1]+=10
>++++++++++ arr[2]+=10
>+++++ arr[3]+=5
>++++++++++++ arr[4]+=12
<<<<- arr[0]-=1
] //循环结束
>++. arr[1]+=2, print(arr[1])
++++++. arr[1]+=6, print(arr[1])
>---. arr[2]-=3, print(arr[2])
<-----. arr[1]-=5, print(arr[1])
>>-.. arr[3]-=1, print(arr[3]), print(arr[3])
>+++. arr[4]+=3, print(arr[4])
<+++++. arr[3]+=5, print(arr[3])
---. arr[3]-=3, print(arr[3])
+. arr[3]+=1, print(arr[3])
---. arr[3]-=3, print(arr[3])
+++++++. arr[3]+=7, print(arr[3])
<+++. arr[2]+=3, print(arr[2])
+. arr[2]+=1, print(arr[2])
>-. arr[3]-=1, print(arr[3])
>++. arr[4]+=2, print(arr[4])
// js 代码:
let arr = [0,0,0,0,0,]
for(let i=0;i<10;i++){
arr[1] += 10
arr[2] += 10
arr[3] += 5
arr[4] += 12
arr[0] -=1
}
arr[1]+=2; console.log(String.fromCharCode(arr[1]))
arr[1]+=6, console.log(String.fromCharCode(arr[1]))
arr[2]-=3, console.log(String.fromCharCode(arr[2]))
arr[1]-=5, console.log(String.fromCharCode(arr[1]))
arr[3]-=1, console.log(String.fromCharCode(arr[3])), console.log(String.fromCharCode(arr[3]))
arr[4]+=3, console.log(String.fromCharCode(arr[4]))
arr[3]+=5, console.log(String.fromCharCode(arr[3]))
arr[3]-=3, console.log(String.fromCharCode(arr[3]))
arr[3]+=1, console.log(String.fromCharCode(arr[3]))
arr[3]-=3, console.log(String.fromCharCode(arr[3]))
arr[3]+=7, console.log(String.fromCharCode(arr[3]))
arr[2]+=3, console.log(String.fromCharCode(arr[2]))
arr[2]+=1, console.log(String.fromCharCode(arr[2]))
arr[3]-=1, console.log(String.fromCharCode(arr[3]))
arr[4]+=2, console.log(String.fromCharCode(arr[4]))
计算结果:
flag11{63418de7}
flag12
未找到,仅作记录
定位到视频灰度加深的地方截图:
这里藏了盲水印,将截图命名为 52pj.png
,用如下python代码转为频域图像并保存到本地:
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('52pj.png', 0)
f = np.fft.fft2(img)
s1 = np.log(np.abs(f))
plt.imsave("01.png",s1)
转换后的效果如图:
在右下角可以看清flag12{3ac97e24}
也可以使用这个在线工具
转换效果如图:
flagC
网页中有一处登录选项 https://2023challenge.52pojie.cn/login ,填入自己的uid后提示不是管理员,用https://10015.io/tools/jwt-encoder-decoder 解密jwt后,将output窗口的json的role改为admin后,粘贴回左侧区域编码,将得到的jwt替换原先的再刷新网页,得到flagC,有效期为10分钟。网页输出内容如下:
欢迎,admin。您的 flag 是 flagC{ccd31755},过期时间是 2023-01-29T12:40:00+08:00