1.附加前言

  最近做了几道CTF中的REVERSE(即逆向工程)的题型,这些题目大多来自平台“攻防世界”(详情谷歌或百度),随后我在网络对比了一下同样一道题目不同人的做法,至少在目前看来网络上大部分博主的做法是先将程序拖到IDA中读取,然后按F5(IDA中自带的插件)把程序的汇编代码转换成高级语言伪代码(一般是C/C++的伪代码)来分析。这样做确实简单了,但是实际上还是分析高级语言代码而不是汇编语言代码,把高级语言再复习了一遍,那么汇编当中的语法以及常见指令的用法还是不熟悉。因此我认为从小白逐渐进步为高手的过程中不能让自己太舒适——只会将程序反编译成伪代码,而要学会一步一步分析汇编语言代码。

  例如有的程序会加壳,手动脱壳的过程中你还能用老办法恢复伪代码吗?很明显,不行。加壳后直接分析得到的根本不是正确的汇编代码,更别说恢复伪代码了。要知道伪代码是基于汇编代码的呀。所以上文提到的“学会分析汇编代码”显得尤为重要。这也就是网上虽然有解题过程,但我还是要再写出的原因(至少在笔者这些文章中通过分析汇编代码得到flag的)。

  这篇文章比较长,所以笔者做了一个目录,望读者耐心阅读!

2.题目描述

  描述:暂无

  来源:高校网络信息安全运维挑战赛

  场景:暂无

  附件:名为“fac4d1290e604fdfacbbe06fd1a5ca39.exe”的可执行文件

3.文件分析

  同之前一样,这也是C/C++的控制台应用程序。没有加壳。

  程序打开之后,提示“Give me your flag:”,输入错误后退出。其实拖动到Windows cmd中运行发现,退出之前还输出一段字符串“Sorry, keep trying!”提示输入错误。

  老套路就是如果输入成功就会提示有关成功的字样。

4.逆向分析

  还是将这个exe文件放到IDA中逆向分析,找到入口函数_main

  进入了字符串长度判断的代码段,下图方框所示。

  先判断字符串长度有没有小于1Eh(30)个字符,再判断字符串长度是否大于4。也就是说字符串长度大于30或小于4则输入错误。【注:strlen函数调用后一般会把字符串长度返回给eax寄存器】

4.1 前4个字符

  根据上面的分析可以先随机打5个字符,笔者这里输入的“abcde”。再观察程序的情况。

  这里的[ebp+var_80]存放的正是刚刚输入的字符串“abcde”,[ebp+var_80]是什么意思呢。ebp寄存器中存放的是某个内存空间的地址,var_80是偏移量,其中var_80 = byte ptr -80h(在main函数汇编代码的开头中就有)。在调试过程中ebp寄存器的值为19FF30h,所以ebp+var_80 = 19FF30h - 80h = 19FEB0h。在下图就可以看到。

调试过程中ebp寄存器的值

