初见
附件为一个apk,无提示信息。
安装apk并打开:
随便输入一个字符串,点击确认:
好大的错误信息。
从使用上看,就获得这些信息,关键在“确认”按键的按下处理函数中。
接下来静态分析看看。
静态分析
使用jadx加载apk,除了R、BuildConfig、JNI就剩下一个MainActivity类。
MainActivity类代码也很简单,按钮点击响应函数为:
this.button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
MainActivity.this.Show(JNI.getResult(MainActivity.this.pwd.getText().toString()));
}
});
就是以输入字符串为参数,调用JNI函数getResult。再以JNI函数的返回值为参数,调用Show:
public void Show(int type) {
switch (type) {
case 0:
this.textView.setText("Wrong");
return;
case 1:
this.textView.setText("Great");
return;
default:
return;
}
}
看来我们需要getResult函数返回1。
下面我们来看看JNI函数getResult。
getResult
将apk后缀名改为zip,解压。
在/lib/armeabi-v7a目录下可以找到JNI库:libNative.so。
使用IDA打开,找到Java_com_example_test_ctf03_JNI_getResult函数。
分析发现这原来是个算法题,不过算法也不复杂。该函数中重要部分代码如下:
bool __fastcall Java_com_example_test_ctf03_JNI_getResult(int a1, int a2, int a3)
{
v3 = 0;
if ( strlen(str_in) == 15 )
{
v5 = (char *)malloc(1u);
v6 = (char *)malloc(1u);
v7 = (char *)malloc(1u);
//1111111111111111111111111111111111111111111
Init(v5, v6, v7, str_in, 15);
//2222222222222222222222222222222222222222222
if ( !First(v5) )
goto LABEL_6;
//33333333333333333333333333333333333333333333
for ( i = 0; i != 4; ++i )
v6[i] ^= v5[i];
//44444444444444444444444444444444444444444444
if ( !strcmp(v6, byte_2888) )
{
//5555555555555555555555555555555555555555555555
for ( j = 0; j != 4; ++j )
v7[j] ^= v6[j];
//6666666666666666666666666666666666666666666666
v3 = strcmp(v7, byte_288E) == 0;
}
else
{
LABEL_6:
v3 = 0;
}
}
return v3;
}
我用注释将上面代码分隔为几部分,方便讲解。
可以看出输入字符串长度应该为15。
从参数看,最上面的Init函数应该对输入字符串进行了处理,并填充v5、v6、v7的内容,因为输入字符串之后再没使用过。先看看这个函数。
Init
该函数伪代码为:
char *__fastcall Init(char *v5, char *v6, char *v7, const char *str, int strlen)
{
int i; // r5
int v16; // r10
int v17; // r6
if ( strlen < 1 )
{
v16 = 0;
}
else
{
i = 0;
v16 = 0;
do
{
v17 = i % 3;
if ( i % 3 == 2 )
{
v7[i / 3u] = str[i];
}
else if ( v17 == 1 )
{
v6[i / 3u] = str[i];
}
else if ( !v17 )
{
++v16;
v5[i / 3u] = str[i];
}
++i;
}
while ( strlen != i );
}
v5[v16] = 0;
v6[v16] = 0;
v7[v16] = 0;
return v5;
}
这个函数就是将输入字符串的0/3/6/9/12字节给v5,1/4/7/10/13字节给v6,2/5/8/11/14字节给v7。
反过来看,知道了调用Init后v5、v6、v7内容,也就知道了输入字符串的内容。
下面分别推导v5、v6、v7。
v5
在注释2和注释3之间,以v5为参数调用了First函数,该函数伪代码为:
bool __fastcall First(char *a1)
{
int i; // r1
for ( i = 0; i != 4; ++i )
a1[i] = (2 * a1[i]) ^ 0x80;
return strcmp(a1, byte_1074) == 0;
}
该函数需要返回1,也就是最后a1内容和byte_1074内容相同。
a1变为byte_1074内容之前,经过的变换就是将前4字节乘以2并异或0x80。
逆变换就是先异或0x80再除以2。
据此,我们就可以得到First的参数a1,也就是Init后v5的内容。
并且调用完First函数,v5的内容为byte_1074。
v7
回到getResult函数继续向下看。
这里需要从后往前,逆推。
先看注释6下面的:
v3 = strcmp(v7, byte_288E) == 0;
v3是返回值,需要为1,所以strcmp函数需要返回0。也就是v7和byte_288E相等,为:
.rodata:0000288E byte_288E DCB 0x41, 0x46, 0x42, 0x6F, 0x7D
再看注释4和注释5之间:
if ( !strcmp(v6, byte_2888) )
这个 if 判断需要为真,也就是strcmp返回0,也就是在注释4和注释5之间,v6内容和byte_2888相同,为:
.rodata:00002888 byte_2888 DCB 0x20, 0x35, 0x2D, 0x16, 0x61
现在,我们知道了注释5之前的v6,知道了注释6之后的v7,注释5和6之间,是将v6和v7的前4个字节异或并赋值给v7,也就是注释5之前,v7的内容为注释6之后的v7异或v6。
总结一下,到注释4为止:
- v7的前4字节为byte_288E和byte_2888的异或,第5字节为byte_288E的第5字节。
- v6内容为byte_2888
v7在注释4之前没有再被修改过,至此我们就得到了Init后v7的内容。
v6
注释3和注释4之间,v5的前4字节与v6进行了异或,并赋值给v6。
再注释4时,v6内容为byte_2888。注释3时,v5内容为byte_1074。
可以得到注释3时,v6内容前4字节为byte_2888与byte_1074的异或,第5字节为byte_2888第5字节。
而注释3之前再没修改过v6,这里就得到了Init后的v6内容。
计算v5、v6、v7脚本
计算这三个值的python脚本为:
def main():
byte_288E = [0x41, 0x46, 0x42, 0x6F, 0x7D]
byte_2888 = [0x20, 0x35, 0x2D, 0x16, 0x61]
v7 = [0, 0, 0, 0, 0]
for i in range(4):
v7[i] = byte_2888[i] ^ byte_288E[i]
v7[4] = byte_288E[4]
byte_1074 = [0x4C, 0x4E, 0x5E, 0x64, 0x6C]
v5 = [0, 0, 0, 0, 0]
for i in range(4):
v5[i] = (byte_1074[i] ^ 0x80) >> 1
v5[4] = byte_1074[4]
v6 = [0, 0, 0, 0, 0]
for i in range(4):
v6[i] = byte_1074[i] ^ byte_2888[i]
v6[4] = byte_2888[4]
print('v5:%x-%x-%x-%x-%x' % (v5[0], v5[1], v5[2], v5[3], v5[4]))
print('v6:%x-%x-%x-%x-%x' % (v6[0], v6[1], v6[2], v6[3], v6[4]))
print('v7:%x-%x-%x-%x-%x' % (v7[0], v7[1], v7[2], v7[3], v7[4]))
if __name__ == "__main__":
main()
运行结果为:
之后我们按列,竖着拼接,即可得到flag:flag{sosorryla}