大家好,我是厂长。
注意看,这是一道软件破解题:
![04291398c948cb38e91d69acf275a035.jpeg](https://i-blog.csdnimg.cn/blog_migrate/338696f106f751114bae70ee0080dbc5.jpeg)
据说第一个破解成功的人,会有神秘奖品!
结果200多位小伙伴参赛,只有十几位成功破解拿到了flag,真的有这么难吗?来看看你能扛到第几关?
题目是这样的:附件是一个压缩包,里面是一个exe文件被拆分成了几部分,需要做的是把这个exe文件重新组装起来,成功运行它,然后破解它拿到flag,这是我给学逆向的同学出的一道典型的CTF Pwn类型的题目。
首先下载这个压缩包,解压后,会发现有4个文件:
![2123d3dfa29087ac2b3810fa80dba23f.png](https://i-blog.csdnimg.cn/blog_migrate/d131f6a885f22ed5fe976aac71c06895.png)
然后我们用十六进制编辑器分别打开这四个文件,看一下:
b.dat:
![ebd4eee888a9122f846bf62aa7bcd802.png](https://i-blog.csdnimg.cn/blog_migrate/1c03757ba55feca1c879d93056b03b21.png)
看不出是个啥,再看看其他的。
d.dat:
![ddccf80759b9aa8d4e235206fb5b4820.png](https://i-blog.csdnimg.cn/blog_migrate/0a8ff158c8c3fdfa8effad220f0aca9e.png)
只要学习了PE文件格式(第10课)这一节的同学,通过MZ标记很容易看得出来,这是PE文件中的DOS头部,是整个文件最开始的部分。
然后用十六进制编辑器创建一个新文件,把这个第一部分d.dat的内容复制进去。
接下来看看其他块。
p.dat:
![052813dce8fa835e35570c59ca7ade65.png](https://i-blog.csdnimg.cn/blog_migrate/fb2d63a4aba0a506d01619f4753f28b9.png)
这个也很容易看出,是NT头,是紧接着前面DOS头后的第二部分。
把这部分内容也复制到咱们刚刚创建的新文件后面。
s.dat:
![4f1ab24a6f70d83fc84b3ccbb06ebcbe.png](https://i-blog.csdnimg.cn/blog_migrate/7fd192d3ba271b76f7d27c66630158cc.png)
这一个部分,看右边的.text、.rdata、.data就能看得出来,这是节表的内容,是在NT头后的第三部分。
这样推断出来,最开始的b.dat应该就是最后文件的正文内容了,它的体积也是最大的。
把这四部分内容按顺序复制到我们新建的那个文件中,然后另存为一个exe文件,尝试去双击执行它。
PS:
实际上,这四个块的命名也有讲究,d就是dos头,p就是PE头,s就是节表section,b就是文件正文体body。
然后,有很多小伙伴发现了一个问题,双击运行程序报错了:
![25f0cace22fe1f2c5f5e586969323636.png](https://i-blog.csdnimg.cn/blog_migrate/92c791e6b45fa8da8ff315d42653d361.png)
难道拼的有问题?
有不少小伙伴都倒在了这里。
实际上,这里我埋了一个小小的坑,其中有个节我多塞了一个字节进去,就是这一个字节,让拼出来的PE文件格式错误,运行不起来。
不过毕竟很多同学都是刚刚学,我也不会太难为大家,这一个字节一般都是在某个块开头或者结尾。
仔细去检查刚才的四个数据块,你就会发现节表的那个块,前面多了一个00:
![a658bbf398f44b7342c841cc77d41079.png](https://i-blog.csdnimg.cn/blog_migrate/fb772e5f275eb241bdce50f4884c1cd4.png)
只要认真看了第10课的同学都能查得出来。
删除这一个字节,我们再次尝试双击这个exe程序,结果发现还是不行,还在报错:
![759f19e2ac39073bc9e72fc293fcc45c.png](https://i-blog.csdnimg.cn/blog_migrate/4cce4466ad42ca90bfaff54a50bcbb2a.png)
不过仔细看,报错类型不一样了,提示是找不到一个动态链接库。其实看到这个报错,就能确定一件事,我们的PE文件组装已经OK了,接下来要解决这个新问题了。
一个exe程序要运行,它通常会依赖一些其他的动态链接库,有系统库,比如kernel32.dll,也有程序自己依赖的其他库。不管哪种情况,这些依赖的动态库都记录在它的文件结构中,这就是PE文件的导入表。
上面这个报错,就是系统在创建进程时,在解析这个PE文件的导入表过程中,发现了它需要依赖一个license.dll文件,然后尝试去加载这个dll,然后又没有找到,所以报了这个错,进程创建失败。
我们可以用DEPENDS工具来看下这个程序的导入表:
![e0807c5860301021d20dbeae1f4a2ac3.png](https://i-blog.csdnimg.cn/blog_migrate/f43a9d367e4f1a278aadfa34ef5c6131.png)
可以看到,这个程序引用了一个叫license.dll动态库中的GetLicense函数。
接下来是我们的逆向分析神器IDA登场的时候了。
把这个组装出来的程序放入IDA中来分析分析:
![1af13695c101ebec119c8bec604d66d9.png](https://i-blog.csdnimg.cn/blog_migrate/e4a698c56f4907becce8a1a81ae49861.png)
IDA定位到了main函数,然后很容易看出main函数的代码逻辑,这里在调用前面的GetLicense函数,然后检查函数的返回值,检查通过就打印输出flag,失败则输出一个错误信息。
当然,为了避免一眼就直接拿到flag,我对flag进行了一个简单的编码,打印输出的时候,需要先解码还原。
如果大家看汇编有些吃力,还可以直接用IDA反编译成C语言,这看起来就容易得多了:
![d984754d0d37d45c7d4331887db3734f.png](https://i-blog.csdnimg.cn/blog_migrate/0c5fdd0a516d3d2ae7b583330a72776f.png)
根据我们刚才的分析,把上面那些函数和变量重新命个名,看起来更清晰(关于这些操作,在第13课IDA的操作使用中有详细的介绍):
![69cf3cf6d9342c9aeefb4656613fd947.png](https://i-blog.csdnimg.cn/blog_migrate/49d90e16b523de7070f46c067d5cff0e.png)
是不是好理解得多了?
看到这里,思路就出来了:
方法1、自己编写一个这个名字的DLL文件,然后导出一个名为GetLicense的函数,让程序成功运行起来,打印出flag。
方法2、直接暴力破解,修改关键汇编指令,让程序强行走入打印flag的分支。
方法3、最简单的,找到解码flag的函数,直接分析它是如何解的,自己写程序模拟解一遍就行了。
我们三种方法都试一下。
方法1:编写DLL
观察一下IDA分析视角下,程序中调用GetLicense函数的汇编指令,可以看得出来这个GetLicense函数只有一个整型参数,然后返回值是一个字符串指针。函数的声明就出来了:
char* GetLicense(int n);
第一种方法的关键,是要让GetLicense函数返回一个符合要求的字符串,也就是license,这样才能通过程序里的检查。
所以,我们先来分析一下,原程序中,是怎么在检查license的:
![a3f1cad5cd1d2472cf3dd66e7ce9dbb3.png](https://i-blog.csdnimg.cn/blog_migrate/8a557301b97270b2bc72f161f234e182.png)
检查函数返回的是一个bool,里面的检查逻辑也比较简单。
首先通过strlen获取license的长度,检查长度是不是16,如果不是,则返回false。
接着是一个for循环,我们先跳过,最后来看它。
最后的return是一组&&连接的检查,首先检查license的第4-7个字符的和(ASCII的和)减去第0-3个字符的和是不是为1。然后检查第8位是不是45。换成ASCII字符就是短横线-:
![97db780aa75e2c57b8f57fd723759ee7.png](https://i-blog.csdnimg.cn/blog_migrate/58271b69085157d04c33e9c1d97c7769.png)
最后回过头看看中间那个for循环。里面在调用另一个sub_401096函数,然后把license的每一个字符传进去。
进去看一下这个函数:
![ae3a1dbb1f86b45bf80645643105a420.png](https://i-blog.csdnimg.cn/blog_migrate/c2d75caf4487da90c23fe51f9e727761.png)
这个函数里面又是一个for循环,在检查参数传进来的字符是不是在一个byte数组中,如果是就返回1,否则返回0。
这个数组里面是啥呢?进去看看:
![50f8fe9db5b84d73f08e3b3b8cade31f.png](https://i-blog.csdnimg.cn/blog_migrate/f87c6ac7f06d35fac67700c980df62a3.png)
好了,总结一下,前面的检查license的函数里面在干四件事:
检查license字符串长度是不是16
检查license字符串中的每一个字符是不是在上面的byte数组中
检查license字符串第0-3个字符的和比第4-7位字符的和小1
检查license字符串的第8位字符是一个短横线
其实这个license检查规则,就是根据我的微信号:xuanyuan-zhifeng设计出来的,嘿嘿,有没有猜到?
但不是说license必须是这个,你的字符串只要符合上面的要求都可以。
好了,咱们来编写一个DLL:
extern "C"
__declspec(dllexport)
char* GetLicense(int code)
{
return "xuanyuan-zhifeng";
}
注意,需要使用extern "C",否则导出的函数名字会发生变化。
然后把DLL放置到前面我们拼接的exe目录下,这时候再来运行我们的程序:
![4ca5595e2cf314b7cabcda8c8d780d50.png](https://i-blog.csdnimg.cn/blog_migrate/c632736f3e3ca339b65f6136e71807d0.png)
flag已经出来了:xuanyuan@biancheng.universe
方法2:暴力破解
因为我们还没有讲到调试器,所以这里先不动用动态分析,还是用IDA来搞定。
具体怎么暴力破解呢?首先想到的最简单的,就是这条JZ指令,只要能把它窜改成JNZ,即便license检查不通过,也会走入打印flag的分支。
![eb61471220ed06d4925c7184a8759c5a.png](https://i-blog.csdnimg.cn/blog_migrate/3a51be12897853a5c70971b39337b9db.png)
但在这之前,我们需要让程序能够先运行起来,不受那个DLL的依赖束缚。
前面讲过,程序的依赖项是在导入表中,那为今之计,就是找到导入表,把这个DLL干掉。
首先需要定位到导入表在PE文件中的哪个位置,这又需要用到第13课的知识了,先找到文件头的数据目录,数据目录的第二项是导入表的RVA。然后根据节表中每个节的RVA,定位到导入表在哪个节。然后再根据那个节的FOA,定位到文件中的位置。
然后通过导入表描述符的Name字段,就能知道这第一项就是license.dll。
![e89659a1f75b6570f0160507d31b0327.png](https://i-blog.csdnimg.cn/blog_migrate/e23d1b623ad505fa49fa1dbeb4c8c9e6.png)
用导入表的后面第二项kernel32.dll的内容覆盖license.dll的导入表项。然后把原来kernel32.dll的项清零,就完成把license.dll从导入表中抹去的工作。
我们用Depends工具来看下现在这个程序的导入表:
![9466def386b8633390d5a6debbd7563f.png](https://i-blog.csdnimg.cn/blog_migrate/ae57b09164f71aa36f66b7f8bce936e7.png)
可以看到,现在只依赖kernel32.dll了,不需要license.dll了。
但你现在去执行还是有问题,因为咱们程序里面使用了外部引入的GetLicense函数啊,你现在都没license.dll了,这个函数没法调用了。
![a997e6da72d3d891e53acb39e2dfac7a.png](https://i-blog.csdnimg.cn/blog_migrate/2d03cc289b19a3cf5b9df9ca5e448c63.png)
那干脆一不做二不休,直接HOOK main函数的流程,让它一进来就直奔打印flag的分支。先不管他堆栈平衡的问题,反正咱们的目的是拿到flag,拿到以后哪管它崩不崩溃。(关于HOOK相关的知识,咱们在接下来的课程中会专门来讲解)
![a2026f59bd6afc21698af37bc2f2ff9f.png](https://i-blog.csdnimg.cn/blog_migrate/c09b3531a883b517b90041540b83a57e.png)
所以,我们可以把main函数入口地方直接篡改成一条短跳指令,直接跳到flag解码打印的分支:
![ac40c65f0d64894d051523c06cde1169.png](https://i-blog.csdnimg.cn/blog_migrate/d7873f0558e36101a4bd1aff0b127ed8.png)
然后来执行这个程序:
![3a3315a71b22988a4112ada182cedca4.png](https://i-blog.csdnimg.cn/blog_migrate/b33a9cd5e1beeea2c4b883f893797331.png)
flag又被我们召唤出来了!
方法三:直接解码flag
前面两种方法还是有一点麻烦,既然我们要的是flag,那么直接对flag下手,看它藏在哪里,然后想办法把它解出来就行了!
通过IDA反编译出来的C代码,可以看到这个函数就是在负责对flag进行解码。
![b1533c524244876526c1247605ab59b2.png](https://i-blog.csdnimg.cn/blog_migrate/acfa552bac7d9196df0455b36e5ac8e6.png)
双击这个函数,然后反编译看一下解码函数的逻辑:
![fd1d4c35115b91fcc851ae98157f76a8.png](https://i-blog.csdnimg.cn/blog_migrate/cc6a6c1f7e2c56f605d750cc043baced.png)
经过对汇编指令的分析,这个函数实际上是没有返回值的,我们对其中的一些变量名称以及类型、函数的返回值类型进行人工修正,让它看起来更清晰:
![bd970cf4a59dcedf35451c361aba703c.png](https://i-blog.csdnimg.cn/blog_migrate/38414d17b64859d381c6a3e135873b47.png)
这下简单明了了吧,实际上就是在对flag字符串的每一位,与一个key进行异或运算(看吧,真的不难,毕竟是从零开始学逆向嘛)。
双击看一下这个key是个啥:
![ab1f73f16d4a93e16054337e018e318d.png](https://i-blog.csdnimg.cn/blog_migrate/7ee95d5ab6fd56f265cdd0a9b1dcd7dd.png)
就是一个字节的0xCC。
然后回去看一下解码前的那个flag:
![3d6057b06e0b4fb3d7ba072b160bde61.png](https://i-blog.csdnimg.cn/blog_migrate/d36f915314bf6b5f08f495925f56cde3.png)
切换到十六进制窗口:
![283896d8b2a256f9370833a5cbb54891.png](https://i-blog.csdnimg.cn/blog_migrate/994370d45cb9b484a5b2fcbe7de162f4.png)
都到这儿了,问题都好办了吧,自己写个简单的程序,按照解码函数那样把这段数据处理一遍就出来了:
void my_decode_flag() {
char flag_bytes[] = {
0xB4, 0xB9, 0xAD, 0xA2, 0xB5, 0xAD, 0xA2, 0x8C,
0xAE, 0xA5, 0xAD, 0xA2, 0xAF, 0xA4, 0xA9, 0xA2,
0xAB, 0xE2, 0xB9, 0xA2, 0xA5, 0xBA, 0xA9, 0xBE,
0xBF, 0xA9, 0x00
};
for (int i = 0; i < strlen(flag_bytes); i++) {
flag_bytes[i] ^= 0xCC;
}
printf(flag_bytes);
printf("\n");
}
来编译运行下试试:
![15d492b1344a07bb97142f11a0c8af66.png](https://i-blog.csdnimg.cn/blog_migrate/a356e1955336854c4fa34e370292ef58.png)
再一次成功拿到了flag!
< END >
给大家推荐朋友写的小册子《小白也能用AI编程》,他是小米的AI业务高管,同时又找了百度、阿里的几位高管,一起写了这个小册子,内容覆盖编程、副业赚钱、提示词、办公等,适合所有有AI提效需求的朋友。
小册内测阶段仅需10元终身买断,送5大福利,还送陪伴群,七天不满意,无理由退款!