SCTF ---- RE50静态分析


天孤剑同学在《IDA一日速成记》一文中介绍了IDA的使用方法,并对贪吃蛇和RE50进行了逆向分析。本文也是针对RE50的逆向分析,与天孤剑动态调试不同的是这里采用静态分析的方式,尝试通过汇编分析来还原出对应的C代码,希望对大家能有所帮助。

P.S. 许多writeup都给出了RE50的逆向分析且直指关键,但对于像我这样的小白,看到分析后只能是不明觉厉,下次遇到同样的问题可能依旧束手无策,所以本文不仅会介绍RE50,而且还会更多地去介绍关联知识,这样后续对于逆向分析也许能更快地找到切入点。

IDA绝对是逆向分析的神器,二进制文件丢进去后直接F5反编译,后续的任何问题基本就很容易分析了。不过大家手头的IDA很多都是6.6之前的版本,这些版本不支持64位程序的反编译,所以遇到64位程序时只能硬着头皮看汇编代码了。(IDA 6.6 有谁能提供一个不 ^_^)

废话少说,通过IDA64打开RE50,通过View->Open subviews我们可以查看文件中的functions、strings信息:

RE50 symbols

"The flag is SCTF..."这绝对是敏感信息,我们可以查找哪个函数中使用了它,然后定位到了sub_4006ED。IDA进行汇编分析时,通过空格可以在汇编视图和Graph视图间切换,sub_4006ED的逻辑图如下:

RE50 graph

sub_4006ED的逻辑结构比较简单,这里将它拆分为5部分进行分析。当你去仔细查看和思考时,你会收获更多 :-)

在分析(1)之前,我们先来看看函数调用和栈空间等相关概念:

这里的栈是指函数执行时的栈空间,很多人都知道栈相关的概念,但有没思考过为什么用栈呢?

其实它与我们的编程语言紧密相关,如何限定局部变量的作用域、如何实现函数调用、如何解决递归问题,栈模型给出了很好的答案。

我们要知道是:

    a.函数进行调用时,栈空间会扩张,函数结束时,栈空间会收缩

    b.函数内部的局部变量,几乎全部都保存在栈上(static的除外)

    c.栈空间的平衡,由函数自身进行保障,与调用者无关

    d.进入函数调用时的call指令会将下一条指令地址(EIP)保存在栈上

我们回到(1),汇编如下:

sub_4006ED proc near

var_60= qword ptr -60h                                    ---->IDA生成的伪代码,用于索引当前栈帧的偏移
var_54= dword ptr -54h
var_48= dword ptr -48h
var_44= dword ptr -44h
var_40= byte ptr -40h
s= byte ptr -30h
var_28= word ptr -28h
s2= byte ptr -20h
var_8= qword ptr -8

push    rbp                                               ---->将调用者的栈底指针保存在栈上
mov     rbp, rsp                                          ---->开辟当前函数的栈帧
sub      rsp, 60h                                          ---->开辟当前函数的栈空间,栈空间为60字节
mov     [rbp+var_54], edi
mov     [rbp+var_60], rsi
mov     rax, fs:28h                                       ---->在第五部分会提到它
mov     [rbp+var_8], rax
xor       eax, eax
mov     rax, 366475466733724Ah                            ---->在rbp+s和rbp+var_28处,写入一些数据
mov     qword ptr [rbp+s], rax
mov     [rbp+var_28], 6Eh
mov     edi, offset format ; "input your password:"
mov     eax, 0
call       _printf
lea       rax, [rbp+var_40]                                 ---->获取用户输入字符串,保存在rbp+var_40处
mov     rsi, rax
mov     edi, offset aS  ; "%s"
mov     eax, 0
call       ___isoc99_scanf
lea       rax, [rbp+s]                                      ---->获取rbp+s处字符串的长度(这里可能是出题者弄错了)
mov     rdi, rax        ; s
call       _strlen
mov     [rbp+var_44], eax
cmp     [rbp+var_44], 9
jz          short loc_400760

我们书写自己的函数,对上面代码进行还原:

int sub_4006ED()
{
    char s2[16];
    char s[16];
    char var_40[16];
    short *var_28 = (short *)(s + 8); 
    int  var_44, var_48;    

    *(long long *)s = 0x366475466733724A; 
    *var_28 = 0x6e;

    printf("input your password:");
    scanf("%s", var_40);

    var_44 = strlen(s);
    if (var_44 == 9) {
    
    }
    
    return 0;
}

