Hgame2025复盘

Week1

RE

Compress dot new

查一下脚本语言是nushell,直接让ai识别是什么算法,结果爆出flag了

image-20250211010051633

Turtle

开局用die查壳,魔改upx且魔改较多,用dbg脱壳(esp定律)

image-20250211010157051

里面一看是个rc4,exp:

#include <stdio.h>
void aa(unsigned __int8 *a1, int a2, unsigned __int8 *a3)
{
    unsigned __int8 v3; // [rsp+3h] [rbp-Dh]
    int i;              // [rsp+4h] [rbp-Ch]
    int v5;             // [rsp+8h] [rbp-8h]
    int v6;             // [rsp+Ch] [rbp-4h]

    v6 = 0;
    v5 = 0;
    for (i = 0; i < a2; ++i)
    {
        v6 = (v6 + 1) % 256;
        v5 = (a3[v6] + v5) % 256;
        v3 = a3[v6];
        a3[v6] = a3[v5];
        a3[v5] = v3;
        a1[i] += a3[(unsigned __int8)(a3[v6] + a3[v5])];
    }
}
int main()
{
    unsigned char enc[] =
        {
            0xF8, 0xD5, 0x62, 0xCF, 0x43, 0xBA, 0xC2, 0x23, 0x15, 0x4A,
            0x51, 0x10, 0x27, 0x10, 0xB1, 0xCF, 0xC4, 0x09, 0xFE, 0xE3,
            0x9F, 0x49, 0x87, 0xEA, 0x59, 0xC2, 0x07, 0x3B, 0xA9, 0x11,
            0xC1, 0xBC, 0xFD, 0x4B, 0x57, 0xC4, 0x7E, 0xD0, 0xAA, 0x0A};
    unsigned char box[] =
        {
            0x65, 0xC9, 0xDC, 0x3A, 0xCE, 0x59, 0xC0, 0x24, 0x48, 0xA0,
            0x41, 0x62, 0x8F, 0x20, 0x26, 0xF8, 0x7C, 0xB4, 0xBA, 0x96,
            0xE0, 0x5A, 0x2C, 0x19, 0x9D, 0x22, 0x93, 0xE4, 0x10, 0xE5,
            0xC7, 0xBD, 0x3E, 0x76, 0xBE, 0xC6, 0x01, 0xFC, 0x86, 0x4F,
            0xDD, 0xD9, 0xD4, 0x83, 0xD3, 0x77, 0x63, 0x97, 0xFD, 0x4A,
            0xF7, 0xD5, 0xFA, 0x60, 0xF3, 0x6E, 0x32, 0x9E, 0x5C, 0x73,
            0x61, 0xB5, 0x40, 0xDF, 0xE8, 0xF6, 0x80, 0x28, 0xCA, 0x45,
            0xF0, 0xBC, 0xB8, 0xD7, 0x58, 0xCF, 0x9C, 0x69, 0x25, 0x52,
            0x15, 0xCC, 0x70, 0x07, 0x7E, 0x06, 0x2E, 0x54, 0x1A, 0x35,
            0x3B, 0x6F, 0x3C, 0x31, 0x7F, 0x1D, 0xF4, 0xE3, 0x82, 0xA7,
            0x37, 0xF9, 0x50, 0x6D, 0x13, 0x46, 0x8D, 0x95, 0xAB, 0xB7,
            0xAF, 0x72, 0xA8, 0xBB, 0x94, 0xAE, 0x5B, 0x67, 0xC1, 0xB3,
            0xA4, 0x1C, 0x8C, 0x36, 0x14, 0xC4, 0xA5, 0xB2, 0x8A, 0xB0,
            0x2D, 0x0B, 0x34, 0xCD, 0xA6, 0xFF, 0x21, 0x8B, 0xC8, 0x43,
            0x00, 0x09, 0xF1, 0xD0, 0xB6, 0x23, 0x53, 0x84, 0x57, 0x64,
            0xA2, 0x4B, 0x18, 0x0D, 0x5D, 0x78, 0x05, 0x02, 0x44, 0x92,
            0x29, 0x7D, 0xFE, 0x08, 0x8E, 0xC3, 0x90, 0xE2, 0x1E, 0xE6,
            0x81, 0x49, 0xE7, 0x6B, 0x12, 0x79, 0x0C, 0x33, 0xE1, 0x68,
            0x27, 0xD1, 0x99, 0x03, 0x5F, 0xD2, 0xED, 0x0E, 0xB9, 0xCB,
            0xEC, 0x4E, 0x56, 0x42, 0xDA, 0x87, 0xFB, 0x3D, 0xA1, 0x6A,
            0x3F, 0x89, 0x0F, 0x51, 0x9B, 0x1B, 0x7A, 0x88, 0xEE, 0x30,
            0x16, 0xEF, 0xC5, 0x9F, 0x74, 0x4C, 0xEB, 0x66, 0xB1, 0xDB,
            0x6C, 0xD8, 0x47, 0x4D, 0xA9, 0x7B, 0x71, 0x2F, 0x1F, 0xAA,
            0xD6, 0x2A, 0x2B, 0x91, 0x0A, 0x38, 0x85, 0xBF, 0xA3, 0x9A,
            0x75, 0x55, 0x11, 0x98, 0x17, 0xC2, 0xF5, 0x39, 0xF2, 0xE9,
            0xDE, 0x04, 0x5E, 0xEA, 0xAC, 0xAD};
    aa(enc, 40, box);
    printf("%s\n", enc);
    return 0;
}

