2022-syc-bin二面

level5

逆向一个虚拟机,编写适应于其的二进制文件,实现tea算法;

main

上图左侧为main函数,逻辑就是读取名称 “binary” 的内容,然后将其赋给code,之后将code扔进vm函数充当指令集;

每条指令分三个数值,一个指令数,两个操作数,分别给了instru和One,Two变量;

根据输入不同的instru变量来调用不同的函数,这些函数就是指令执行的操作了,翻译如下:

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
code[8] 是计数器 -> i

code[9] 是flag 控制数

code[7] 是栈针

code[6] 用于实现加法

---

0 code[one] = two ++i

1 code[code[7] + 10] = one ++code[7] ++i

2 code[code[7] + 10] = code[one] ++code[7] ++i

3 code[code[7] + 10] = code[8] + 1 ++code[7] code[8] = one

4 code[one] = code[two] ++i

5 code[8] = one

6 code[one + 10 + code[6]] += code[two] ++i

7 code[one] = code[code[6] + 10 + two] ++i

8 code[one] < < = two ++i

9 code[one] > > = two ++i

a code[one] += code[two + 10 + code[6]] ++i

b code[one] ^= code[two] ++i

c if( !code[9] ) -> code[8] = one ; else ++i

d end

e if(two < = code[one + 10 + code[6]])

​ {

if(two = code[one + 10 + code[6]])

​ code[9] = 1

else

​ code[9] = 2

​ }

else -> code[9] = 0

​ ++i

f --code[7] code[one] = code[code[7] + 10] ++i

10 --code[7] code[8] = code[code[7] + 10] ++i

11 code[code[6] + 10 + one] += two ++i

12 code[code[6] + 10 + one] += code[code[6] + 10 + two] ++i

13 code[one] = cin ++i

14 cout code[one] ++i
---

所以123都表示入栈,3表示call因为改变了计数器;f,10表示出栈,10表示return

由此对照机械码手撸汇编:

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//赋初值

0 6 32 mov reg6, 50

1 11223344 push 11223344h

1 22334455 push 22334455h

1 33445566 push 33445566h

1 44556677 push 44556677h

---

//循环输入

11 0 0 add reg60, 0

tag1:

13 0 mov reg0, cin

2 0 push [reg0]

11 0 1 add reg60, 1



e 0 4 cmp reg60, 4

c 43 jnz tag1

---

//调用cry以及输出

3 4E call cry

14 0 mov cout, reg0

14 1 mov cout, reg1

14 2 mov cout, reg2

14 3 mov cout, reg3

d retn

---

//cry实现

cry:

1 9E3779B9 push delta (push后code[7] = 10)

// v[4]

4 0 E mov reg0, [esp + 6]

4 1 F mov reg1, [esp + 5]

4 2 10 mov reg2, [esp + 4]

4 3 11 mov reg3, [esp + 3]

---

//循环

0 3C 0 mov reg60, 0

11 0 0 add reg60, 0

tag2:

//sum

6 1 13 add reg61, [esp + 2]

//v0

4 3E 1 mov reg62, reg1

8 3E 4 shl reg62, 4

6 2 A add reg62, [esp + a]



4 3F 1 mov reg63, reg1

6 3 3D add reg63, reg61



4 40 1 mov reg64, reg1

9 40 5 shr reg64, 5

6 4 B add reg64, [esp + 9]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 0 add reg62, reg0

4 0 3E mov reg0, reg62



//v1

4 3E 0 mov reg62, reg0

8 3E 4 shl reg62, 4

6 2 C add reg62, [esp + 8]



4 3F 0 mov reg63, reg0

6 3 3D add reg63, reg61



4 40 0 mov reg64, reg0

9 40 5 shr reg64, 5

6 4 D add reg64, [esp + 7]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 1 add reg62, reg1

4 1 3E mov reg1 reg62



//v2

4 3E 3 mov reg62, reg3

8 3E 4 shl reg62, 4

6 2 A add reg62, [esp + a]



4 3F 3 mov reg63, reg3

6 3 3D add reg63, reg61



4 40 3 mov reg64, reg3

9 40 5 shr reg64, 5

6 4 B add reg64, [esp + 9]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 2 add reg62, reg2

4 2 3E mov reg2 reg62



//v3

4 3E 2 mov reg62, reg2

8 3E 4 shl reg62, 4

6 2 C add reg62, [esp + 8]



4 3F 2 mov reg63, reg2

6 3 3D add reg63, reg61



4 40 2 mov reg64, reg2

9 40 5 shr reg64, 5

6 4 D add reg64, [esp + 7]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 3 add reg62, reg3

4 3 3E mov reg3, reg62



11 0 1 add reg60, 1



e 0 20 cmp reg60, 32

c 55 jnz tag2

---



f 13 pop delta

10 retn

---


没实现栈平衡,不过芜锁胃;反正最后return回去输出就行;

注意:写二进制文件时用小端序,而且以DWORD为基本单位,并以3个DWORD对齐,比如 c 55 写成:0c 00 00 00 55 00 00 00 00 00 00 00 ;

还有个问题,根据调试,每次第一条指令开始是code[8] = 3C; 所以二进制文件需要填充垃圾信息,填多少?第一幅图中instru = code[3 * code[8] + 1010], 所以 括号里的内容为 : 1190 ;而写二进制文件需要以DWORD为单位,所以需要填充 1190 * 4 个 00 ;

通过:

pass

level1

ida:

main

调试可以发现main挂不上,然后就发现旁边的函数长得和main都差不多,一个一个断点试,找到第三个是真正的main函数;

主要思路就是输入15个内容,进行异或和加运算,然后和v4开始的数据比较;

写出逆运算:

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
#include <iostream>
using namespace std;

int main()
{
int i, j;
unsigned int buf[15] = {
0x04, 0x46, 0x81, 0x63, 0x14, 0x53, 0x17, 0x6D, 0x6A, 0x67, 0x76, 0x16, 0x34, 0x14, 0x34
};
int v0 = 0;
char ans[16];
for (i = 0; i <= 14; ++i) //自加v0到加密完状态
{
for (j = 0; j <= 2; ++j)
{
v0++;
}
}
v0--; //多加一个减掉
for (i = 14; i >= 0; --i) //逆
{
for (j = 2; j >= 0;--j)
{
buf[i] -= v0--;
buf[i] ^= i ^ j ^ 0x32;
}
ans[i] = buf[i];
}
ans[15] = '\0';

printf("%s", ans);
return 0;
}

得到flag:SYC{0h_y0u1_finD0V0}

level2

ida:

main

进去之后第一感觉会发现main是个scanf函数,但是点进去之后会发现这个东西,让调试才能查看代码;点进这个函数之后会发现是个线程创建,注意调试时改变if判断的变量值为1;

在线程中可以发现以下代码:

thread

在thread main 函数里有两个函数:check() 和 encrypt() ;

check一开始就执行,判断长度,以及输入的内容必须为数字;

encrypt读入key和输入的数据,将数据前12位与key加密运算;

最后比较数据;

写出encrypt的逆向算法:

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
for (i = 0; i < 12; ++i)
{
switch (key[i])
{
case 3:
v4 += 23; //无用
v3 ^= 5;
break;
case 9:
us[i] ^= key[i]; //不变
break;
case 161:
us[i] -= 2 * key[i]; //减等
break;
case 196:
us[i] -= 10; //减等
break;
case 229:
//v3 = 8 * ((v4 + 6) >> (v4 & 3)); 无用
us[i] = inter(us[i]);
break;
default:
break;
}
}

int inter(int a)
{
return a - 0x19;
}

将比较数据经过以上运算得到flag:SYC{03062639056784}

level3

ida进去可以发现是加壳了,函数很少;

打开二进制格式搜索upx,果然就找到了老朋友:

unpack

upx加壳的标识码是 UPX! 全大写,拿不准改哪个就全部 Upx 都改成 UPX;

改完就可以脱壳了;

看代码:

main

看不懂子进程作用,反正主要内容在父进程里:输入内容后,先进入change函数,把4个4个的char内容放到4单位的int里;然后把这个int数组放到xor函数里,把每个字节都和j做异或运算,最后和v19比较数据,v19的内容就是cpy的16长度的字符串;

异或的逆运算还是异或,写出复原代码:

1
2
3
4
5
6
7
8
9
10
for (i = 0; i < 16; i = i + 4)
{
for (j = 0; j <= 9; ++j)
{
we[i] = we[i] ^ j;
we[i + 1] = we[i + 1] ^ (j + 1);
we[i + 2] = we[i + 2] ^ (j + 2);
we[i + 3] = we[i + 3] ^ (j + 3);
}
}

将逆向得到的cpy字符串带入we得到flag:SYC{0k_y0u_s0lv3_it_}

level4

这道题缺库不能调,直接看静态;

ida:

main

左图main函数,右图rc4函数,将enter用rc4加密了,密钥是 syclover:)

使用大厨把enter加密后的内容烤出来:(CyberChef (gchq.github.io)

chef

发现开头是ELF,说明这加密出来的内容是个elf文件,将其写入二进制文件再用ida打开:

main

属于就正常了;

func

如上可知,输入16长度内容,然后进行tea算法(小魔改,每次异或了i),之后比较数据;

写出逆算法:

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
int i;
unsigned int j = 0,l , r ,sum = 0;
int v[4] = { 0x6A318EC6 , 0x5B898EC2 , 0x42FB5DD1 , 0x50AC4C5F };
int k[4] = {0x11 , 0x22 , 0x33 , 0x44};

while (j < 3)
{
l = v[j];
r = v[j+1];

for (i = 0; i < 32; ++i)
sum += DELTA;

for (i = 31; i >= 0; --i)
{
r -= (k[(sum >> 11) & 3] + sum) ^ (((l >> 5) ^ (16 * l)) + l) ^ i;
sum -= DELTA;
l -= (k[sum & 3] + sum) ^ (((r >> 5) ^ (16 * r)) + r) ^ i;
}

v[j] = l;
v[j + 1] = r;

sum = 0;
j += 2;
}

char* p;
p = (char *)v;
p[16] = '\0';


printf("%s", p);

得到flag:SYC{w3f-2hs-ij7-9is}

调试GLIBC_2.34小技巧

安装glibc-all-in-one

1
2
3
sudo git clone https://github.com/matrix1001/glibc-all-in-one.git 
cd glibc-all-in-one/
sudo python3 update_list

下载glibc

1
sudo ./download 2.35-0ubuntu3_amd64

安装patchelf

1
2
3
4
git clone https://github.com/NixOS/patchelf.git
cd patchelf
sudo apt-get install autoconf automake libtool
./bootstrap.sh

继续:

1
2
3
4
./configure
make
make check
sudo make install

配置ld.so

1
patchelf --set-interpreter path/to/.so the/elf/you/debug

配置环境

1
patchelf --set-rpath path/to/.so the/elf/you/debug

level6

逆向一个CPP服务器,得到flag,并编写socket客户端和远程服务器提交flag;

ida查看服务器:

main

这是一个服务端框架,首先创建套接字类型文件,并返回fd文件饰描述符;

然后和IP端口进行绑定;

之后一直监听这个端口,直到接收客户端请求,执行处理,并且是多线程的处理;

而处理的主体在 CTask_server 里可以找到:

true

先发送 “Please …” (send) 到客户端,然后等待输入,被inside变量接收,之后进入cc加密,和tt生成的v7进行比较数据;

看看里面加密吧,都tea ptsd了,不想放图了;直接来吧:

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
int i;
unsigned int j = 0, l, r, sum = 0;
int v[3] = { 0xED3E9980 , 0x57284856 ,0 }; //从tt里抄的
int k[4] = { 0x6C , 0x30 , 0x76 , 0x33 };


l = v[0];
r = v[1];

for (i = 0; i < 32; ++i)
sum += DELTA;

for (i = 31; i >= 0; --i) //小魔改tea
{
r -= ((l >> 5) + k[3]) ^ (l + sum) ^ (16 * l + k[2]) ^ i;
l -= ((r >> 5) + k[1]) ^ (r + sum) ^ (16 * r + k[0]) ^ i;
sum -= DELTA;
}

v[0] = l;
v[1] = r;

char* p = (char*)v;
p[9] = '\0';


printf("%s", p);

得到flag:D0Y0uKSk

如同服务端,自己写的客户端也需要一个框架,然后把发送的flag放到主体里就行;

根据题目中的链接,可以知道客户端只需要使用socket创建套接字后通过ip端口连接就行;

那么大概的框架就是: socket() -> 结构地址 -> connect() 连接到地址 -> 读 & 写 -> close() 结束;

内容:

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
printf("Client Start\n");
int fd = socket(2, 1, 0); //逆向得到的参数

char lines[] = "D0Y0uKSk";

struct sockaddr_in serv_addr; //套接字结构地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; //IPv4
serv_addr.sin_addr.s_addr = inet_addr("1.14.92.115"); //地址
serv_addr.sin_port = htons(1234); //端口

printf("Connecting..\n");
connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); //接通accept
printf("Done !\n");

char buffer[40]; //回响容器
read(fd, buffer, sizeof(buffer) - 1);
printf("%s\n", buffer);
memset(&buffer, 0, sizeof(buffer));

printf("%s\n", lines);
write(fd, lines, sizeof(lines) - 1);

read(fd, buffer, sizeof(buffer) - 1);
printf("%s\n", buffer);
memset(&buffer, 0, sizeof(buffer));

close(fd);
return 0;
}

结果:

pass

更多socket学习

https://blog.csdn.net/m0_37947204/article/details/80489431

level7

给二进制加载器实现更多功能:1、转储节内容 2、输出数据符号 3、使用capstone反汇编.text段;

称之为环境恶心人之题;

题不难,但在wsl上装环境会变得千奇百怪,反正就是跑不起来,只有vm搞;

1
2
3
sudo
apt-get install binutils-dev
apt-get install libcapstone-dev

完成这道题需要知晓一点点bfd和capstone,以及更多的模仿;

