⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 轩辕的编程宇宙 「轩辕之风」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

注意看,这是一道软件破解题:

题目是这样的:附件是一个压缩包,里面是一个exe文件被拆分成了几部分,需要做的是把这个exe文件重新组装起来,成功运行它,然后破解它拿到flag。

这是我给学逆向的同学出的一道典型的CTF Pwn类型的题目。结果200多位小伙伴参与,只有十几位成功破解拿到了flag,真的有这么难吗?来看看你能扛到第几关?

破解思路

首先下载这个压缩包,解压后,会发现有4个文件:

然后我们用十六进制编辑器分别打开这四个文件,看一下:

b.dat:

看不出是个啥,再看看其他的。

d.dat:

只要学习了PE文件格式的同学,通过MZ标记很容易看得出来,这是PE文件中的DOS头部,是整个文件最开始的部分。

然后用十六进制编辑器创建一个新文件,把这个第一部分d.dat的内容复制进去。

接下来看看其他块。

p.dat:

这个也很容易看出,是NT头,是紧接着前面DOS头后的第二部分。

把这部分内容也复制到咱们刚刚创建的新文件后面。

s.dat:

这一个部分,看右边的.text、.rdata、.data就能看得出来,这是节表的内容,是在NT头后的第三部分。

这样推断出来,最开始的b.dat应该就是最后文件的正文内容了,它的体积也是最大的。

把这四部分内容按顺序复制到我们新建的那个文件中,然后另存为一个exe文件,尝试去双击执行它。

PS:

实际上,这四个块的命名也有讲究,d就是dos头,p就是PE头,s就是节表section,b就是文件正文体body。

然后,有很多小伙伴发现了一个问题,双击运行程序报错了:

难道拼的有问题?

有不少小伙伴都倒在了这里。

实际上,这里我埋了一个小小的坑,其中有个节我多塞了一个字节进去,就是这一个字节,让拼出来的PE文件格式错误,运行不起来。

仔细去检查刚才的四个数据块,你就会发现节表的那个块,前面多了一个00:

删除这一个字节,我们再次尝试双击这个exe程序,结果发现还是不行,还在报错:

不过仔细看,报错类型不一样了,提示是找不到一个动态链接库。其实看到这个报错,就能确定一件事,我们的PE文件组装已经OK了,接下来要解决这个新问题了。

一个exe程序要运行,它通常会依赖一些其他的动态链接库,有系统库,比如kernel32.dll,也有程序自己依赖的其他库。不管哪种情况,这些依赖的动态库都记录在它的文件结构中,这就是PE文件的导入表。

上面这个报错,就是系统在创建进程时,在解析这个PE文件的导入表过程中,发现了它需要依赖一个license.dll文件,然后尝试去加载这个dll,然后又没有找到,所以报了这个错,进程创建失败。

我们可以用DEPENDS工具来看下这个程序的导入表:

可以看到,这个程序引用了一个叫license.dll动态库中的GetLicense函数。

接下来是我们的逆向分析神器IDA登场的时候了。

把这个组装出来的程序放入IDA中来分析分析:

IDA定位到了main函数,然后很容易看出main函数的代码逻辑,这里调用前面的GetLicense函数,然后检查函数的返回值,检查通过就打印输出flag,失败则输出一个错误信息。

当然,为了避免一眼就直接拿到flag,我对flag进行了一个简单的编码,打印输出的时候,需要先解码还原。

如果大家看汇编有些吃力,还可以直接用IDA反编译成C语言,这看起来就容易得多了:

根据我们刚才的分析,把上面那些函数和变量重新命个名,看起来更清晰:

是不是好理解得多了?

看到这里,思路就出来了:

方法1:自己编写一个这个名字的DLL文件,然后导出一个名为GetLicense的函数,让程序成功运行起来,打印出flag。

方法2:直接暴力破解,修改关键汇编指令,让程序强行走入打印flag的分支。

方法3:最简单的,找到解码flag的函数,直接分析它是如何解的,自己写程序模拟解一遍就行了。

我们三种方法都试一下。

方法1:编写DLL

观察一下IDA分析视角下,程序中调用GetLicense函数的汇编指令,可以看得出来这个GetLicense函数只有一个整型参数,然后返回值是一个字符串指针。函数的声明就出来了:

char* GetLicense(int n);

第一种方法的关键,是要让GetLicense函数返回一个符合要求的字符串,也就是license,这样才能通过程序里的检查。

所以,我们先来分析一下,原程序中,是怎么检查license的:

