网络安全实验总结以及一些想法

这学期担任了网络安全与网络管理的助教,主要负责实验部分。
总得来说,实验的结果超出了原本预期,但是也暴露了很多问题。

实验内容

在实验部分主要设计了如下 7 个实验:

  1. 凯撒密码和频率分析攻击
  2. RC4加密算法
  3. Fork Bomb
  4. 自打印程序
  5. Snort
  6. 缓冲区溢出实验
  7. DES加密算法实现

凯撒密码和频率分析攻击

在去年,该实验的内容为实现凯撒密码,但由于过于简单,因此增加了两部分内容——任意代换表的代换密码和频率分析攻击

实际上该部分主要在于频率分析部分,在最初的设计里是去掉凯撒密码,由同学互相生成密文,并发给另一组解密的(不过这个操作太麻烦,而且并没有实际意义)。一般来说,频率分析可能人工的工作要大于程序的工作(虽然可以由程序来替代人工部分,但是代码实现太花费时间),因此该实验主要是希望大家能够体验一下这个过程——不择手段来实现目的。

单纯的统计各个字母、单词的频率,应该只能确定出4、5个字母。剩下的分析工作全部都需要使用手动推测,在这个阶段,如果有一个合适的辅助工具,将可以极大提升分析效率(最后为了降低难度直接给了一个分析工具,不过还是有个别组自己实现了工具)。另外,对于数字和大写字母部分,反推难度是大于小写字母的,就汇报情况而言,大家还是充分利用了文章内容,反推出了完整的代换表。

如果要重新设计,可以考虑不提供辅助工具,并且给每一组分配不同的密文。
(另外指导书的文字描述可能会产生歧义,或许需要重新写一下)

RC4加密算法

这个实验是直接照抄的去年的实验内容,算是一个常用的加密算法的实现,主要是锻炼下编码能力

Fork Bomb

这个实验是直接照抄的去年的实验内容,而且在演示上造成了很大的麻烦。
按照个人的理解,该实验主要目的应该是在开发中要合理申请、释放资源——比如mallocfree成对出现,申请了资源就要记得释放(类似 go 的defer),但是就实验结果来看,大家只是单纯地死循环 fork 自己,似乎并没有从中体会到什么(当然,这个主要是实验设计的问题)

应该考虑去掉该实验,尝试和缓冲区溢出结合一下?或者考虑一下多进程中的僵尸进程问题?写一个数据库连接池?

自打印程序

这个实验是直接照抄的去年的实验内容

这是一个很有意思的语言课实验,但是在安全上可能意义不太大。落实到实验中,可能大部分人只是搜到了自打印程序,然后发出“哇,原来还能这么写”的感叹,但在安全方面,可能并不会有什么感悟。

当然,原意上,该实验是为了模拟病毒自我复制的过程(但是我怀疑学生是不是能联想到这一层)

如果可以的话,似乎来考虑分别开发一个根据特征码识别病毒的程序和一个能自我进化避免被识别的病毒似乎更有意思一点,不过似乎难度有点高

Snort

在去年的基础上改进的实验,可以说是本次实验中翻车最严重的实验了。完全做到了配置一分钟,安装一星期。
原本实验室都是装好的环境,由于疫情大家只能自己安装,于是……(这也体现了在基础上,大家的能力有所欠缺,基本的编译安装不够熟练)。
不过起码所有组都成功安装了Snort本体,但是起码一半的组在 Guardian 上翻车。 Guardian 是一个 Snort 联动 iptables 的工具,之所以翻车是因为网上搜到的都是基于 Guardian 的教程,然而这东西已经长时间没更新,其无法直接运行在目前版本的 Perl 上。

某种意义上来说,这不是大家实验失败的接口,因为实验要求上并没有要求使用 Guardian,自己写一个脚本联动并不需要很大工作量(有几组自己实现了联动工具);退一步说即使必须用 Guardian,跟着报错去修改源码仍然可以成功运行(有几组做到了)。所以在这里出现问题的小组确实在这方面存在很大的问题。

该实验设计本身的思路,这里主要是希望大家知道如何进行入侵检测(抓流量包、分析、并做出相应的响应)。在实验设计时虽然预想到了安装会有问题,但是没想到翻车率那么高。似乎是现在成熟的 IDE 削弱了大家在配置环境的能力(即使一个人能用写出很优秀的 Web 服务,但是他可能还是只会点运行按钮执行,并不知道这个按钮到底干了什么)。后米有和老师讨论直接让大家写抓包程序、分析、处理,不过感觉难度过高所以作罢(但是,如果真这么设计实验,可能翻车的人会更少)