注意引头 bfd.h 和 capstone/capstone.h;

题目已经把各种各样的代码都实现好了,只要求增添几个功能,逐一实现:

转储节内容

这里要求命令行输入三个参数,而第三个参数为节名称,并打印节的原始字节;

那么可以在原来打印节的地方(main.cc)镶嵌一个东西进去:

one

flag一开始设置为0,找到同名节后设置为1;

第一个判断是否有三个参数并且调控数为0,则执行这个函数;

外面的判断是如果没有找到第三个参数一样的节名称,则此时flag依然是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
int print_origin_bytes(Section* sec, char *one)
{
int i,j = 0;
if(sec->name == one) //找到同名则进入
{
printf("\n");
printf("contents of %s section\n", one);
printf(" ");
for(i = 0 ; i < 16 ; i++) //打印开头的 00~0f
{
printf("%02jx ", i);
}
printf("\n\n");
i = 1;
printf(" ");
while(j < sec->size) //循环打印
{
printf("%02jx ", sec->bytes[j++]);
if(i == 16)
{
i = 0;
printf("\n");
printf(" ");
}
i++;
}
printf("\n\n");
return 1; //设置flag为1
}
return 0;
}

输出数据符号

找到 loader.h 中的symbol类,可以发现 SymbolType 里面只有 SYM_TYPE_UKN SYM_TYPE_FUNC 两个,需要打印 DATA 符号,则添加一个 SYM_TYPE_DATA = 2, ;

之后找到 loader.cc 中的一个函数:load_symbol_bfd,可以发现其中有一步是给函数添加 FUNC项的,镶嵌如下内容:

two

当不添加FUNC项的内容时,添加DATA就好了;

最后改变下main.cc里面打印符号的地方为:

1
2
3
4
printf(" %-40s 0x%016jx %s\n",
sym->name.c_str(),
sym->addr,
(sym->type & Symbol::SYM_TYPE_FUNC) ? "FUNC" : "DATA");

使用capstone反汇编.text段

吐槽一下,edge搜索capstone 反汇编会出现一个博客,详细的记录了如何使用capstone;

具体函数实现:

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
int disass_text(Binary bin)
{
csh dis;
cs_insn *insns;
Section *text;
size_t n, i;
Section* sec;

//获取text节
for(i = 0; i < bin.sections.size(); i++)
{
sec = &bin.sections[i];
if(sec->name == ".text")
text = sec;
}

if(!text) goto fail;

//初始化capstone
if(cs_open(CS_ARCH_X86, CS_MODE_64, &dis) != CS_ERR_OK)
goto fail;

//反汇编把内容放insns里; ( 返回0就是版本问题 ((
n = cs_disasm(dis, text->bytes, text->size, text->vma, 0, &insns);

if(n <= 0)
goto fail;

//打印
printf("disassembly of .text section:\n");
for(i = 0;i < n;i++)
{
printf("0x%016jx\t%s\t\t%s\n", insns[i].address, insns[i].mnemonic, insns[i].op_str);
}

cs_free(insns,n);
cs_close(&dis);

return 0;

fail:
printf("err\n");
return 1;

}

然后在main.cc的打印节后一部分调用就好了;

通过:

pass

总结

纯粹对今年的有兴趣,5和7都比较新鲜,也是第一次手撸汇编了解bfd库,还挺有意思;

阅读全文
C++学习日记

命名空间

namespace NameSpace{},为解决变量以及函数重名而出现;

只能在全局中声明,可以嵌套,使用NameSpace::Items,调用内容;

可以在函数开头用 using NameSpace::Items,来声明引用内容,或者用 using namespace NameSpace,来声明引用全部内容;

可以匿名,引用内容则为::Items,等效于全局内容;

引用

定义的时候就需要赋值:int &ra = a; ,前面加地址符号,意义是使得ra和a共享地址(取个别名);

数组定义时记得加括号明确计算意义:int (&ra)[10] = a[10]

类似指针,函数传参时可以间接影响外部变量,并且返回时不能返回局部变量,因为是一个地址,内容会发生改变;

本质

int &ra = a 等价于 int * const ra = &a

目的为了简化源码理解,不需要构造多级指针,传参时传入引用可以体现出改变外部变量的意思;

面向对象与类

特征:封装,继承,多态;

封装

将一类的函数和数据装在一个类里,设置私有数据域,和公共方法称为封装

结构体等同于类,类里的对象拥有属性(数据),行为(函数),以及构造函数(初始化数据的函数,与类同名无返回);

初始化定义:声明同时赋值;

1
2
//构造函数后面加冒号变量为初始化,意义在于控制常量
matrix(int input):line(input)

析构函数:与类名同名,前面加~,局部变量返回时自动执行,一般做扫尾工作,如free;

调用类函数的时候,实际上编译器将外部变量的引用(匿名 / this )传入了类函数中;

定义在类中的函数,可以使用 this 指针,这个指针用于操控类里的变量,防止同名冲突;

继承

将一个类的内容添加到另一个类的起始;

1
2
3
4
5
6
7
8
9
10
11
12
class Father
{
public:
int same;
int age;
}
class Child:Father //继承
{
public:
int same;
int height;
}

相当于:

1
2
3
4
5
6
7
8
class Child
{
public:
int same;
int age;
int same;
int height;
}

可以通过父类引用访问子类,但只能指到age结束,要访问子类后续可以用操作指针;

同样可以通过子类引用访问父类,但需要强制转换,且指针可以访问父类后面的空白区域;

1
2
3
4
5
6
7
Child ch;
Father *pfa = &ch;
pfa->age; //通过父类指针访问继承age;
---
Father fa;
Child *pch = (Child *)&fa;
pch->age; //通过子类指针访问父类age;

若继承里有同名变量,则使用就近原则使用子类的内容;若要访问父类的同名变量,加上父类名:

1
2
3
Child a;
a.same = 1; //改变原子类内容
a.Father::same = 2; //改变继承内容

C++拥有多继承,按顺序排的类,哪个在前,其内容内存地址继承在最上方;

权限继承:

class修饰默认为private,若以public继承,则父类内容里保持不变(public还是public,private还是private);

private修饰的内容继承,子类不能访问,但protected可以;(protected和private就只有这个区别)

构造析构函数也会继承;

new&delete

创建类指针时,在堆开辟类内存空间并执行构造函数: Child *pch = new Child(); 类似于java;

使用delete释放空间并执行析构函数: delete pch; ;

拷贝构造函数

1
2
3
4
Test::Test(Test &testaddr)
{

}

用于对象,复制属性时候执行,只是浅拷贝,值转换,指针可能出错;一般对象作为参数和返回值时就会调用;

虚继承

用于避免多继承的多义变量产生;如:A -> B,A -> C;B,C -> D;使得D里有两组A内容;

继承时,在继承类的前面用 virtual 修饰,称其为虚基类;此时,虚基类不会直接继承其内容给子类,而是会给子类一个虚基类表,这是一个指针,指向两个数据,第一个表示虚基类表位于当前所在类的偏移,第二个表示继承父类位于虚基类表的偏移;

一般虚继承的父类内容放在子类内存的下方;

友元

在类中定义,用 friend 修饰,可以为另类和函数,使其能够直接使用 private 修饰的内容;

运算符重载

类类型的对象进行运算是没有意义的,所以可以自己给运算符定义意义;

使用以下内容重载运算符 加号,使得当两个对象相加时执行 “+” 函数的语句:

1
2
3
4
5
6
7
8
9
class A
{

}

A operator+ (A a1, A a2)
{

}

多态

明确一些专有名词:

  1. 重载:同一类里,同名函数,不同参数表;
  2. 重写:不同类,且类有继承关系,同名函数;
  3. 静态联编:程序编译时定死函数符号以及类指针的引用;动态联编则相反;
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
class A
{
public:
void HaHa()
{
cout<<"233";
}
}
class B:public A
{
public:
void HaHa()
{
cout<<"lol";
}
}

---
//此为静态联编,当 *p 定为 A 类型时,默认A -> HaHa() 调用 A 类里的函数;则两个都输出 233 ;
//当父类A里函数用 virtual 修饰时为动态联编,两个分别输出 233 和 lol;

A a;
B b;
A *p = NULL;
A = &a;
A -> HaHa();
A = &b;
A -> HaHa();

可以抽象地将函数理解成是存在于类中(实际上并没有),继承后盖在子类头上,一共有两个HaHa函数,就近原则选择父类的函数(父类指针),而虚函数的声明免除了这个误会,就类似于虚继承了;

同一个指针,指向不同对象,展现出不同效果的情况,称其为多态,为了实现多态而不产生歧义,才有的虚函数

定义:一个类中声明了虚函数,但是没给出实现,此称为纯虚函数,这个类叫做抽象类;抽象类不配有对象;

1
2
3
4
class A
{
virtual void func() = 0;
}

Q:为什么这么做?

A:用于构造模型,用子类去实现具体内容,并由一个指向父类的指针去实现多个子类的多态;

如果定义了虚函数,则类里会存在虚表指针独占内存,指向虚表,虚表里包含各个虚函数的地址;

继承会合并父类虚表为一个,如果是多继承,则合并第一个继承的虚表,后面的会保留下来;

模板

为解决多次重载不同**类型(重点)**参数的函数而出现;

1
2
3
4
5
template<typename DD>
DD func(DD a, DD b)
{
return a-b;
}

使用时,直接代入函数就行,DD需同类型,但不能指针;

需要指针时,需要使模板特化

1
2
3
4
5
6
7
//紧跟上面的内容写
---
template<>
int *func<int *>(int *a, int*b) //尖括号内容可有可无
{
return *(a + b);
}

如果有重载函数,模板特化,函数模板都满足使用的类型,则优先级由最特化到最不特化;

类也有模板,用 template<class DD> 声明;DD则可替换类里属性的类型;

使用时,用 ClassName<type> a; 来创建一个类对象;

模板的机制:

实际上,模板通过把一个数据类型用通用参数符号来代替,实际使用时,用某种数据类型进行替换;

达到处理类型不同,实现功能相同的效果;

模板技术成为泛型编程

异常处理

三个关键词:

throw : 手动抛出异常,一般满足if关系式后执行,也一般存在try包括中;

1
throw code;		//code为之后的捕获catch返回的内容

trycatch 成对出现,前者会用大括号包住可能出现异常的语句块;后者作为函数跟随: catch(type code) , 接收抛出的code,并执行catch函数里的内容;

catch里也能放 ... ,意思是捕获任意类型(包括类)异常(接收任意类型code);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int func(int a, int b)
{
if(b == 0)
{
throw 12;
}
return a/b;
}

try
{
int a = func(1,0);
}
catch(int code)
{
cout << "异常了,且code为" << code << endl;
}

上面代码会使得打印catch中的语句,且code值为12;

STL

标准模板库;

其部件:容器,算法,迭代器;目的是为了更好地存储数据(如排序和查找遍历)

容器

顺序容器:

  • Vector:向量,动态数组

    可扩大数组,每次扩大自身2倍;

    用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    push_back	//插入到末尾
    insert //指定位置插入
    Pop_back //删末尾元素
    erase //删除指定位置
    Clear //清空所有
    Vec[i] //访问索引元素
    at //返回指定下标处元素
    begin //返回 iterator 指向第一个元素
    End //返回 iterator 指向末尾下一个
    empty //是否为空?
    size //获取元素个数
    swap //交换两个元素

    ---
    vector<type> item; //创建
    vector::iterator it = item.begin(); //创建迭代器(指针)
  • String:字符串

  • List:双向链表

    不能操控下标,只能添加和删除以及遍历;

    用法和维克托差不多,多了个对头的操作:

    1
    2
    3
    4
    5
    6
    Push_front		//插入头部
    Pop_front //头部删除

    ---
    list<type> item; //创建
    list::iterator it = item.begin(); //创建迭代器(指针)

    若要操作插入,需要创建迭代器,并且用for循环让iterator++,对list而言,迭代器不能直接加一个数;

  • Deque:双端队列

关联容器:

  • Set

  • Map

    类似于python字典,有 key 和 value,其类型位pair;

    可以用 map[key] = value 实现赋值;

    用法:

    1
    2
    3
    4
    5
    6
    7
    using namespace std;

    map<const char*, int> item;
    item.insert(make_pair("BC",12));
    map::iterator it = item.begin();
    it -> first //访问key值
    it -> second//访问value值
  • Multiset

  • Multimap

容器适配器:

  • Stack
  • Queue
  • Prority queue

接口的概念

面向对象的过程中,接口就是公共属性的函数,是类内部私有属性和用户的桥梁;

多态的解释中,接口是抽象类函数,运用这个接口去实现不同子类的多态;

总结

实际学下来花了接近2个星期吧,说多不多说少不少,要真的掌握和吃牢固还是比较难受;

这么多内容实际上和C的差别也就是编译器,底层展现的代码其实都差不多(比如引用),按java的说法,其实对于这类语言还有更多说法,比如类里定义另类型对象这种聚集关系,以及链式异常(两种不同嵌套);

关于多态的思考

类型变量实际上也是一个引用,只不过CPP是声明即创建,只不过没赋值,而java是需要声明和创建的;所以导致一个结果:java只需要声明后创建子类类型就能实现多态,而CPP需要创建一个类指针来接收子类引用;

关于类的思考

STL的思想就很像java,感觉STL在往java和python靠,把一些容器的实现都弄成类,而且是泛型的;但是用之前记得调用头文件和std命名空间;

阅读全文
git学习日记

git是分布式版本控制系统;

什么是版本控制系统?

比如写游戏,会分版本,从一代的基础上复制并修改为二代,一代保留(类似MC的1.7.10和1.19);

当出了很多个版本之后,一点是想要回去玩之前的经典版本,但在版本丛中不好找;第二点是两个部门合作一个版本时分工不同,需要对一个共享文件进行改动,但是不知道另一个部门在什么时候做了什么改动,要合并内容,会比较难顶;

而一个软件能解决这些毛病,记录每次文件的改动,且允许共同编辑,称其为版本控制系统,目的为了方便开发大项目;