得到结果hgame{Y0u'r3_re4l1y_g3t_0Ut_of_th3_upX!}�<�

Delta Erro0000ors

image-20250211010506132

引入了一个windows的函数,查了下是增量补丁相关的函数,根据其他人的介绍可以得知两点:

  1. 有hash校验
  2. 可以在md5比较前获得目标文件

先运行,发现有一次换md5,伪代码没有,改为汇编一看,果然是异常处理.

image-20250211010634821

然后直接强行建立函数看伪代码.

image-20250211010724636

既然给改md5的机会,那么第二个思路就比较好了.

在md5的地方下硬件断点跟踪dump出md5值然后输入

接下来写exp:

#include<stdio.h>
#include<string.h>
unsigned char ida_chars[] =
{
  0x53, 0x65, 0x76, 0x65, 0x6E, 0x20, 0x73, 0x61, 0x79, 0x73, 
  0x20, 0x79, 0x6F, 0x75, 0x27, 0x72, 0x65, 0x20, 0x72, 0x69, 
  0x67, 0x68, 0x74, 0x21, 0x21, 0x21, 0x21,0x00
};

unsigned char enc[] =
    {
        0x3B, 0x02, 0x17, 0x08, 0x0B, 0x5B, 0x4A, 0x52, 0x4D, 0x11,
        0x11, 0x4B, 0x5C, 0x43, 0x0A, 0x13, 0x54, 0x12, 0x46, 0x44,
        0x53, 0x59, 0x41, 0x11, 0x0C, 0x18, 0x17, 0x37, 0x30, 0x48,
        0x15, 0x07, 0x5A, 0x46, 0x15, 0x54, 0x1B, 0x10, 0x43, 0x40,
        0x5F, 0x45, 0x5A,0x00};

int main(){
    for (int i = 0; i < strlen(enc); i++){
        enc[i] ^= ida_chars[i%0x1c];
    }
    printf("%s", enc);
    return 0;
}

得flag:hgame{934b1236-a124-4150-967c-cb4ff5bcc900}

尊嘟假嘟

(没做出来,是复盘)

首先,看看shoiri的wp🤭

总而言之,他是一个叫做fragment的结构,通过一个按钮来回切换,就像windows的死循环一样.

中间的图片是一个函数,有两个,一个叫做zundu,另一个jiadu:

public void onClick(View v) {
                String ZunduJiadu;
                String ZunduJiadu2 = bundle.getString("zunjia");
                if (ZunduJiadu2 == null) {
                    ZunduJiadu = "o.0";
                } else if (ZunduJiadu2.length() < 36) {
                    ZunduJiadu = ZunduJiadu2 + "o.0";
                } else {
                    ZunduJiadu = "The length is too large";
                }
                bundle.putString("zunjia", ZunduJiadu);
                toast to = new toast(jiadu.this.getContext());
                to.setText(ZunduJiadu);
                to.setDuration(0);
                to.show();
            }

都有这个,一个加的是"o.0",另一个是"0.o"

接着是这个:

package com.nobody.zunjia;

import android.content.Context;
import android.widget.Toast;

/* loaded from: classes3.dex */
public class toast extends Toast {
    private Context mycontext;

    static native void check(Context context, String str);

    public toast(Context context) {
        super(context);
        this.mycontext = context;
    }

    @Override // android.widget.Toast
    public void setText(CharSequence s) {
        super.setText(s);
        check(this.mycontext, (String) DexCall.callDexMethod(this.mycontext, this.mycontext.getString(R.string.dex), this.mycontext.getString(R.string.classname), this.mycontext.getString(R.string.func1), s));
    }
}

然后是这个:

package com.nobody.zunjia;

import android.content.Context;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/* loaded from: classes3.dex */
public class DexCall {
    static native File copyDexFromAssets(Context context, String str, File file);

    static {
        System.loadLibrary("zunjia");
        System.loadLibrary("check");
    }

    public static Object callDexMethod(Context context, String dexFileName, String className, String methodName, Object input) {
        File dexDir = new File(context.getCacheDir(), "dex");
        if (dexDir.mkdir() || dexDir.setWritable(true)) {
            File dexFile = copyDexFromAssets(context, dexFileName, dexDir);
            try {
                if (dexFile.exists() && dexFile.setReadOnly()) {
                    ClassLoader classLoader = context.getClassLoader();
                    DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), dexDir.getAbsolutePath(), null, classLoader);
                    Class<?> targetClass = dexClassLoader.loadClass(className);
                    Constructor<?> constructor = targetClass.getConstructor(new Class[0]);
                    constructor.setAccessible(true);
                    Object instance = constructor.newInstance(new Object[0]);
                    Method targetMethod = targetClass.getMethod(methodName, input.getClass());
                    Object result = targetMethod.invoke(instance, input);
                    dexFile.delete();
                    return result;
                }
            } catch (Exception e) {
                if (dexFile.exists()) {
                    dexFile.delete();
                }
                e.printStackTrace();
            }
        }
        return null;
    }
}

可以看到这个是解密了一个dex,我们动调,然后pull出解密的dex.

adb root
adb pull dex的位置 存储的路径

当然这里的解密根据学长官方的wp是这样讲的:

解密逻辑,特征分析是IDEA解密(频繁出现0x10001)

我也不知道是什么.

然后发现解密dex的内容是:

package com.nobody;

public class zundujiadu {
    private static final String CUSTOM_ALPHABET = "3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5";
    private static final int[] DECODE_TABLE;

    static {
        zundujiadu.DECODE_TABLE = new int[0x80];
    }

    public zundujiadu() {
        for(int v1 = 0; v1 < zundujiadu.DECODE_TABLE.length; ++v1) {
            zundujiadu.DECODE_TABLE[v1] = -1;
        }

        for(int v = 0; v < 65; ++v) {
            zundujiadu.DECODE_TABLE["3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v)] = v;
        }
    }

    public String decode(String s) {
        if(s == null) {
            return null;
        }

        String s1 = s.replace("=", "");
        int v = s1.length();
        if(v % 4 != 0) {
            throw new IllegalArgumentException("输入的 Base64 字符串长度不是 4 的倍数");
        }

        int v1 = v * 3 / 4;
        byte[] arr_b = new byte[v1];
        int v2 = 0;
        int v3 = 0;
        while(v2 < v) {
            int v4 = 0;
            int v5 = 0;
            while(v5 < 4) {
                int v6 = s1.charAt(v2);
                if(v6 < 0 || v6 >= zundujiadu.DECODE_TABLE.length || zundujiadu.DECODE_TABLE[v6] == -1) {
                    throw new IllegalArgumentException("输入的 Base64 字符串包含非法字符: " + ((char)v6));
                }

                v4 |= zundujiadu.DECODE_TABLE[v6] << (3 - v5) * 6;
                ++v5;
                ++v2;
            }

            int v7 = v3 + 1;
            arr_b[v3] = (byte)(v4 >> 16 & 0xFF);
            if(v7 < v1) {
                arr_b[v7] = (byte)(v4 >> 8 & 0xFF);
                ++v7;
            }

            if(v7 < v1) {
                v3 = v7 + 1;
                arr_b[v7] = (byte)(v4 & 0xFF);
            }
            else {
                v3 = v7;
            }
        }

        for(int v8 = 0; v8 < v3; ++v8) {
            arr_b[v8] = (byte)(arr_b[v8] ^ v8);
        }

        return new String(arr_b, 0, v3);
    }

    public String encode(String s) {
        int v6;
        int v5;
        int v4;
        if(s == null) {
            return null;
        }

        byte[] arr_b = s.getBytes();
        for(int v = 0; v < arr_b.length; ++v) {
            arr_b[v] = (byte)(arr_b[v] ^ v);
        }

        byte[] arr_b1 = new byte[(arr_b.length + 2) / 3 * 4];
        int v2 = 0;
        for(int v1 = 0; v1 < arr_b.length; v1 = v4) {
            int v3 = arr_b[v1];
            if(v1 + 1 < arr_b.length) {
                v4 = v1 + 2;
                v5 = arr_b[v1 + 1];
            }
            else {
                v4 = v1 + 1;
                v5 = 0;
            }

            if(v4 < arr_b.length) {
                v6 = arr_b[v4];
                ++v4;
            }
            else {
                v6 = 0;
            }

            int v7 = (v3 & 0xFF) << 16 | (v5 & 0xFF) << 8 | v6 & 0xFF;
            arr_b1[v2] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 >> 18 & 0x3F);
            int v8 = v2 + 2;
            arr_b1[v2 + 1] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 >> 12 & 0x3F);
            arr_b1[v8] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 >> 6 & 0x3F);
            v2 = v8 + 2;
            arr_b1[v8 + 1] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 & 0x3F);
        }

        return new String(arr_b1);
    }

    public String encode(byte[] arr_b) {
        int v6;
        int v5;
        int v4;
        if(arr_b == null) {
            return null;
        }

        for(int v = 0; v < arr_b.length; ++v) {
            arr_b[v] = (byte)(arr_b[v] ^ v);
        }

        byte[] arr_b1 = new byte[(arr_b.length + 2) / 3 * 4];
        int v2 = 0;
        for(int v1 = 0; v1 < arr_b.length; v1 = v4) {
            int v3 = arr_b[v1];
            if(v1 + 1 < arr_b.length) {
                v4 = v1 + 2;
                v5 = arr_b[v1 + 1];
            }
            else {
                v4 = v1 + 1;
                v5 = 0;
            }

            if(v4 < arr_b.length) {
                v6 = arr_b[v4];
                ++v4;
            }
            else {
                v6 = 0;
            }

            int v7 = (v3 & 0xFF) << 16 | (v5 & 0xFF) << 8 | v6 & 0xFF;
            arr_b1[v2] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 >> 18 & 0x3F);
            int v8 = v2 + 2;
            arr_b1[v2 + 1] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 >> 12 & 0x3F);
            arr_b1[v8] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 >> 6 & 0x3F);
            v2 = v8 + 2;
            arr_b1[v8 + 1] = (byte)"3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5".charAt(v7 & 0x3F);
        }

        return new String(arr_b1);
    }
}

换表base64.

check函数没有细看,好像是RC4解密.

看了下shoiri的,可以直接爆破,比对前面的hgame{

WEB

Level 24 Pacman

在index.js中搜索找到两个疑似是base64的文字,转化一下,顺序乱了说明是栅栏密码,获得flag.一个是假的,还被嘲讽了.

(打不开靶机了,所以就没有图片了)

misc

(名字我忘了,懒得翻找)

拿到文件是二进制码转化成zip压缩包,提示密码:

image-20250211011210015

看了加密方式AES,不可能破解.

看看文件,结尾有个GNP,把倒置的GNP图片dump出来并导致回来.

image-20250211011350517

with open('output.bin','rb') as f:
   with open('flag','wb') as g:
      g.write(f.read()[::-1])

image-20250211011452021

获得学长的帅照,然后猜了下是高宽也没看crc用一把梭工具试了下,出了

image-20250211011605430

这是密码,输入zip可获得flag:hagme{h4kyu4_w4nt_gir1f3nd_+q_931290928} (难绷)

Computer cleaner

ai是好东西,先把题目要求告诉他,他给了我这个:

grep -rn "eval(" /var/www

获得第一部分

image-20250211012145542

搜了下好像这个地方就是ubuntu服务器用来联网的地方,那就搜搜记录吧,遍历以后发现

121.41.34.25这个IP,原本以为要社工,后面发现直接访问即可(大道至简)

可以看到

Are you looking for me
Congratulations!!!
hav3_cleaned_th3

接着回到log:

image-20250211012510416

这里执行了一个cmd命令去看看找到第三部分(但我是先找到才来看的,随便一翻就看到了)

_c0mput3r!}

