吾爱破解2024红包题部分WriteUP
2024-3-15 17:26:51 Author: mp.weixin.qq.com(查看原文) 阅读量:9 收藏

作者坛账号:cattie

前言

第一年参加红包题目,其中有些操作可能不是那么熟练,感谢各位大佬批评指正,嗯。
(后面的高级题太菜了不会做)

解题领红包之二 {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动态调试

  1. 打开OD,导入文件

  2. 使用中文搜索引擎,找到一个Success和两个Try again,以及一个Tip

    (Caesar Cipher恺撒密码......怪不得IDA看不懂)

  3. 先看看第一个Try again
    上方有个cmp和je,也就是说比较当前字符串长度是否长为36,如果是的话就继续判断,否则就输出 "Error, please try again"

    我们输入36个1,输出变为了"Wrong, please try again",证明上述分析没有问题

  4. 找到"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(原神启动?)

分析:

  1. 主进程调用Webview运行抓猫游戏

  2. 抓住猫以后播放ys.mp4

  3. 视频播放完以后用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文件的操作。下面我来逐个分析这几个函数的作用:

  1. fix(ByteBuffer byteBuffer0, int v, int v1, int v2, Context context0)

    • ByteBuffer byteBuffer0:包含Dex文件内容的缓冲区。

    • int v, int v1, int v2:用于指定要修改的数据的位置和值。

    • Context context0:上下文对象,用于获取应用程序的目录等信息。

    • 这个函数的作用是修复一个ByteBuffer中的Dex文件,其中包括了修改Dex文件中的一些数据。

    • 参数:

    • 主要步骤:

    1. 解析Dex文件中的类数据和方法数据。

    2. 根据传入的参数修改方法数据中的某个值。

    3. 将修改后的数据写回ByteBuffer中。

    4. 计算并更新Dex文件的SHA1校验和和校验码。

    5. 将修改后的ByteBuffer写入到应用程序的私有目录中,并返回该文件。

  2. 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:方法参数的类型数组。

    • 这个函数的作用是获取一个静态方法。

    • 参数:

    • 主要步骤:

    1. 调用fix()函数修复Dex文件,并获取修复后的文件。

    2. 使用修复后的Dex文件创建一个DexClassLoader。

    3. 加载指定类名的类,并获取其中声明的静态方法。

    4. 删除修复后的文件和其在私有目录中的引用。

    5. 返回获取到的静态方法。

  3. isValidate(Context context0, String s, int[] arr_v)

    • Context context0:上下文对象。

    • String s:要验证的字符串。

    • int[] arr_v:一个整型数组,用于指定修复Dex文件时所需的参数。

    • 这个函数的作用是验证某个字符串。

    • 参数:

    • 返回值:返回验证结果的字符串。

    • 主要步骤:

    1. 调用getStaticMethod()函数获取指定类的静态方法。

    2. 调用获取到的静态方法来进行验证,并返回结果。

  4. read(Context context0)

    • Context context0:上下文对象。

    • 这个函数的作用是读取Dex文件。

    • 参数:

    • 返回值:返回读取到的Dex文件的ByteBuffer。

    • 主要步骤:

    1. 构造Dex文件的路径。

    2. 读取Dex文件内容到一个字节数组中。

    3. 将字节数组包装成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题越来越像猜灯谜了,而且还是眼睛有点酸的那种......
(说的就是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到1081的数字字符串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。
具体而言,代码执行以下步骤:

  1. i32.const 1213159497:将值1213159497(0x483CEEE9)压入堆栈。

  2. i32.const 0:将值0压入堆栈。

  3. local.get \$var0:将函数参数\$var0的值压入堆栈。

  4. i32.const 1103515245:将值1103515245(0x41C64E6D)压入堆栈。

  5. i32.mul:将栈顶两个值相乘。

  6. i32.const 1:将值1压入堆栈。

  7. i32.eq:比较栈顶两个值是否相等,如果相等则将1压入堆栈,否则将0压入堆栈。

  8. 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

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,不会错过新的消息通知
开放注册、精华文章和周边活动等公告


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5Mjc3MDM2Mw==&mid=2651140291&idx=1&sn=4bb26af6072119b8e0757b8aece7ddec&chksm=bd50a0978a272981aa38bdeb24ded75053a2fa778362ee986b6c8dcade3091baaae0e0072437&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh