操作日志:
24.2.27:合并Web题解
24.10.30:图床失效,更换图源
Windows与安卓CM部分
前言
第一年参加红包题目,其中有些操作可能不是那么熟练,感谢各位大佬批评指正,嗯。
(后面的高级题太菜了不会做)
解题领红包之二 {Windows 初级题}
新手题:送分题完成啦来试试新手题吧,点击下方“立即申请”任务,即可获得本题Windows CrackMe题目下载地址,通过分析CrackMe获得本题正确口令的解题方法。
查壳
嗯,很好,没有壳,直接分析。
IDA静态分析
(这是什么鬼啊,撤了撤了)
后来发现ioCj~KCss|bQ6zbhCu$5r57$Iljkwlqj$$$?
这一串东西是凯撒密码加密过的密文,偏移量为3,但字典是自定义的,懒得跟踪了,直接用OD了
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
unsigned int v4; // ebx
void **v5; // edi
int v6; // eax
void **v7; // ecx
void **v8; // edx
unsigned int v9; // esi
bool v10; // cf
int v11; // eax
void *v12; // ecx
void **v13; // eax
void *v14; // ecx
void *Src[5]; // [esp+14h] [ebp-58h] BYREF
unsigned int v17; // [esp+28h] [ebp-44h]
void *Block[5]; // [esp+2Ch] [ebp-40h] BYREF
unsigned int v19; // [esp+40h] [ebp-2Ch]
void *v20[4]; // [esp+44h] [ebp-28h] BYREF
int v21; // [esp+54h] [ebp-18h]
unsigned int v22; // [esp+58h] [ebp-14h]
int v23; // [esp+68h] [ebp-4h]
Src[4] = 0;
v17 = 15;
LOBYTE(Src[0]) = 0;
sub_402560(Src, "ioCj~KCss|bQ6zbhCu$5r57$Iljkwlqj$$$?", 0x24u);
v23 = 0;
SetConsoleTitleA(&ConsoleTitle);
v21 = 0;
v22 = 15;
LOBYTE(v20[0]) = 0;
LOBYTE(v23) = 1;
v3 = sub_4027D0();
sub_402A80(v3);
sub_4031E0(&dword_42E088, v20);
v4 = v22;
v5 = (void **)v20[0];
if ( v21 == 36 )
{
sub_402490(Src);
sub_401FE0();
LOBYTE(v23) = 2;
v7 = Block;
v8 = v20;
if ( v19 >= 0x10 )
v7 = (void **)Block[0];
if ( v4 >= 0x10 )
v8 = v5;
if ( Block[4] == (void *)36 )
{
v9 = 32;
do
{
if ( *v8 != *v7 )
break;
++v8;
++v7;
v10 = v9 < 4;
v9 -= 4;
}
while ( !v10 );
}
v11 = sub_4027D0();
sub_402A80(v11);
if ( v19 >= 0x10 )
{
v12 = Block[0];
if ( v19 + 1 >= 0x1000 )
{
v12 = (void *)*((_DWORD *)Block[0] - 1);
if ( (unsigned int)(Block[0] - v12 - 4) > 0x1F )
_invalid_parameter_noinfo_noreturn();
}
sub_406010(v12);
}
sub_40A6EE("Pause");
}
else
{
v6 = sub_4027D0();
sub_402A80(v6);
sub_40A6EE("Pause");
}
if ( v4 >= 0x10 )
{
v13 = v5;
if ( v4 + 1 >= 0x1000 )
{
v5 = (void **)*(v5 - 1);
if ( (unsigned int)((char *)v13 - (char *)v5 - 4) > 0x1F )
_invalid_parameter_noinfo_noreturn();
}
sub_406010(v5);
}
if ( v17 >= 0x10 )
{
v14 = Src[0];
if ( v17 + 1 >= 0x1000 )
{
v14 = (void *)*((_DWORD *)Src[0] - 1);
if ( (unsigned int)(Src[0] - v14 - 4) > 0x1F )
_invalid_parameter_noinfo_noreturn();
}
sub_406010(v14);
}
return 0;
}
OD动态调试
- 打开OD,导入文件
- 使用中文搜索引擎,找到一个Success和两个Try again,以及一个Tip
(Caesar Cipher恺撒密码......怪不得IDA看不懂)
- 先看看第一个Try again
上方有个cmp和je,也就是说比较当前字符串长度是否长为36,如果是的话就继续判断,否则就输出 "Error, please try again"
我们输入36个1,输出变为了"Wrong, please try again",证明上述分析没有问题
- 找到"Wrong, please try again"所在位置,找到判断条件,下断点,再次输入36个1,在MMX中即可找到CM的flag
Flag:fl@g{H@ppy_N3w_e@r!2o24!Fighting!!!}
解题领红包之三 {Android 初级题}
题目简介:小明和李华是同学,最近小明发现李华技术进步很快,他太想进步了,于是他一直在观察李华,却发现他老是在玩圈小猫,直到一次偶然发现,小明惊呼:“WC,原。。。”
游戏
大家一定玩过论坛的抓小猫吧(404界面),没玩过的也没事,现在打开链接也能玩【此处感谢Ganlv佬提供的游戏】
作为常在水区抓猫的“抓猫高手”,是时候展现真正的“寄”术了
(emmm,确实有些汗流浃背)
Hacker(破解游戏,调低难度)
用7-zip打开apk文件,发现里面有抓猫猫的主程序catch-the-cat.js
,
这和论坛的抓猫游戏是一样诶,那就好办了
我们直接参照Ganlv佬的代码
修改一下抓猫猫的js主程序catch-the-cat.js
,定位到变量initialWallCount
把墙的数量改多一点,初始是10个所以抓不住,那么就把墙的数量改一下变成30吧(doge)
然后替换js文件,因为文件被修改过了,所以apk需要重新签名一下
这里采用Lucky Patcher,打个测试签名就行了,以下安卓部分修改后都用此方式打签名,不再重复赘述
“这下是谁该汗流浃背了呢......”
抓住猫,熟悉的bgm响起,看描述就猜到的标准结局......
“Genshin,Start!”
Flag:flag{happy_new_year_2024}
Disassemble(反编译dex看函数)
说完了上面的破解游戏本体,接下来就是直接对安装包本体下手了,上apktool
解包发现了一个以作者名字命名的文件夹,
里面有MainActivity和YSQDActivity(原神启动?)
分析:
-
主进程调用Webview运行抓猫游戏
-
抓住猫以后播放ys.mp4
-
视频播放完以后用SetText显示Flag
(最后的flag是通过字符操作得到的,没有直接给出,而在该文件中也没有提及具体操作步骤,猜想flag可能藏在那个播放的视频ys.mp4
的文件末尾,因时间原因就不跟下去了)
解题领红包之四 {Android 初级题}
寄语:如果不会解题还想拿分那赶紧来现学现卖吧,只要认真看完并动手练习,肯定能解出来本题,吾爱破解安卓逆向入门教程《安卓逆向这档事》。
游戏
第二个小游戏,居然是抽卡
(吐槽一句:这0.6%概率真有点低啊......)
BUG玩法
这个程序没有采用数据库方式,退出重新进入程序即刷新次数为10次
于是乎,“只要我不停地抽,0.6%也不算什么”(doge)
Flag:flag{52pj_HappyNewYear2024}
Hacker(破解游戏,调低难度)
用英文含义命名Activity是个好习惯,至少对于CM而言是这样
用apktool反编译apk文件,在程序中,我们又发现了作者的信息
各位是否发现增加一抽所需要的时间在不断递增?我们简单修改一下增加一抽所需要时间(修改概率、保底同理,只要修改对应的数值就行了)
将array_1
中的数据全部改成0x1(即增加一抽仅需要1s),最坏情况下也只需要90s
修改、编译一气呵成(注意这时不要签名,因为flag在签名里面),直接核心破解安装即可,发现此时软件已经变成1秒增加1抽了(doge)
解题领红包之五 {Android 中级题}
题目简介:我,玄天帝,,解封!!!
游戏
emmm,九宫格图形解锁
运用强大的搜索引擎,找到了一个类似的开源项目GestureLock
小知识,安卓系统的图形是以数字密码形式存放在文件中的,所以推断该图形代表的也是一个数字字符串
静态分析
使用jde反编译得到一堆smali文件,打开以作者名字命名的文件夹
GestureUnlock本体
既然是游戏,那么我们就研究一下这款游戏的本体吧,找到以下定义:
public GestureUnlock(Context context0, AttributeSet attributeSet0, int v) {
super(context0, attributeSet0, v);
this.cicleRadius = 10;
this.firstInit = false;
this.points = new ArrayList();
this.selectP = new ArrayList();
this.alreadyTouch = false;
this.isUp = false;
this.lockTouch = false;
this.returnFun = 0;
this.defaultKey = "01234";
this.setUpKey = "";
this.errorKey = "";
}
在MainActivity里面也没找到修改密码的Set函数,所以我们试试它的默认密码"01234"
嗯,看上去是对了,然后啥都没发生(我的flag呢!
【注意:此处为GestureUnlock(也就是这个密码锁)的密码,但不是Flag的密码,Flag的密码s在后续会说到】
后续分析发现输入该密码时会进入函数isSuccessful
,补充一句该函数是GestureUnlock自带密码正确的回调函数,但此题关键不在此处
public void isSuccessful(String s) {
Log.e("zj595", s);
}
而只有输入"错误"的密码才会进入真正的函数isError
public void isError(String s) {
Log.e("zj595", s);
MainActivity.this.checkPassword(s); //checkPassword是个很重要的函数,接下来就会说到
}
解密函数
如上部分所述,在MainActivity中提及了一个重要的函数:checkPassword
,先看它的smali code:
.method public checkPassword(String)Z
.registers 11
00000000 const/4 v0, 0
:try_2
00000002 invoke-virtual MainActivity->getAssets()AssetManager, p0 #读取Assets中的classes.dex
00000008 move-result-object v1
0000000A const-string v2, "classes.dex"
0000000E invoke-virtual AssetManager->open(String)InputStream, v1, v2
00000014 move-result-object v1
00000016 invoke-virtual InputStream->available()I, v1
0000001C move-result v2
0000001E new-array v2, v2, [B
00000022 invoke-virtual InputStream->read([B)I, v1, v2
00000028 new-instance v3, File
0000002C const-string v4, "data"
00000030 invoke-virtual MainActivity->getDir(String, I)File, p0, v4, v0
00000036 move-result-object v4
00000038 const-string v5, "1.dex"
0000003C invoke-direct File-><init>(File, String)V, v3, v4, v5
00000042 new-instance v4, FileOutputStream
00000046 invoke-direct FileOutputStream-><init>(File)V, v4, v3
0000004C invoke-virtual FileOutputStream->write([B)V, v4, v2 #将classes.dex释放至"/data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex"
00000052 invoke-virtual FileOutputStream->close()V, v4
00000058 invoke-virtual InputStream->close()V, v1
0000005E const-string v1, "dex"
00000062 invoke-virtual MainActivity->getDir(String, I)File, p0, v1, v0
00000068 move-result-object v1
0000006A invoke-virtual Object->getClass()Class, p0
00000070 move-result-object v2
00000072 invoke-virtual Class->getClassLoader()ClassLoader, v2
00000078 move-result-object v2
0000007A new-instance v4, DexClassLoader
0000007E invoke-virtual File->getAbsolutePath()String, v3
00000084 move-result-object v3
00000086 invoke-virtual File->getAbsolutePath()String, v1
0000008C move-result-object v1
0000008E const/4 v5, 0
00000090 invoke-direct DexClassLoader-><init>(String, String, String, ClassLoader)V, v4, v3, v1, v5, v2
00000096 const-string v1, "com.zj.wuaipojie2024_2.C"
0000009A invoke-virtual DexClassLoader->loadClass(String)Class, v4, v1 # 加载classes.dex里面的C Activity,和上面的C.smali对应
000000A0 move-result-object v1
000000A2 const-string v2, "isValidate"
000000A6 const/4 v3, 3
000000A8 new-array v4, v3, [Class
000000AC const-class v6, Context
000000B0 aput-object v6, v4, v0
000000B4 const-class v6, String
000000B8 const/4 v7, 1
000000BA aput-object v6, v4, v7
000000BE const-class v6, [I
000000C2 const/4 v8, 2
000000C4 aput-object v6, v4, v8
000000C8 invoke-virtual Class->getDeclaredMethod(String, [Class)Method, v1, v2, v4
000000CE move-result-object v1
000000D0 new-array v2, v3, [Object
000000D4 aput-object p0, v2, v0
000000D8 aput-object p1, v2, v7
000000DC invoke-virtual MainActivity->getResources()Resources, p0 # actual call site: Landroidx/appcompat/app/AppCompatActivity;->getResources()Landroid/content/res/Resources;
000000E2 move-result-object p1
000000E4 sget v3, R$array->A_offset:I
000000E8 invoke-virtual Resources->getIntArray(I)[I, p1, v3 # 传入Gesture构成的数字数组
000000EE move-result-object p1
000000F0 aput-object p1, v2, v8
000000F4 invoke-virtual Method->invoke(Object, [Object)Object, v1, v5, v2
000000FA move-result-object p1
000000FC check-cast p1, String
00000100 if-eqz p1, :12E # 如果比较结果等于0,则跳转12E
:104
00000104 const-string v1, "唉!"
00000108 invoke-virtual String->startsWith(String)Z, p1, v1
0000010E move-result v1
00000110 if-eqz v1, :12E
:114
00000114 iget-object v1, p0, MainActivity->tvText:TextView
00000118 invoke-virtual TextView->setText(CharSequence)V, v1, p1
0000011E iget-object p1, p0, MainActivity->myunlock:GestureUnlock
00000122 const/16 v1, 8
00000126 invoke-virtual GestureUnlock->setVisibility(I)V, p1, v1
.catch Exception {:try_2 .. :tryend_12C} :catch_130
:tryend_12C
0000012C return v7
:12E
0000012E return v0
:catch_130 # used for: Ljava/lang/Exception;
00000130 move-exception p1
00000132 invoke-virtual Exception->printStackTrace()V, p1
00000138 return v0
.end method
反编译为java就是:
public class MainActivity extends AppCompatActivity {
private GestureUnlock myunlock;
private TextView tvText;
static {
System.loadLibrary("52pj");
}
public boolean checkPassword(String s) {
try {
InputStream inputStream0 = this.getAssets().open("classes.dex");
byte[] arr_b = new byte[inputStream0.available()];
inputStream0.read(arr_b);
File file0 = new File(this.getDir("data", 0), "1.dex");
FileOutputStream fileOutputStream0 = new FileOutputStream(file0);
fileOutputStream0.write(arr_b);
fileOutputStream0.close();
inputStream0.close();
File file1 = this.getDir("dex", 0);
ClassLoader classLoader0 = this.getClass().getClassLoader();
String s1 = (String)new DexClassLoader(file0.getAbsolutePath(), file1.getAbsolutePath(), null, classLoader0).loadClass("com.zj.wuaipojie2024_2.C").getDeclaredMethod("isValidate", Context.class, String.class, int[].class).invoke(null, this, s, this.getResources().getIntArray(array.A_offset));
if(s1 != null && (s1.startsWith("唉!"))) {
this.tvText.setText(s1);
this.myunlock.setVisibility(8);
return true;
}
}
catch(Exception exception0) {
exception0.printStackTrace();
return false;
}
return false;
}
@ Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle bundle0) {
super.onCreate(bundle0);
this.setContentView(layout.activity_main);
TextView textView0 = (TextView)this.findViewById(id.tv_text);
this.tvText = textView0;
textView0.setText(" 吾名玄天帝,昔为诸界之尊,因古诅咒,沉睡亿载。今幸苏醒,欲召百万神兵仙将,复掌万界,重铸天序。此举,需汝解封印,贡力之源。若助吾破诅归位,赐汝万界神尊,封一神域为土,永居众神之巅。");
GestureUnlock gestureUnlock0 = (GestureUnlock)this.findViewById(id.myunlock);
this.myunlock = gestureUnlock0;
gestureUnlock0.setIGestureListener(new IGestureListener() {
@ Override // com.example.gesturelock.IGestureListener
public void isError(String s) {
Log.e("zj595", s);
MainActivity.this.checkPassword(s);
}
@ Override // com.example.gesturelock.IGestureListener
public void isSetUp(String s) {
}
@ Override // com.example.gesturelock.IGestureListener
public void isSuccessful(String s) {
Log.e("zj595", s);
}
});
}
}
程序逻辑还是比较明确的,复制一份classes.dex,命名为1.dex,动态加载dex文件中“com.zj.wuaipojie2024_2.C”类中的“isValidate”函数,传入图形密码字符串,读取返回字符串,如果返回字符串以“诶!”开头,则隐藏密码锁,展示该字符串。
接下来分析这个“isValidate”函数,同样转为java方便观看
public static String isValidate(Context context0, String s, int[] arr_v) throws Exception {
try {
return (String)C.getStaticMethod(context0, arr_v, "com.zj.wuaipojie2024_2.A", "d", new Class[]{Context.class, String.class}).invoke(null, context0, s);
}
catch(Exception exception0) {
Log.e("ZJ595", "咦,似乎是坏掉的dex呢!");
exception0.printStackTrace();
return "";
}
}
"isValidate"这个函数调用了同文件下的"getStaticMethod"函数
private static Method getStaticMethod(Context context0, int[] arr_v, String s, String s1, Class[] arr_class) throws Exception {
try {
File file0 = C.fix(C.read(context0), arr_v[0], arr_v[1], arr_v[2], context0);
ClassLoader classLoader0 = context0.getClass().getClassLoader();
File file1 = context0.getDir("fixed", 0);
Method method0 = new DexClassLoader(file0.getAbsolutePath(), file1.getAbsolutePath(), null, classLoader0).loadClass(s).getDeclaredMethod(s1, arr_class);
file0.delete();
new File(file1, file0.getName()).delete();
return method0;
}
catch(Exception exception0) {
exception0.printStackTrace();
return null;
}
}
"getStaticMethod""函数又调用了"fix"函数对dex文件进行修复,生成2.dex
后加载修复后的com.zj.wuaipojie2024_2.A
类中的d
函数至method0,然后删除生成的2.dex
文件后返回(此处为重点)
注:修复前的A类是这样的
public class A {
private static final String SUCCESS_TAG = "唉!";
public static boolean b() {
return false;
}
public static String c(String s) {
return "?" + s + "?";
}
public static String d(Context context0, String s) {
return "?" + s + "?";
}
}
"fix"函数以及与之对应的"read"函数是这样的
private static File fix(ByteBuffer byteBuffer0, int v, int v1, int v2, Context context0) throws Exception {
try {
File file0 = context0.getDir("data", 0);
int v3 = (int)(((Integer)D.getClassDefData(byteBuffer0, v).get("class_data_off")));
HashMap hashMap0 = D.getClassData(byteBuffer0, v3);
((int[][])hashMap0.get("direct_methods"))[v1][2] = v2;
byte[] arr_b = D.encodeClassData(hashMap0);
byteBuffer0.position(v3);
byteBuffer0.put(arr_b);
byteBuffer0.position(0x20);
byte[] arr_b1 = new byte[byteBuffer0.capacity() - 0x20];
byteBuffer0.get(arr_b1);
byte[] arr_b2 = Utils.getSha1(arr_b1);
byteBuffer0.position(12);
byteBuffer0.put(arr_b2);
int v4 = Utils.checksum(byteBuffer0);
byteBuffer0.position(8);
byteBuffer0.putInt(Integer.reverseBytes(v4));
byte[] arr_b3 = byteBuffer0.array();
File file1 = new File(file0, "2.dex");
FileOutputStream fileOutputStream0 = new FileOutputStream(file1);
fileOutputStream0.write(arr_b3);
fileOutputStream0.close();
return file1;
}
catch(Exception exception0) {
exception0.printStackTrace();
return null;
}
}
private static ByteBuffer read(Context context0) {
try {
File file0 = new File(context0.getDir("data", 0), "decode.dex");
if(!file0.exists()) {
return null;
}
FileInputStream fileInputStream0 = new FileInputStream(file0);
byte[] arr_b = new byte[fileInputStream0.available()];
fileInputStream0.read(arr_b);
ByteBuffer byteBuffer0 = ByteBuffer.wrap(arr_b);
fileInputStream0.close();
return byteBuffer0;
}
catch(Exception unused_ex) {
return null;
}
}
看看chatgpt对以上代码的分析:
这段代码是一个Android应用中的一些Java代码,主要涉及到动态加载Dex文件的操作。下面我来逐个分析这几个函数的作用:
-
fix(ByteBuffer byteBuffer0, int v, int v1, int v2, Context context0)
- 这个函数的作用是修复一个ByteBuffer中的Dex文件,其中包括了修改Dex文件中的一些数据。
- 参数:
ByteBuffer byteBuffer0
:包含Dex文件内容的缓冲区。
int v, int v1, int v2
:用于指定要修改的数据的位置和值。
Context context0
:上下文对象,用于获取应用程序的目录等信息。
- 主要步骤:
- 解析Dex文件中的类数据和方法数据。
- 根据传入的参数修改方法数据中的某个值。
- 将修改后的数据写回ByteBuffer中。
- 计算并更新Dex文件的SHA1校验和和校验码。
- 将修改后的ByteBuffer写入到应用程序的私有目录中,并返回该文件。
-
getStaticMethod(Context context0, int[] arr_v, String s, String s1, Class[] arr_class)
- 这个函数的作用是获取一个静态方法。
- 参数:
Context context0
:上下文对象。
int[] arr_v
:一个整型数组,用于指定修复Dex文件时所需的参数。
String s, String s1
:要调用的类名和方法名。
Class[] arr_class
:方法参数的类型数组。
- 主要步骤:
- 调用
fix()
函数修复Dex文件,并获取修复后的文件。
- 使用修复后的Dex文件创建一个DexClassLoader。
- 加载指定类名的类,并获取其中声明的静态方法。
- 删除修复后的文件和其在私有目录中的引用。
- 返回获取到的静态方法。
-
isValidate(Context context0, String s, int[] arr_v)
- 这个函数的作用是验证某个字符串。
- 参数:
Context context0
:上下文对象。
String s
:要验证的字符串。
int[] arr_v
:一个整型数组,用于指定修复Dex文件时所需的参数。
- 返回值:返回验证结果的字符串。
- 主要步骤:
- 调用
getStaticMethod()
函数获取指定类的静态方法。
- 调用获取到的静态方法来进行验证,并返回结果。
-
read(Context context0)
- 这个函数的作用是读取Dex文件。
- 参数:
- 返回值:返回读取到的Dex文件的ByteBuffer。
- 主要步骤:
- 构造Dex文件的路径。
- 读取Dex文件内容到一个字节数组中。
- 将字节数组包装成ByteBuffer,并返回。
这些函数主要用于动态加载并修复Dex文件,然后执行其中的方法来实现某些功能。
思路
目前flag的关键在于两点,一个是图形锁代表的字符串s,一个是修复dex以后两个"?"的内容,
动态分析&dex修复*3
修复1
静态分析结束,下面用010Editor打开classes.dex,图中可以明显看到dex的Adler32校验值和sha1校验值是错误的
也就是说附件里的classes.dex文件是损坏的
dex校验值损坏后,在反射加载类过程中会被系统拒绝,因此必须进行修复操作
读取logcat可见如下错误
E System : Unable to Load dex file: /data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex
E System : java.io.IOException: Failed to open dex files : from /data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex because: Failure to verify dex file ' /data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex': Bad Checksum (c607ea12, expected 22dcea4c)
可见系统确实因校验值异常,拒绝加载了assets下复制的classes.dex
由于dex文件在010Editor中除了头部外并无明显异常情况,因此我们使用DexRepair对dex头部进行修复:java -jar DexRepair.jar /path/to/dex
修复后头部变为全绿:
将修复完的dex命名为classes.dex
,置入assets文件夹,重新打包、签名
打开jeb调试,发现依然无法正常运行
修复2
单步调试,定位到错误发生点,发现是程序在
try {
File file0 = new File(context0.getDir("data", 0), "decode.dex");
if(!file0.exists()) {
return null;
}
}
处返回了null,即这个file0,也就是decode.dex
不存在
然后程序跳转到了
catch(Exception exception0) {
Log.e("ZJ595", "咦,似乎是坏掉的dex呢!");
exception0.printStackTrace();
return "";
}
打开adb logcat
工具可见返回的错误信息:
E ZJ595 咦,似乎是坏掉的dex呢!
可见确实是文件不存在的原因导致程序运行异常
不存在我们就手动新建一个,为了方便起见我就直接复制了一份1.dex,将其改名为decode.dex,使用RE文件管理器放置于与1.dex同目录下(即data/user/0/com.zj.wuaipojie2024_2/app_data
),这样这部分代码就能正常跑起来了
根据"静态分析"部分代码分析,由于2.dex
文件在写入、加载后即会进行自我删除,于是需要在删除代码位置下断点以保存该中间文件进行分析,断点位置如下:
.method private static varargs getStaticMethod(Context, [I, String, String, [Class)Method
.registers 11
.annotation system Signature
value = {
"(",
"Landroid/content/Context;",
"[I",
"Ljava/lang/String;",
"Ljava/lang/String;",
"[",
"Ljava/lang/Class<",
"*>;)",
"Ljava/lang/reflect/Method;"
}
.end annotation
.annotation system Throws
value = {
Exception
}
.end annotation
00000000 const/4 v0, 0
:try_2
00000002 invoke-static C->read(Context)ByteBuffer, p0
00000008 move-result-object v1
0000000A const/4 v2, 0
0000000C aget v3, p1, v2
00000010 const/4 v4, 1
00000012 aget v4, p1, v4
00000016 const/4 v5, 2
00000018 aget p1, p1, v5
0000001C invoke-static C->fix(ByteBuffer, I, I, I, Context)File, v1, v3, v4, p1, p0
00000022 move-result-object p1
00000024 invoke-virtual Object->getClass()Class, p0
0000002A move-result-object v1
0000002C invoke-virtual Class->getClassLoader()ClassLoader, v1
00000032 move-result-object v1
00000034 const-string v3, "fixed"
00000038 invoke-virtual Context->getDir(String, I)File, p0, v3, v2
0000003E move-result-object p0
00000040 new-instance v2, DexClassLoader
00000044 invoke-virtual File->getAbsolutePath()String, p1
0000004A move-result-object v3
0000004C invoke-virtual File->getAbsolutePath()String, p0
00000052 move-result-object v4
00000054 invoke-direct DexClassLoader-><init>(String, String, String, ClassLoader)V, v2, v3, v4, v0, v1
0000005A invoke-virtual DexClassLoader->loadClass(String)Class, v2, p2
00000060 move-result-object p2
00000062 invoke-virtual Class->getDeclaredMethod(String, [Class)Method, p2, p3, p4
00000068 move-result-object p2
0000006A invoke-virtual File->delete()Z, p1 #在此处下断点,防止文件删除
00000070 new-instance p3, File
00000074 invoke-virtual File->getName()String, p1
0000007A move-result-object p1
0000007C invoke-direct File-><init>(File, String)V, p3, p0, p1
00000082 invoke-virtual File->delete()Z, p3
.catch Exception {:try_2 .. :tryend_88} :catch_8A
:tryend_88
00000088 return-object p2
:catch_8A # used for: Ljava/lang/Exception;
0000008A move-exception p0
0000008C invoke-virtual Exception->printStackTrace()V, p0
00000092 return-object v0
.end method
然后在data/user/0/com.zj.wuaipojie2024_2/app_data
下即可找到修复后的2.dex
文件
修复后的2.dex
中的A类如下:
package com.zj.wuaipojie2024_2;
import android.content.Context;
public class A {
private static final String SUCCESS_TAG = "唉!";
public static boolean b() {
return false;
}
public static String c(String s) {
return "?" + s + "?";
}
public static String d(Context context0, String s) {
MainActivity.sSS(s);
String s1 = Utils.getSignInfo(context0);
if(s1 != null && (s1.equals("fe4f4cec5de8e8cf2fca60a4e61f67bcd3036117"))) {
StringBuffer stringBuffer0 = new StringBuffer();
for(int v = 0; stringBuffer0.length() < 9 && v < 40; ++v) {
String s2 = "0485312670fb07047ebd2f19b91e1c5f".substring(v, v + 1);
if(!stringBuffer0.toString().contains(s2)) {
stringBuffer0.append(s2);
}
}
return s.equals(stringBuffer0.toString().toUpperCase()) ? "唉!哪有什么亿载沉睡的玄天帝,不过是一位被诅咒束缚的旧日之尊,在灯枯之际挣扎的南柯一梦罢了。有缘人,这份机缘就赠予你了。坐标在B.d" : "";
}
return "";
}
}
此处的A.d函数给出了密码s
的真值以及一个提示
写个小程序获取解锁密码s
:
import java.io.*;
public class GetPass {
public static void main(String[] args) {
StringBuffer stringBuffer0 = new StringBuffer();
for(int v = 0; stringBuffer0.length() < 9 && v < 40; ++v) {
String s2 = "0485312670fb07047ebd2f19b91e1c5f".substring(v, v + 1);
if(!stringBuffer0.toString().contains(s2)) {
stringBuffer0.append(s2);
}
}
System.out.println(stringBuffer0.toString().toUpperCase());
}
}
输出为:
048531267 (九宫格密码的真值,后续会用到)
下面来看看提示:
"唉!哪有什么亿载沉睡的玄天帝,不过是一位被诅咒束缚的旧日之尊,在灯枯之际挣扎的南柯一梦罢了。有缘人,这份机缘就赠予你了。坐标在B.d"
找到B类......
(怎么还是讨厌的"?"+s+"?")
public class B {
public static String d(String s) {
return "?" + s + "?";
}
}
看来这个B.d还是坏的,还得修复一次
修复3
原先程序里的那个fix函数只能修复A.d部分,接下来修复B.d
回到MainActivity,还是定位到这句
String s1 = (String)new DexClassLoader(file0.getAbsolutePath(), file1.getAbsolutePath(), null, classLoader0).loadClass("com.zj.wuaipojie2024_2.C").getDeclaredMethod("isValidate", Context.class, String.class, int[].class).invoke(null, this, s, this.getResources().getIntArray(array.A_offset));
语句中出现了A_offset,说明是修复A类型的,定位到array.A_offset这个变量
public static final class array {
public static int A_offset = 0x7F030000; // array:A_offset
public static int B_offset = 0x7F030001; // array:B_offset
}
(诶,A_offset下面那个B_offset是啥?这不是我们要找的“B类”的偏移嘛,这下不用手算了)
直接jeb跑起来,在A_offeset处下断点,把A_offset的值换成B_offset
000000E4 sget v3, R$array->A_offset:I #此处下断点,修改Locals中v3的值为B_offset,即将其改为7F30001h,如图
000000E8 invoke-virtual Resources->getIntArray(I)[I, p1, v3
运行程序,断点至本文"修复2"部分提及的删除函数,得到B.d修复后的2_new.dex
(此处为了避免与上部分中2.dex混淆,将其命名为2_new.dex,实际由程序生成的文件名仍为2.dex)
打开2_new.dex,反编译得到其中的B类,定位至B.d
public class B {
public static String d(String s) {
return "机缘是{" + Utils.md5(Utils.getSha1("password+你的uid".getBytes())) + "}";
}
}
Flag:"{" + Utils.md5(Utils.getSha1("password+你的uid".getBytes())) + "}"
其中的password上面已经提到了,是"048531267"
到此,这个CM终于完事了
此处特别感谢 正己 佬提供的题目以及指点
附——Utils中的两个算法(由smali代码反编译得到,无法直接运行):
public static byte[] getSha1(byte[] arr_b) {
try {
return MessageDigest.getInstance("SHA").digest(arr_b);
}
catch(Exception unused_ex) {
return null;
}
}
public static String md5(byte[] arr_b) {
int v;
try {
String s = new BigInteger(1, MessageDigest.getInstance("md5").digest(arr_b)).toString(16);
v = 0;
while(true) {
label_2:
if(v >= 0x20 - s.length()) {
return s;
}
s = "0" + s;
break;
}
}
catch(NoSuchAlgorithmException unused_ex) {
throw new RuntimeException("ops!!");
}
++v;
goto label_2;
}
写个小程序计算一下flag(注册机)
import java.io.*;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Scanner;
public class GetFlag {
public static byte[] getSha1(byte[] arr_b) {
try {
return MessageDigest.getInstance("SHA").digest(arr_b);
}
catch(Exception unused_ex) {
return null;
}
}
public static String md5(byte[] arr_b) {
try {
String s = new BigInteger(1, MessageDigest.getInstance("md5").digest(arr_b)).toString(16);
return s;
}
catch(NoSuchAlgorithmException unused_ex) {
throw new RuntimeException("ops!!");
}
}
public static void main(String args[]) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Please Input Your UID:");
String s = "048531267" + scanner.next();
System.out.println("Flag:" + "{" + GetFlag.md5(GetFlag.getSha1(s.getBytes())) + "}" );
}
}
}
总结
正如开头所说,这是我第一次参与红包活动,也是第一次接触安卓类的逆向
因文章篇幅所限,一些细节的内容(比如环境配置、设置apk为可调试等)不能详尽地涉及,如果有对以上部分有疑问和建议也欢迎各位在楼下交流讨论
总体而言这几道题目相对来说难度还行(主要还是我太菜了)
Web部分
前言
好家伙,这下Web题越来越像猜灯谜了,而且还是眼睛有点酸的那种......
(说的就是flag1和3,愿称之为opencv独家赞助伙伴【bushi】)
题目简介
活动地址:https://www.52pojie.cn/thread-1889163-1-1.html
解题线索视频:https://www.bilibili.com/video/BV1ap421R7VS
题目共包含 12 个静态 flag: flag1~flag12,另外还需要寻找到 3 个动态 flag: flagA~flagC,每个难度需提交对应的4个静态flag和1个动态flag
准备工作
本次的视频隐藏的信息有点复杂,有两个flag都需要逐帧分析,因此我们先用ffmpeg工具把视频转换为图帧
(我没有阿B的大会员,下载的视频是30fps的,因此r的参数是30,若下载的是60fps的,只要调整以下命令中r的参数为60就行了)
ffmpeg -i inputfile.mp4 -r 30 ./images/%1d.jpg
得到一文件夹的图片:
接下来就是处理那个切成了四段的二维码,
认真看的小伙伴应该已经发现了,这个二维码的几个帧拼在一起就是一个完整的二维码
使用修图软件框选太麻烦了,有的二维码部分甚至和文字的白色部分连在了一起,
既然我对cv领域比较熟悉,因此我还是用opencv叠加吧
下面是一个用python写的叠加小程序:
import cv2
import os
def add_images_in_folder(folder_path):
# Get a list of all files in the folder
image_files = [f for f in os.listdir(folder_path) if f.endswith('.jpg')]
if len(image_files) == 0:
print("No JPG images found in the folder.")
return None
# Read the first image to initialize the accumulator
result = cv2.imread(os.path.join(folder_path, image_files[0]))
# Loop through the rest of the images and add them to the accumulator
for image_file in image_files[1:]:
image_path = os.path.join(folder_path, image_file)
image = cv2.imread(image_path)
result = cv2.add(result, image)
return result
folder_path = "./images"
result_image = add_images_in_folder(folder_path)
if result_image is not None:
cv2.imwrite("./result.png",result_image)
手动处理去除文字部分,得到完整的二维码如图:
解码得到网址:https://2024challenge.52pojie.cn/
Flag1
视频中出现52pojie四个字的时候,后面点阵散开处点阵缺少了一些点,而这些缺少的点就隐藏了flag1,我们用准备部分提到的“叠加小程序”对此部分帧图片进行叠加,得到可见flag
(这个flag也可以直接看,就是有点费眼【doge】)
拼接结果:
flag1{52pj2024}
Flag2
参考去年的官方题解中flag2部分解释:
因为页面会自动重定向,我本来想将 X-Dynamic-Flag: flagA{Header X-52PoJie-Uid Not Found} 藏在这个重定向之前的页面的,但是我怕藏得太深了,没这么搞。
今年的flag2果然藏在了重定向页面前......
访问上述二维码指向的的网页会产生重定向,打开F12打开控制台后重新访问该链接即可看到X-Flag2:
flag2{xHOpRP}
Flag3
视频开头那段东西就是,一看到就想到了这个视频,
二值杂色视觉暂留效应嘛,“二值杂色”+“视觉暂留”。
前者是指把一个复杂的图像,按照灰度不同去四舍五入为黑与白两种噪点。后者则意味着需要借助你眼睛与脑子的时间差,去串联起前后的噪点位置的变化,让你的脑子中形成轨迹图片。
这玩意常用在验证码上,用来过AI的,所以只能人工看了。
flag3{GRsgk2}
Flag4
打开上述提到的网址,F12,查看网络,会发现有一个文件名叫做flag4_flag10.png
的空白图片作为背景(实际上是透明的,浏览器看不出而已)
body {
margin: 0;
padding: 0;
background: url("flag4_flag10.png") white center center no-repeat;
background-size: contain;
height: 100vh;
overflow: hidden;
}
下载下来使用图片应用打开即可看到flag4;
flag4{YvJZNS}
FlagA
同样是开上述提到的网址,F12,查看网络,输入uid登录,会发现有一个叫做login
的网页写入了cookie信息
而cookie信息中提到了flagA
flagA=guFOgjwXg5haqETMpDMLPyHfY7sP5sf32rW7l3XtVr+9T+LyBKQhmslLNA==; expires=Sat, 17 Feb 2024 06:00:00 GMT; path=/; SameSite=Lax
看起来flagA被加密了,而且不是base64
但与此同时,uid好像也采用了类似的加密方式,而且网址里好像有个script API用来把cookie转换为uid(嗯?)
fetch('/auth/uid').then(res => res.text()).then(res => {
if (res) {
document.querySelector('#uid').textContent = res;
document.querySelector('#logout-form').style.display = '';
document.querySelector('#login-form').style.display = 'none';
}
});
那么就好办了,我们来偷梁换柱一下,把cookie中的flagA设置到uid中,再fetch......
flagA{f96a1e5e}
注:flagA每10分钟刷新
Flag5
还是那个网页,F12,网页里用注释提到了Flag5
我们把style中的属性全部去掉,得到以下一串东西(用图片了,直接发论坛MD渲染会崩):
我们暂且不管. _ / \
那一堆符号(flag9的地方会说),剩下的就是flag5
flag5{P3prqF}
Flag6
还是那个网页,下方有个flag6的按钮,点击进入flag6
网页很干净,就一个按钮,点了就开始炼丹,电脑风扇呼呼响(doge)
还是来看看源码吧:
document.querySelector("button").addEventListener("click", () => {
const t0 = Date.now();
for (let i = 0; i < 1e8; i++) {
if ((i & 0x1ffff) === 0x1ffff) {
const progress = i / 1e8;
const t = Date.now() - t0;
console.log(
`${(progress * 100).toFixed(2)}% ${Math.floor(
t / 1000
)}s ETA:${Math.floor(t / progress / 1000)}s`
);
}
if (MD5(String(i)) === "1c450bbafad15ad87c32831fa1a616fc") {
document.querySelector("#result").textContent = `flag6{${i}}`;
break;
}
}
});
简而言之就是它跑了一个从0到$10^8 - 1$的数字字符串i,当该字符串的md5为
1c450bbafad15ad87c32831fa1a616fc时,输出flag6{${i}}
,否则在console中定期输出计算进度
(好家伙,暴力破解md5,真有你的)
直接md5彩虹表反查,发现是今天的日期,绝了......
flag6{20240217}
Flag7
作者在视频里面留下了一个Github网址,打开发现这个:
"删除不小心提交的flag内容"
提示够明显了,我们直接点击commit寻找历史提交记录,找到了这个
flag7{Djl9NQ}
Flag8 & FlagB
2048小游戏
首先肯定是玩咯,轻轻松松通过玩游戏顺利拿到 flag8
flag8{OaOjIK}
接着拿剩下的金币V了作者50(doge)
竟然真的有人v我50,真的太感动了。作为奖励呢,我就提示你一下吧,关键词是“溢出”。
首先想到的肯定是多买点,然后让它溢出,可惜有可能弄得太猛了,导致溢出后金币数量增加了,作者也想到了这个,购买请求直接被拦截了
猜想是做了检验,即购买后金币数量不能高于现有数量。
手算了几个临界值,罢了,完全没用,
因为题目没有写明白它后端到底用的啥数据类型,因此放弃思考,直接用request组件爆破
# 导入requests模块
import requests
for i in [2 ** j for j in range(2,64)]:
print(i)
# 请求的url地址
url = 'https://2024challenge.52pojie.cn/flagB/buy_item'
# 请求头
headers = {"content-type":"application/x-www-form-urlencoded","cookie":"Hm_lvt_46d556462595ed05e05f009cdafff31a=1707280828,1707352290,1707440981,1708065094; wzws_sessionid=gmY5MmRiY4AxODMuMTkzLjE1My4yMjCBMTNmOWIzoGXQNBg=; guFOgjwXg5haqETMpDMLPyHfY7sP5sf32rW7l3XtVr+9T+LyBKQhmslLNA==; uid=BTtCuUGDQGSkBsn/UatmT1VT4wNkVf1j4O5UsVxg9yguZA==; game2048_user_data=I1xnNzcQVLZgwF2jXweH+0MFEE3RglZSqpAhElrNkr5VWSjGb885YMYIqMyGAZJGqCFvZ1oCV50LnAJbBvQuPLM0deHxcni4v3dvVKohNEaWNui6WbpPusQ2ff13MWv7wkO1jX/cfa0fZQOJK7UtfQvrUlJD+1GqDCYs7TCYLLEtrObxDt74D2Jswg4ViV9/1o5HHtDI"}
# payload 为传入的参数
payload = {"shop_item_id":5,"buy_count":i}
# json形式,参数用json
res = requests.post(url,data=payload,headers=headers)
print(res.text)
运气还不错,跑到2^62 = 4611686018427387904时返回值为{"code":0,"msg":"OK"}
,也就是说买4611686018427387904个flagB时符合要求
(其他的要不返回{"code":1,"msg":"购买商品之后钱怎么还变多了?不知道出什么 bug 了,暂时先拦一下 ^_^"}
,要不返回{"code":1,"msg":"钱不够"}
后来尝试发现此时并未扣除任何金币,猜测此时乘上任何单价都会溢出,溢出后花销值变为0
flagB{2a3ec954} 过期时间: 2024-02-17 12:10:00
Flag9
之前说到的那一串符号,调节窗口大小,即可看到立体的Flag9
flag9{KHTALK}
Flag10
和Flag4是一张图片,图片misc类嘛,首先用binwalk看看是不是有什么不对劲的地方。
好像还挺正常的,没有隐藏压缩包也没有藏图
小插曲,这边diss一波edge浏览器,打开图片链接他会自动给你跳到它这个“边缘图像查看器”,即使你选择其中的“另存为”功能保存图片,下载到的图片也是被处理过的,隐写数据就丢了!!
(“强大”???)
所以,做这题时 千 万 不 要 用edge下载图片!!
发现这个问题以后,直接用curl命令下载图片curl -l https://2024challenge.52pojie.cn/flag4_flag10.png -o ./flag4_flag10.png
接下来就简单了,用stegsolve工具中的Analyse-Stereogram Solver(立体视图)工具,设置偏移量为1或2即可看到隐藏的flag10
上图是curl下载图片(含隐写信息);下图是edge下载图片(无隐写信息)
flag10{6BxMkW}
Flag11
拼图游戏,修改html:root中的css属性--var1
和--var2
,复原图片即可【两个变量的值分别为71和20】
(小技巧,使用鼠标滚轮滚动参数,使图片块往聚合方向运动就没啥问题了)
flag11{HPQfVF}
Flag12
WebAssembly(Wasm)技术,直接看关键部分
(func $get_flag12 (;0;) (export "get_flag12") (param $var0 i32) (result i32)
i32.const 1213159497
i32.const 0
local.get $var0
i32.const 1103515245
i32.mul
i32.const 1
i32.eq
select
)
用chatgpt解释一下代码:
这段WebAssembly(Wasm)代码定义了一个名为get_flag12的函数,该函数接受一个32位整数作为参数$var0,并返回一个32位整数作为结果。这个函数的作用是检查传入的参数是否与特定值相关联,如果是则返回1,否则返回0。
具体而言,代码执行以下步骤:
- i32.const 1213159497:将值1213159497(0x483CEEE9)压入堆栈。
- i32.const 0:将值0压入堆栈。
- local.get \$var0:将函数参数\$var0的值压入堆栈。
- i32.const 1103515245:将值1103515245(0x41C64E6D)压入堆栈。
- i32.mul:将栈顶两个值相乘。
- i32.const 1:将值1压入堆栈。
- i32.eq:比较栈顶两个值是否相等,如果相等则将1压入堆栈,否则将0压入堆栈。
- select:根据栈顶的布尔值选择两个值中的一个放回栈顶。
因此,这段代码的主要目的是将参数$var0与特定的数值进行处理,并根据结果返回1或0。
又是溢出问题,即1103515245*输入值
= 1,那么这个输入值一定很大(超出上限),
这边提到了i32,即32位整型数据,最大值为2^31-1 = 2147483647
写个C语言程序跑一下
#include <bits/stdc++.h>
#include <iostream>
using namespace std;
int main()
{
for(long j = 0; j<= 4294967295; j++){
int i = 1103515245;
if(i*j == 1) {
cout<<j<<endl;
break;
}
}
return 0;
}
嗯?结果跑出来个负数?不管了,填进去。
-289805467
这也能出答案是我没想到的......看来是满足条件就能出......
flag12{HOXI}
FlagC
好家伙,直接内嵌TF.js跑Yolo目标检测模型了(不过看起来好像是yolov5n,没上v8不是很认可【doge】)......
当小游戏直接玩吧,它都直接告诉你那些正确了,调整一下位置即可。
我搭出的阵:
下面来简单看一下这个网页的逻辑:
fetch('/flagC/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
boxes,
scores,
classes,
}),
credentials: "include",
}).then((res) => res.json()).then((res) => {
const {hint, labels, colors} = res;
document.querySelector('#result').textContent = hint; // 错误时显示提示,正确时显示 flag
})
简而言之就是调用TF.js(Tensorflow,Google开发的一款深度学习框架)加载yolo目标检测模型,对上传的图片执行本地目标检测
然后将检测结果(boxes->【目标框】,scores【置信度】,classes【分类标签】)以POST形式传递至后端,接受后端返回的提示,并在图中框出来
emmm,这个玩玩就好了,修改也没啥意思,因为判断逻辑在后端,程序能帮你做的也只能是根据提示不断修改,意义不大(doge)
flagC{ce92f978} 过期时间: 2024-02-18 10:10:00