flag:hgame{y0u_hav3_cleaned_th3_c0mput3r!}

Week2

RE

signin

int __fastcall main_0(int argc, const char **argv, const char **envp)
{
  char *v3; // rdi
  __int64 i; // rcx
  __int64 v5; // rdx
  __int64 v6; // rcx
  __int64 v7; // r8
  char v9; // [rsp+20h] [rbp+0h] BYREF
  char Str1[6]; // [rsp+30h] [rbp+10h] BYREF
  char Source[82]; // [rsp+36h] [rbp+16h] BYREF
  char Destination[264]; // [rsp+88h] [rbp+68h] BYREF

  v3 = &v9;
  for ( i = 42i64; i; --i )
  {
    *(_DWORD *)v3 = -858993460;
    v3 += 4;
  }
  j___CheckForDebuggerJustMyCode(&unk_7FF67C4370A3, argv, envp);
  memset(Str1, 0i64, 64i64);
  printf("password:");
  scanf("%44s", Str1);
  if ( (unsigned int)undebug(v6, v5, v7) && (unsigned int)crc32() )
  {
    if ( !j_strncmp(Str1, "hgame{", 6ui64)
      && (j_strncpy(Destination, Source, 0x24ui64), (unsigned int)xtea(Destination)) )
    {
      j_puts("right");
    }
    else
    {
      j_puts("wrong");
    }
  }
  else
  {
    j_puts("error\n");
  }
  return 0;
}

首先是反调试:

__int64 __fastcall sub_7FF67C37F730(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 result; // rax
  HANDLE CurrentThread; // rax
  LPCONTEXT lpContext; // [rsp+28h] [rbp+8h]

  j___CheckForDebuggerJustMyCode(&unk_7FF67C4370A3, a2, a3);
  lpContext = (LPCONTEXT)VirtualAlloc(0i64, 0x4D0ui64, 0x1000u, 4u);
  if ( lpContext )
  {
    sub_7FF67C3734FE(lpContext, 1232i64);
    lpContext->ContextFlags = 1048592;
    CurrentThread = GetCurrentThread();
    if ( GetThreadContext(CurrentThread, lpContext) )
    {
      qword_7FF67C42B880[0] = lpContext->Dr0;
      qword_7FF67C42B880[1] = lpContext->Dr1;
      qword_7FF67C42B880[2] = lpContext->Dr2;
      qword_7FF67C42B880[3] = lpContext->Dr3;
      if ( qword_7FF67C42B880[0]
        || qword_7FF67C42B880[1]
        || qword_7FF67C42B880[2]
        || (result = 24i64, qword_7FF67C42B880[3]) )
      {
        j_puts("Debug error.");
        j_exit(0);
      }
    }
    else
    {
      return 0i64;
    }
  }
  else
  {
    j_puts("VirtualAlloc failed.");
    return 0i64;
  }
  return result;
}

搜了一下,dr寄存器是控制硬件断点的地址和状态的,所以打硬件断点会被gank.这里是通过上下文直接获取dr寄存器内容

接下来是crc32校验:

__int64 __fastcall sub_7FF67C378670(__int64 a1, __int64 a2, __int64 a3)
{
  char *v4; // [rsp+48h] [rbp+28h]
  int i; // [rsp+64h] [rbp+44h]

  j___CheckForDebuggerJustMyCode(&unk_7FF67C4370A3, a2, a3);
  v4 = (char *)j_j_j__malloc_base(0x10000ui64);
  memset(v4, 0i64, 0x10000i64);
  memcpy(v4, main, 0x10000i64);
  sub_7FF67C3711D6();
  for ( i = 0; i < 4; ++i )
    dword_7FF67C42B2A0[i] = sub_7FF67C371AB4(&v4[0x4000 * i], 0x4000i64);
  return 1i64;
}

__int64 __fastcall sub_7FF67C378760(__int64 a1, unsigned __int64 a2, __int64 a3)
{
  unsigned int v4; // [rsp+24h] [rbp+4h]
  unsigned __int64 i; // [rsp+48h] [rbp+28h]

  j___CheckForDebuggerJustMyCode(&unk_7FF67C4370A3, a2, a3);
  v4 = -1;
  for ( i = 0i64; i < a2; ++i )
    v4 = dword_7FF67C42D1A0[(unsigned __int8)(*(_BYTE *)(i + a1) ^ v4)] ^ (v4 >> 8);
  return ~v4;
}

是没魔改过的crc校验(findcrypto可查),逻辑是检查代码有无改变.

加密逻辑

__int64 __fastcall sub_7FF67C378820(__int64 a1, __int64 a2, __int64 a3)
{
  int i; // [rsp+24h] [rbp+4h]

  j___CheckForDebuggerJustMyCode(&unk_7FF67C4370A3, a2, a3);
  enc(a1, dword_7FF67C42B2A0, qword_7FF67C42B880);
  for ( i = 0; i < 36; ++i )
  {
    if ( *(unsigned __int8 *)(a1 + i) != (unsigned __int8)a0[i] )
      return 0i64;
  }
  return 1i64;
}

