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