分布式与集中式?

集中式:版本库集中于中央服务器,每次改动会从其中获取新版本,之后推送回去;

分布式:每个人的文件中都有版本库,所以工作时不需联网,合作时,只需要将改动推送给对方,多人合作时,会有一人充当中央服务器;

Git工作流程

do

Workspace:工作区,平时存放项目代码的地方。
Index / Stage:暂存区,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表信息。
Repository:仓库区(或本地仓库),就是安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本。
Remote:远程仓库(github) ,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换。

一个文件夹即可成为工作区,其中初始化后有一个.git后缀子目录,存放Git管理信息;.git里又有暂存区和仓库区;

命令

基本操作

使文件夹成为工作区:

1
git init (指定目录)

添加文件到暂缓区:(name可以是*.加上后缀名,表示全部的一类后缀名都加入)

1
git add (name)

告知后提交,真正加入仓库中:

1
git commit -m "初始化项目版本"

查看仓库当前状态,显示有变更的文件:

1
git status

回退版本:

1
git reset (place)

从暂缓区和工作区中删除:

1
git rm

移动或重命名工作区文件:

1
git mv

远程操作

控制远程仓库:

1
git remote (基本操作)

从远程获取代码库:

1
git fetch

下载远程代码并合并:

1
git pull

上传远程代码并合并:

1
git push

撤回:

1
git revert

实际上是将之前的提交记录的相同状态再提交了一遍;

分支管理

创建分支:(不加name则是列出分支)

1
git branch (name)

带参:-d,表示delete,删除该分支;

​ -f,强制移动;例如:git branch -f main HEAD~3 将main分支移动到HEAD所指的提交记录上;

切换分支:

1
git checkout (name)

带参:-b,意思是先branch,再checkout,可以直接切换到新创的分支里;

name是某个具体提交记录那么就会分离HEAD,name如果是分支名加^,就会移到前一个提交记录;

如果是HEAD加~x,就会移动到前x个提交记录;

合并分支:(当前分支上融合另一个,另一个会存在一个副本)

1
git merge

合并同一分支上且不留副本:

1
git rebase 

带参:-i,交互式rebase;

将其他提交记录直接放到当前分支下:

1
git cherry-pick (name1) (name2)

结尾

更多练习:learngitbranching.js.org

github添加一个远程库命令:

1
2
3
git remote add origin git@github.com:XXXXX
git branch -M main
git push -u origin main
阅读全文
0x41414141 CTF

Backupkeys

Can you recover my backup keys to get the flag , they probably are hardcoded ?

提示说明 flag 是硬编码;

进入IDA只有start和零散的几个函数,说明加壳了;

用16进制查看器搜UPX可以发现 UPX! 标志;

脱壳后看main函数:

main

在最下方的输出 try harder的另一条线上有一个输出:

1
"Phew Phew collect the keys below , don't forget to put them in flag{} format"

消除逗号得到硬编码的flag:flag{Hardcodedpasswordsareuseless}

X-and-or

查看main:

main

code是一个运行后设置的地址,跳转到主要函数;从code里的判断可得知,输入长度为38;进入运算后循环38次,内部有固定数字进行异或运算并与输入内容比较;

code

循环的结尾是比较数据,需要使得eax最终为0;经过调试可以发现每次异或0~5的立方,满6归0;

写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
origin = [0x66, 0x6D, 0x69, 0x7C, 0x3B, 0x48, 0x36, 0x31, 0x3E, 0x28, 0x77, 0x19, 0x63, 0x31, 0x6C, 0x78, 0x24, 0x4E, 0x33, 0x63, 0x3D, 0x7D, 0x26, 0x4E, 0x37, 0x39, 0x30, 0x2B, 0x23, 0x1C, 0x31, 0x31, 0x6A, 0x29, 0x74, 0x1B, 0x62, 0x7C]

flag = [0] * 38

k = 0

for i in range(38):
flag[i] = chr(((k*k*k)) ^ origin[i])
k += 1
if k == 6:
k = 0

print(''.join(flag))

由此得到flag:flag{560637dc0dcd33b5ff37880ca10b24fb}

这题最有意思的是init函数,他把code的二进制内容异或上了0x42,需要将其变回来则再异或0x42,然后写在新的txt里,用IDA反编译,设置sp值,然后就能看到伪代码了:

code

Hash

I received a corrupted program the keys are probably lost within the game can you just find it for me to get the flag?.

Flag format : flag{key1+key2}

一开始看main觉得很奇怪,明明汇编有其他分支为什么伪代码始终显示的是 oops wrong path ?

结果发现是因为跳转的地方动了手脚:

jmp

它始终都是判断必走另一条路,所以找不到正确的上下文;本来以为很难的题一下就变成了渣渣题;

里里外外都改一下jmp,再运行一遍,它就自己吐flag了;

得出:flag{456789JKLq59U1337}

Cage

Are you aware of the scopes yet?

开场patch main_one函数得到正确的上下文;

main

发现需要输入一系列magic code,然后它会吐出已有的字符串,直接将字符串拼起来得到flag:

flag{0xm4tr1xreal}

Ware

My plaintext has been encrypted by an innocent friend of mine while playing around cryptographic libraries, can you help me to recover the plaintext , remembers it’s just numbers and there’s a space between some numbers which you need to remove the space and submit the recovered plain text as a flag.

开始一个upx直接脱掉;

搜索运行时的字符串得到flag:flag{32117406899806798980909}

WrongDownload

My key has been missing inside these two binaries can you help me to find it out ,as per my friend the key is divided in two parts between the two binaries so, remember you need to join them up before submitting as a flag.

直接反编译就能找到:flag{S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy}

阅读全文
Proxy_lab

实现

一个web代理,并有多线程和缓存功能,所以一一来实现;

  • 根据 write up 中所说,首先需要实现 HTTP/1.0 GET 请求的顺序代理:读取整个请求并解析请求(是否是有效HTTP请求),如果是则建立自己到适当 web服务器的连接,请求客户端指定对象,再将响应转发回客户端;注意:HTTP请求每行以\r\n结束,并以\r\n为尾行;
    • 具体要做到将url解析为三部分:host,后半url,HTTP版本;
    • 请求头中包含ua,host,connection,proxy-connection;
    • 请求端口无论在url中还是默认的都必须正确;
    • 处理过早关闭的连接,需要捕获SIGPIPE;
  • 实现多线程工作(生产者-消费者);
  • 实现缓存最近内存中使用的web对象(LRU策略);
    • 设置缓存的最大内存,以及单个对象的最大内存;

handout给出了tiny服务器的源码,只需要在这个基础上进行改装;

Tiny解析

main函数:

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
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

//输入端口参数
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

//监听描述符
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(clientaddr);
//接受请求成为描述符
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//读取套接字信息,IP和端口
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
//响应
doit(connfd);
//关闭接受描述符
Close(connfd);
}
}

doit函数:

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
void doit(int fd) 
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)


is_static = parse_uri(uri, filename, cgiargs); //解析uri
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}

if (is_static) {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size); //静态
}
else {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //动态
}
}

serve_static函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];

//发送响应行和报头
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n", filesize);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
Rio_writen(fd, buf, strlen(buf));

//回响载体
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //映射内存保证原文件纯净
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
}

serve_dynamic函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void serve_dynamic(int fd, char *filename, char *cgiargs) 
{
char buf[MAXLINE], *emptylist[] = { NULL };

//行与报头
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));

//子进程
if (Fork() == 0) {

setenv("QUERY_STRING", cgiargs, 1); //用url参数初始化环境变量
Dup2(fd, STDOUT_FILENO); //重定向输出到fd
Execve(filename, emptylist, environ); //运行CGI程序
}
Wait(NULL); //等待子进程结束回收
}

I . 顺序代理GET请求

writeup中的要求:

  1. 处理 HTTP/1.0 版本,如果遇到1.1,则需要将其作为1.0版本转发;

  2. 转发合法 HTTP 请求(实现中所示);

  3. 头中的 ua 和 两个 connection 都有给定的值:

    1
    2
    3
    "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305Firefox/10.0.3\r\n"
    "Proxy-Connection: close"
    "Connection: close"

实际上要做的,就是将doit内的操作变为转发与回复,而不是单纯回响;

那么需要将发送的包写给目标服务器,之后把目标服务器的回响写给发送者;

要看uri中是否有端口那就应该解析uri,但和上面解析是不一样的,上面是在看读取的文件是静态还是动态;

主函数和tiny一样,只是需要在 listen之前加一条:

1
signal(SIGPIPE,SIG_IGN);

新建三个全局变量:

1
2
3
4
//uri解析记录变量
char send_port[MAXLINE];
char send_host[MAXLINE];
char send_path[MAXLINE];

doit:

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
void doit(int fd) 
{
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char backbuf[MAXLINE],newhd[MAXLINE];
char *send;
rio_t rio,serverfd_rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)

//解析uri为host port path
parse_uri(uri);

//改写
sprintf(newhd, "GET %s HTTP/1.0\r\n", send_path);
send = built_message(newhd,&rio);

//开启远程服务器
int serverfd = Open_clientfd(send_host,send_port);
if (serverfd < 0)
{
printf("connection failed\n");
return;
}

Rio_readinitb(&serverfd_rio, serverfd);
//写入服务器
Rio_writen(serverfd, send, strlen(send));

size_t n;

//回响
while((n = Rio_readlineb(&serverfd_rio,backbuf,MAXLINE)) != 0)
{
printf("proxy received %d bytes,then send\n", (int)n);
Rio_writen(fd,backbuf,n);
}

Close (serverfd);
}

两个神奇函数:

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
void parse_uri(char *uri)
{
//是否有host:port,port默认80
char *hostpath = strstr(uri,"//");
if(hostpath != NULL) //有
{
//是否有port
char *portpath = strstr(hostpath + 2,":");
if(portpath != NULL) //有
{
int num;
sscanf(portpath+1,"%d%s",&num,send_path);
sprintf(send_port,"%d",num);
*portpath = NULL;
}
else //无
{
char *path = strstr(hostpath + 2,"/");
if(path != NULL)
{
strcpy(send_path,path);
strcpy(send_port,"80");
*path = NULL;
}
}
strcpy(send_host,hostpath + 2);
return;
}
else //无
{
char *path = strstr(uri,"/");
if(path != NULL)
{
strcpy(send_path,path);
}
strcpy(send_port,"80");
return;
}
}

char *built_message( char *getit,rio_t *rp)
{
//构造新头
char buf[MAXLINE];
char rio[MAXLINE];
sprintf(buf,"%s",getit);
sprintf(buf,"%sHost: %s\r\n",buf,send_host);
sprintf(buf,"%sUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n",buf);
sprintf(buf,"%sConnection: close\r\n",buf);
sprintf(buf,"%sProxy-Connection: close\r\n\r\n",buf);

//补上原内容
Rio_readnb(rp,rio,MAXLINE);
sprintf(buf,"%s%s",buf,rio);
return buf;
}

II . 多线程的并发

实现多线程使用 消费者-生产者 模型:

消费者和生产者共同使用一个 n个槽的优先缓冲区,生产者产生新的项目并插入缓冲区;消费者取出这些项目并使用;

因此两者的访问需要互斥,并且调度地访问:空状态(消费者等待),满状态(生产者等待);

在这个实验里,消费者就是服务端,接受各样的连接;生产者就是客户端,发送各样的连接;

实现缓冲区:

1
2
3
4
5
6
7
8
9
typedef struct {
int *buf; // 缓冲区数组
int n; // 槽的最大数量
int front; // buf[(front+1)%n] 是第一个项目
int rear; // buf[rear%n] 是最后一个项目
sem_t mutex; //互斥锁,初始化1
sem_t slots; //记录槽,初始化n
sem_t items; //记录项目,初始化0
} sbuf_t;

客户端插入函数:

1
2
3
4
5
6
7
8
void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); // 对slots加锁,保证槽位满时,客户端挂起
P(&sp->mutex); // 对缓冲区互斥访问
sp->buf[(++sp->rear)%(sp->n)] = item; // 添加项目
V(&sp->mutex); // 解锁
V(&sp->items); //与slots对应地调整items
}

服务端实现后移除项目的函数:

1
2
3
4
5
6
7
8
9
10
int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); // 如果项目没有,服务端挂起
P(&sp->mutex); // 加锁缓冲区
item = sp->buf[(++sp->front)%(sp->n)]; // 移除项目
V(&sp->mutex); // 解锁
V(&sp->slots);
return item; //返回客户端的描述符
}

主函数(和tiny的main差不多):

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
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

//输入端口参数
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
//阻塞SIGPIPE信号
signal(SIGPIPE,SIG_IGN);
//监听描述符
listenfd = Open_listenfd(argv[1]);

//创建线程
sbuf_init(&sbuf, SBUFSIZE);
for(int i = 0; i < NTHREADS; i++)
{
Pthread_create(&tid, NULL, thread, NULL);
}

while (1) {
clientlen = sizeof(clientaddr);
//接受请求成为描述符
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//插入描述符
sbuf_insert(&sbuf, connfd);
//读取套接字信息,IP和端口
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
}
}

线程执行函数:

1
2
3
4
5
6
7
8
9
10
void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while(1){
//从缓冲区中读出描述符
int connfd = sbuf_remove(&sbuf);

doit(connfd);
Close(connfd);}
}

III . 缓存web对象

目的是为了让多次访问的web对象不用再连接服务器,直接响应;

这里会使用 读者-写者 模型 ,让线程从缓存中读和写:

只读的线程叫读者,只写的进程叫写者,读者可以和其他读者共享只读部分,写者需要有独立的访问;

这个模型有两种情况:

读者优先,写者优先;

这里使用读优先:

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
int read_cnt;		//记录读者数量
sem_t mutex, w; //都初始化为1,w导使有读无写,有写无读