其中a1是hgame{右边的输入,dword_7FF67C42B2A0是上下文获得的dr寄存器内容,肯定要为0,qword_7FF67C42B880为crc32校验结果,肯定是能动源码的.

而软件断点会加入int3中断导致代码变动,故而我们只能打硬件断点并且修改dword_7FF67C42B2A0的内容为0.

加密是tea类函数,估计是xtea,解密脚本如下:

#include <stdio.h>

int main()
{
    unsigned char data[] =
        {
            0x23, 0xEA, 0x50, 0x30, 0x00, 0x4C, 0x51, 0x47, 0xEE, 0x9C,
            0x76, 0x2B, 0xD5, 0xE6, 0x94, 0x17, 0xED, 0x2B, 0xE4, 0xB3,
            0xCB, 0x36, 0xD5, 0x61, 0xC0, 0xC2, 0xA0, 0x7C, 0xFE, 0x67,
            0xD7, 0x5E, 0xAF, 0xE0, 0x79, 0xC5, 0x00};
    unsigned int *enc = (unsigned int *)data;
    unsigned char crc_data[] =
        {0xB5, 0x5F, 0xA2, 0x97, 0xBA, 0x6D, 0x75, 0xE1, 0x4A, 0x46,
         0x43, 0xA1, 0x4F, 0x28, 0x8F, 0x5A};

    unsigned int *CRC = (unsigned int *)crc_data;
    unsigned int key[4] = {0};

    unsigned int j = 0;
    unsigned int last;
    unsigned int cur_enc;
    unsigned int nex;
    for (int i = 1; i <= 11; i++)
    {
        j += key[i % 4];
    }

    for (int i = 1; i <= 11; i++)
    {
        unsigned int jj = (j >> 2) & 3;
        for (int k = 8; k >= 0; k--)
        {
            if (k == 8)
            {
                last = enc[k - 1];
                cur_enc = enc[k];
                enc[k] = cur_enc - (((last ^ CRC[jj ^ k & 3]) + (*enc ^ j)) ^ (((16 * last) ^ (*enc >> 3)) + ((4 * *enc) ^ (last >> 5))));
            }
            else
            {
                if (k == 0)
                    last = enc[8];
                else
                    last = enc[k - 1];
                cur_enc = enc[k];
                nex = enc[k + 1];
                enc[k] = cur_enc - (((last ^ CRC[jj ^ k & 3]) + (nex ^ j)) ^ (((16 * last) ^ (nex >> 3)) + ((4 * nex) ^ (last >> 5))));
            }
        }
        j -= key[i % 4];
    }
    printf("hgame{%s", enc);
}

flag为hgame{3fe4722c-1dbf-43b7-8659-c1c4a0e42e4d}

以下是复盘

Mysterious signals(待解决2点)

原本能搞出期望解了,第一次逆这种,太急躁了,而且week1没ak,道心破碎,导致后面去玩大表哥不想打了...

先看看apk

  public void onClick(View view) {
                ((TextView) MainActivity.this.findViewById(R.id.ret)).setText(new SSSign().a("admin", "hello", ((TextView) MainActivity.this.findViewById(R.id.Port)).getText().toString()));
            }



package com.nobody.andsign;

import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/* loaded from: classes.dex */
public class SSSign {
    public String result = "NULL";

    public native String b(String str);

    public native String c(String str);

    static {
        System.loadLibrary("sssign");
    }

    // a("admin", "hello", ((TextView) MainActivity.this.findViewById(R.id.Port)).getText().toString()));
    public String a(String str, String str2, String str3) {
        OkHttpClient build = new OkHttpClient().newBuilder().build();
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty(c("104406435e045957"), str);
        jsonObject.addProperty(c("035e0f545e045957"), str2);
        RequestBody create = RequestBody.create(MediaType.parse("application/json"), jsonObject.toString());
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        build.newCall(new Request.Builder().url("http://node1.hgame.vidar.club:" + str3 + "/flag").header(c("165e045f"), b(str + str2)).post(create).build()).enqueue(new Callback() { // from class: com.nobody.andsign.SSSign.1
            static final /* synthetic */ boolean $assertionsDisabled = false;

            @Override // okhttp3.Callback
            public void onFailure(Call call, IOException iOException) {
                SSSign.this.result = "onFailure";
                countDownLatch.countDown();
            }

            @Override // okhttp3.Callback
            public void onResponse(Call call, Response response) throws IOException {
                Log.e("OkHttp", "请求成功,码值: " + response.code());
                if (response.isSuccessful()) {
                    SSSign.this.result = response.body().string();
                    countDownLatch.countDown();
                }
            }
        });
        try {
            if (!countDownLatch.await(5L, TimeUnit.MINUTES)) {
                this.result = "请求超时";
            }
        } catch (InterruptedException unused) {
            this.result = "等待过程被中断";
            Thread.currentThread().interrupt();
        }
        String asString = ((JsonObject) new Gson().fromJson(this.result, JsonObject.class)).get(NotificationCompat.CATEGORY_MESSAGE).getAsString();
        this.result = asString;
        return asString;
    }
}

大致就是输入port,然后向那里传递一个json,c是文本解密函数,一个是filename,一个是username.b的那个是hash值.

期望解是这样的:

接收函数大致逻辑是:

 p_main_Auth_0 = (main_Auth_0 *)runtime_newobject((internal_abi_Type *)&RTYPE_main_Auth_0);
  v = (string *)p_main_Auth_0;
  p_main_Auth_0->Username.ptr = 0LL;
  p_main_Auth_0->Filename.ptr = 0LL;

这个p_main_Auth_0是把输入获取,因为我们可以看到Username这些.然后后续有比对的地方也可以看出它是输入.

最后在这里:

if ( name == '\n' && *(_QWORD *)v55.str == '1m1a1g1h' && *((_WORD *)v55.str + 4) == '1e' )

我们会发现,它比较的不是hello,而是h1g1a1m1e

那么可以hook,但是这个native层是有反frida的,要用去特征的frida-serve.(待研究这个,安卓还不会)

另一个是硬解,我解密脚本无问题,但是密文找不到,我不知道这个密文怎么得,到时候问问老登

middleman

先看apk

static native boolean middlemen(String str);

    static {
        System.loadLibrary("middlemen");
    }


public void onClick(View v) {
                String input = MainActivity.this.password.getText().toString();
                if (!input.contains("hgame")) {
                    Toast.makeText(MainActivity.this.getBaseContext(), "????????", 1).show();
                } else if (MainActivity.middlemen(input)) {
                    Intent RightAcitvity = new Intent(MainActivity.this, (Class<?>) Right.class);
                    MainActivity.this.startActivity(RightAcitvity);
                } else {
                    Intent WrongAcitvity = new Intent(MainActivity.this, (Class<?>) Wrong.class);
                    MainActivity.this.startActivity(WrongAcitvity);
                }
            }

这里让我们直接输入flag.

那我们就看看meddiemen这个so库.

bool __fastcall sub_22DC(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 v4[14]; // [xsp+0h] [xbp-C0h] BYREF
  __int64 v5; // [xsp+70h] [xbp-50h]
  const char *v6; // [xsp+78h] [xbp-48h]
  __int64 v7; // [xsp+80h] [xbp-40h]
  __int64 v8; // [xsp+88h] [xbp-38h]
  __int64 v9; // [xsp+90h] [xbp-30h]
  bool v10; // [xsp+98h] [xbp-28h]
  char v11[4]; // [xsp+9Ch] [xbp-24h] BYREF
  __int64 v12[4]; // [xsp+A0h] [xbp-20h] BYREF

  v12[3] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v9 = a1;
  v8 = a2;
  v7 = a3;
  memset(v12, 0, 20);
  v4[12] = (__int64)v12;
  v4[11] = (__int64)&v12[1];
  v6 = (const char *)sub_2454(a1, a3, v11);
  v4[13] = (__int64)v4;
  v10 = sscanf(
          v6,
          "hgame{%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x}",
          v12,
          (char *)v12 + 1,
          (char *)v12 + 2,
          (char *)v12 + 3,
          (char *)v12 + 4,
          (char *)v12 + 5,
          (char *)v12 + 6,
          (char *)v12 + 7,
          &v12[1],
          (char *)&v12[1] + 1,
          (char *)&v12[1] + 2,
          (char *)&v12[1] + 3,
          (char *)&v12[1] + 4,
          (char *)&v12[1] + 5,
          (char *)&v12[1] + 6,
          (char *)&v12[1] + 7)
     && (v5 = syscall(
                172LL,
                v12[0],
                *(__int64 *)((char *)v12 + 4),
                v12[1],
                *(__int64 *)((char *)&v12[1] + 4),
                2232865LL),
         v5 >= 0x1000000000001LL);
  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  return v10;
}

把我们输入的flag传参传进去,这里syscall明显有问题.

去init_array看看

__int64 sub_2020()
{
  __int64 result; // x0
  __int128 v1; // [xsp+0h] [xbp-40h] BYREF
  __int64 v2; // [xsp+10h] [xbp-30h] BYREF
  int v3; // [xsp+18h] [xbp-28h] BYREF
  __int64 (__fastcall *v4)(__int64, __int64, __int64); // [xsp+20h] [xbp-20h]
  __int64 v5; // [xsp+28h] [xbp-18h]
  __int64 v6; // [xsp+38h] [xbp-8h]

  v6 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v1 = *(_OWORD *)&off_48D8;
  result = prctl(38, 1LL, 0LL, 0LL);
  if ( (_DWORD)result != -1 )
  {
    result = prctl(22, 2LL, &v1);
    if ( (_DWORD)result != -1 )
    {
      sigfillset((sigset_t *)&v2);
      v4 = handleer;
      v5 = v2;
      v3 = 4;
      result = sigaction(31, (const struct sigaction *)&v3, 0LL);
    }
  }
  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  return result;
}

第一个prctl是禁用所有系统调用,第二个是每次系统调用前都先经过v1这个函数(是由bpf的二进制码组成的)

[原创]基于seccomp+sigaction的Android通用svc hook方案-Android安全-看雪-安全社区|安全招聘|kanxue.com

从prctl函数开始学习沙箱规则 - Riv4ille - 博客园

v4 = handleer;
v5 = v2;
v3 = 4;

这其实是个结构体,但ida没识别出来,我不知道()

seccomp-tools disasm cbpf

bpf反汇编出来是

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x26 0xc00000b7  if (A != ARCH_AARCH64) goto 0040
 0002: 0x20 0x00 0x00 0x00000020  A = args[2]
 0003: 0x02 0x00 0x00 0x00000000  mem[0] = A
 0004: 0x20 0x00 0x00 0x00000028  A = args[3]
 0005: 0x02 0x00 0x00 0x00000001  mem[1] = A
 0006: 0x64 0x00 0x00 0x00000004  A <<= 4
 0007: 0x04 0x00 0x00 0x65766573  A += 0x65766573
 0008: 0x02 0x00 0x00 0x00000002  mem[2] = A
 0009: 0x60 0x00 0x00 0x00000001  A = mem[1]
 0010: 0x07 0x00 0x00 0x00000000  X = A
 0011: 0x00 0x00 0x00 0x22122122  A = 571613474
 0012: 0x0c 0x00 0x00 0x00000000  A += X
 0013: 0x07 0x00 0x00 0x00000000  X = A
 0014: 0x60 0x00 0x00 0x00000002  A = mem[2]
 0015: 0xac 0x00 0x00 0x00000000  A ^= X
 0016: 0x07 0x00 0x00 0x00000000  X = A
 0017: 0x60 0x00 0x00 0x00000000  A = mem[0]
 0018: 0x0c 0x00 0x00 0x00000000  A += X
 0019: 0x15 0x00 0x14 0x93cd6340  if (A != 2479711040) goto 0040
 0020: 0x02 0x00 0x00 0x00000000  mem[0] = A
 0021: 0x74 0x00 0x00 0x00000005  A >>= 5
 0022: 0x04 0x00 0x00 0x6e6e6e6e  A += 0x6e6e6e6e
 0023: 0x02 0x00 0x00 0x00000002  mem[2] = A
 0024: 0x60 0x00 0x00 0x00000000  A = mem[0]
 0025: 0x07 0x00 0x00 0x00000000  X = A
 0026: 0x00 0x00 0x00 0x22122122  A = 571613474
 0027: 0x0c 0x00 0x00 0x00000000  A += X
 0028: 0x07 0x00 0x00 0x00000000  X = A
 0029: 0x60 0x00 0x00 0x00000002  A = mem[2]
 0030: 0xac 0x00 0x00 0x00000000  A ^= X
 0031: 0x07 0x00 0x00 0x00000000  X = A
 0032: 0x60 0x00 0x00 0x00000001  A = mem[1]
 0033: 0x0c 0x00 0x00 0x00000000  A += X
 0034: 0x15 0x00 0x05 0xb5f40d3f  if (A != 3052670271) goto 0040
 0035: 0x20 0x00 0x00 0x00000000  A = sys_number
 0036: 0x15 0x00 0x03 0x000000ac  if (A != aarch64.getpid) goto 0040
 0037: 0x20 0x00 0x00 0x00000030  A = args[4]
 0038: 0x15 0x00 0x01 0x00221221  if (A != 0x221221) goto 0040
 0039: 0x06 0x00 0x00 0x00030000  return TRAP
 0040: 0x06 0x00 0x00 0x7fff0000  return ALLOW

是个tea类加密的部分,用来变化输入,根据这个加密方式获得key.

通过信号,进入另一个函数

__int64 __fastcall sub_1ECC(__int64 a1, __int64 a2, __int64 a3)
{
  int v3; // w9
  __int64 result; // x0
  int i; // [xsp+14h] [xbp-5Ch]
  char v7[24]; // [xsp+30h] [xbp-40h] BYREF
  char v8[16]; // [xsp+48h] [xbp-28h] BYREF
  _QWORD v9[3]; // [xsp+58h] [xbp-18h] BYREF

  v9[2] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  strcpy(v7, "Sevenlikeseccmop");
  v9[0] = *(_QWORD *)(a3 + 184);
  v9[1] = *(_QWORD *)(a3 + 200);
  for ( i = 0; i <= 15; ++i )
  {
    if ( i <= 0 )
      v3 = -(-i & 7);
    else
      v3 = i & 7;
    v7[i] ^= *((_BYTE *)&v9[1] + v3);
  }
  sub_32C4(v7, 16LL, v9);
  result = strncmp(v8, byte_5B58, 0x10uLL);
  if ( (_DWORD)result )
  {
    result = getpid();
    *(_QWORD *)(a3 + 184) = (int)result;
  }
  else
  {
    *(_QWORD *)(a3 + 184) = 0x1145141919810LL;
  }
  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  return result;
}

ucontext_t结构体,通过shoiri人工利用ai识别可得.

还原后是

output

然后直接aes正常解密就行了.(说起来我现在还不会aes解密)

Nop'd

源码:https://github.com/CSharperMantle/hgame2025_nopd_public
动态调试是解题的最快方法,但纯静态分析也是可行的。
背景资料:
• Semihosting in RISC-V:https://riscv-software-src.github.io/riscv-unifieddb/
manual/html/isa/20240411/chapters/rv32.html#_environment_call_and_breakp
oints
• Multibyte NOP encodings in x86-64:
https://www.felixcloutier.com/x86/nop#operation
RISC-V复用 ebreak 指令来实现semihosting,通过利用特殊编码的nop指令来区分普通ebreak 和实现semihosting的ebreak 。在x86平台中,也可以使用类似的思路复用部分trap指令,实现子执行环境与监视环境之间的通讯。以上述方法复用 syscall 指令,即可达成在未监视环境下执行无害控制流的可抵赖性。

模拟了一个半主机(SemiHosting)系统:

Host System:launcher

Target System:game

根据题目提示,可知启动器通过某种方式改变了game的运行方式,实际运行也是.

那我们先看看启动器.

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __pid_t v3; // eax

  if ( a1 <= 1 )
  {
    __fprintf_chk(stderr, 2LL, "usage: %s <path-to-game> [args...]\n", *a2);
    return 1LL;
  }
  else
  {
    v3 = fork();
    pid = v3;
    if ( v3 == -1 )
    {
      perror("fork");
      return 1LL;
    }
    else if ( v3 )
    {
      return 0LL;
    }
    else
    {
      ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
      execv(a2[1], a2 + 1);
      perror("execv");
      return 0xFFFFFFFFLL;
    }
  }
}

