RE
1. Replace
程序给加了 UPX 的壳,直接在 linux 下面脱掉,注意脱完壳的程序就不能正常运行了。
脱了壳直接丢到 IDA 里面去看,逻辑很清晰,主函数只有一点点代码
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
关键函数是 sub_401090,如果对加密算法熟悉的话很容易可以看出来这是 AES 加密,给出了密钥和密文,那直接写解密脚本就行。
1 | target = [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22, 72] |
flag : flag{Th1s_1s_Simple_Rep1ac3_Enc0d3}
2. Highwayhash
上网找找 highwayhash,在 github 上面找到了它,是 google 优化现有 hash 算法的一个项目,看看源代码发现和反汇编的结果很相似,应该是出题人拿源码直接修改的(事实证明只修改了 HighwayHashReset 函数里面的一些东西),不同的地方是 key。
回头继续看伪代码,有一个提示:Please enter flag(Note:hxb2018{digital}: , flag 的格式应该是 hxb2018{纯数字},接着是调用了两次 hash,第一次加密了 flag 的长度,第二次加密的 flag 的内容(刨除 hxb2018{} 外壳),两次加密都只给出了 hash 的结果。
想要算出第一个 hash 比较简单,直接动态调试 + 手动爆破就可以,最后算出的 flag 长度是 19,除去 flag 的外壳纯数字的部分只有 10 位,也就是说 flag 的可能性有 10^10 = 一百亿个
不算太多,应该可以爆破出来,这道题简单的思路有两种,第一个是把 github 上面的源代码拖下来,修改一下直接爆破,另一个是调程序里面的函数,需要先修改一下 PE 头,然后编写另一个程序,把这道题当做 dll 加载。
第一种方法:克隆 github 上面的代码,key 什么的就不需要管了,修改好代码之后去爆破,先把 highwayhash.c 里面的 HighwayHashReset 函数修改成
1 | void HighwayHashReset(const uint64_t key[4], HighwayHashState* state) { |
然后另写一份代码(可以参考给出的 highwayhash_test.c 来写),先拿长度测试一下修改的是否正确:
1 | // g++ 0.c highwayhash.c -o test -O3 |
注意要用 g++ 0.c highwayhash.c -o test -O3 进行联合编译,开启 3 级优化。
然后运行代码:
结果正确,现在就可以开始爆破了。
借用大佬的代码(链接)
1 |
|
这是反向爆破的,比正向爆破节省一些时间,大概 10 分钟左右跑出了答案:
第二种方法:修改 exe,使用另外一个程序加载,调用程序自己的 hash 函数,直接加载当然是不行的,这和 PE 的结构有关系,首先使用十六进制编辑器编辑源文件,推荐使用 010,找到 PE 头中的 NtHeader –> Characteristics –> IMAGE_FILE_DLL 这个字段,修改为 1,这样就能把这个程序当做 DLL 调用了。
修改之后的效果,再次双击这个程序不能运行:
现在还有两个问题需要解决。
第一个问题,仅仅修改了 IMAGE_FILE_DLL 并不能直接使用这个文件,还需要修改程序的入口点(基地址),由于 EXE 不存在导出表,所以我们在调用内部函数的时候需要根据 EXE 的 RVA 加上基地址才能找到对应的函数,RVA(即相对虚拟地址)可以使用 IDA 查看,修改入口点可以使用工具 PE Editor。
然后就可以使用这个 “DLL” 了!
1 |
|
另一个问题是编译参数,题目是 PE32+(64 位的 PE),那我们自己写的外挂也需要是 64 位的,在 windows 上编译 64 位程序有许多办法,我去网上下载了 mingw64 ,安装到电脑上就可以把代码编译成 64 位程序。
编译选项: g++ -m64 0.cpp -o test -O3
由于已经知道了 flag 是多少,直接填写 5299999999 这个值,快速算出答案:
和第一种做法得出的答案相同。
3. More efficient than JS
这是一道 WASM 的逆向题目,之前是见过一次的,WASM 是 WebAssembly 编译格式编译出的二进制文件,这种编译格式可以将 C 或 C++ 代码编译成 wasm 文件,开发者可以用 JS 作为桥梁,在网页上直接应用这种二进制程序。
不过对于逆向这一方面,WASM 是一种很难看懂的东西,工具的静态分析效果不理想,而动态调试只能使用浏览器的控制台。
WebAssembly 本质是一个基于栈的虚拟机,所有的操作指令(针对操作数的)都可以被简化成 从栈上取值 –> 进行运算 –> 存回栈上,但实际上在调试题目的过程中,我们所面对的只有一堆指令和局部/全局变量。
打开浏览器,把 hello.html 拖到浏览器中,会直接打开题目页面并弹出一个 Input 窗口:
flag 就要输入到这个窗口中,需要注意的是,任意输入一串字符,点击确定按钮之后它并不会消失,就算输入了正确的 flag,这个窗口也会一直存在,点击取消按钮窗口才会消失,我们直接按下F12开启控制台,在调试器一栏中能够找到 wasm:// 这个目录,打开它并双击子目录中的文件,然后刷新页面,就能看到 wasm 的反汇编代码了,如果不小心误操作,刷新页面重置程序,点击代码之前的行号可以下断点,F10是单步步过,F11是单步步入。
我们直接切入正题,要想调试 wasm,首先要在 JS/html 中定位 “驱动函数”,观察 html 代码没有找到调用函数的位置,那就去 js 里面找一找,直接搜索字符串 “Input:” 来定位函数位置。
找到以下代码:
1 | else if (typeof window != 'undefined' && |
虽然找到了关键代码的位置,但是上下翻找并没有发现哪里调用了 wasm 中的函数,下断点跟踪看看也没什么结果,线索到这里似乎断掉了,查看了 wp 之后才发现,在 wasm 文件中存在一个名为 _main 的函数
1 | (export "_main" (func $func24)) |
这个应该就是主函数了,注意在 wasm 中函数名不再是明文标注,而是使用形如 funcxx 的格式,那么主函数就是 func24,直接搜索 func24 定位到主函数下断点,刷新页面,果然断下来。
接下来就可以开始单步调试了,主函数的代码并不复杂,这种代码看起来甚至要比真正的汇编简单
1 | (func $func24 (result i32) |
这道题整个调试过程涉及到的指令翻来覆去只有那么几个,比如 set_local 和 get_local,相当于汇编中的 mov ,i32.const 0,相当于掏出了一个立即数,call 自然是调用函数的意思,更详细的指令解释在这里。
单步调试,前面的 set/get 执行了一些初始化操作,第一个遇到的函数是 $func97,步过它没有什么影响,第二个函数是 $func98,单步步过之后就会弹出 Input 提示框,那么这道题目的入口点可能是在 wasm 中,由 wasm 先执行一部分代码,然后将控制权转交给 JS ,JS 获取用户输入,然后再交换控制权给 wasm。
在提示框中输入一些内容,点击确定再点击取消,程序会断在 func98 的下一行。
接下来是 func42,这个函数步入分析一下,大致看一下内部结构有几个循环,查看函数的返回值时发现正好是输入的字符串的长度,比如
那么它应该就是 strlen 之类的求字符串长度的函数,直接步过,紧接着进入到本题的关键函数,func22。
首先可以看到函数的几个参数
如果这样看不够明显的话,可以搜索 func22
1 | (func $func22 (param $var0 i32) (param $var1 i32) (param $var2 i32) (param $var3 i32) (param $var4 i32) (result i32) |
很明显的看到函数接收 5 个参数,并返回一个值。
通过 call 上方的五个取值语句,容易分析出它们都是什么,4475 属于 wasm 的指针,我们依次点击右边的 Window:Global –> HEAPU8:Uint8Array
这里会列出很多的区间,在里面找到 4475:
将这几个十进制数转换成字符得到:”I_am_key“ ,猜测这就是加密算法的秘钥,之前的 8 也就是秘钥的长度,类似的,分析出后三个参数是 flag 、flag 的长度、一个空的数组。
步入 func22,单步分析这个函数,绝大部分的操作都是取值–运算–保存结果,真正一步运算是一个异或,其他的都是替换一些数据的位置等等。
直接在这个异或下断点,输入的 flag 在 var39,程序内置的数据在 var33,两者异或然后保存到堆栈中。
当这个函数执行完成之后,回到主函数,下面还有最后一个重要函数 func23,同样的,通过单步调试发现是比较函数,关键点在于
1 | set_local $var7 |
将加密好的 flag 和内置的数据进行比较,如果完全匹配就成功。
简单的破解思路是在 func22 里面把和 flag 异或的数组取出来,然后在 func23 中取出目标数组,两者异或即可得到正确的 flag。
但是由于调试器的问题,我一直不能正常断在理想的位置,于是退而求其次,我们可以输入一串相同的字符,比如一串 0,然后直接在 func23 里面取出目标数组和加密之后的数组,将加密之后的数组先逐位异或字符 0,再和目标数组异或即可。
附上解密脚本
1 | s_t = [137, 221, 46, 119, 76, 156, 92, 92, 137, 215, 225, 85, 132, 233, 53, 206, 231] |
flag : flag{happy_rc4}
原来是 rc4 算法,其实在代码中也能看见一些端倪。
实际上 webassembly 编译出来的 wasm 文件和一般的二进制类似,指令所实现的功能也大同小异,甚至从某种意义上讲,wasm 反汇编代码要更加简单一些,但是苦于没有较好的工具,逆向起来还是比较头痛的。
PWN
1. Regex Format
题目模拟了一个正则表达式引擎,输入自定义的正则表达式和待匹配的字符串,程序返回正则匹配的结果。
先不去逆向逻辑,搜索字符串发现 “Before :use$ it, :understand$* it :first$+.”,这个应该是出题人给出的样例正则表达式,运行程序,不输入任何新的表达式,待匹配字符串设置为 “Before use it, understand it first.”,即可匹配成功,返回 “Before u it, understand it first.”。
结合真实的正则表达式猜测 冒号 和 美元符号 标识了匹配的范围, 星号 表示匹配零次或多次, 加号 表示匹配至少一次。
之后分析程序的逻辑,关键点在下面这段代码中
1 | for ( i = 0; i < v34; ++i ) |
匹配函数就是 sub_8048930,传递的三个参数分别是一块空闲内存,输入的待匹配字符串,栈内存。而漏洞就出在这个 v12 上面。
进入匹配函数分析逻辑,结果和我们的猜测相同
1 | const char *__cdecl sub_8048930(const char *a1, const char *a2, char *a3) |
注意代码中匹配完成后,会尝试将匹配的结果放入栈上,由于栈上的变量 v12 长度只有 200,而我们可以输入的字符串长度为 1000,会导致栈溢出,控制返回地址,之后就是拿 shell 了。
先看一下程序开启的保护
啥也没开,利用就有很多方式了,我选择在 BSS 段上写一些 shellcode ,然后劫持返回地址到 shellcode 上面。
1 | from pwn import * |
拿到 shell:
2. hash burger
这道题是 SECCON 2017 的原题,涉及的技术是堆喷射,暂时没有研究。
1 | #!/bin/env python |
CRYPTO
Common Crypto
标准的 AES 加密,先输入 flag ,然后把 flag AES 加密,最后和密文进行比较,通过分析加密函数,不难得出秘钥
1 | char __fastcall sub_140001000(_BYTE *a1) |
秘钥就在 byte_14001DA40 开始的地址空间内,将他们提取出来,然后把密文解密(不知道是不是密钥不可见的原因,使用 python 脚本不能解密出明文,我使用了 PYG 密码工具可以正常解密)。
为什么只有一半 flag? AES 加密密文长度应该是 16 个字节,而给出的密文明显多于 16 个字节,仔细观察剩余的字符发现都在 ASCII 范围内,直接转换成字符即可得到 flag。
hxb2018{853ecfe52aeb60989e8d3351
最后缺了一个括号,补上就好了。
- 本文作者: Catalpa
- 本文链接: https://wzt.ac.cn/2018/11/20/hxb-2018-online/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。