Hgame2025复盘
Week1
RE
Compress dot new
查一下脚本语言是nushell,直接让ai识别是什么算法,结果爆出flag了
Turtle
开局用die查壳,魔改upx且魔改较多,用dbg脱壳(esp定律)
里面一看是个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
引入了一个windows的函数,查了下是增量补丁相关的函数,根据其他人的介绍可以得知两点:
- 有hash校验
- 可以在md5比较前获得目标文件
先运行,发现有一次换md5,伪代码没有,改为汇编一看,果然是异常处理.
然后直接强行建立函数看伪代码.
既然给改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压缩包,提示密码:
看了加密方式AES,不可能破解.
看看文件,结尾有个GNP,把倒置的GNP图片dump出来并导致回来.
with open('output.bin','rb') as f:
with open('flag','wb') as g:
g.write(f.read()[::-1])
获得学长的帅照,然后猜了下是高宽也没看crc用一把梭工具试了下,出了
这是密码,输入zip可获得flag:hagme{h4kyu4_w4nt_gir1f3nd_+q_931290928}
(难绷)
Computer cleaner
ai是好东西,先把题目要求告诉他,他给了我这个:
grep -rn "eval(" /var/www
获得第一部分
搜了下好像这个地方就是ubuntu服务器用来联网的地方,那就搜搜记录吧,遍历以后发现
121.41.34.25
这个IP,原本以为要社工,后面发现直接访问即可(大道至简)
可以看到
Are you looking for me
Congratulations!!!
hav3_cleaned_th3
接着回到log:
这里执行了一个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识别可得.
还原后是
然后直接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一样,算了.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)