XCTF_MOBILE17_Android2.0

初见

附件为一个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}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值