栈上buf的大小取决于函数内部的局部变量:

    1.用s[16],并假设指针s对应栈上地址rbp-30。var_28 - s = 8,那么var_28对应s + 8。

    2.用var_40[16],并假设指针var_40对应栈地址rbp-40。scanf输入的字符串保存在var_40指向的buf中。

    3.经过前面的设置,strlen(s)结果保存到var_44中,它必然等于9(除非scanf输入的数据超过16字节,将s尾部写坏了),所以我觉得这里是否是strlen(var_40),出题时这里弄错了吗?

接下来我们看(2),汇编如下:

loc_400760:                             ; CODE XREF: sub_4006ED+6Aj
                lea     rax, [rbp+s2]
                mov     edx, 0Ah        ; n
                mov     esi, 0          ; c
                mov     rdi, rax        ; s
                call    _memset
                mov     [rbp+var_48], 0
                jmp     short loc_40079B

它的作用是调用memset函数,其中内存地址为rbp+s2,设置值为0,长度为10,即 memset(s2, 0, 0xa)。然后它将var_48设置为0。

第(3)部分稍微长一点,也是很多writeup中指明的关键部分,这里我们看它的逻辑图进行分析:

var_48初始化为0,若小于var_44(前面设置为了9),则将var_40中的字符拷贝到s2中,然后var_48加一。

仔细思考下,这不是一个for循环吗:

		for (var_48 = 0; var_48 < var_44; var_48++)
    *(s2+var_48) = *(var_40+var_48) + 3;

ok,我们继续看第(4)部分:

                lea     rdx, [rbp+s2]
                lea     rax, [rbp+s]
                mov     rsi, rdx        ; s2
                mov     rdi, rax        ; s1
                call    _strcmp
                test    eax, eax
                jz      short loc_4007C1
                mov     eax, 0
                jmp     short loc_4007DC

很简单,比较字符串s与s2,若s与s2相等,则跳转到最后的printf进行输出,若不相等,则返回。

至此,汇编基本分析完成,我们可以写出sub_4006ED的代码如下:

int sub_4006ED()
{
    char s2[16];
    char s[16];
    char var_40[16];
    short *var_28 = (short *)(s + 8); 
    int  var_44, var_48;    

    *(long long *)s = 0x366475466733724A; 
    *var_28 = 0x6e;

    printf("input your password:");
    scanf("%s", var_40);

    var_44 = strlen(s);

    if (var_44 == 9) {
        memset(s2, 0, 0xa);
        for (var_48 = 0; var_48 < var_44; var_48++)
            *(s2 + var_48) = *(var_40 + var_48) + 3;

        if(strcmp(s2, s) == 0)
            printf("The flag is SCTF{%s}\n", var_40);
    }   

    return 0;
}

等等,第(5)部分呢?

loc_4007DC:                             ; CODE XREF: sub_4006ED+71j
                                        ; sub_4006ED+D2j
                mov     rcx, [rbp+var_8]
                xor     rcx, fs:28h
                jz      short locret_4007F0
                call    ___stack_chk_fail

还记得(1)在开辟栈空间后做的一个特殊处理吗:

mov     rax, fs:28h
mov     [rbp+var_8], rax

我们都知道栈溢出漏洞,其最直观的利用方式就是覆盖栈顶的EIP指针,使其指向JMP ESP指令。其原理是:当函数返回时retn会pop前面压入栈中EIP的指针,通过栈溢出使得EIP被覆盖(由输入控制),从而实现对程序流程的劫持(比如JMP ESP会跳转到栈上的shellcode区域进行执行)。

这里通过将fs:28h处的随机值写到栈上,在函数返回时进行栈上保存值的校验,若栈被写坏那么这里的值会发生变化,会进入stack_check_fail函数做异常处理,达到对栈溢出攻击的防护。

P.S. 栈溢出检查是由编译器自动完成的,并不需要在代码中显示书写。

关于栈溢出攻击,可参考这篇文章:http://my.oschina.net/sincoder/blog/115944

受文章篇幅限制,许多细节处没进行扩展,读者若感兴趣可进行自行查阅,我觉得比较有趣的是Linux中的栈模型(栈的扩展和收缩,如何保持栈平衡等)、x64相关的汇编指令和寄存器信息等。如果文章中有不对的地方,也欢迎大家进行指正,谢谢~

SCTF相关题目和程序,我们进行了收集整理,见这里:http://pan.baidu.com/s/1eQCeitW

P.S. IDF实验室目前在收集整理CTF相关的工具,感兴趣的朋友可以看这里:https://github.com/woldy/CTF-Tools

你可以将自己喜欢的工具告知我们,也可以将安装包或下载地址发送给我们。我们收集整理后,会维护一份list,并将程序放到百度网盘中提供给对CTF感兴趣的小伙伴们~

Author:IDF实验室 cumirror

Mail:tongjinam#qq.com

(全文完)


评论



个人信息

求注册,求登录!

解题动态

精选练习

精选教程

联系我们