void reader(void)
{
while(1){
P(&mutex);
readcnt++;
if(readcnt==1) //第一个读者导致w加锁,则写者挂起;
P(&w);
V(&mutex);

P(&mutex);
readcnt--;
if(readcnt==0) //最后一个读者结束解锁w
V(&w);
V(&mutex);
}
}

void writer(void)
{
while(1){
P(&w);

...

V(&w)
}
}

设置缓存区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct
{
char obj[MAX_OBJECT_SIZE];
char uri[MAXLINE];
int LRU;
int isEmpty;

int read_cnt; //读者数量
sem_t w; //Cache信号量
sem_t mutex; //read_cnt信号量

} block;

typedef struct
{
block data[MAX_CACHE];
int num;
} Cache;

修改doit函数中的内容,得到请求后,判断uri是否在缓存中,不在就添加进去:

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
void doit(int fd) 
{
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char backbuf[MAXLINE],newhd[MAXLINE];
char *send;
char cache_tag[MAXLINE];
rio_t rio,serverfd_rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
strcpy(cache_tag,uri);
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)

//uri是否存在缓存中
int i;
if ((i = get_Cache(cache_tag)) != -1)
{
//加锁
P(&cache.data[i].mutex);
cache.data[i].read_cnt++;
if (cache.data[i].read_cnt == 1)
P(&cache.data[i].w);
V(&cache.data[i].mutex);

Rio_writen(connfd, cache.data[i].obj, strlen(cache.data[i].obj));

P(&cache.data[i].mutex);
cache.data[i].read_cnt--;
if (cache.data[i].read_cnt == 0)
V(&cache.data[i].w);
V(&cache.data[i].mutex);
return;
}

//解析uri为host port path
parse_uri(uri);

//改写
sprintf(newhd, "GET %s HTTP/1.0\r\n", send_path);
send = built_message(newhd,&rio);

//开启远程服务器
int serverfd = Open_clientfd(send_host,send_port);
if (serverfd < 0)
{
printf("connection failed\n");
return;
}

Rio_readinitb(&serverfd_rio, serverfd);
//写入服务器
Rio_writen(serverfd, send, strlen(send));

char cache_buf[MAX_OBJECT_SIZE];
int size_buf = 0;
size_t n;

//回响
while((n = Rio_readlineb(&serverfd_rio,backbuf,MAXLINE)) != 0)
{
size_buf += n;
if(size_buf < MAX_OBJECT_SIZE)
strcat(cache_buf, buf);
printf("proxy received %d bytes,then send\n", (int)n);
Rio_writen(fd,backbuf,n);
}

Close (serverfd);

//没有就写入缓存
if(size_buf < MAX_OBJECT_SIZE){
write_Cache(cache_tag, cache_buf);
}

}

总结

虽然迷迷糊糊的,但跟着线程走了一遍,多多少少学会了更多的东西:比如信号量的运用,线程创建和运作方式,以及状态机和模型的特点;但这个lab确实感受到了难度,等往后学的深入再返回看的话应该还会有收获;

阅读全文
Malloc_Lab

forest


实现

一个动态内存申请器,能实现:malloc, free, realloc;

前置知识

CSAPP第9章:动态内存申请器,内存中的堆,链表;

蛋疼的检测工具

首先需要;

1
gcc -Wall -O2 -m32   -c -o mdriver.o mdriver.c

然后;

1
make mdriver mdriver.o mm.o memlib.o fsecs.o fcyc.o clock.o ftimer.o

以上两个出错请用下面指令解决;

1
sudo apt-get install gcc-multilib

下载出错请更新镜像,或者添加清华园下载路径:

1
2
deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free
deb-src https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free

输入以下命令打开下载源:

1
sudo vim /etc/apt/sources.list

按 i 进入编辑模式,完成后输入 :w 保存, :q退出;之后输入一遍:

1
sudo apt-get update && apt-get upgrade && apt-get dist-upgrade

然后就能输入第三条指令了,接着编译就行了;

其次,traces是缺失的,需要下载以进行检测;地址:

https://github.com/Davon-Feng/CSAPP-Labs/tree/master/yzf-malloclab-handout/traces

将10个文件装入文件夹,将文件夹放到和 mdriver 同级的地方;

并修改config.h里第15行的内容为自己的traces文件夹目录;

分析

  • 需要内存空间模拟堆;
  • 需要模拟已分配和未分配的块;
  • 需要管理这个空间(开始,结束,填充);
  • 需要操控这个空间(放置,分割,合并,释放);

已知内容

mem_heap,mem_brk两个指针分别指向堆的开始和结尾,mem_sbrk函数可以调节brk并返回旧brk的值,两个指针已初始化相等;

堆中的最小单位为4字节(1字),第一个字是双字边界对齐不使用的填充字;后面跟着两字的序言块,分配器使用私有的全局变量 heap_listp 指向序言块的第二个字开头;以一个0内容的已分配块作为结束的一个字;

使用隐式空闲链表,下一次适配,边界标记的堆块格式(最小4字,开头和结尾2字是标志字),立即合并;

标志字由整个块的大小或上分配位组成;

代码

I . 操作空闲链表的常数和宏

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
#define WSIZE 4     //一字的字节
#define DSIZE 8 //两字的字节
#define CHUNKSIZE (1<<12) //延展一次堆的字节数

#define MAX(x,y) ((x) > (y) ? (x) : (y))
#define MIN(x,y) ((x) < (y) ? (x) : (y))

//大小或上分配位打包的标志字
#define PACK(size,alloc) ((size) | (alloc))

//读写p指针处的一个字
#define GET(p) (*(unsigned int *)(p))
#define PUT(p,val) (*(unsigned int *)(p) = (val))

//读取标志字中的大小和分配状态
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)

//给出开头或结尾标志字的位置(bp指针指向块的有效载体)
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) - DSIZE + GET_SIZE(HDRP(bp)))

//给出前一个或下一个块的bp指针
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE((char *)(bp) - DSIZE))

static char * heap_listp;
//下次适配记录之前的块
static char * prev_listp;

p是一个void * 指针,所以强制转换是必要的;

II . 创建空闲列表(堆)

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
int mm_init(void)
{
//分配4个字(1个初始填充,2个序言,1个结尾)
if((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)
return -1;
PUT(heap_listp,0);
PUT(heap_listp + WSIZE,PACK(DSIZE,1));
PUT(heap_listp + (2 * WSIZE),PACK(DSIZE,1));
PUT(heap_listp + (3*WSIZE),PACK(0,1));
heap_listp += (2*WSIZE);
prev_listp = heap_listp;

//延展空闲块
if(extend_heap(CHUNKSIZE/WSIZE) == NULL)
return -1;
return 0;
}

static void * extend_heap(size_t words)
{
char * bp;
size_t size;

//满足2字对齐
size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE;
if((long)(bp = mem_sbrk(size)) == -1)
return NULL;

//设置填充空闲块的头和尾,以及延展一个结束字
PUT(HDRP(bp),PACK(size,0));
PUT(FTRP(bp),PACK(size,0));
PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1));

//合并空闲块,如果前面是的话
prev_listp = coalesce(bp);
}

mm_init函数在创建初始堆空间;extend_heap函数在延展空闲块,用在init里,当然也可以用在当申请空间不足时的地方,所以单独成为一个函数;coalesce之后会讲到,是合并空闲块的函数,在填充和释放时用到;

注意,首次适配所记录的当前块指针如果被向前合并,则记录指针也需要随之改变,所以最好运用合并函数的地方都使其返回的值成为prev_listp;

III . 释放和合并块

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
void mm_free(void * bp)
{
size_t size = GET_SIZE(HDRP(bp));

//设置头尾分配位0
PUT(HDRP(bp),PACK(size,0));
PUT(FTRP(bp),PACK(size,0));

prev_listp = coalesce(bp);
}

static void *coalesce(void *bp)
{
size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
size_t size = GET_SIZE(HDRP(bp));

//4种情况,前后块都是分配的,前后块都没分配,前后块有一者分配
if(prev_alloc && next_alloc)
{
return bp;
}
else if(!prev_alloc && next_alloc)
{
size += GET_SIZE(FTRP(PREV_BLKP(bp)));
PUT(FTRP(bp),PACK(size,0));
PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
bp = PREV_BLKP(bp);
}
else if(prev_alloc && !next_alloc)
{
size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
PUT(HDRP(bp),PACK(size,0));
PUT(FTRP(bp),PACK(size,0));
}
else
{
size += GET_SIZE(HDRP(NEXT_BLKP(bp))) + GET_SIZE(FTRP(PREV_BLKP(bp)));
PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0));
PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
bp = PREV_BLKP(bp);
}
return bp;
}

在第二个else if 的地方,HDRP需要写前面,因为FTRP会用HDRP处的内容;

IV . 放置和分割块

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
void *mm_malloc(size_t size)
{
// 调整后大小
size_t asize;
//延展大小
size_t extendsize;
char *bp;

//忽视无用请求
if(size == 0)
return NULL;

//调整对齐要求
if(size <= DSIZE)
asize = 2 * DSIZE;
else
//向上取整
asize = ((size + DSIZE + (DSIZE - 1)) / DSIZE) * DSIZE;

//放置块
if((bp = find_fit(asize)) != NULL)
{
place(bp,asize);
return bp;
}

//空间不够延展
extendsize = MAX(asize,CHUNKSIZE);
if((bp = extend_heap(extendsize/WSIZE)) == NULL)
return NULL;
place(bp,asize);
return bp;
}