看到有附加

execv的作用:

execv() 是 Linux 系统调用的一部分,用于在当前进程中执行一个新的程序。调用 execv() 后,当前进程的代码和数据将被新程序替换,且不会返回到调用点。execv() 是 exec 系列函数之一,这些函数提供了在进程中启动另一个程序的方法。

perror()作用:

perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。

在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和errno所对应的错误一起输出。

这里就是把子进程变成game.

然后官方wp的v3是一个全局变量,不知道为什么是一个普通变量.总之去init_array看看.

__int64 __fastcall sub_1B9B(unsigned int pid)
{
  unsigned int pid1; // ebx
  char i; // bl
  int v3; // ebx
  __int64 v4; // rax
  __int64 v5; // r15
  char v6; // dl
  __int64 v7; // r15
  __int64 (__fastcall *v8)(); // rax
  unsigned int pida; // [rsp+4h] [rbp-64h]
  __int64 v11; // [rsp+18h] [rbp-50h] BYREF
  char v12[4]; // [rsp+22h] [rbp-46h] BYREF
  unsigned __int8 v13; // [rsp+26h] [rbp-42h]
  unsigned __int8 v14; // [rsp+27h] [rbp-41h]
  unsigned __int64 v15; // [rsp+28h] [rbp-40h]

  pid1 = pid;
  v15 = __readfsqword(0x28u);
  waitpid(pid, 0LL, 0);                         // 等待子进程暂停,不知道这个是什么时候等的
  ptrace(PTRACE_SETOPTIONS, pid, 0LL, 0x100000LL);// 不明白,大概是附加
  while ( ptrace(PTRACE_SYSCALL, pid1, 0LL, 0LL) >= 0 )// //PTRACE_SYSCALL:使子进程在每次系统调用入口(syscall-entry)和出口(syscall-exit)时暂停。
                                                // RIP 寄存器会指向 SYSCALL 指令的下一条指令
  {
    waitpid(pid1, 0LL, 0);                      // 又等一次,这里是系统调用的
    ptrace(PTRACE_GETREGS, pid1, 0LL, &qword_55E0);// PTRACE_GETREGS:将子进程的寄存器保存到 qword_55E0,用于后续分析和修改。
    readcontext(PTRACE_PEEKTEXT, pid1, (__int64)byte_55C0, *(&qword_55E0 + 16) - 8, 6uLL);// 读取内存指令
    if ( (byte_55C0[0] & 0xFC) != 72
      || byte_55C0[1] != 15
      || byte_55C2 != 31
      || (byte_55C3 & 0xC7) != 68
      || byte_55C5 != 127 )
    {
      v11 = *(&qword_55E0 + 16);
      goto LABEL_41;
    }
    memset(byte_5540, 0, sizeof(byte_5540));
    qword_5520 = 0LL;
    pida = pid1;
    for ( i = 0; ; i = 1 )
    {
      v5 = qword_5520;
      readcontext(PTRACE_PEEKTEXT, pida, (__int64)v12, qword_5660 + 6 * qword_5520, 6uLL);
      if ( (v12[0] & 0xFC) != 72 || v12[1] != 15 || v12[2] != 31 || (v12[3] & 0xC7) != 68 || v14 == 126 )
        break;
      v3 = (v14 - 1) % 128;
      if ( v13 > 0x3Fu )
      {
        sub_1464(pida, &v11, qword_5678 + ((8 * (v13 & 7 | (unsigned __int8)(8 * (v12[0] & 1)))) & 0x78), 8LL);
        qword_5120[(unsigned __int8)v3] = v11;
      }
      else
      {
        switch ( v13 & 7 | (unsigned __int8)(8 * (v12[0] & 1)) )// 以上是传头,数据,尾
        {
          case 0:
            v4 = qword_5658;                    // 寄存器
            break;
          case 1:
            v4 = qword_5638;
            break;
          case 2:
            v4 = qword_5640;
            break;
          case 3:
            v4 = qword_5608;
            break;
          case 4:
            v4 = qword_5678;
            break;
          case 5:
            v4 = qword_5600;
            break;
          case 6:
            v4 = qword_5648;
            break;
          case 7:
            v4 = qword_5650;
            break;
          case 8:
            v4 = qword_5628;
            break;
          case 9:
            v4 = qword_5620;
            break;
          case 10:
            v4 = qword_5618;
            break;
          case 11:
            v4 = qword_5610;
            break;
          case 12:
            v4 = qword_55F8;
            break;
          case 13:
            v4 = qword_55F0;
            break;
          case 14:
            v4 = qword_55E8;
            break;
          case 15:
            v4 = qword_55E0;
            break;
        }
        qword_5120[(unsigned __int8)v3] = v4;
      }
      byte_5540[(unsigned __int8)v3] = 1;
      qword_5520 = v5 + 1;
    }
    v6 = i;
    pid1 = pida;
    v11 = *(&qword_55E0 + 16);
    if ( v6 )
    {
      v7 = -38LL;
      if ( byte_5540[0] )
      {
        if ( qword_5120[0] <= 9uLL )
        {
          v8 = funcs_1DBD[qword_5120[0]];
          if ( v8 )
            v7 = ((int (__fastcall *)(_QWORD, __int64 *, char *, __int64 *))v8)(pida, qword_5120, byte_5540, &v11);
        }
        *(&qword_55E0 + 15) = -1LL;
        ptrace(PTRACE_SETREGS, pida, 0LL, &qword_55E0);
      }
      ptrace(PTRACE_SYSCALL, pida, 0LL, 0LL);
      waitpid(pida, 0LL, 0);
      ptrace(PTRACE_GETREGS, pida, 0LL, qword_5040);
      qword_5040[10] = v7;
      qword_5040[16] = v11;
      ptrace(PTRACE_SETREGS, pida, 0LL, qword_5040);
    }
    else
    {
LABEL_41:
      ptrace(PTRACE_SYSCALL, pid1, 0LL, 0LL);
      waitpid(pid1, 0LL, 0);
    }
  }
  return 0LL;
}

在程序结束注册了一个函数,根据pid判断,故而这里只有父进程可以进入.

该函数通过ptrace(详见https://www.man7.org/linux/man-pages/man2/ptrace.2.html)拦截子进程中的每个syscall,检查该syscall的静态前驱指令是否为满足 [0x40-0x43] 0x0F 0x1F 0x44 0x7F 。若是,则读取指令并进行某种解码,直到遇到[0x40-0x43] 0x0F 0x1F 0x44 0x7E或任何不以[0x40-0x43] 0x0F 0x1F 0x44 为前缀的指令。

--官方wp

这种题目恐怕只有mantle学长会写了...

大概是一个chacha20算法,如果想看函数的流程,我觉得可以hook掉ptrace,那就知道流程了.

我懒,就不逆了.其实是菜

这位佬写得很清楚:[hgame week2 WP | Britney](https://1ncharles.github.io/2025/02/26/hgame week2/)

shoiri也是,但我懒得放()

Fast and frustrating

看不懂,PYC一样,算了.

posted @   T0fV404  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示