最近在做XCTF新手区的题目,发现了这个Ph0ne1x-100这道题十分的有意思,这道题的难度不高,但是十分适合新手,它的解题思路多种多样,不仅可以动态分析,还可以静态分析,包括Hook,改源码等技巧。
经过最近的研究,我通过这道题也研究出五种解法,其中有一些方法的思想相同,只是角度和难度不同,希望可以给大家提供一些思路。
- 老规矩,安装上去看一下,有一个输入flag的窗口,登录显示Fail:
- 再扔到JEB中打开,这个APK有SO库,主要类就是MainActivity,BuildConfig里面Debug为false,MainActvity中加载了库phcm,接下来是两个类库中的关键函数encrypt和getFlag,MainActvity中有一个getSecret函数通过调用那两个native函数完成加密:
-
分析一下MainActivity中的getSecret函数,这个函数参与运算的外来参数只有一个传入参数,这就意味着如果我们在放入getsecurity之前获得了flag,不经过getsecurity也能直接比较,那么这道题的主要逻辑就是:
内置的字符串和函数通过setflag加工,生成加密密文。输入的flag通过encrypt进行加密生成密文,最后这两个密文是一致的,所以接下来我们就要先通过setflag进行加工,生成密文,再逆向encrpt加密生成flag。
-
接下来看so库的文件:
- getFlag如下,主要是getFlag没有输入,这就意味着这里面使用的是内置的参数进行的加密,看起来挺简单的,但是里面有一个unk_2784并没有被成功解析出来,但是v10又被用于接下来的计算,unk_2784内存处是一些无法理解的东西,这就导致了静态分析陷入了僵局:
- 静态分析失败,接下来转为动态,既然v10进行复制的是内存的一段,我们就在动态运行时截取下来getflag的返回值这一段进行读取,再放入encrypt进行逆向分析,解析出需要我们输入的Flag
动态分析1,借用maketext输出:
- 先找到MainActivity中调用getFlag的函数:
- 拷贝到下方输出Failed的位置,覆盖掉原有的Failed。
``c++ string ciphertext = "ek
fz@q2^x/t^fn0mF^6/^rbqanqntfg^E
hq|";
for(int x=0; x < ciphertext.length(); x++){
ciphertext[x] = ciphertext[x] + 1;
}
cout<<ciphertext;
输出:
动态分析二:
使用AndroidStudio进行分析:
- 创建一个remote,对APK拆包出来的APK中的smali文件夹导入到项目中,并设置成resourceroot
- 接下来在虚拟机上运行smali文件,并attach到进程:
- 将断点打在MainActivity的getFlag之后,传入getsecret函数之后,单步跳入查看传入getsecret函数的string值:
动态分析三:
使用Frida框架对So库getFlag()进行So层Hook操作:
Hook代码:
```python
import frida
import sys
jscode = """
Java.perform(function(){
Interceptor.attach(Module.findExportByName("libphcm.so","Java_com_ph0en1x_android_1crackme_MainActivity_getFlag"),{
onEnter: function(args) {
},
onLeave: function(retval){
var String_java = Java.use('java.lang.String');
var args_4 = Java.cast(retval, String_java);
send("getFlag()输出信息为:"+args_4);
}
});
});
"""
def printMessage(message,data):
if message['type'] == 'send':
print(' {0}'.format(message['payload']))
else:
print(message)
process = frida.get_usb_device().attach('Android_crackme')
script = process.create_script(jscode)
script.on('message',printMessage)
script.load()
sys.stdin.read()
输出结果:
静态分析补充:
- 刚刚遇到了无法解决的unk_2748,经过动态分析我们知道了这个每次都是不变的,那么我们可以直接将这40个按int型抄下来,再转化成char返回给v10,代码如下:
```c++
include<iostream>
using namespace std;
include<stdlib.h>
int main(){
int v2; // r0
int v3; // r3
char *v4; // ST04_4
char v5; // ST08_1
int v6; // r1
int v8; // [sp+0h] [bp-68h]
char v9[16];
char v10[40];
int v11[40] = {0x2E, 0x36, 0x42, 0x4C, 0x5F, 0xBF, 0xE0, 0x3A, 0xA8, 0xC3, 0x20, 0x63,
0x89, 0xB7, 0xC0, 0x1C, 0x1D, 0x44, 0xC2, 0x28, 0x7F, 0xED, 0x02, 0x0E, 0x5D, 0x66, 0x8F, 0x98,
0xB5, 0xB7, 0xD0, 0x16, 0x4D, 0x83, 0xF8, 0xFB, 0x01, 0x43, 0x47, 0x00};
strcpy(v9,"Hello Ph0en1x");
//memcpy(v10, &v11, 40);
//memcpy(v10, &v9, 40);
for(int x=0; x<=40;x++){
v10[x] = v11[x];
}
//cout<<strlen(v10);
v8 = strlen(v9);
v2 = strlen(v10) - 1;
while ( v2 > 0 )
{
v3 = ((unsigned __int8)v10[v2]++ + 1) & 0xFF;
v4 = &v10[v2 - 1];
v5 = v3 - v10[v2 - 1];
v6 = v2-- % v8;
v4[1] = (v9[v6] ^ v5) - 1;
}
v10[0] = (v10[0] ^ 0x48) - 1;//至此获得了用于比较的字符数组v10,长度为40
//接下来是反向处理
string str(v10);
//cout<<str;
string ciphertext = "ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq|";
for(int x=0; x < ciphertext.length(); x++){
ciphertext[x] = ciphertext[x] + 1;
}
cout<<ciphertext;
}
动态分析4:
使用Frida对Java层函数getSecret()函数进行Hook操作:
```python
import frida
import sys
jscode = """
if(Java.available){
Java.perform(function(){
var MainActivity = Java.use("com.ph0en1x.android_crackme.MainActivity");
//找到getSecret函数,将输入参数str返回即可
MainActivity.getSecret.implementation=function(str){
send("getFlag()输出信息为:"+str);
return str;
}
});
}
"""
def printMessage(message,data):
if message['type'] == 'send':
print(' {0}'.format(message['payload']))
else:
print(message)
process = frida.get_usb_device().attach('Android_crackme')
script = process.create_script(jscode)
script.on('message',printMessage)
script.load()
sys.stdin.read()
FLAG:flag{Ar3_y0u_go1nG_70_scarborough_Fair}