static void *find_fit(size_t asize)
{
void * bp;

//初始化使prev_listp指向了序言块
for(bp = prev_listp; GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp))
{
if(!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
{
prev_listp = bp;
return bp;
}
}
//后面找没有之后从头找
for(bp = heap_listp; bp != prev_listp; bp = NEXT_BLKP(bp))
{
if(!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
{
prev_listp = bp;
return bp;
}
}
return NULL;
}

static void place(void * bp,size_t asize)
{
//原块长度
size_t csize = GET_SIZE(HDRP(bp));

//分割后大小大于或等于最小块则执行分割
if((csize - asize) >= (2 * DSIZE))
{
PUT(HDRP(bp),PACK(asize,1));
PUT(FTRP(bp),PACK(asize,1));
bp = NEXT_BLKP(bp);
PUT(HDRP(bp),PACK((csize - asize),0));
PUT(FTRP(bp),PACK((csize - asize),0));
}
else
{
PUT(HDRP(bp),PACK(csize,1));
PUT(FTRP(bp),PACK(csize,1));
}
}

V . 重分配块大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *mm_realloc(void *bp, size_t size)
{
void * old_bp = bp;
void * new_bp;
size_t oldsize, newsize;

//创建新的分配块
new_bp = mm_malloc(size);
if(new_bp == NULL)
return NULL;
oldsize = GET_SIZE(HDRP(old_bp));
newsize = GET_SIZE(HDRP(new_bp));
//比较新旧大小,如果新的更大,则复制原数据过去,如果更小,复制小的长度个原数据
if(oldsize < newsize)
newsize = oldsize;
//取消结尾字
memcpy(new_bp, old_bp, newsize-WSIZE);
mm_free(old_bp);
return new_bp;
}

VI . 检测

将代码打包到mm.c,使用之前的编译命令,搞出有代码的 mdriver;输入以检测:

1
./mdriver -V

如图:

check

总结

至此,实现了所分析的内容:

模拟空间,填充——extend_heap;

已分配和未分配的块——有头尾的列表模式以及宏定义的操作;

开头结尾——mm_init;

以及有小标题的放置,分割,释放,合并小函数;


forest

阅读全文
假期复现

1 . havetea

IDA:

main

左边是主函数里可以找到的,可以发现首先让输入key,且为16个长度,并且把输入的key分成两段进行了两次加密;在加密函数crypto里,是一个简单TEA运算,只不过IDA抽风把 +=delta 翻译成了 -=补码;crypto又把输入的数据截成两段进行运算,使用的key可以在程序里找到;之后用加密数据进行比较;

知晓key和加密后的数据使用对应解密方式解密:

1
2
3
4
5
6
for(i=0;i<32;i++)
{
r -= ((l<<4) + c) ^ (l + sum) ^ ((l>>5) + d);
l -= ((r<<4) + a) ^ (r + sum) ^ ((r>>5) + b);
sum -= delta;
}

解密后得到输入为:please_drink_tea

end

之后又让输入32长度的内容进行加密,并且用之前输入的16长度作为第二次加密cry的密钥;可以看出这次32长度的内容被分成了4段进行cry加密,而cry其实是和第一次的crypto差不多的TEA运算;加密完之后进行数据比较;

对应解密方式:

1
2
3
4
5
6
for(i=0;i<32;i++)
{
r -= (sum + key[(sum >> 11) & 3]) ^ (l + ((16 * l) ^ (l >> 5)));
sum -= delta;
l -= (sum + key[sum & 3]) ^ (r + ((16 * r) ^ (r >> 5)));
}

解密后得到:flag{c616454f52a6334273b5f455a10ef818}

2.maze

IDA:

main

通过字符串搜索,找到主要函数,可以看到通过输入的v2来与v3进行 domaze 函数运算;右图为 domaze 函数,可以看出这是个三线迷宫,迷宫整体由v3控制,输入的v2代表玩家移动方向;

maze

通过调试可知,这是个指针制作的迷宫,前24位每8位代表一个方向,第25位开始往后是控制数;比如地址0x112E86A 为1,对应0x112E860处的方向的控制数,当这个控制数为 1 的时候,根据 domaze 函数的计算规则可知,会触发 sub_4C6470 结束函数;而最后一个 0 是代表是否走过这个路口,走过之后会变成 1;

之后通过这个规则去逆推回去:(这些是地址低三位)

test

正着写回去便是:rrrrtltltlllltlltrtrrr

md5之后得到:flag{988b0f23719099efcbd66586a168bab9}

3.rota

IDA:

main

最上面图展示了最终的比较数据;中间左边的图则是一开始的base64编码,下面的图展示了base64的变种码表;中间右边的图展示了最后是生成了一个BOX,用BOX与base64编码后的内容进行加密;

中间有BOX的生成内容,但是无关紧要,因为生成的数据和输入的内容无关,所以是固定的,BOX也就是固定的;

所以只需要破解这个加密就能够得出最终结果;

crypto

以上为crypt函数的内容;

调试加分析加软磨硬泡得出爆破代码的核心内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for v10 in range(66):
v11 = v5
v12 = (v5 + BOX[(v3 + v10) & 0x3F]) & 0x3F
v13 = (v3 + 1) & 0x3F
v14 = BOX[((result + BOX[v12 + 64]) & 0x3F) + 128]
if(ans[j] == b64box[v14]):
print(chr(b64box[v10]))
BOX[192] = v13
break
elif(v10 == 65):
print('erorr')
exit(0)
if(not v13):
v5 = (v5 + 1) & 0x3F
BOX[193] = v5
if(((v11 + 1) & 0x3F) == 0):
result = (result + 1) & 0x3F
BOX[194] = result

然后和原代码一样,该循环几次循环几次,该有几个有几个;

得出base64编码后的内容为:cAJ7BzX+6zHrHwnTc/i7Bz6f6t6EBQDvc/xfHt9d6S9XX

再base64解码一遍:

base64

得到:flag{8cdd01062b7e90dd372c3ea9977be53e}

4.gocode

IDA:

main

gocode提示了这是go语言写的,所以搜索函数main_main找到主函数,通过右上角的图可知输入总长度为37,且是由 PCL{} 括起来的;

看到while(1)和switch 再根据题目名称,可知道这是个类似VM的东西,而根据docode变量可知第一站经过的便是右下角图中的函数,作用是把flag括起来的32个长度内容两两拼接成十六进制数,一共变成16个;

然后便是对不同指令码对应操作进行翻译:

do

逻辑就是每次经过AA开始判断,如果错误就退出程序,直到走完全部的code码就算成功;

把翻译的写成代码然后用z3来解:(重要代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s = Solver()
flag = [BitVec('flag[%d]' % i,64) for i in range(16)]

def check():
global ip
global code
global ex
if ip + 2 >= 374 :
print('ip out of range')
exit(0)
s.add(ex[code[ip+1]] == ex[code[ip+2]])

if ip >= 374:
if s.check() == sat:
print(s.model())
break
else:
print('no')
break

解出数字后换成十六进制再拼写在一起便得到:PCL{bdcc4f46d73ec09ee628633d2f227b47}

5.analgo

IDA:

main

第一眼看去会和上道题很像,也是类似虚拟机的构造,v23是指令,main_anal函数是虚拟机函数,下面的十六进制数是比较数据,判断输入的长度是42;

但是由于这个VM反编译出很多控制数不好分析各个指令码在做什么,同时发现输入是包含flag{}的,且每输入一个,比较结果也对应的变换一个,称之为一一对应;(蓝线是对应关系)

company

可以看到随着 flag{ 的输入,每输入一个,RCX 和 RDX 就相同一个字节;

那么可以使用之前hgame中 hardasm 题目的解法,将加密后的RCX值输出,与比较数据判断从而爆破;

patch0

先把判断搞掉,全nop,直接进入输出 wrong(SecondBC) 的地方;之后修改原程序比较的地方,改为将加密数据放到 SecondBC 这个地方:

patch1

但因为 SecondBC 是 rdata段的,拥有只读权限,所以要修改权限:

change

搜索 .rdata,将 40 00 00 40 改为 40 00 00 C0;

之后写代码爆破:(使用subprocess模组)

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
import subprocess

ans = 'this is answer' '''比较数据'''
hexs = '0123456789abcdef-' '''约束范围,输入其他的程序会提前退出'''
hexs = list(hexs)
for i in range(17):
hexs[i] = ord(hexs[i])

real_flag="flag{"
cur_index = 5 '''当前位置'''
k = 0

while cur_index < 42:
for i in hexs:
real_flag_arr = [0] * 42

for j in range(len(real_flag)): '''爆破储存位置'''
real_flag_arr[j] = ord(real_flag[j])
real_flag_arr[len(real_flag_arr)-1] = ord("}")

for j in range(len(real_flag_arr)-2,cur_index,-1):
real_flag_arr[j] = 48 '''未知位填充0'''

real_flag_arr[cur_index] = i
real_flag_arr_s = ''.join(chr(k) for k in real_flag_arr)
p = subprocess.Popen(["C:\\Users\\Second_BC\\Desktop\\analgo.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(real_flag_arr_s.encode()) '''输入程序'''
p.stdin.close()
out = p.stdout.read() '''读取输出'''
out = list(out)

if (out[k] == ans[k] ):
real_flag += chr(i)
k += 1
cur_index += 1
print(real_flag)
break

由于这个程序是 8字节 8字节来比较的,所以手动多调几次,传入之前先 add rsi 8 获取之后的加密数据;

然后每轮都改下cur_index += 8;

然后得到:flag{568a3cdd-77e1-4c42-9fee-127e27a5744e}

6.puzzle

一开始发现是个加壳程序,跑一遍发现和UPX加壳很像,一开始是循环解码,然后进入原入口;用PE也显示其为UPX加壳,但是提示不能用指令脱壳;

使用十六进制查看器发现原UPX标记处被改为了vmp,将其改回并用指令对其脱壳;

unpack

之后进去过后看IDA:

main

在scanf之后的是一段循环,通过调试可以知道,这里允许通过 0 ~ 9 字符,并且一共输入56个,否则失败;

这段循环将输入的56个数字放到一些地址里,而地址原来就有些数据;填完之后一共是 9*9 = 81个数据;

然后来到判断 judge 函数,这里它将这81个内容作为参数传入;

经过调试呢,可以发现,输入的内容中,有些是不能重复的,而且不能有 0 ;这可以让想起数独游戏;

把里面给的数据拿出来做成 9 * 9 的数独表,然后进行求解:

solve

解出输入的56个内容为:76135283549798674164925733849217386455934161872359295314

输入源程序之后,便得到: flag{23c3cb3aedbbfdd009d1bf52e530676a}

阅读全文
Shell_Lab

壳实验,对应于csapp第8章,异常控制流;根据其提示可知,该实验要求编写一个完整的简单的壳;

在完成之后,有许多的检测关卡等待壳的成果;

实际上,此实验已经将大部分内容编写完毕,只要求完成7个函数的构造来完整壳即可;

这7个函数都是有关于信号,以及异常处理的;

现在来先说说这7个函数的大致功能以及目的:

  1. eval:解析和解释命令行的主例程;

  2. builtin_cmd:识别并解释内置命令;

    • 内置命令:

    • quit:退出shell;

    • fg:发送 SIGCONT(继续)来重启 job,位于前台运行;(前台只允许1个job运行)

    • bg:发送 SIGCONT(继续)来重启 job,位于后台运行;

    • jobs:列出所有后台作业;

  3. do_bgfg:执行bg和fg指令;

  4. waitfg:等待前台作业完成;

  5. sigchld_handler:SIGCHLD(子程序退出)信号处理;

  6. sigint_handler:SIGINT(中断)信号处理;

  7. sigtstp_handler:SIGTSTP(暂停)信号处理;

辅助的已有函数:

  • parseline:解析命令行构建argv列表;
  • clearjob:清除job结构体中的内容;
  • initjobs:初始化job列表;
  • maxjid:返回允许的最大job ID;
  • addjob:添加一个作业到job列表;
  • deletejob:从job列表中删除pid的作业;
  • fgpid:返回前台job的pid;
  • getjobpid:根据pid从job列表中找到作业;
  • getjobjid:根据job ID从job列表中找到作业;
  • pid2jid:根据pid返回对应jid;
  • listjobs:显示job列表;

之后有经典的实验约束规则:

  • 提示符为:tsh>
  • 用户键入的命令行应包含一个名称和零个或多个参数,所有参数均由一个或多个空格分隔。 如果名称是内置命令,则shell应该立即处理它并等待下一个命令行;否则,shell应该假定名称是可执行文件的路径,它在初始子进程的上下文中加载并运行;
  • shell不用支持管道或I/O重定向;
  • 输入 ctrl-c 导致 SIGINT (输入 ctrl-z 导致 SIGTSTP)发送到当前前台作业以及该作业的任何后代,如果没有前台作业,那么信号没有效果;
  • 如果命令行以 & 结束,则shell应该在后台运行作业,否则它将在前台运行该作业;
  • 每个作业都可以通过进程ID(PID)或作业ID(JID)进行标识,该ID是tsh分配的正整数;
  • shell支持内置命令;
  • shell应该回收所有僵死子进程,如果任何作业由于接收到未捕获到的信号而终止,则shell应该识别此事件并打印一条消息,其中包含该作业的PID和有问题的信号的描述;

先来看Shell的主函数:

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
int main(int argc, char **argv) 
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */

/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);

/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}

/* Install the signal handlers */

/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */

/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);

/* Initialize the job list */
initjobs(jobs);

/* Execute the shell's read/eval loop */
while (1) {

/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}

exit(0); /* control never reaches here */
}

第一个while是在选择模式;输入h参数显示提示,输入v参数发出附加诊断信息,输入p不显示命令行;

之后需要捕获信号,就需要通过Signal函数将信号和对应处理函数绑定,然后进入第二个while使用eval一条一条地重复解析输入的内容;

I . Eval

第一解析命令行,可以套用parseline函数来帮忙,并根据结尾符号是否为 & 来判断前后台关系;

第二要做到的,查看解析出的 argv[0] 是否为内置命令,是,则转交给builtin_cmd函数,不是则创建子进程来运行;之后在shell中通过 addjob 来添加作业,如果是前台作业,就等待前台作业运行完毕,如果是后台作业,就执行解析下一条命令;

由此:

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
void eval(char *cmdline)
{
static char array[MAXLINE];
char *buf = array; //cmdline容器
char *argv[MAXARGS]; //命令行参数
int bg; //前后台?
pid_t pid; //进程id
sigset_t mask, prev, all; //阻塞块

strcpy(buf,cmdline);
bg = parseline(buf,argv);

if(argv[0]==NULL)
return;

if(!builtin_cmd(argv)) //是否内置命令
{
sigemptyset(&mask); //清空mask块
sigaddset(&mask,SIGCHLD); //添加SIGCHLD到mask
sigfillset(&all); //所有信号进入all块
sigprocmask(SIG_BLOCK,&mask,&prev) //阻塞SIGCHLD信号,防止addjob和deletejob竞争

if((pid = fork()) == 0) //子进程
{
fflush(sdout); //printf("in process:%d\n",pid);
setpgid(0,0); //更换子进程进程组,以免和shell冲突
sigprocmask(SIG_SETMASK,&prev,NULL)
if(execve(argv[0],agrv,environ) < 0) //通过execve加载到子进程
{
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}

//printf("parent:%d\n",getpid());
sigprocmask(SIG_BLOCK,&all,NULL) //访问job列表需阻塞所有信号
addjob(jobs,pid,bg?BG:FG,buf);
sigprocmask(SIG_SETMASK,&prev,NULL) //解除阻塞

if(bg)
printf("[%d] (%d) %s", pid2jid(pid), pid, buf);
else
waitfg(pid);
}
}

需要注意的有两点:

  • 阻塞SIGCHLD信号以防止addjob与deletejob竞争;
  • 访问全局数据jobs列表前阻塞所有信号;

为什么要在fork创建进程之前阻塞SIGCHLD呢?因为fork的进程可能在任意时刻暂停或终止;使得Shell跳转通过对应信号去处理程序,并在信号处理中对该作业进行修改;如果在 addjob 之前跳转,则会由于未保存该作业而导致错误,所以需要在fork之前阻塞,并在子进程中取消阻塞;

设置独立的进程组与Shell分开,以免Shell接收到的其他例如 ctrl-c 之类的信号而导致进程受到影响;

其次,从安全信号处理的角度,在修改读取jobs时如果不阻塞所有信号,则会有可能中断而导致jobs的各部分状态不同;

II . builtin_cmd

第一,前面可以知道用这个函数套用在eval里,使得分辨是否为内置命令,所以让内置命令返回1,而非内置命令返回0;

第二,已知这个函数会用于bg和fg的内置命令,所以可以套用do_bgfg的函数;

那么代码就可知了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0],"quit"))
exit(0);
if(!strcmp(argv[0],"jobs"))
{
listjobs(jobs);
return 1;
}
if(!strcmp(argv[0],"&"))
return 1;
if((!strcmp(argv[0],"bg")) || (!strcmp(argv[0],"fg")))
{
do_bgfg(argv);
return 1;
}
return 0;
}

III . do_bgfg

根据之前提到的,需要用用到SIGCONT信号,那么也需要使用kill来发送给整个进程组;

在这之前需要修改job结构的状态;

而在修改前台或者后台的再之前,需要寻找到这个工作的ID或者pid;

由此代码:

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
void do_bgfg(char **argv)
{
int jid;
pid_t pid;
struct job_t *job; //单独的job指针
sigset_t mask,prev; //修改job之前阻塞所有信号

if(argv[1] == NULL)
{
printf("%s command requires PID or %%jobid argument\n",argv[0]);
return;
}

if(sscanf(argv[1],"%%%d",&jid) > 0)
{
job = getjobjid(jobs,jid);
if(job == NULL || job->state == UNDEF)
{
printf("%s: No such job\n", argv[1]);
return;
}
}
else if(sscanf(argv[1],"%d",&pid) > 0)
{
job = getjobpid(jobs, pid);
if(job == NULL || job->state == UNDEF)
{
printf("(%s): No such process\n", argv[1]);
return;
}
}
else
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//上面的都是在排除异常情况

//修改job状态
sigfillset(&mask);
sigprocmask(SIG_BLOCK,&mask,&prev);

if(!strcmp(argv[0], "fg"))
job->state = FG;
else
job->state = BG;

sigprocmask(SIG_SETMASK,&prev,NULL);

pid = job->pid;
kill(-pid,SIGCONT); //负的则发送给进程组
if(!strcmp(argv[0], "fg"))
waitfg(pid);
else
printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
}

