2014 NAGA&PIOWIND APP应用攻防竞赛 Crackme01
Java层用于传字符串,输入用户名和密码到Native层校验
protected void onCreate(Bundle arg3) {
super.onCreate(arg3);
this.setContentView(2130903040);
this.txt_name = this.findViewById(2131165184);
this.txt_passwd = this.findViewById(2131165185);
this.btn_login = this.findViewById(2131165186);
this.btn_reset = this.findViewById(2131165187);
this.txt_result = this.findViewById(2131165188);
this.btn_login.setOnClickListener(new View$OnClickListener() {
public void onClick(View arg7) {
String v0 = MainActivity.this.txt_name.getText().toString();
String v1 = MainActivity.this.txt_passwd.getText().toString();
if("".equals(v0)) {
System.out.println("name is null or \'\'");
MainActivity.this.txt_result.setText("账户为空");
}
else if("".equals(v1)) {
System.out.println("passwd is null or \'\'");
MainActivity.this.txt_result.setText("密码为空");
}
else {
System.out.println("name:" + v0);
System.out.println("passwd:" + v1);
System.out.println("Please treat me gently, you have to go a long way.");
MainActivity.this.txt_result.setText(MainActivity.this.crackme(v0, v1));
}
}
});
this.btn_reset.setOnClickListener(new View$OnClickListener() {
public void onClick(View arg3) {
MainActivity.this.txt_name.setText("");
MainActivity.this.txt_passwd.setText("");
MainActivity.this.txt_result.setText("");
}
});
}使用IDA查看so,发现加密了

动态调试把解密后的so文件dump出来
先查看加载的内存基址

dump脚本如下,地址需要根据自己的调试环境确定
然后再打开,可以看到代码已经还原了

先传入用户名和密码,然后转为char *类型的字符串,,接着调用两个函数sub_536C()和sub_597C()
跟入sub_536C("Failure", szUserName, szPassword)
这个函数比较简单

先传进来一个字符串指针,这个指针非常重要,后续的栈变量要使用这个字符串指针作为基址来寻找
函数sub_5328()用于初始化某些栈空间

然后有两处判断,判断传入的两个字符串是否为空
判断密码是否为空
判断用户名是否为空
申请空间
通过返回的内存分配地址来判断是否申请成功
第二处判断
接下来进行拷贝操作,存储用户名和密码,需要注意到新申请的两个变量的寻址方式为[pFailure + offset]
此时两个关键的变量在栈中的位置
初始化完栈空间以及相应的内存空间后,进入校验逻辑
传入"Failure"字符串的指针,该函数稍微有点长

存储"pFailure"后调用函数sub_53E4()
sub_53E4()主要是校验用户名和密码的长度合法性
从中我们得出用户名和密码的长度范围

校验密码的合法性,格式为xxx-xxx-xxx-xxx

调用sub_5430()
这个函数的作用是将密码中的-去掉

获取一个Table,此Table一开始是空的
全部都是00

动态运行时会填充数据,第一次运行时会进行Table的生成,通过对这个Table第一个字节的判断,如果是00,表示未生成,如果是01,表示Table已生成,则跳过初始化Table的代码段
动态运行时进行初始化

接下来逐步进行计算,将Table的[2, 256]字节赋值为0x80
开始循环赋值

赋值完成后开始处理Table,初始化一些值
从Table偏移65的位置开始赋值0,长度为26,整个表应该是偏移第67位,因为第一个字节跳过,下标从0开始

取第98位
开始赋值,赋值的数据跟着上面的R3后面继续,上面赋值到0x19,这里从0x1A开始

再次定位到49的位置
再次赋值

最后处理几个单个的位置

整个表处理完是下面这样的,因为最开始是判断是否初始化的标志,所以整个表长度为257,由于多次调试,所以下面的内存地址和上面图中可能不一样
判断处理后的密码是否为空,前面去除了密码中的-
再申请一个存储密码的内存空间
这里其实可以猜出来是Base64,因为判断3位长度,这个比较看经验了
如果没看出来,我们可以手动分析,前提是清楚Base64的计算过程,编码过程是3位转4位,还原过程是4位转3位
比如ABCD,以3个字符为一组,计算每个的ASCII十六进制
连起来
以三字节为单位切开,这样3个字符就变成了4个字符每组
前面补00,最后除了补零,最后的两个不做处理
转为十进制数字
然后到Base64编码Table里寻找对应的下标
计算出来,最后没有数据的补上=,以4字节为一组补
第一题的理解程度对于后续的解题很重要,所以我们多写点
入口判断了长度跟3的关系,长度如果不够说明已经计算到结尾,所以进入特殊处理的分支

接下来手动分析,进入解码前先进行长度的判断
初始化一个下标
然后进入计算的循环,以4字节为一组进行循环获取,获取到的4字节每字节进行查表,这个表就是前面初始化的Table
通过一个变量进行判断4字节每组内部取表操作是否完成
4字节取表完成后,进行计算
关键的三句,这已经是很明显的Base64解码操作了
接着又进行循环操作,解码完成退出循环,进入数据的存储
清理一下临时空间
再次存储数据
再清理内存
最后进入一个对比函数
sub_548C()将用户名和解码后的数据进行对比
循环对比
不相等则异常退出

所以整个校验逻辑就是,输入用户名以及用户名的Base64编码作为密码即可,编码后的数据需要每3位插入一个-
长度也需要注意范围的校验,所以简单写个Java程序来计算即可,代码写的挫,不贴了
大概就是这样

Last updated