《逆向工程核心原理》学习笔记4 5

《逆向工程核心原理》学习笔记4

逆向工程核心原理第七章

准备阶段
  • 软件:Ollydbg,visual studio community

  • 使用visual studio创建一个名为StackFrame的c++控制台项目,粘入以下代码,然后运行生成x86(Win32)的StackFrame.exe文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include "stdio.h"
    long add(long a, long b) {
    long x = a, y = b;
    return (x + y);
    }
    int main(int argc, char* argv[]) {
    long a = 1, b = 2;
    printf("%d\n", add(a, b));
    return 0;
    }
栈帧
  • 简单来说,栈帧是利用EBP(栈帧指针,注意不是ESP)寄存器访问栈内局部变量、参数、函数返回地址等的手段

  • 跟据前面所学知识,我们知道ESP为栈顶指针,它会随着数据出入栈,而不断改变,如果以它为基准来访问函数的局部变量、参数,那么编写程序将十分困难,这时候就要用到EBP栈帧指针来解决这一问题。调用函数时,把函数起始地址的ESP值记录到EBP当中,这样函数运行时无论ESP怎么变化,用EBP作为基准值能够安全访问函数的局部变量、参数、返回地址,这就是EBP这个寄存器作为栈帧指针的作用

  • 栈帧对应的汇编代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    PUSH EBP				; 函数开始(使用EBP前先把已有值保存到栈中)
    MOV EBP, ESP ; 保存当前的ESP(函数起始地址)到EBP中

    ... ; 函数体


    MOV ESP, EBP ; 将函数的起始地址返回到ESP中
    POP EBP ; 函数返回前,将最开始保存在栈中的EBP值恢复至EBP
    RETN ; 函数终止
  • 下面用图片来描述上面汇编代码发生变化时,EBP和ESP的变化,测试使用的exe为上述编译的StackFrame.exe

    • 函数开始时,ESP指向"0019FE78"地址,该地址储存着"00B320B3"地址,"00B320B3"地址是当前函数执行完后的返回地址,EBP则指向"0019FE94"地址,这是调用者所在函数(也就是上一个函数)的基础地址

      image-20211123153654338

      截屏2021-11-23 下午3.18.09

    • 接下来,我们执行push ebp,可以看到,由于执行了PUSH命令,ESP栈顶指针往栈顶方向移动,ESP指向的地址变为"0019FE74","0019FE74"所储存的则为我们EBP指向的地址"0019FE94"

      image-20211123154418096

      截屏2021-11-23 下午3.19.40

    • 接着运行mov ebp, esp,可以看到,EBP指向了ESP所指向的"0019FE74"地址,这样当前函数的栈帧就生成了(设置好了EBP)

      image-20211123154907891

      截屏2021-11-23 下午3.20.02

    • 接下来运行sub esp, 0xD8,模拟执行函数体,改变ESP的值,查看EBP和ESP的编号情况

      image-20211123155609652

      截屏2021-11-23 下午3.22.16

    • 可以看到执行函数体时,ESP的值会发生改变,但EBP的值则不变,因此可以用EBP为基准点来访问局部变量、函数参数。因为局部变量和函数参数储存的位置就在EBP和ESP所指的两块地址之间

    • 接着我们nop掉中间其他语句,直接测试函数返回时,EBP和ESP的变化情况

    • 运行mov esp, ebp,可以看到,此时ESP重新指向了"0019FE74"地址

      image-20211123160345570

      截屏2021-11-23 下午3.26.29

    • 执行pop ebp,可以看到,EBP和ESP又回到了函数运行前的状态

      image-20211123160827249

      截屏2021-11-23 下午3.28.34

    • 最后我们执行RETN,可以看到,函数返回到了"00B320B3"所对应的代码语句处

      截屏2021-11-23 下午3.30.32

    • 此时EBP和ESP的值则如下图所示,由此可知,函数运行RETN时,所对应的操作是从栈中弹出函数返回地址,让CPU从该处开始执行,所以ESP才会往POP方向移动

      image-20211123161317268

  • 小结:学习了有关栈帧的内容,了解了函数开始运行和结束运行时ESP和EBP两个寄存器的运作机制,又多学会了两个寄存器。

《逆向工程核心原理》学习笔记5

逆向工程核心原理第八章

abex' crackme2
  • 首先运行软件,了解一些软件基本信息

    image-20211124142604406

  • 可以看到软件需要注册的序列号,其中名字长度还要大于4,根据这些信息,我们接下来的调试就简单多了

    image-20211124142718701

