本文最后更新于 2025年2月12日 凌晨
最近 SWDD 师傅(Android 神!)给了几道简单的 Android 题目,我拿来做了做,感觉 Android 真有意思吧hhhhhh可惜我不会呜呜呜。做题期间在 ida 的深色和浅色模式下反复横跳,最终选择了深色(果然一开始干正事什么都变得有趣了起来)
APK 反编译-level1 jeb 反编译
1 flag{Your_are_go0d_at_Uncompile_Android!}
Native 层反编译 -level2Native
层反编译,那么我们得去把 so
层提取出来,用 apktool
1 apktool d apk名 -o "需要生成的文件夹"
找到 \lib\x86_64\libsummertrain.so
文件丢入 ida 就出来
1 flag {Now_You_Know_Native_uncompile}
静态注册 -level3提取 .so
文件丢入 ida,找到 Java_com_swdd_summertrain_MainActivity_Check
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 __int64 __fastcall Java_com_swdd_summertrain_MainActivity_Check (__int64 a1, __int64 a2, __int64 a3) { ... v27 = v3; v5 = 0LL ; v6 = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL ))(a1, a3, 0LL ); if ( v6 ) { v7 = v6; v8 = strlen (v6); v9 = (char *)operator new[](v8 + 1 ); if ( !v8 ) goto LABEL_11; if ( v8 < 8 || v9 < &v7[v8] && v7 < &v9[v8] ) { v10 = 0LL ; LABEL_7: v11 = v8 + ~v10; v12 = v8 & 3 ; if ( (v8 & 3 ) != 0 ) { do { v9[v10] = v7[v10] ^ 0x33 ; ++v10; --v12; } while ( v12 ); } if ( v11 >= 3 ) { do { v9[v10] = v7[v10] ^ 0x33 ; v9[v10 + 1 ] = v7[v10 + 1 ] ^ 0x33 ; v9[v10 + 2 ] = v7[v10 + 2 ] ^ 0x33 ; v9[v10 + 3 ] = v7[v10 + 3 ] ^ 0x33 ; v10 += 4LL ; } while ( v8 != v10 ); } goto LABEL_11; } if ( v8 < 0x20 ) { v10 = 0LL ; LABEL_22: v26 = v10; v10 = v8 & 0xFFFFFFFFFFFFFFF8 LL; do { *(_QWORD *)&v9[v26] = *(_QWORD *)&v7[v26] ^ 0x3333333333333333 LL; v26 += 8LL ; } while ( v10 != v26 ); if ( v8 != v10 ) goto LABEL_7; LABEL_11: v9[v8] = 0 ; v13 = memcmp (v9, "U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to " , v8) == 0 ; v14 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL ))(a1, "java/lang/Boolean" ); v15 = v14; v16 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL ))( a1, v14, "<init>" , "(Z)V" ); v5 = _JNIEnv::NewObject(a1, v15, v16, v13, v17, v18, v27); (*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL ))(a1, a3, v7); operator delete[](v9); return v5; } v10 = v8 & 0xFFFFFFFFFFFFFFE0 LL; v20 = (((v8 & 0xFFFFFFFFFFFFFFE0 LL) - 32 ) >> 5 ) + 1 ; if ( (v8 & 0xFFFFFFFFFFFFFFE0 LL) == 32 ) { v22 = 0LL ; if ( (v20 & 1 ) == 0 ) goto LABEL_20; } else { v21 = v20 & 0xFFFFFFFFFFFFFFFE LL; v22 = 0LL ; do { v23 = _mm_xor_ps(*(__m128 *)&v7[v22 + 16 ], (__m128)xmmword_CA80); *(__m128 *)&v9[v22] = _mm_xor_ps(*(__m128 *)&v7[v22], (__m128)xmmword_CA80); *(__m128 *)&v9[v22 + 16 ] = v23; v24 = _mm_xor_ps(*(__m128 *)&v7[v22 + 48 ], (__m128)xmmword_CA80); *(__m128 *)&v9[v22 + 32 ] = _mm_xor_ps(*(__m128 *)&v7[v22 + 32 ], (__m128)xmmword_CA80); *(__m128 *)&v9[v22 + 48 ] = v24; v22 += 64LL ; v21 -= 2LL ; } while ( v21 ); if ( (v20 & 1 ) == 0 ) goto LABEL_20; } v25 = _mm_xor_ps(*(__m128 *)&v7[v22 + 16 ], (__m128)xmmword_CA80); *(__m128 *)&v9[v22] = _mm_xor_ps(*(__m128 *)&v7[v22], (__m128)xmmword_CA80); *(__m128 *)&v9[v22 + 16 ] = v25; LABEL_20: if ( v8 == v10 ) goto LABEL_11; if ( (v8 & 0x18 ) == 0 ) goto LABEL_7; goto LABEL_22; } return v5; }
代码主要流程为:对字符串 v6
进行异或,分为几种情况:
如果字符串长度小于 8,逐字节进行异或,每个字符都与 0x33
异或
如果长度在 8 ~ 32 之间,按 8 字节为一组进行异或,异或值是 0x3333333333333333LL
如果长度超过 32 字节,使用 SSE 指令 (_mm_xor_ps
),每次处理 16 字节,用一个全局常量 xmmword_CA80
进行异或
解码后的字符串会与一个固定字符串 U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to
进行比较,相等则正确,exp 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include "stdafx.h" #include <iostream> #include <cstring> #include <immintrin.h> #include <cstdint> void decode (char * encoded, size_t length) { if (length < 8 ) { for (size_t i = 0 ; i < length; ++i) { encoded[i] ^= 0x33 ; } } else if (length < 32 ) { for (size_t i = 0 ; i < length; i += 8 ) { uint64_t * block = reinterpret_cast <uint64_t *>(encoded + i); *block ^= 0x3333333333333333LL ; } } else { for (size_t i = 0 ; i < length; ++i) { encoded[i] ^= 0x33 ; } } }int main () { const char * originalEncodedString = "U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to" ; size_t length = strlen (originalEncodedString); char * encodedString = new char [length + 1 ]; strcpy (encodedString, originalEncodedString); std::cout << "加密字符串: " << encodedString << std::endl; decode (encodedString, length); std::cout << "解密后的字符串: " << encodedString << std::endl; delete [] encodedString; return 0 ; }
1 flag{Now_You_Know_Native_dynamic_registration}
动态注册 -level4动态注册的方法一般来说需要分析 JNI_OnLoad
函数,把 libsummertrain.so
丢进 ida分析
emmmmm,这里我似乎没分析出什么来,然后看了眼其他函数,发现了 sub_172E0
函数有东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 int __cdecl sub_110B0 (int a1, int a2, int a3) { ... v3 = (const char *)(*(int (__cdecl **)(int , int , _DWORD))(*(_DWORD *)a1 + 676 ))(a1, a3, 0 ); if ( v3 ) { v4 = (unsigned int )v3; v5 = strlen (v3); v6 = (_BYTE *)operator new[](v5 + 1 ); v7 = v5; v8 = v6; if ( !v7 ) goto LABEL_21; v9 = 0 ; v24 = v7; if ( v7 < 8 || (unsigned int )v8 < v4 + v7 && v4 < (unsigned int )&v8[v7] ) goto LABEL_17; v9 = 0 ; if ( v7 < 0x20 ) { LABEL_14: v17 = v9; v9 = v7 & 0xFFFFFFF8 ; do { *(_QWORD *)&v8[v17] = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)(v4 + v17)), (__m128i)-1LL ).m128i_u64[0 ]; v17 += 8 ; } while ( v9 != v17 ); if ( v7 != v9 ) goto LABEL_17; LABEL_21: v8[v7] = 0 ; v25 = memcmp (v8, &unk_78FD, v7) == 0 ; v20 = (*(int (__cdecl **)(int , const char *))(*(_DWORD *)a1 + 24 ))(a1, "java/lang/Boolean" ); v21 = (*(int (__cdecl **)(int , int , const char *, const char *))(*(_DWORD *)a1 + 132 ))(a1, v20, "<init>" , "(Z)V" ); v22 = _JNIEnv::NewObject(a1, v20, v21, v25); (*(void (__cdecl **)(int ))(*(_DWORD *)a1 + 680 ))(a1); operator delete[](v8); return v22; } v9 = v7 & 0xFFFFFFE0 ; v10 = (v7 & 0xFFFFFFE0 ) - 32 ; v11 = (v10 >> 5 ) + 1 ; if ( v10 ) { v12 = v11 & 0xFFFFFFFE ; v13 = 0 ; do { v14 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 16 )), (__m128i)-1LL ); *(__m128i *)&v8[v13] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13)), (__m128i)-1LL ); *(__m128i *)&v8[v13 + 16 ] = v14; v15 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 48 )), (__m128i)-1LL ); *(__m128i *)&v8[v13 + 32 ] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 32 )), (__m128i)-1LL ); *(__m128i *)&v8[v13 + 48 ] = v15; v13 += 64 ; v12 -= 2 ; } while ( v12 ); if ( (v11 & 1 ) == 0 ) goto LABEL_12; } else { v13 = 0 ; if ( (v11 & 1 ) == 0 ) goto LABEL_12; } v16 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 16 )), (__m128i)-1LL ); *(__m128i *)&v8[v13] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13)), (__m128i)-1LL ); *(__m128i *)&v8[v13 + 16 ] = v16; LABEL_12: v7 = v24; if ( v24 == v9 ) goto LABEL_21; if ( (v24 & 0x18 ) == 0 ) { LABEL_17: v18 = v7 + ~v9; for ( i = v7 & 3 ; i; --i ) { v8[v9] = ~*(_BYTE *)(v4 + v9); ++v9; } v7 = v24; if ( v18 >= 3 ) { do { v8[v9] = ~*(_BYTE *)(v4 + v9); v8[v9 + 1 ] = ~*(_BYTE *)(v4 + v9 + 1 ); v8[v9 + 2 ] = ~*(_BYTE *)(v4 + v9 + 2 ); v8[v9 + 3 ] = ~*(_BYTE *)(v4 + v9 + 3 ); v9 += 4 ; } while ( v24 != v9 ); } goto LABEL_21; } goto LABEL_14; } return 0 ; }
首先用 v5
存储调用 (*(_QWORD *)a1 + 1352LL)
所返回的字符串,v6
指向指向 v5
,然后 v8
来存储字符串
而且代码根据字符串长度进行不同的解密处理:
长度小于 8 的情况 ,逐字节进行按位取反
长度大于或等于 8 小于 32 的情况 ,利用 _mm_xor_si128
进行 8 字节为单位的批量解密
长度大于或等于 32 的情况 ,一次处理 64 字节
**_mm_xor_si128
**:通过向量化操作,将 16 字节数据与 -1
进行异或,相当于每个字节都执行 ~
操作(按位取反)
代码中有一句 v12 = memcmp(v8, &unk_DCF0, v7) == 0
,主要作用是通过 memcmp
函数比较两个内存区域的内容是否相等,那么 v5
中的字符串就存储在 unk_DCF0
中,提取出来
1 0x99 , 0 x93, 0 x9E, 0 x98, 0 x84, 0 xB1, 0 x90, 0 x88, 0 xA0, 0 xA6, 0 x90, 0 x8A, 0 xA0, 0 xB4, 0 x91, 0 x90, 0 x88, 0 xA0, 0 xB1, 0 x9E, 0 x8B, 0 x96, 0 x89, 0 x9A, 0 xA0, 0 x8D, 0 x9A, 0 x9E, 0 x93, 0 xA0, 0 x9B, 0 x86, 0 x91, 0 x9E, 0 x92, 0 x96, 0 x9C, 0 xA0, 0 x8D, 0 x9A, 0 x98, 0 x96, 0 x8C, 0 x8B, 0 x8D, 0 x9E, 0 x8B, 0 x96, 0 x90, 0 x91, 0 x82
exp
1 2 3 4 5 6 7 8 9 10 11 12 ida_chars = [ 0x99 , 0x93 , 0x9E , 0x98 , 0x84 , 0xB1 , 0x90 , 0x88 , 0xA0 , 0xA6 , 0x90 , 0x8A , 0xA0 , 0xB4 , 0x91 , 0x90 , 0x88 , 0xA0 , 0xB1 , 0x9E , 0x8B , 0x96 , 0x89 , 0x9A , 0xA0 , 0x8D , 0x9A , 0x9E , 0x93 , 0xA0 , 0x9B , 0x86 , 0x91 , 0x9E , 0x92 , 0x96 , 0x9C , 0xA0 , 0x8D , 0x9A , 0x98 , 0x96 , 0x8C , 0x8B , 0x8D , 0x9E , 0x8B , 0x96 , 0x90 , 0x91 , 0x82 ] decrypted_string = '' .join(chr (~byte & 0xFF ) for byte in ida_chars)print (decrypted_string)
1 flag{Now_You_Know_Native_real_dynamic_registration}
GAME-level5 Jeb 打开发现了关于 Unity 的东西,猜测这个程序估计是用 Unity 写的
把 \assets\bin\Data\Managed\
下的 Assembly-CSharp.dll
文件丢进 dnSpy 看看,在 GameWindow
找到 flag
1 flag{justaeasyunitygame}
GAME-level6 第一次见这个东西,还挺新奇hhhhh
发现有一个 Mono 文件夹
Unity 使用 Mono 方式打出来的 apk,如果没有加密我们可以直接从包内拿到 Assembly-CSharp.dll
,可以直接使用 dnSpy.exe
对其进行反编译。 如果使用 IL2CPP
方式出包,则没有 Assembly-CSharp.dll
,有一个 IL2CppDumper
工具,https://github.com/Perfare/Il2CppDumper,通过它,我们可以逆向得到 Assembly-CSharp.dll
创建一个 input
和 output
,将 libil2cpp.so
与 global-metadata.dat
拷贝到 input
目录中,
可以创建一个批处理文件到 input
1 ..\Il2CppDumper.exe libil2cpp.so global-metadata.dat ..\output
环境 .NET
需要用 6.0.0
版本
然后打开这个 .dll
文件发现有一个 FlagText
,但是是没有内容的
打开 libil2cpp.so
,加载 IL2CppDumper 文件夹里的 ida_py3.py 文件把 dump 出来的 script.json 加载进去,FlagText
偏移值为 0x40
,在 ida 里面跳转,可以看到有 Flags 字样,但是还是什么都没有
查看 LoadLevel
函数,加载到了 UnityEngine_Transform__SetParent_8767024
函数,但是我 ida 后面加载不出来了,唉唉唉
Init_Array-level7 看源码,调用了 native
层 Check
,因此就应该去分析 .so
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.swdd.summertrain;import android.os.Bundle;import android.view.View;import android.widget.TextView;import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;import com.swdd.summertrain.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; static { System.loadLibrary("summertrain" ); } public native Boolean Check (String arg1) { } public void onClick (View V) { if (this .Check(((TextView)this .findViewById(id.input)).getText().toString()).booleanValue()) { Toast.makeText(this , "Good!" , 0 ).show(); return ; } Toast.makeText(this , "Try Again" , 0 ).show(); } @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); ActivityMainBinding activityMainBinding0 = ActivityMainBinding.inflate(this .getLayoutInflater()); this .binding = activityMainBinding0; this .setContentView(activityMainBinding0.getRoot()); } }
查看 Java_com_swdd_summertrain_MainActivity_Check
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 __int64 __fastcall Java_com_swdd_summertrain_MainActivity_Check (__int64 a1, __int64 a2, __int64 a3) { ... v3 = a3; v4 = 0LL ; v5 = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL ))(a1, a3, 0LL ); if ( !v5 ) return v4; v6 = v5; v26 = v3; v7 = strlen (v5); v8 = (char *)operator new [](v7 + 1 ); if ( !v7 ) goto LABEL_11; if ( v7 < 8 || v8 < &v6[v7] && v6 < &v8[v7] ) { v9 = 0LL ; LABEL_7: v10 = v7 + ~v9; v11 = v7 & 3 ; if ( (v7 & 3 ) != 0 ) { do { v8[v9] = ~v6[v9]; ++v9; --v11; } while ( v11 ); } if ( v10 >= 3 ) { do { v8[v9] = ~v6[v9]; v8[v9 + 1 ] = ~v6[v9 + 1 ]; v8[v9 + 2 ] = ~v6[v9 + 2 ]; v8[v9 + 3 ] = ~v6[v9 + 3 ]; v9 += 4LL ; } while ( v7 != v9 ); } goto LABEL_11; } if ( v7 >= 0x20 ) { v9 = v7 & 0xFFFFFFFFFFFFFFE0LL ; v19 = (((v7 & 0xFFFFFFFFFFFFFFE0LL ) - 32 ) >> 5 ) + 1 ; if ( (v7 & 0xFFFFFFFFFFFFFFE0LL ) == 32 ) { v21 = 0LL ; if ( (v19 & 1 ) == 0 ) goto LABEL_22; } else { v20 = v19 & 0xFFFFFFFFFFFFFFFELL ; v21 = 0LL ; do { v22 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 16 ]), (__m128i)-1LL ); *(__m128i *)&v8[v21] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21]), (__m128i)-1LL ); *(__m128i *)&v8[v21 + 16 ] = v22; v23 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 48 ]), (__m128i)-1LL ); *(__m128i *)&v8[v21 + 32 ] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 32 ]), (__m128i)-1LL ); *(__m128i *)&v8[v21 + 48 ] = v23; v21 += 64LL ; v20 -= 2LL ; } while ( v20 ); if ( (v19 & 1 ) == 0 ) goto LABEL_22; } v24 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 16 ]), (__m128i)-1LL ); *(__m128i *)&v8[v21] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21]), (__m128i)-1LL ); *(__m128i *)&v8[v21 + 16 ] = v24; LABEL_22: if ( v7 == v9 ) goto LABEL_11; if ( (v7 & 0x18 ) == 0 ) goto LABEL_7; goto LABEL_24; } v9 = 0LL ; LABEL_24: v25 = v9; v9 = v7 & 0xFFFFFFFFFFFFFFF8LL ; do { *(_QWORD *)&v8[v25] = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)&v6[v25]), (__m128i)-1LL ).m128i_u64[0 ]; v25 += 8LL ; } while ( v9 != v25 ); if ( v7 != v9 ) goto LABEL_7; LABEL_11: v8[v7] = 0 ; v12 = 0 ; if ( v7 == (int )__strlen_chk(&storedBytes, 256LL ) ) v12 = memcmp (v8, &storedBytes, v7) == 0 ; v13 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL ))(a1, "java/lang/Boolean" ); v14 = v13; v15 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL ))( a1, v13, "<init>" , "(Z)V" ); v4 = _JNIEnv::NewObject (a1, v14, v15, v12, v16, v17, v26); (*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL ))(a1, v27, v6); operator delete [](v8); return v4; }
大概流程和 “动态注册” 这个题差不多,对传入字符串按位取反,最后存储在 storedBytes
中,把密文提取出来,解出来居然是假的,what can i say.jpg
然后看了下 Shangwendada 的《对init_array段调用的方法进行Hook》这篇文章,首先我们在 segments
窗口中看到各个段的偏移,找到了 .init_array
找到了 sub_17360
函数
1 2 3 4 5 __int64 sub_17360 () { memcpy (&storedBytes, &unk_CA70, 0x100uLL ); return __android_log_print(4LL , "GenFunction" , "Gen function called." ); }
原来 storedBytes
保存了从 unk_CA70
复制的数据,那 unk_CA70
应该就是正确的密文
exp 也是和之前那个题一样,换一下密文就行
1 2 3 4 5 6 7 ida_chars = [ 0x99 , 0x93 , 0x9E , 0x98 , 0x84 , 0xBA , 0x9E , 0x8C , 0x86 , 0xB6 , 0x91 , 0x96 , 0x8B , 0xBE , 0x8D , 0x8D , 0x9E , 0x86 , 0x82 ] decrypted_string = '' .join(chr (~byte & 0xFF ) for byte in ida_chars)print (decrypted_string)
Frida 端口检测 -level8先反编译一下,在 native
层调用了 Check
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.swdd.summertrain;import android.os.Bundle;import android.view.View;import android.widget.TextView;import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;import com.swdd.summertrain.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; static { System.loadLibrary("summertrain" ); } public native Boolean Check (String arg1) { } public void onClick (View view0) { String s = ((TextView)this .findViewById(id.input)).getText().toString(); if (this .Check(s).booleanValue()) { Toast.makeText(this , "Good! Your flag is flag{" + s + "}" , 0 ).show(); return ; } Toast.makeText(this , "Try Again" , 0 ).show(); } @Override protected void onCreate (Bundle bundle0) { super .onCreate(bundle0); ActivityMainBinding activityMainBinding0 = ActivityMainBinding.inflate(this .getLayoutInflater()); this .binding = activityMainBinding0; this .setContentView(activityMainBinding0.getRoot()); } }
没啥有用的信息,直接运行一下程序,正常是可以运行的,在启动了 frida
后程序停止运行了
这个时候就可以查看 .so
文件了,在 segments
窗口中看各个段的偏移,找到了 .init_array
然后进入sub_F40
函数
下面的一大堆什么玩意儿我也看不懂qwq,但是前面有一个 check_ports
函数
大概内容是通过 connect
检测连接 127.0.0.1
的两个端口检测是否被连接
1 2 *&addr.sa_family = 0 xA2690002 *addr.sa_data = 0 x8A5D
这里有一个网络字节转换的知识点(emmm计网的东西还没学),网络字节序是大端模式(Big-Endian),而主机字节序可能是小端模式(Little-Endian),关于这个大小端(死去的回忆突然开始攻击我),简单说一下
大端字节序(Big Endian):最高有效位存于最低内存地址处,最低有效位存于最高内存处
小端字节序(Little Endian):最高有效位存于最高内存地址,最低有效位存于最低内存处
所以我们可以根据信息得出端口
1 2 0 xA2690002 -> 0 xA269 -> 0 x69A2 -> 27042 0 x8A5D -> 0 x5D8A -> 23946
23946
端口是 IDA android_server 的默认端口,27042
是 Frida 默认端口
因此我们解决办法就是开启 frida 时把端口改了,不然会被检测到
1 ./frida-server -l 0 .0 .0 .0 :9997
然后我打开程序的时候老是跳出这个弹窗,烦死了
可以使用这个命令来关掉(菜鸡,还不知道有什么其他方法)
1 adb shell am clear-debug-app
写一个脚本 hook.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function hook ( ) { var targetAddress = Module .findExportByName ("libc.so" , "strcmp" ); Interceptor .attach (targetAddress, { onEnter : function (args ) { try { var input = Memory .readUtf8String (args[0 ]); if (input.includes ("aaa" )) { console .log ("FLAG: " + Memory .readUtf8String (args[1 ])); } } catch (e) { console .error ("Error: " + e); } } }); }function main ( ) { Java .perform (function ( ) { hook (); var MainActivity = Java .use ("com.swdd.summertrain.MainActivity" ); var MainActivityInstance = MainActivity .$new(); MainActivityInstance .Check ("aaa" ); }); }setImmediate (main);
这里需要先把端口转发一下,把模拟器的 9997
端口转发到本地的 9997
,因为通过 adb forward
把它的端口映射到了本地,hook 时需要写一个 127.0.0.1:9997
(其实还可以用本机的 IP,但是有时候可能会比较麻烦,模拟器有时候无法直接通过 IP 访问设备上的一些服务)连接到这个端口
1 2 3 ./frida-server 0.0.0.0 :9997 adb forward tcp:9997 tcp:9997 frida -H 127.0.0.1 :9997 -f com.swdd.summertrain -l hook.js
1 flag{53 cd3f37664bd01357182ca13bc2f9b6}
Frida检测之Maps-level9 Java 层没什么东西,同样是看 Native
层,这里有一个检测 maps
的。
/proc/self/maps
是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:
起始地址(Start Address)
结束地址(End Address)
权限(如可读、可写、可执行)
共享/私有标志(Shared or Private)
关联的文件或设备(如果内存区域是文件映射的)
内存区域的偏移量
内存区域的类型(如匿名映射、文件映射、设备映射等)
当注入frida后,在maps文件中就会存在 frida-agent-64.so
、frida-agent-32.so
等文件。
(上面这段专业术语 copy 的 52 的正己师傅的hhhhh)
这个函数流程就是打开 proc/self/maps
,然后检测是否有这些内存特征,如果有就直接终止程序
frida
:检测 Frida 注入的 Agent 或 Server
gadget
/agent
:检测 Frida Gadget 或调试工具组件
/data/local/tmp/
:检测临时目录下的注入文件(如 Frida Server)
-64.so
/-32.so
:检测特定架构的动态库(如 Frida 的 64/32 位库)
hook 脚本,重定向一个 maps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 function mapsRedirect ( ) { var FakeMaps = "/data/data/com.swdd.summertrain/maps" ; const openPtr = Module .getExportByName ('libc.so' , 'open' ); const open = new NativeFunction (openPtr, 'int' , ['pointer' , 'int' ]); var readPtr = Module .findExportByName ("libc.so" , "read" ); var read = new NativeFunction (readPtr, 'int' , ['int' , 'pointer' , "int" ]); var MapsBuffer = Memory .alloc (512 ); var MapsFile = new File (FakeMaps , "w" ); Interceptor .replace (openPtr, new NativeCallback (function (pathname, flag ) { var FD = open (pathname, flag); var ch = pathname.readCString (); if (ch.indexOf ("/proc/" ) >= 0 && ch.indexOf ("maps" ) >= 0 ) { console .log ("open : " , pathname.readCString ()); while (parseInt (read (FD , MapsBuffer , 512 )) !== 0 ) { var MBuffer = MapsBuffer .readCString (); MBuffer = MBuffer .replaceAll ("/data/local/tmp/re.frida.server/frida-agent-64.so" , "1" ); MBuffer = MBuffer .replaceAll ("re.frida.server" , "1" ); MBuffer = MBuffer .replaceAll ("frida" , "1" ); MBuffer = MBuffer .replaceAll ("agent" , "1" ); MBuffer = MBuffer .replaceAll ("-32.so" , "1" ); MBuffer = MBuffer .replaceAll ("-64.so" , "1" ); MBuffer = MBuffer .replaceAll ("gadget" , "1" ); MBuffer = MBuffer .replaceAll ("/data/local/tmp" , "/1" ); MapsFile .write (MBuffer ); } var filename = Memory .allocUtf8String (FakeMaps ); return open (filename, flag); } return FD ; }, 'int' , ['pointer' , 'int' ])); }function hook ( ) { mapsRedirect (); var targetAddress=Module .findExportByName ("libc.so" ,"strcmp" ); Interceptor .attach (targetAddress,{ onEnter :function (args ){ var input =Memory .readUtf8String (args[0 ]); if (input.includes ("aaa" )){ console .log ("FLAG:" +Memory .readUtf8String (args[1 ])); } },onLeave :function (retval ){ } }) var MainActivity =Java .use ("com.swdd.summertrain.MainActivity" ); var MainActivityInstance =MainActivity .$new(); MainActivityInstance .Check ("aaa" ); }function main ( ){ Java .perform (function ( ){ hook (); }) }setImmediate (main);
运行得出 flag
1 flag{f5d02d7eede3a75ee6e6cc0a9673c76f}
Frida 检测之 inlineHook -level10看 native
层的 check
函数
读取磁盘中 libc.so
文件 signal
函数的原始 8 字节数,获取内存中 signal
函数的实际代码比较二者是否一致,如果不一致程序就会退出,所以我们要模拟 fread
读取了预期数据,伪造返回值
获取 hook 前面的 8 个字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let bytes_count = 8 let address = Module .getExportByName ("libc.so" ,"signal" )let before = ptr (address)console .log ("" )console .log (" before hook: " )console .log (hexdump (before, { offset : 0 , length : bytes_count, header : true , ansi : true }));
得出
hook 脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 function hook_memcmp_addr ( ){ var memcmp_addr = Module .findExportByName ("libc.so" , "fread" ); if (memcmp_addr !== null ) { console .log ("fread address: " , memcmp_addr); Interceptor .attach (memcmp_addr, { onEnter : function (args ) { this .buffer = args[0 ]; this .size = args[1 ]; this .count = args[2 ]; this .stream = args[3 ]; }, onLeave : function (retval ) { console .log (this .count .toInt32 ()); if (this .count .toInt32 () == 8 ) { Memory .writeByteArray (this .buffer , [0xe9 , 0x7f , 0xf7 , 0x97 , 0xf4 , 0x83 , 0xe4 , 0xf0 ]); retval.replace (8 ); console .log (hexdump (this .buffer )); } } }); } else { console .log ("Error: memcmp function not found in libc.so" ); } }function hook ( ) { hook_memcmp_addr (); var targetAddress=Module .findExportByName ("libc.so" ,"strcmp" ); Interceptor .attach (targetAddress,{ onEnter :function (args ){ var input =Memory .readUtf8String (args[0 ]); if (input.includes ("aaaaaaa" )){ console .log ("FLAG:" +Memory .readUtf8String (args[1 ])); } },onLeave :function (retval ){ } }) var MainActivity =Java .use ("com.swdd.summertrain.MainActivity" ); var MainActivityInstance =MainActivity .$new(); MainActivityInstance .Check ("aaaaaaa" ); }function main ( ){ Java .perform (function ( ){ hook (); }) }setImmediate (main);
得出 flag
1 flag{a8490cd255d3a0a982fac16130183b76}