IV . waitfg

等待前台作业完成就使用sleep挂起:

1
2
3
4
5
void waitfg(pid_t pid)
{
while(pid == fgpid(jobs))
sleep(1);
}

V . sigint_handler

当使用 Ctrl+c ,内核发送中断信号给这个Shell程序,而Shell程序通过kill发送信号给子进程,而停止信号也同理;

1
2
3
4
5
6
7
8
void sigint_handler(int sig){
int old_errno = errno; //首先需要保存原始的errno
pid_t pid = fgpid(jobs);
if(pid!=0)
kill(-pid,sig);

errno = old_errno;
}

保存之前的errno并在返回时重新赋值,是为了防止它被改变;

VI . sigstp_handler

1
2
3
4
5
6
7
8
void sigtstp_handler(int sig){
int old_errno = errno; //首先需要保存原始的errno
pid_t pid = fgpid(jobs);
if(pid!=0)
kill(-pid,sig);

errno = old_errno;
}

VII . sigchld_handler

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
void sigchld_handler(int sig)
{
int old_errno = errno; //首先需要保存原始的errno
pid_t pid;
sigset_t mask, prev;
int state; //保存waitpid的状态,用来判断子进程是终止还是停止
struct job_t *job;

sigfillset(&mask);
//由于信号不存在队列,而waitpid一次只会回收一个子进程,所以用while
while((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0) //要检查停止和终止的,并且不要卡在这个循环中
{
//对全局结构变量jobs进行修改时,要阻塞所有信号
sigprocmask(SIG_BLOCK, &mask, &prev);
if(WIFEXITED(state)) //子进程通过调用exit或return正常终止,需要从jobs中删除该作业
{
deletejob(jobs, pid);
}
else if(WIFSIGNALED(state)) //子进程因为一个未捕获的信号终止
{
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(state));
deletejob(jobs, pid);
}
else if(WIFSTOPPED(state)) //如果子进程是停止的,需要修改改作业的状态
{
job = getjobpid(jobs, pid);
job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(state));
}
sigprocmask(SIG_SETMASK, &prev, NULL); //恢复信号接收
}
errno = old_errno;
}

总结:

这个实验主要是考察了安全信号处理内容,以及竞争关系;

帮助疏通了对Shell的理解:接受指令,处理指令,以及增加进程和如何回收进程;

对于小方向的话便是细节的考虑,阻塞顺序以及分类情况;

阅读全文
Angr.Lab

EldenRing

介绍:

angr是一款针对于CTF的工具,说实话并不觉的它对复杂的逆向程序有什么更优的作用;

它的常用功能则是根据使用者自己写的求解约束,附加在程序上计算如何输入进而求出得到的效果来获取正确输入,类似一款爆破计算器;

如何下载呢? 终端输入 -> pip install angr;

具体的练习上手题则需要去GitHub上搜寻:https://github.com/jakespringer/angr_ctf;

主要内容:

Project -> 附加的程序,在angr里叫项目;

State -> 状态,模拟的PC所指;

Simulation -> 模拟空间,为状态不断更新使程序执行指令,模拟运行所提供空间;

Explore -> 模拟运行程序并附加内容;

这4个便是angr使用的主要内容,基本解题脚本都离不开这4个,接下来就用GitHub上的题目来一一解释使用方法,以及进阶内容;

00_angr_find

IDA分析:

main

一个很简单的函数,按照介绍所说,需要让angr帮忙计算出输入的内容就是这里的比较数据:FPQPMQXT;那要怎么去写约束得到正确的输入呢?当然是要让状态走到输出’Good Job.’这一条,而不能走向’Try again.’;如此一来输入只能是比较数据;所以找到这条指令的地址:

address

接下来就可以写执行脚本了:

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
import sys
import angr

def main(argv):
# 目标文件的路径
path_to_binary = '../program/00_angr_find'
# 创建angr项目
project = angr.Project(path_to_binary)

# 设置项目起点,entry_state代表程序的入口点,即main函数
initial_state = project.factory.entry_state()
# 设置模拟器
simulation = project.factory.simgr(initial_state)

# 设置目标地址
print_good_addr = 0x0804867D
simulation.explore(find=print_good_addr)

# 如果到达目标地址,打印此时的符号向量
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
# 否者抛出失败异常
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main(sys.argv)

小结:

运用sys库是需要得到标准输入 -> sys.stdin.fileno() ;

angr.Project(执行的二进制文件地址) -> 打开二进制文件;

project.factory.entry_state() -> 创建空白的执行环境;

project.factory.simgr(上下文对象) -> 创建模拟器;

simulation.explore(find = 搜索程序执行路径的地址) -> 执行路径探索;

01_angr_avoid

这道题和00其实很像,只是在main函数里塞了很多大量的垃圾代码,直接用find输出正确的地址就找不到;

看到maybe_good函数:

function

以及在main函数里经常出现的avoid_me函数:

function

可以知道,如果进入了avoid_me后,再进入maybe_good就与输出Good Job无缘了,所以在寻找怎样输入才能导致输出正确的时候,可以再加一个约束,约束状态不要进入avoid_me;

代码:

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
import sys
import angr

def main(argv):
# 目标文件的路径
path_to_binary = '../program/01_angr_avoid'
# 创建angr项目
project = angr.Project(path_to_binary)

# 设置项目起点,entry_state代表程序的入口点,即main函数
initial_state = project.factory.entry_state()
# 设置模拟器
simulation = project.factory.simgr(initial_state)

# 设置目标地址
print_good_addr = 0x080485E0
aovid_me_addr = 0x080485A8

# simulation.explore(find=print_good_addr)
# avoid=try_again_addr
# 在这里可以添加 avoid 来约束到达的目的地址
simulation.explore(find=print_good_addr, avoid=aovid_me_addr)
# 如果到达目标地址,打印此时的符号向量
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
# 否者抛出失败异常
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main(sys.argv)

小结:

simulation.explore(find = 要搜索的路径地址, avoid = 要排除执行路径地址) -> 路径探索

simulation.found -> 搜索结果集合,这是一个python list 对象

02_angr_find_condition

IDA:

main

与00比较,在进行判断字符串的时候进行了一次运算,而在汇编层可以看到 puts(“Good Job.”) 这条指令来自很多地址,被混淆打乱了:

Xrefs

所以这次不能用 find=地址 来得到要找到的正确输入了;所以需要构建explore() 函数的回调函数;

代码:

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
import sys
import angr

# 到达目标地址,打印此时的符号向量
def good_job(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

# 否则抛出失败异常
def try_again(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

def main(argv):
path_to_binary = './02_angr_find_condition'
# 创建angr项目
project = angr.Project(path_to_binary)
# 设置项目起点,entry_state代表程序的入口点,即main函数
initial_state = project.factory.entry_state()
# 设置模拟器
simulation = project.factory.simgr(initial_state)
# 设置目标地址
simulation.explore(find=good_job, avoid=try_again)
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main(sys.argv)

小结:

simulation.explore(find = 回调函数, avoid = 回调函数) -> 路径探索

explore() 函数的回调函数格式为:

def recall_explore(state) :

​ …

​ return True / False # True 意思是发现了该路径,False 则是忽略

state.posix.dumps(sys.stdout.fileno()) -> 获取模拟执行的控制台输出

03_angr_symbolic_registers

IDA:

main

这次让输入三次内容,三次经过不同的加密,最后经过 if 判断来找结果;

汇编层:

asam

虽然伪代码显示v5,v6,v8都变量都是栈里的数据,但汇编层显示出,它们是由寄存器eax,ebx,edx搬运进栈后才会是对应栈数据;

那么已知运算结果(怎样是正确的输出),以及运算过程由angr自己去运行;那么就需要设未知数进行求解,把寄存器设为未知数的过程,便称为符号化寄存器,也可以叫变量化寄存器;这一步就类似于Z3里的设置未知变量模型了;

这里需要用到一个库:claripy,下载angr自带的;

代码:

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
import sys
import angr
import claripy

def main():
binary_path = '../program/03_angr_symbolic_registers'
project = angr.Project(binary_path)

# 设置项目开始地址
start_addr = 0x0804890E
initial_state = project.factory.blank_state(addr=start_addr)


# 将寄存器符号化
bit_length = 32
psd0 = claripy.BVS('psd0', bit_length)
psd1 = claripy.BVS('psd1', bit_length)
psd2 = claripy.BVS('psd2', bit_length)
# 将符号化的寄存器对应到相应的寄存器
initial_state.regs.eax = psd0
initial_state.regs.ebx = psd1
initial_state.regs.edx = psd2
# 设置模拟
simulation = project.factory.simgr(initial_state)

def good_job(state):
stdout_content = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_content

def fail(state):
stdout_content = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_content

simulation.explore(find=good_job, avoid=fail)

if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(psd0)
solution1 = solution_state.se.eval(psd1)
solution2 = solution_state.se.eval(psd2)

solution = '%x %x %x' % (solution0, solution1, solution2)
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main()

小结:

project.factory.blank_state(addr=start_address) -> 创建自定义入口的状态上下文

initial_state.regs -> 操作状态上下文的寄存器

claripy.BVS(‘变量名’, 变量大小) -> 创建求解变量

solution_state.se.eval(变量) -> 求解符号变量

solution = ‘%x %x %x’ % (solution0, solution1, solution2) -> 标准输出格式

04_angr_symbolic_stack

IDA:

function

这个函数是main里的唯一一个指令;查看汇编可以发现v1,v2变量不是由寄存器传到栈上,是直接输入的栈上的,那么这次做的便是符号化栈;将栈上的数据设置为未知数,所以需要去平衡栈;

代码:

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
import sys
import angr
import claripy


def main():
binary_path = './04_angr_symbolic_stack'
project = angr.Project(binary_path)

start_addr = 0x08048697 #scanf之后的地址
initial_state = project.factory.blank_state(addr=start_addr)

initial_state.regs.ebp = initial_state.regs.esp # 初始化栈,令ebp等于esp

password0 = claripy.BVS('password0', 32) # 初始化两个位向量
password1 = claripy.BVS('password1', 32)

padding_length_in_bytes = 8 # 填充栈,8字节,2个int数据
initial_state.regs.esp -= padding_length_in_bytes

initial_state.stack_push(password0) # 将位向量压入栈中
initial_state.stack_push(password1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)

solution = '%u %u' % (solution0, solution1)
print(solution)
else:
raise Exception('could not find the solution')

if __name__ == '__main__':
main()

05_angr_symbolic_memory

IDA:

main

发现这次输入的内容被存到了4个地方,这4个地方都是.bss段上的内存(unk开头的指针以及user_input),之后计算并比较;和之前2题一样,这次需要的是符号化内存;

代码:

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
import angr
import sys
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = './05_angr_symbolic_memory'
project = angr.Project(binary_path)

start_addr = 0x08048601
initial_state = project.factory.blank_state(addr=start_addr)

password0 = claripy.BVS('password0', 64) # 64 = 8(8个字符) * 1(每个字符一字节) * 8(每个字节8比特)
password1 = claripy.BVS('password1', 64)
password2 = claripy.BVS('password2', 64)
password3 = claripy.BVS('password3', 64)

password0_addr = 0x09FD92A0
password1_addr = 0x09FD92A8
password2_addr = 0x09FD92B0
password3_addr = 0x09FD92B8

initial_state.memory.store(password0_addr, password0) # 将位向量存入内存
initial_state.memory.store(password1_addr, password1)
initial_state.memory.store(password2_addr, password2)
initial_state.memory.store(password3_addr, password3)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)
solution2 = solution_state.se.eval(password2)
solution3 = solution_state.se.eval(password3)
solution = long_to_bytes(solution0)+b' '+long_to_bytes(solution1)+b' '+long_to_bytes(solution2)+b' '+long_to_bytes(solution3)
print(solution.decode())
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main()

小结:

initial_state.memory.store(地址,数据) -> 初始化内存地址中的数据

long_to_byte函数 -> 规范输出

06_angr_symbolic_dynamic_memory

IDA:

main

可以看到这次用了malloc分配了动态内存,而scanf输入则直接放到了这些内存上,下面步骤都和之前一样,所以这次要做的就是符号化动态内存;但动态内存没有固定的地址,所以需要用到buffer在.bss段上的指针;Angr可以不用创建新内存(malloc),直接指向内存中一个任意位置即可;

代码:

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
import sys
import angr
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = '../program/06_angr_symbolic_dynamic_memory'
project = angr.Project(binary_path)

start_addr = 0x08048699
initial_state = project.factory.blank_state(addr=start_addr)

password0 = claripy.BVS('password0', 64)
password1 = claripy.BVS('password1', 64)
fake0_addr = 0x09FD9160 # 伪造malloc得来的内存
fake1_addr = 0x09FD9180

buffer0_addr = 0x09FD92AC # 指向伪造内存的指针
buffer1_addr = 0x09FD92B4
initial_state.memory.store(buffer0_addr, fake0_addr, endness=project.arch.memory_endness) # 将指针指向伪造的内存
initial_state.memory.store(buffer1_addr, fake1_addr, endness=project.arch.memory_endness)

initial_state.memory.store(fake0_addr, password0) # 将伪造的内存符号化
initial_state.memory.store(fake1_addr, password1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)

solution = long_to_bytes(solution0) + b' ' + long_to_bytes(solution1)
print(solution)
print(solution.decode())
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main()

小结:

initial_state.memory.store(地址,数据,endness = 数据字节顺序) -> 设置初始化内存数据

project.arch.memory_endness -> 指的是内存字节顺序

07_angr_symbolic_file

IDA:

main

ignore_me函数:

ignore_me

输入的内容先保存到buffer里,接着用ignore_me函数将buffer里的内容存到叫做MRXJKZYR.txt的新建文件里;之后返回到主函数,初始化buffer;然后打开这个新建文件,读取里面的内容再到buffer里,最后运算比较;这次需要做的便是符号化文件;

代码:

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
import sys
import angr
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = '../program/07_angr_symbolic_file'
project = angr.Project(binary_path)

start_addr = 0x080488EA

filename = 'MRXJKZYR.txt' # 文件名称
symbolic_file_size_bytes = 64 # 文件大小(字节)

password = claripy.BVS('password', symbolic_file_size_bytes * 8) # 初始化位向量
password_file = angr.SimFile(filename, content=password, size=symbolic_file_size_bytes) # 符号化文件

initial_state = project.factory.blank_state(addr=start_addr, fs={filename: password_file}) # 再初始状态中添加一个虚拟的文件系统
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = long_to_bytes(solution_state.solver.eval(password))
print(solution.decode())
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main()

小结:

angr.storage.SimFile(文件名,文件内容, size = 文件大小) -> 创建一个模拟文件,当有被执行的程序fopen 打开文件时,可以控制其里面的内容

initial_state.posix.fs -> 状态上下文的文件系统对象

08_angr_constraints

IDA:

main

输入字串后进行加密,之后经过检查函数判断;

这次可以控制输入的内容最后导致password地址的字串是否变为了正确的;

代码:

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
import sys
import angr
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = './08_angr_constraints'
project = angr.Project(binary_path)

start_addr = 0x08048625 # 在输入函数之后
initial_state = project.factory.blank_state(addr=start_addr)

password = claripy.BVS('password', 16*8)
password_addr = 0x0804A050
initial_state.memory.store(password_addr, password)

simulation = project.factory.simgr(initial_state)

addr_to_check_constraint = 0x08048669 # 在检查函数之前
simulation.explore(find=addr_to_check_constraint)

if simulation.found:
solution_state = simulation.found[0]

constrained_parameter_addr = 0x0804A050 # 加密后的password的地址
constrained_parameter_size_bytes = 16 # password的长度(字节)
constrained_parameter_bitvector = solution_state.memory.load(constrained_parameter_addr, constrained_parameter_size_bytes) # 从内存中加载password

constrained_parameter_desired_value = 'MRXJKZYRKMKENFZR' # reference string

constrained_expression = constrained_parameter_bitvector == constrained_parameter_desired_value # 约束表达式

solution_state.add_constraints(constrained_expression) # 添加约束

solution = long_to_bytes(solution_state.se.eval(password))
print(solution.decode())
else:
raise Exception('Could not find the sokution')

if __name__ == '__main__':
main()

小结:

solution_state.memory.load(内存地址,内存大小) -> 加载内存

solution_state.add_constraints(约束条件) -> 添加约束条件

09_angr_hooks

IDA:

main

分别输入两次加密比较;

这题是要求注入,模拟equals函数的功能:

equals

注入地址当然就是调用这个函数的地址;angr里的注入类似于CE,开辟一块新区块,然后在这里写入注入内容,最后跳回注入地址的后一地址;

代码:

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
import sys
import angr
import claripy


def main():
binary_path = './09_angr_hooks'
project = angr.Project(binary_path)

initial_state = project.factory.entry_state()

# 绕过函数的地址
check_equals_caller_addr = 0x080486A9
# 通过 hook 跳过目标函数的长度
instruction_to_skip_length = 0x080486BB - 0x080486A9

# 创建一个 hook 函数
# 参数为绕过函数的地址,绕过函数长度
@project.hook(check_equals_caller_addr, length = instruction_to_skip_length)
def skip_check_equals(state):
user_input_buffer_addr = 0x0804A054 # 保存输入变量地址
user_input_buffer_length = 16 # 第一个 scanf 的输入长度,此处为字节大小
# 将输入载入内存
user_input_string = state.memory.load(
user_input_buffer_addr,
user_input_buffer_length
)
# 目的字符串
check_against_string = 'MRXJKZYRKMKENFZB'
# 创建判断条件 -> 字符串的比较
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32), # 程序的返回值是给寄存器 eax 保存
claripy.BVV(0, 32) # eax 为 32 bit 的寄存器,所以大小设置为 32
) # claripy.BVV(返回数据,返回 bit 大小)