这是一个基于 Go 的自动监控alert.log改动,并增加到 iptables 的程序

package main

import (
        "bytes"
        "fmt"
        "log"
        "os"
        "os/exec"
        "regexp"
        "strings"

        "github.com/fsnotify/fsnotify"
)

const filename = "/home/ohyee/projects/network_security/snort/log/alert"

var f *os.File

// var reader *bufio.Reader
var regex *regexp.Regexp
var iptables string
var buffer = make([]byte, 1024)
var offset int64 = 0

type Alert struct {
        srcip    string
        srcport  string
        dstip    string
        dstport  string
        protocol string
}

func alert() {
        for {
                buf := bytes.NewBuffer([]byte{})
                for {
                        length, err := f.ReadAt(buffer, offset)
                        fmt.Println(offset, err)
                        if length == 0 {
                                break
                        }
                        offset += int64(length)
                        buf.Write(buffer)
                }
                if buf.Len() == 0 {
                        break
                }

                line := buf.String()
                fmt.Println(line)

                res := regex.FindAllStringSubmatch(line, -1)
                for _, item := range res {
                        alerts := Alert{
                                srcip:    item[1],
                                srcport:  item[2],
                                dstip:    item[3],
                                dstport:  item[4],
                                protocol: item[5],
                        }

                        fmt.Printf("%+v", alerts)
                        ban(alerts)
                }
        }

}

func ban(a Alert) {
        args := []string{"-I", "INPUT", "-s", a.srcip, "-d", a.dstip, "-p", strings.ToLower(a.protocol), "-j", "REJECT"}
        if a.dstport != "" {
                args = append(args, "--dport", a.dstport[1:])
        }
        cmd := exec.Command(iptables, args...)

        if b, err := cmd.CombinedOutput(); err != nil {
                fmt.Printf("Error %s %+v\n", b, err)
        } else {
                fmt.Printf("Ban %s -> %s (%s)\n", a.srcip, a.dstip, a.protocol)
        }
}

func main() {
        var err error
        if f, err = os.Open(filename); err != nil {
                return
        }
        defer f.Close()

        if regex, err = regexp.Compile("([\\d.]+)([:\\d]*) -> ([\\d.]+)([:\\d]*)[\\n\\s]*([a-zA-Z]+)"); err != nil {
                panic(err)
        }

        if iptables, err = exec.LookPath("iptables"); err != nil {
                panic(err)
        }

        watcher, err := fsnotify.NewWatcher()
        if err != nil {
                log.Fatal(err)
        }
        defer watcher.Close()

        alert()

        done := make(chan bool)
        go func() {
                for {
                        select {
                        case event := <-watcher.Events:
                                if event.Op&fsnotify.Write == fsnotify.Write {
                                        fmt.Println("modified file:", event.Name)
                                        alert()
                                }
                        case err := <-watcher.Errors:
                                fmt.Println("error:", err)
                        }
                }
        }()

        err = watcher.Add(filename)
        if err != nil {
                log.Fatal(err)
        }

        <-done
}

缓冲区溢出实验

这个实验分为两个部分,分别是基于去年的基本部分,以及增加的附加部分。
基础部分就是很著名的看上去打印 00 实际打印 11 的缓冲区溢出程序,这个可以让大家理解程序调用函数究竟是怎么入栈的,以及代码和数据在内存中是一样的。
单就结果上而言,很多组不仅使用了理论分析,还使用反汇编+逐步调试进行了更深入的研究。这已经达到了该实验的设计要求。

附加实验是 CMU 的 Bufbomb1 的第一部分。这个实验设计的很好,不过整个流程太长,不太适合作为本课程的一个实验。因此只取出了其第一个部分。按照最初设计的想法,可能大家在理解缓冲区溢出后,还是没有更直观的认识,通过这个补充实验,大家可以知道,程序有 Bug 是真的会有影响的。

附加实验没有强制要求完成,不过大部分组都完成了。可以考虑下一次直接强制要求实现。并且可以考虑再从 Bufbomb 吸收几个实验加进来。

DES加密算法实现