image-20211124142832266

VisualBasic程序特征
  • abex's crackme2文件由VB编写的,这里了解下VB程序的文件特征

  • VB使用名为MSVBVM60.dll(Microsoft Visual Basic Virtual Machine 6.0)的VB专用引擎,又称(The Thunder Runtime Engine)

  • VB使用例子:显示消息框时,VB代码中要调用MsgBox函数,其实,VB编辑器真正调用的是MSVBVM60.dll里的rtcMsgBox()函数,在该函数内部通过调用user32.dll里的MessageBoxW()函数(Win32 API)来工作,此外也可以在VB代码中直接调用user32.dll里的MessageBoxW()。用图片来描述的话就像下图一样,相当于在开发者代码和底层代码user32.dll之间加了一层MSVBVM60.dll,而且也保留了开发者代码直接调用底层代码的方式。

    image-20211124144122133

  • 根据便于选项不同,可以将VB编译为本地代码(N Code)和伪代码(P Code)。

  • VB程序采用Windows操作系统的事件驱动方式工作,因此main()或WinMain()中并不存在用户代码,用户代码存在于各个事件处理程序(event handler)之中

  • 接下来开始调试,用OD打开abex's crackme2.exe查看反汇编文件,可以看到EP地址为"00401238",该命令将把VB的RT_MainStruct结构体地址"00401E14"压入栈,然后"0040123D"地址处命令call 00401232调用"00401232"处的jmp dword ptr ds:[0x4010A0]指令,该指令会跳转至VB引擎主函数ThunRTMain()(前面压入栈的值作为函数参数),这3行代码就是VB的全部启动代码。

    image-20211124145709950

间接调用
  • "0040123D"地址处的CALL命令用于调用ThunRTMain()函数,但它不是直接调用,而是通过中间"00401232"地址处的JMP命令跳转,这是VC++、VB编译器中常用的间接调用法(Indirect Call)
RT_MainStruct结构体
  • ThunRTMain()函数的参数为RT_MainStruct结构体,RT_MainStruct结构体存在于地址"00401E14"处,微软未公开这个结构体的细节,但有逆向高手已经完成了对这个结构体的分析。

    image-20211124151422774

  • 《逆向工程核心原理》书中没有直接提及RT_MainStruct结构体的信息,谷歌直接搜索"RT_MainStruct",发现了一篇VB的逆向文章,可能就是书中所提到的分析,VISUAL BASIC REVERSED - A decompiling approach,这里先记下链接,关于RT_MainStruct的细节,有待后面学习补充。