def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output
# 开始模拟
simulation = project.factory.simgr(initial_state)
simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())

print(solution.decode())
else:
raise Exception('Could not find the solution')


if __name__ == '__main__':
main()

小结:

Hook回调函数格式:

@project.hook(Hook地址,执行完Hook函数后指令往后跳转n字节)
def skip_check_equals_(state):

pass

claripy.If(条件,条件为True时的返回值,条件为False时的返回值) -> 创建条件判断

claripy.BVV(值,值大小) -> 创建一个数值

10_angr_simprocedures

IDA:

main

相对于上一道题更简单,只用输入一次;

但equals函数却混淆为了多个分支,和02一样,这样就没办法在一个地址注入;

所以可以用Angr 的Hook Symbol 来实现对check_equals() 函数的注入;

代码:

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
import angr
import sys
import claripy


def main():
binary_path = './09_angr_hooks'
project = angr.Project(binary_path)
initial_state = project.factory.entry_state()

# 创建一个类
class mySimPro(angr.SimProcedure):
def run(self, user_input_addr, user_input_length):
# angr 输入的符号向量
angr_bvs = self.state.memory.load(
user_input_addr,
user_input_length
)
# 目标字符串
desired = 'MRXJKZYRKMKENFZB'
return claripy.If(
desired == angr_bvs, # 条件判断
claripy.BVV(1,32), # 返回值设置
claripy.BVV(0,32)
)

# hook 的函数名
check_symbol = 'check_equals_MRXJKZYRKMKENFZB'
# 创建 hook
project.hook_symbol(check_symbol,mySimPro()) # 创建一个类来继承 angr.SimProcedure
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())

print(solution.decode())
else:
raise Exception('Could not find the solution')


if __name__ == '__main__':
main()

小结:

Hook 回调函数格式:

class ReplacementCheckEquals(angr.SimProcedure):

def run(self, Hook的函数参数列表):

​ ….

​ return 函数返回值 # 如果是void函数可以省略

project.hook_symbol(要Hook的函数名,SimProcedure类实例)

11_angr_sim_scanf

IDA:

main

这道题是注入系统函数scanf改变符号;

代码:

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
import angr
import sys
import claripy

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, scanf0_address, scanf1_address ):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 4 * 8)

self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
stored_solutions0 = solution_state.globals['solution0']
stored_solutions1 = solution_state.globals['solution1']
solution0 = solution_state.se.eval(stored_solutions0)
solution1 = solution_state.se.eval(stored_solutions1)

print(solution0,solution1)

else:
raise Exception('Could not find the solution')


if __name__ == '__main__':
main()

12_angr_veritesting

这个示例和01 题是一样的,唯独不同的一点是这个循环比之前的要大,导致直接用01 题的解题方法不能直接计算出结果,因为循环过大导致路径爆炸,所以在执行的时候会消耗很多资源.

project.factory.simgr() 函数提供veritesting 参数来指定是否要自动合并路径,避免路径爆炸的问题.具体细节参考论文:https://users.ece.cmu.edu/~dbrumley/pdf/Avgerinos%20et%20al._2014_Enhancing%20Symbolic%20Execution%20with%20Veritesting.pdf

代码:

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
import sys
import angr

def main():
binary_path = './/12_angr_veritesting'
project = angr.Project(binary_path)

initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state, veritesting=True) # 设置自动合并路径

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main()

小结:

project.factory.simgr(初始化状态,veritesting = True) -> veritesting 默认为False

13_angr_static_binary

与01一样,唯一不同的这个程序是静态链接编译,程序中包含libc的函数实现;在CTF中,这些函数会隐藏一些出题人的坑,或者这些函数不适配当前的系统;所以需要注入这些libc函数;

Angr库里自带一部分打包好的libc函数,直接导入即可;

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
import angr
import sys


project = angr.Project(sys.argv[1])
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state,veritesting = True)

project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job.' in str(stdout_output) # :boolean

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again.' in str(stdout_output) # :boolean

simulation.explore(find = is_successful,avoid = should_abort)

if simulation.found :
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))

小结:

angr.SIM_PROCEDURES[ 系统库名 ] [ 系统函数名 ] () -> 获取Angr 内部实现的系统函数

14_angr_shared_library

IDA:

main

类似01,但validate函数是一个动态链接库的函数;

对动态链接库中的_validate 函数进行符号执行;

代码:

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
def main(argv):
path_to_binary = sys.argv[1] # 注意是要load so 库而不是执行程序

base = 0x400000 # base 基址是随意定的,可以随意修改
project = angr.Project(path_to_binary, load_options={
'main_opts' : {
'custom_base_addr' : base
}
})

buffer_pointer = claripy.BVV(0x3000000, 32) # 创建一个buffer 指针值
validate_function_address = base + 0x6D7
initial_state = project.factory.call_state(validate_function_address, buffer_pointer,claripy.BVV(8, 32)) # 调用validate_function,因为函数声明validata_function(buffer_point,buffer_length) ,所以构造出调用validata_function(0x3000000,0x8) .

password = claripy.BVS('password', 8 * 8) # 创建一个求解对象,大小为8 字节
initial_state.memory.store(buffer_pointer, password) # 保存到0x30000000

simulation = project.factory.simgr(initial_state)

simulation.explore(find = base + 0x783) # 执行到validate 函数的RETN 指令

if simulation.found:
solution_state = simulation.found[0]

solution_state.add_constraints(solution_state.regs.eax != 0) # 记得,要求validate 函数的返回值为1 的时候就是有解的,那么就需要在求解的时候添加上这么一个求解约束条件EAX 不能为False .
solution = solution_state.se.eval(password)
print(solution)

15_angr_arbitrary_read

IDA:

main

控制输入;

代码:

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
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure): # 实现Scanf Hook 函数

def run(self, format_string, check_key_address,input_buffer_address):
scanf0 = claripy.BVS('scanf0', 4 * 8) # check_key
scanf1 = claripy.BVS('scanf1', 20 * 8) # input_buffer

for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z') # 对input_buffer 的输入约束

self.state.memory.store(check_key_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer_address, scanf1,endness=project.arch.memory_endness) # 保存求解变量到指定的内存中

self.state.globals['solution0'] = scanf0 # 保存这两个变量到state 中,后续求解需要用到
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # Hook scanf 函数

def check_puts(state):
puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取puts() 函数的参数

if state.se.symbolic(puts_parameter): # 检查这个参数是否为符号化对象
good_job_string_address = 0x4D525854B

copied_state = state.copy() # 复制执行状态上下文进行约束求解,不影响原理的执行上下文

copied_state.add_constraints(puts_parameter == good_job_string_address) # puts 的参数地址是否可以被指定为0x4D525854B ,如果可以的话,那就证明这个值是可控的

if copied_state.satisfiable(): # 判断添加了上面这个约束是否有解
state.add_constraints(puts_parameter == good_job_string_address) # 如果有解的话就保存到执行的那个状态对象
return True
else:
return False
else:
return False

simulation = project.factory.simgr(initial_state)

def is_successful(state):
puts_address = 0x8048370 # 当程序执行到puts() 函数时,就认为路径探索到了这里,然后再去通过check_puts() 判断这里是否存在漏洞,告诉Angr这是不是需要找的那条执行路径

if state.addr == puts_address:
return check_puts(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes) # 输出字符串序列化的内容

print(solution0,solution1)

小结:

state.copy() -> 复制状态上下文

state.satisfiable() -> 判断当前的所有约束是否有解

solution_state.se.eval(求解变量,cast_to=bytes) -> 序列化变量内容为字符串

16_angr_arbitrary_write

IDA:

main

控制写入内存;

代码:

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
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, check_key ,input_buffer):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 20 * 8)

for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')

self.state.memory.store(check_key, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer, scanf1, endness=project.arch.memory_endness)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())

def check_strncpy(state):
strncpy_dest = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取strncpy() 的参数,strncpy_dest ..
strncpy_src = state.memory.load(state.regs.esp + 8, 4, endness=project.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp + 12, 4, endness=project.arch.memory_endness)
src_contents = state.memory.load(strncpy_src, strncpy_len) # 因为参数中只保存了地址,需要根据这个地址去获取内容

if state.se.symbolic(strncpy_dest) and state.se.symbolic(src_contents) : # 判断dest 和src 的内容是不是符号化对象
if state.satisfiable(extra_constraints=(src_contents[ -1 : -64 ] == 'KZYRKMKE' ,strncpy_dest == 0x4D52584C)): # 尝试求解,其中strncpy_dest == 0x4D52584C 的意思是判断dest 是否可控为password 的地址;src_contents[ -1 : -64 ] == 'KZYRKMKE' 是判断input_buffer 的内容是否可控为'KZYRKMKE' ,因为这块内存是倒序,所以需要通过[ -1 : -64 ] 倒转(contentes 的内容是比特,获取8 字节的大小为:8*8 = 64),然后判断该值是否为字符串'KZYRKMKE'
state.add_constraints(src_contents[ -1 : -64 ] == 'KZYRKMKE',strncpy_dest == 0x4D52584C)
return True
else:
return False
else:
return False

simulation = project.factory.simgr(initial_state)

def is_successful(state):
strncpy_address = 0x8048410

if state.addr == strncpy_address:
return check_strncpy(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes)

print(solution0,solution1)

小结:

state.satisfiable(extra_constraints=(条件1,条件2)) -> 合并多个条件计算是否存在满足约束的解(注意两个或多个条件之间是And 合并判断,不是Or )

17_angr_arbitrary_jump

IDA:

main

这是一个栈溢出;

代码:

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
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()

simulation = project.factory.simgr(
initial_state,
save_unconstrained=True,
stashes={
'active' : [initial_state],
'unconstrained' : [],
'found' : [],
'not_needed' : []
}
)

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, input_buffer_address):
input_buffer = claripy.BVS('input_buffer', 64 * 8) # 设置一个较大的input_buffer

for char in input_buffer.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')

self.state.memory.store(input_buffer_address, input_buffer, endness=project.arch.memory_endness)

self.state.globals['solution'] = input_buffer

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # 对scanf() 做Hook