考虑到前面的编码部分“太水”,因此加了这个。而且最初设计上还包括 DES 差分分析攻击,不过所有人都说太复杂了,只保留了加密算法部分。
单就该实验而言,实际上就是锻炼一下大家写代码的能力,本身并没有考虑太多设计上的问题。
但是在实际验收过程中,却意外发现了一些问题。先不说部分组压根就不知道 DES 是一个分组加密,输入要求 64Bit64Bit。很多组在考虑到可能输入会有问题,于是直接补零。这直接引入了更多的问题。
如果输入要求为 4Bit4Bit,那么 10110110101010 会得到相同的结果,并且在解密时程序根本不可能知道明文是谁。在考虑到输入不符合要求时会出现问题的前提下,很多组的补零反而导致了更大的问题。
因此在后面着重考察了在这方面的问题,不过只有极少数组给出了合适的解决方案。

如果说是懒得写,只考虑了最基础的 DES 倒还可以理解。既然想到了这个问题,但是却使用了一个不恰当的解决方案,其实问题更大一点(当然,这两种在给分上一样)

如果要进一步改进该实验,仍然不应该考虑点明这部分问题。应该给出一定的程序要求,比如:

  • 加密并解密字符串Hello World!
  • 加密并解密字符串你好,世界!
  • 加密并解密二进制数据 0xCF00CA50015

一些其他想法

单就实验结果来看,实际上大家的结果是超出预期的。比如由于不太相信大家的编码能力,觉得在代码上可能会有很大的问题。但是不管是抄还是自己写,起码大家都能跑起来,并且实现了要求(抄不抄的,没法判断,但是确实有些值得怀疑,不过没有被纳入评分依据)

但是在这个过程中还是引出了一些其他问题

科班和非科班

以 DES 为例,它真的难么?一个会写代码的人可能对着算法介绍,花 2、3 天都能写出来,但是可能绝大部分人(非科班)并不会写,甚至都不知道 DES。比如我在大三阶段,并不知道 DES 是什么。

不会写会有什么影响么?实际上并不会。首先已经有很多封装的很好的密码学库供我们使用,我们没有必要去了解其细节。
但是如果很多类似的内容积少成多呢?需要用到哈希算法时候,是闭着眼随便选一个还是有理有据选择最合适的?这些知识可能大部分情况下对大部分程序员都没有任何意义,但是如果遇到相关的问题,如果学过可能很自然地就拥有了大概思路;如果不知道,可能就要花费很大的经历才能找到思路。

这个问题也是很多平台上,尽管就是一个水到不能再水的文章(甚至还没我博客写的用心),但是靠甩几个名词,就能获得大量的阅读和点赞。很多非科班的人没接触过一些很基础的概念,而这种文章则补充了他们的这部分薄弱点。

解决报错

似乎很多人在遇到报错仿佛是自己不守规则而收到惩罚。看着报错只会一脸蒙蔽。
实际上目前大部分报错,都可以靠着蹩脚的英语以及调用栈来直接解决。少部分不熟悉的内容,完全可以借助搜索引擎解决。如果还不能解决大概是用到的东西本来就有问题。

似乎很多人觉得看到报错就判了死刑,直接放弃尝试(上面的 Guardian)

创造能力

在遇到一个问题没有靠谱的解决方案,如果力所能及的范围内,完全可以自己实现相关的程序来解决。
比如频率分析工具、和 Snort 联动 iptables 工具。明明自己写一个脚本可能只需要十几分钟,但是大家却愿意花几个小时配置,然后失败放弃。
而且类似 Guardian 这种东西,已经这么久没有更新,如果在实验过程中发现这个问题并且自己开发一个替代品,可能更有意义。

不过似乎只有极少数的组做到了这一点

测试

测试不应该是一个额外的内容,而应该和程序是一体的。一个函数的实现,就应该有其对应的单例测试。每个功能的实现就应该有完整的功能测试。

不过可惜貌似除去大厂有测试岗位,其他很多小团队不仅没有测试岗位,甚至都不做测试。能跑就能上线……
在敏捷开发的前提下虽然不至于批判这种行为,但是如果不补上相应的测试,随着时间流逝,可能会引发不必要的问题。

如果有了测试,上面的 DES 问题可能很容易就会被发现并解决。(实验中有一组做到了测试)

参考资料


  1. Bufbomb缓冲区溢出攻击实验详解-CSAPP ↩︎