ebp+var_80 = 19FEB0h,为输入字符串首地址

  所以[ebp+var_80]代表的就是字符串“abcde”。方括号 [] 表示取该地址中的值。

  进一步调试发现[ebp+var_88]中的前4个字符为“EIS{”(不含引号)

  上图的代码块1是表明比对所输入字符串的前4个,代码块2则是比较前四个字符是不是“EIS{”。代码块2中有这几行值得特别注意。

......
movsx   edx, [ebp+ecx+var_80]
......
movsx   ecx, [ebp+eax+var_88]
cmp     edx, ecx
jz      short loc_40111E

  在第一次操作时,edx寄存器存放“abcde”字符数组的首个元素“a”,ecx寄存器存放“EIS{”字符数组的首个元素“E”,随后比较这两个首元素(cmp edx, ecx)。然后计数加一,以此这样循环直到判断完前4个字符为止。其中[ebp+var_8C]首位是计数器存放的空间,所以会有语句mov ecx, [ebp+var_8C](计数值赋给ecx寄存器);然后下一句movsx edx, [ebp+ecx+var_80],ecx存放的计数值被用作偏移量加到[ebp+ecx+var_80] 中。这里ecx的值在循环过程中不断增加1,所以把要读取的内存空间向后偏移1位,即读取下一个字符。

  再进一步调试分析。很明显输入的“abcde”不符合要求,因为前4个字符“abcd”不是“EIS{”。

4.2 之后的字符

  上一小节输入的“abcde”不符合要求,为了拿到flag,所以在接下来的调试过程中输进程序的前4个字符一定是“EIS{”。笔者此次输入“EIS{abc”

  然而调试发现下图中红框的这段代码实在是莫名奇妙。

loc_401120:
    movsx   edx, [ebp+var_64]
    cmp     edx, 7Dh
    jz      short loc_40114B

  明明输入了“符合要求”的字符串,为什么[ebp+var_64]中的值为CCh?而且与7Dh比较呢?CCh与7Dh比较不相等,程序又提示失败……

  目前我们已知ebp+var_80的空间存放了我们输入字符串的首个字符。那ebp+var_64对应的又是什么?此时可以再看看IDA下面的十六进制查看器。

  再次说明一下ebp寄存器以及相关的值:

ebp: 19FF30h

ebp+var_80:  19FF30h - 80h = 19FEB0h

ebp+var_64:  19FF30h - 64h = 19FECCh

  这样看来,如果字符串足够长,那么[ebp+var_64]是不是会有一个值呢?同时不要忘记了这个值还要同7Dh比较(cmp edx,7Dh),7Dh = 125,在ASCII表中字符为“}”。推断[ebp+var_64]应该是最后一个字符,因为笔者之前输入的“EIS{abc”太短所以[ebp+var_64]对应的空间没有初始化——为CCh。那么合法的flag字符串长度应为 (ebp+var_64) - (ebp+var_80) + 1 = 19FECCh - 19FEB0h + 1 = 1Dh = 29

  小结一下,合法的flag字符串长度应该大于4,小于30,同时字符串长度最好是29,最后一个字符是“}”

4.3 后续的字符变换

  根据以上小节4.2得知flag的格式是这样的“EIS{…………}”。

  总计29个字符,除去“EIS”和花括号“{}”的5个长度外,省略号处应输入24个字符。所以笔者输入的测试字符串为“EIS{abcdefghijklmnopqrstuvwx}”。此后会进入一个关键的函数“sub_4011C0”,这个函数执行后有两条路径,“成功”或“失败”,下图示。

  所以“sub_4011C0”函数需要做重点分析。在执行这段指令时按下F7进入该函数体。

  从以下图片和调试过程得知,[ebp+var_84]是偏移量,表示从字符串的第5个字符开始(下标索引为4),然后在循环体中自增1逐个扫描后面的字符,同时还有 sub eax, 1语句,说明不计入flag字符串最后的“}”字符。简单说这段循环体代码是为了获得花括号“{}”里的字符,共计24个。

  随后经过的代码就是为花括号里的字符串又开辟一块内存空间,再次计算长度,这一段没什么好说的。此时字符串是“abcdefghijklmnopqrstuvwx”。

  下面代码块则是判断每个字符的大小写,如下图示。先判断字符是否小于“a”,然后再判断是否大于“z”。调试过程发现小写字母转为大写字母(sub ecx, 20h,这句指令ecx自减20h,即32,减去32就能转换为大写字母了);[ebp+var_84]还是偏移量,同样在循环体中向后偏移。

  以下函数“sub_4013C0”是比较关键的了。按下F7进入查看函数的内部逻辑。

sub_4013C0函数逻辑结构

  函数sub_4013C0的结构就是这么一点点,其中有几句代码必须注意。

mov     eax, [ebp+arg_0]
xor     eax, 55h
add     eax, 48h

  简言之这3句代码是把转换后的字母与55h进行异或运算,然后再加上48h

  函数sub_4013C0执行后返回。还定义了一个数组byte_4420B0,笔者把这个数组的值列出来……从0Dh开始

.data:004420B0 byte_4420B0 db 0Dh          ; DATA XREF: sub_4011C0+1A0↑r
.data:004420B1 db  13h
.data:004420B2 db  17h
.data:004420B3 db  11h
.data:004420B4 db    2
.data:004420B5 db    1
.data:004420B6 db  20h
.data:004420B7 db  1Dh
.data:004420B8 db  0Ch
.data:004420B9 db    2
.data:004420BA db  19h
.data:004420BB db  2Fh ; /
.data:004420BC db  17h
.data:004420BD db  2Bh ; +
.data:004420BE db  24h ; $
.data:004420BF db  1Fh
.data:004420C0 db  1Eh
.data:004420C1 db  16h
.data:004420C2 db    9
.data:004420C3 db  0Fh
.data:004420C4 db  15h
.data:004420C5 db  27h ; 
.data:004420C6 db  13h
.data:004420C7 db  26h ; &
.data:004420C8 db  0Ah
.data:004420C9 db  2Fh ; /
.data:004420CA db  1Eh
.data:004420CB db  1Ah
.data:004420CC db  2Dh ; -
.data:004420CD db  0Ch
.data:004420CE db  22h ; 
.data:004420CF db    4

  转换后的每个字符需要与数组byte_4420B0中的数值对应异或运算。简单说如果字符串为str,用C/C++语言表示就是str[i]^byte_4420B0[i]i表示第i个位置,“^”表示异或运算符。下图红框代码展示的正是这个过程。

  花括号里的24个字符转换完毕后,会同字符串“GONDPHyGjPEKruv{{pj]X@rF”比较,巧合的是这段字符串长度也是24,[ebp+var_AC] 存放转换后的字符串。下图示。

  当然,若转换后的字符串与给定的“GONDPHyGjPEKruv{{pj]X@rF”完全一致,说明已经获得了正确的flag。

  值得注意的是,笔者输入的测试字符串“abcdefghijklmnopqrstuvwx”全部为小写,还有一段代码没有涉及到。下图示。

  同之前的相似,这段代码是把大写字母转换成小写字母。(add edx, 20h是指将字母的ASCII值加上20h,即32)。

4.4 程序算法小结

  程序分析已经到了尾声,笔者这一小节用数学化的语言来总结以下这个程序的算法。

  设输入的字符串为,其长度为;提取花括号内的字符串设为;转换后字符串表示字符串、数组中某个位置的字符或数值;表示某个字符的ASCII值;定义数组
  1. 判断字符串长度是否在区间
  2. 判断字符串的前4个字符是否为“EIS{
  3. 判断字符串的最后一个字符是否为“}
  4. 大小写转换和逻辑运算:设数组 。当为大写字母时取,为小写字母时取赋值给
  5. 比对:比较的每个字符是否与GONDPHyGjPEKruv{{pj]X@rF中的完全一致,如果完全一致则为最终flag。


  逆运算:根据“GONDPHyGjPEKruv{{pj]X@rF”反推花括号里的字符串,以第一个字母“G”为例,“G”的ASCII值为 47h,数组byte的值在上面(byte_4420B0)。算式如下:

  47h xor 0D h = 4Ah;

  4Ah - 48h = 2h;

  2h xor 55h = 57h;

  57h 在ASCII对应的字符是“W”;“W”是大写字母,所以转换为小写

  57h + 20h = 77h,对应的则是“w”

  注意字母“P”的计算结果为5Fh,ASCII对应的下划线“_”,不需要再进行大小写转换了。

  所以最终flag是“EIS{wadx_tdgk_aihc_ihkn_pjlm}”;

5.总结

  这个REVERSE题目与之前相比略显复杂,要得到最终flag需要不断地调试分析,不能通过“欺骗”的手段来获得flag,同时还要理解其中所作的各种运算。注意异或运算xor的逆运算还是异或运算。

  笔者花了好长时间截图并撰写的这篇文章,然而在网络上看见关于“IgniteMe”的解法,大多是用IDA转为伪代码再分析的,内容都是互相抄的,看来大家都挺懒哦。我没有转换为伪代码,而是一步一步地分析,把该表达的做题细节尽量展现出来。此外,居然有人说这个题目很水,不知道他是新手还是老手,这个人也是将程序转换成伪代码分析的,贴了几张图又没说清楚具体过程是什么,老手便罢,但是新手有啥骄傲的,转换为伪代码就没什么意思了哈,又不动脑筋还抄别人的应付了事,这样是无法进步的。