while (simulation.active or simulation.unconstrained) and (not simulation.found): #
for unconstrained_state in simulation.unconstrained:
def should_move(s):
return s is unconstrained_state

simulation.move('unconstrained', 'found', filter_func=should_move) # 保存

simulation.step() # 步进执行

if simulation.found:
solution_state = simulation.found[0]

solution_state.add_constraints(solution_state.regs.eip == 0x4D525849) # 判断EIP 地址是否可控

solution = solution_state.se.eval(solution_state.globals['solution'],cast_to = bytes) # 生成Payload
print(solution)

总结:

0002讲解的是基础操作;0307讲解的是符号化常见内容;08讲解的是求解内容约束;0908讲解如何注入来替换函数或者增加函数;1114讲解的都是进阶的内容;15~17讲解的都和控制有关,与pwn题相关;

真正吃透angr会花更多的时间,但真正强化二进制能力的并不是如何去使用angr,而是明白angr函数针对于汇编层的操作;

阅读全文
VM实验复现
Bin | VM

详细实验地址:https://justinmeiners.github.io/lc3-vm/index.html#1:12

本质上是在用C语言描述16位机器的操作过程,因此可以以此为一个架接运行一个16位程序,因此成为虚拟机(Virtual Machine);

前置要求:C语言(写出虚拟机的语言),位运算,汇编代码的运行模式(虚拟机的工作方式,不懂汇编代码的意思也没关系),丁点API知识(键盘传输和屏幕显示,以及内存收取等),LC-3指令集(模拟指令OP);

指令集地址:https://justinmeiners.github.io/lc3-vm/supplies/lc3-isa.pdf

思路:

1. 读取文件

既然要读取其他程序和文件,那需要构造一个内存池容纳16位的程序,总大小也就是二的十六次方;

代码1-1

1
2
/* 65536 locations */
uint16_t memory[UINT16_MAX]; //Memory Storage

文件读入主要代码1-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (argc < 2)
{
/* show usage string */
printf("usage: %s [image-file1] ...\n", argv[0]);
exit(2);
}

for (int j = 1; j < argc; ++j)
{
if (!read_image(argv[j]))
{
printf("failed to load image: %s\n", argv[j]);
exit(1);
}
}

补充说明:argc 和 argv 是 main 函数参数,第一个代表main参数个数,第二个代表为地址的参数;

作用:如果没有输入main函数的参数,第一个判断就会提示用法为: ./main [image-file1] ;如果输入[image-file1]这个参数,那么就会将其读入for循环中,用 read_image() 函数计算参数并判断,若返回值是0,就会说:装载映像失败;

这里显示出一个函数叫 read_image() ,下面说说它的作用:

代码1-3

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
void read_image_file(FILE* file)
{
/* the origin tells us where in memory to place the image */
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

/* we know the maximum file size so we only need one fread */
uint16_t max_read = UINT16_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

/* swap to little endian */
while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}

int read_image(const char* image_path)
{
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; };
read_image_file(file);
fclose(file);
return 1;
}

这段代码中有两个函数,最后一个就是1-2中提到的,他的作用就是判断输入的参数地址是否为正确文件地址;如果不是就返回1,进而让1-2的判断输出错误并中断程序;如果是就执行 read_image_file() 函数;

read_image_file() 函数的作用便是将读取的程序内存装入之前设定好的memory数组中;首先计算的起始地址:origin,使用fread() C原装函数,读取起始地址;之后计算最大可容纳地址,并用p指针标记,最后使用循环不断缩小范围;

至于为什么要使用swap16()函数呢?因为LC-3程序是大端序排列,一般电脑用的都是小端序,所以要交换高低8位;

代码1-4

1
2
3
4
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}

2. 内存访问

某些特殊寄存器无法从普通寄存器表中访问。相反,在内存中为它们保留一个特殊地址。要读取和写入这些寄存器,只需读取和写入它们的内存位置即可。这些称为内存映射寄存器。它们通常用于与特殊硬件设备进行交互(如键盘);

LC-3 具有两个需要实现的内存映射寄存器。它们是键盘状态寄存器 () 和键盘数据寄存器 ()。指示是否已按下某个键,并标识按下了哪个键;

代码2-1

1
2
3
4
5
enum	//Memory Mapped Registers
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};

上面是模拟LC-3 的两个内存寄存器;第一个为状态管理,第二个是数据管理;

代码2-2

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
uint16_t check_key()
{
return WaitForSingleObject(hStdin, 1000) == WAIT_OBJECT_0 && _kbhit();
}

void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}

uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}

这里给出三个函数;第一个是设置windows终端输入的代码(调用API);第二个是写入内存的代码;第三个是读入内存的代码;

3. 模拟寄存器

既然要模拟汇编代码的运行模式,那就少不掉寄存器;在汇编代码中,寄存器就相当于C的变量,保存数据用;

LC-3中,一共只有10个寄存器,8个通用,1个指向即将执行的代码的寄存器(PC),1个条件控制寄存器;

代码3-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum	//Registers
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};

这里多设置了一个,R_COUNT不是寄存器,只是计数用的,因为是从0开始数的;

之后用这个来控制寄存器:

代码3-2

1
uint16_t reg[R_COUNT];	//Register Storage

而控制寄存器需要特别加一个枚举:

代码3-3

1
2
3
4
5
6
enum	//condition flags
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};

结果为正数则是P,为0则是Z,为负数则是N;它们的计算结果分别是:1,2,4;用到它们的时候,这些数字就代表它们的意义;而实际上是用移位来模拟这些条件位在寄存器中的形式;

4. 模拟指令

这里就是LC-3需要用到的指令,于是模拟出所有会用到的:

代码4-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum
{
OP_BR = 0, /* branch */
OP_ADD, /* add */
OP_LD, /* load */
OP_ST, /* store */
OP_JSR, /* jump register */
OP_AND, /* bitwise and */
OP_LDR, /* load register */
OP_STR, /* store register */
OP_RTI, /* unused */
OP_NOT, /* bitwise not */
OP_LDI, /* load indirect */
OP_STI, /* store indirect */
OP_JMP, /* jump */
OP_RES, /* reserved (unused) */
OP_LEA, /* load effective address */
OP_TRAP /* execute trap */
};

它们实现机器所需要的运算;

而LC-3的运算中,因为是16位,所以这些指令会放在最高的4位判断,剩下的位数就会放参数一类的东西,每个运算的参数需求位都不同,详细请看LC-3指令集;指令集中会要求使用的参数需要扩展为16位运算;所以需要接下来的函数:

代码4-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}


void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO;
}
else if (reg[r] >> 15) /* a 1 in the left-most bit indicates negative */
{
reg[R_COND] = FL_NEG;
}
else
{
reg[R_COND] = FL_POS;
}
}

第一个函数的作用便是 带符号扩展 位数;第二个函数的作用便是每次运算结束后,调整条件控制寄存器中的三个值;

5. 模拟执行过程

这是逆向题中的核心思想,便是C语言如何执行虚拟机模式的,如何找到flag的指令生成顺序;

以下是我们需要编写的过程:

  1. 从寄存器地址处的内存中加载一条指令。PC
  2. 递增寄存器。PC
  3. 查看操作码以确定它应该执行哪种类型的指令。
  4. 使用指令中的参数执行指令。
  5. 返回步骤 1。

这样一来,就能模拟出虚拟机的内核了;

代码5

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
int main(int argc, const char* argv[])
{
{代码1-2}
{代码7-3}

/* since exactly one condition flag should be set at any given time, set the Z flag */
reg[R_COND] = FL_ZRO;

/* set the PC to starting position */
/* 0x3000 is the default */
enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

int running = 1;
while (running)
{
/* FETCH */
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;

switch (op)
{
case OP_ADD:
{代码6-1}
break;
case OP_AND:
{代码6-2}
break;
case OP_NOT:
{代码6-3}
break;
case OP_BR:
{代码6-4}
break;
case OP_JMP:
{代码6-5}
break;
case OP_JSR:
{代码6-6}
break;
case OP_LD:
{代码6-7}
break;
case OP_LDI:
{代码6-8}
break;
case OP_LDR:
{代码6-9}
break;
case OP_LEA:
{代码6-10}
break;
case OP_ST:
{代码6-11}
break;
case OP_STI:
{代码6-12}
break;
case OP_STR:
{代码6-13}
break;
case OP_TRAP:
{代码6-15}
break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
}
{代码7-4}
}

初始化条件控制寄存器后,从0x3000的地址出发,并进入运行状态( while(1) ),instr便是每次会执行的PC所指的指令内容;OP即为操作指令,因为总共16位的寄存器最高4位都是操作指令,所以只需要将instr右移12位就能得到OP;

之后根据switch选择OP,执行相应的指令;每执行完一条便循环回去,于是PC+1,开始执行下一条;

6. C语言模拟指令清单

核心内容,需要结合指令集理解怎么实现的;

代码6-1:和的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
/* destination register (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* first operand (SR1) */
uint16_t r1 = (instr >> 6) & 0x7;
/* whether we are in immediate mode */
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}

update_flags(r0);
}

代码6-2:按位和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] & imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
}

代码6-3:按位非

1
2
3
4
5
6
7
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;

reg[r0] = ~reg[r1];
update_flags(r0);
}

代码6-4:分支

1
2
3
4
5
6
7
8
{
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
uint16_t cond_flag = (instr >> 9) & 0x7;
if (cond_flag & reg[R_COND])
{
reg[R_PC] += pc_offset;
}
}

代码6-5:跳转

1
2
3
4
5
{
/* Also handles RET */
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}

代码6-6:寄存器跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
uint16_t long_flag = (instr >> 11) & 1;
reg[R_R7] = reg[R_PC];
if (long_flag)
{
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR */
}
else
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1]; /* JSRR */
}
break;
}

代码6-7:加载

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}

代码6-8:简介加载的实现

1
2
3
4
5
6
7
8
9
{
/* destination register (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* PCoffset 9*/
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
/* add pc_offset to the current PC, look at that memory location to get the final address */
reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);
}

代码6-9:加载寄存器

1
2
3
4
5
6
7
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}

代码6-10:加载有效地址

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}

代码6-11:存储

1
2
3
4
5
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}

代码6-12:间接存储

1
2
3
4
5
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}

代码6-13:寄存器存储

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}

LC-3 提供了一些预定义的例程,用于执行常见任务和与 I/O 设备交互。例如,有一些例程用于从键盘获取输入以及用于向控制台显示字符串。这些称为trap routines,您可以将其视为LC-3的操作系统或API。每个trap routines都分配有一个trap code来标识它(类似于操作码)。要执行一个,请使用所需routine的code调用该指令;

枚举所有trap代码6-14

1
2
3
4
5
6
7
8
9
enum
{
TRAP_GETC = 0x20, /* get character from keyboard, not echoed onto the terminal */
TRAP_OUT = 0x21, /* output a character */
TRAP_PUTS = 0x22, /* output a word string */
TRAP_IN = 0x23, /* get character from keyboard, echoed onto the terminal */
TRAP_PUTSP = 0x24, /* output a byte string */
TRAP_HALT = 0x25 /* halt the program */
};

为每个trap选择代码6-15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch (instr & 0xFF)
{
case TRAP_GETC:
{代码6-16}
break;
case TRAP_OUT:
{代码6-17}
break;
case TRAP_PUTS:
{代码6-18}
break;
case TRAP_IN:
{代码6-19}
break;
case TRAP_PUTSP:
{代码6-20}
break;
case TRAP_HALT:
{代码6-21}
break;
}

trap指令清单:

代码6-16:输入字符

1
2
3
4
5
{
/* read a single ASCII char */
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
}

代码6-17:输出字符

1
2
3
4
{
putc((char)reg[R_R0], stdout);
fflush(stdout);
}

代码6-18:输出字符

1
2
3
4
5
6
7
8
9
10
{
/* one char per word */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}

代码6-19:准备输入字符

1
2
3
4
5
6
7
8
{
printf("Enter a character: ");
char c = getchar();
putc(c, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)c;
update_flags(R_R0);
}

代码6-20:输出字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
/* one char per byte (two bytes per word)
here we need to swap back to
big endian format */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
}

代码6-21:终止程序

1
2
3
4
5
{
puts("HALT");
fflush(stdout);
running = 0;
}

7. 头部添加以及windows加入API

加入的头部:

代码7-1

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdint.h> // uint16_t
#include <stdio.h> // FILE
#include <signal.h> // SIGINT
/* windows only */
#include <Windows.h>
#include <conio.h> // _kbhit

HANDLE hStdin = INVALID_HANDLE_VALUE;

#define UINT16_MAX 65536

加入API:

代码7-2

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
DWORD fdwMode, fdwOldMode;

void disable_input_buffering()
{
hStdin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hStdin, &fdwOldMode); /* save old mode */
fdwMode = fdwOldMode
^ ENABLE_ECHO_INPUT /* no input echo */
^ ENABLE_LINE_INPUT; /* return when one or
more characters are available */
SetConsoleMode(hStdin, fdwMode); /* set new mode */
FlushConsoleInputBuffer(hStdin); /* clear buffer */
}

void restore_input_buffering()
{
SetConsoleMode(hStdin, fdwOldMode);
}

void handle_interrupt(int signal)
{
restore_input_buffering();
printf("\n");
exit(-2);
}

代码7-3:初始化

1
2
signal(SIGINT, handle_interrupt);
disable_input_buffering();

代码7-4:释放

1
restore_input_buffering();

之后按顺序组装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
7-1
3-1
4-1
3-3
2-1
6-14
1-1
3-2
4-2
1-4
1-3
2-2
7-2

5

之后就可以得到一台16位的虚拟机;

总结

过了一遍VM实验过后对虚拟机有了一定理解,并且对之前所见的VM逆向题有了解题的思路,以及了解了LC-3指令集,和些许API函数的作用,由此更加理解一个源代码如何与其他文件产生共鸣;可以类比shell与程序之间的关系,更好地理解shell的作用和本质;

genshin

阅读全文