分析crackme
  • 接下来我们跳过ThunRTMain()函数,来分析crackme的代码

  • 要定位目标代码,有多种方式,这里采用检索字符串方式,因为前面运行软件时,我们已经知道了软件包含了哪些字符串,右键代码窗口选择 中文搜索引擎=>智能搜索,可以看到软件的所有字符串信息如下

    image-20211124152406392

  • 我们直接双击Wrong serial!查看引用该字符串的代码位置,可以看到,标题("Wrong serial!"),内容("Nope, this serial is wrong!"),以及实际调用消息框函数代码(004034A6)都显示出来了

    image-20211124153235226

  • 从编程角度来看,这种校验序列号软件,一般根据某种算法生成序列号,然后将用户输入的序列号进行比对,如果相同则跳转正确,否则跳转错误。根据这个思路,我们向上查找相应代码。

  • 按照上面思路查找,果然发现了条件语句分支的地方,而且不止一处,一共有两次判断两个字符串是否相等的地方,分别位于"00403332"(调用__vbaVarTstEq()函数)和"00403424"(调用__vbaVarTstNe()函数),从参数来看两个比较的是同两个参数,但是两个函数逻辑相反,前者判断的是否相等,后者是否不相等。

    image-20211124154042910

    image-20211124154139598

  • test指令说明:逻辑比较,与bit-wise logical 'AND'一样(仅改变EFLAGS寄存器而不改变操作数的值,若两个操作数中一个为0则AND运算结果被置为0=>ZF=1),je指令:jump if equal,若ZF=1,则跳转

  • 这里顺便学习下EFLAGS寄存器(标志寄存器)的内容,EFLAGS有32个位元,全部记住比较困难,其中最重要的有ZF(Zero Flag, 零标志)、OF(Overflow Flag,溢出标志)、CF(Carry Flag,进位标志),这三个指令之所以重要是因为在某些汇编指令,特别是Jcc(条件跳转)指令中要检查这3个标志的值,并根据它们的值决定是否执行某个操作

    • ZF:若运算结果为0,则其为1(True),否则其为0(False)
    • OF:有符号整数(signed integer)溢出时,OF值被置为1。此外,MSB(Most Significant Bit,最高有效位)改变时,其值也被设为1
    • CF:无符号整数(unsigned integer)溢出时,CF被设为1
  • 分析到这其实就可以先直接爆破crackme,爆破这个crackme很简单,只需要nop掉第一个跳转语句然后将第二个跳转语句"je"改为"jmp",这样输入任意序列号都会提示成功。(如果只修改第一跳转,那么程序虽然会提示成功,但点确定后又会再弹一次失败的窗。)

  • 还有一种方式能达到破解效果就是,在条件语句前下断点,调试运行,输入任意序列号,点确定,然后就可以在右下角栈窗口查看得到正确的序列号。下图为输入Name:"CCCC",Serial:"11111",后的结果,可以看到,比较的两个字符串为"A7A7A7A7"和"11111",因为Name全是单一字符"C",序列号全是"A7",所以基本可以确定,这个软件的序列号生成算法中"C"映射为"A7"。

    image-20211124185309910

  • 这里决定继续分析,找出软件的序列号的生成算法的位置

  • 要找出序列号生成算法位置,当然得从函数的起始位置开始找起,根据之前学习的栈帧概念,我们继续往上找,直到找到push ebpmov esp, ebp这两行代码为止,这样就可以找到函数的起始位置"00402ED0"

    image-20211124190104892

  • 虽然找到了函数的起始地址,但是这么多行代码,而且还有好几处调用其他函数的代码,该怎么找到生成序列号的算法呢?这里可以输入的Name为思路去入手,因为序列号是随Name变化而变化的,说明生成序列号的算法用到了Name中的字符串,我们只要调试时时刻注意栈内存的变化,什么时候栈内存出现了(获取了)Name的字符串,那么从该处开始,往下的代码就离我们要找的生成序列号算法不远了

  • 按照上面的思路,我们逐步单步测试,并注意dump窗口或者栈窗口的数据变化。由于VB中,字符串是使用字符串对象(与C语言使用char数组不同),直接在dump窗口查看内存,很难认出实际的字符串,需要右键选择 长型=>ASCII数据地址。栈窗口则不用这么设置,可以直接认出字符串。

    image-20211124192820218

    image-20211124192505933

  • 最终我们在运行完"00402F98"的call命令后,内存中出现了我们输入的Name:"CCCC",找到这个基准点后,离我们要找的序列号生成算法代码就不远了

  • 继续往下调试,在"0040300F处"我们可以看到,调用了__vbaLenVar函数,这部分代码代码逻辑就是判断用户序列号长度是否小于4的地方,离序列号算法更近了一步

    image-20211124194613294

  • 继续调试,我们会遇到下面这个循环,这个循环就是我们要找的序列号生成算法的位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    0403102   .  BB 04000000     mov ebx,0x4	; EBX = 4 (循环次数)
    ...
    0040318B . FF15 30104000 call dword ptr ds:[<&MSVBVM60.__vbaVarForInit>>; \__vbaVarForInit(For循环初始化)
    00403191 . 8B1D 4C104000 mov ebx,dword ptr ds:[<&MSVBVM60.#632>] ; msvbvm60.rtcMidCharVar
    00403197 > 85C0 test eax,eax ; 循环开始
    00403199 . 0F84 06010000 je abexcrac.004032A5 ; 符合条件则跳出循环
    ...
    0040329A . FF15 C0104000 call dword ptr ds:[<&MSVBVM60.__vbaVarForNext>>; \__vbaVarForNext(下一个循环)
    004032A0 .^\E9 F2FEFFFF jmp abexcrac.00403197 ; 循环结束
    004032A5 > 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
  • 继续分析这部分代码的话能得出以下序列号生成算法

    1
    2
    3
    4
    5
    取用户输入的Name前4位字符
    将每位字符先转为ASCII码值
    再加上偏移量0x64得到新的16进制值
    将16进制转为字符串
    拼接所有生成的字符串得到序列号
  • 值得一题的是这篇文章很有参考意义VISUAL BASIC REVERSED - A decompiling approach,这篇文章末尾提到,VB中api函数的参数和返回值很多都是对象(Object)而不是具体的值(Value),因此当你分析VB的函数参数和返回值时,你可以考虑把它当作对象来分析

posted @ 2023-03-22 20:33  bonelee  阅读(223)  评论(0编辑  收藏  举报