检查函数返回的是一个bool,里面的检查逻辑也比较简单。

首先通过strlen获取license的长度,检查长度是不是16,如果不是,则返回false。

接着是一个for循环,我们先跳过,最后来看它。

最后的return是一组&&连接的检查,首先检查license的第4-7个字符的和(ASCII的和)减去第0-3个字符的和是不是为1。然后检查第8位是不是45。换成ASCII字符就是短横线-:

最后回过头看看中间那个for循环。里面在调用另一个sub_401096函数,然后把license的每一个字符传进去。

进去看一下这个函数:

这个函数里面又是一个for循环,在检查参数传进来的字符是不是在一个byte数组中,如果是就返回1,否则返回0。

这个数组里面是啥呢?进去看看:

好了,总结一下,前面的检查license的函数里面在干四件事:

  1. 检查license字符串长度是不是16;
  2. 检查license字符串中的每一个字符是不是在上面的byte数组中;
  3. 检查license字符串第0-3个字符的和比第4-7位字符的和小1;
  4. 检查license字符串的第8位字符是一个短横线。

其实这个license检查规则,是我根据字符串xuanyuan-zhifeng设计出来的,但不是说license必须是这个,你的字符串只要符合上面的要求都可以。

好了,咱们来编写一个DLL:

extern "C"
__declspec(dllexport)
char* GetLicense(int code)
{
return "xuanyuan-zhifeng";
}

注意,需要使用extern “C”,否则导出的函数名字会发生变化。

然后把DLL放置到前面我们拼接的exe目录下,这时候再来运行我们的程序:

flag已经出来了:`xuanyuan@biancheng.universe`。

方法2:暴力破解

这里先不动用动态分析,还是用IDA来搞定。

具体怎么暴力破解呢?首先想到的最简单的,就是这条JZ指令,只要能把它窜改成JNZ,即便license检查不通过,也会走入打印flag的分支。

但在这之前,我们需要让程序能够先运行起来,不受那个DLL的依赖束缚。

前面讲过,程序的依赖项是在导入表中,那为今之计,就是找到导入表,把这个DLL干掉。

首先需要定位到导入表在PE文件中的哪个位置。先找到文件头的数据目录,数据目录的第二项是导入表的RVA。然后根据节表中每个节的RVA,定位到导入表在哪个节。然后再根据那个节的FOA,定位到文件中的位置。

然后通过导入表描述符的Name字段,就能知道这第一项就是license.dll。

用导入表的后面第二项kernel32.dll的内容覆盖license.dll的导入表项。然后把原来kernel32.dll的项清零,就完成把license.dll从导入表中抹去的工作。

我们用Depends工具来看下现在这个程序的导入表:

可以看到,现在只依赖kernel32.dll了,不需要license.dll了。

但你现在去执行还是有问题,因为咱们程序里面使用了外部引入的GetLicense函数啊,你现在都没license.dll了,这个函数没法调用了。

那干脆一不做二不休,直接HOOK main函数的流程,让它一进来就直奔打印flag的分支。先不管它堆栈平衡的问题,反正咱们的目的是拿到flag,拿到以后哪管它崩不崩溃。

所以,我们可以把main函数入口地方直接篡改成一条短跳指令,直接跳到flag解码打印的分支:

然后来执行这个程序:

flag又被我们召唤出来了!

方法3:直接解码flag

前面两种方法还是有一点麻烦,既然我们要的是flag,那么直接对flag下手,看它藏在哪里,然后想办法把它解出来就行了!

通过IDA反编译出来的C代码,可以看到这个函数就是在负责对flag进行解码。

双击这个函数,然后反编译看一下解码函数的逻辑:

经过对汇编指令的分析,这个函数实际上是没有返回值的,我们对其中的一些变量名称以及类型、函数的返回值类型进行人工修正,让它看起来更清晰:

这下简单明了了吧,实际上就是在对flag字符串的每一位,与一个key进行异或运算。

双击看一下这个key是个啥:

就是一个字节的0xCC。

然后回去看一下解码前的那个flag:

切换到十六进制窗口:

都到这儿了,问题都好办了吧,自己写个简单的程序,按照解码函数那样把这段数据处理一遍就出来了:

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");
}

来编译运行下试试:

再一次成功拿到了flag!

地铁上争分夺秒:

文章目录
  1. 1. 破解思路
  2. 2. 方法1:编写DLL
  3. 3. 方法2:暴力破解
  4. 4. 方法3:直接解码flag