Frp源代码学习--加解密分析


0x01 介绍

在Frp工具中编写了Socket 对称加密功能,其主要原理是封装golang原生io流,使用AES-128-CFB算法将输入输出流包装起来。该部分代码面向抽象编程,使得无论是文件流还是网络套接字流都可以使用该方法进行流加密。为了研究学习Frp工具代码技术,笔者打算分析加解密模块代码,并总结相关golang代码技术。

0x02 Frp加解密相关功能

Frp工具加解密功能使用的是fatedier模块,该模块应该是专门为Frp设计,但是其中的设计理念能够适应很多加解密场景。主要的实现文件如下图所示
image.png
该模块主要实现了AES-128-CFB加解密算法,而且根据代码注释得到,目前并不支持其他加解密算法。
image.png

0x03 Frp输入输出流加密

要理解这部分知识得先从io.Reader和io.Writer说起。

0x1 io.Writer 和 io.Reader

这两个类型一样都是interface类型,他们的功能特别强大,强大到在任何写入和读取数据的地方,都应该尽可能的使用这两个类型对象。首先我们看看他们的声明代码

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

只要是实现了这两个接口中的方法就可以使用该接口实现多态的功能。

0x2 EncryptWriter 设计思路

writer的设计思路可以归结为以下几点

  • 设计writer结构体,并包含原始套接字(为了发送明文数据)
  • 设计writer结构体,并包含加密套接字
  • 实现Write函数,发送随机IV

1. 结构体设计

与其说是结构体设计不如说是在设计类,struct可以有方法,那么有方法的struct在golang中和类的功能也差不多了(而且可以实现接口)

type Writer struct {
	w      io.Writer
	enc    *cipher.StreamWriter
	key    []byte
	iv     []byte
	ivSend bool
	err    error
}

我们可以看到Writer结构体中几乎包含了我们能够想得到的必要字段。

2. 初始化

首先看下代码

func NewWriter(w io.Writer, key []byte) (*Writer, error) {
	key = pbkdf2.Key(key, []byte(DefaultSalt), 64, aes.BlockSize, sha1.New)

	// random iv
	iv := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return nil, err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	return &Writer{
		w: w,
		enc: &cipher.StreamWriter{
			S: cipher.NewCFBEncrypter(block, iv),
			W: w,
		},
		key: key,
		iv:  iv,
	}, nil
}

(1)生成密钥

第二行代码pbkdf2.Key,是一个用来生成密钥的函数,原理是通过 password 和 salt 进行 hash 加密,然后将结果作为 salt 与 password 再进行 hash,多次重复此过程,生成最终的密文。一般来说采用PBKDF2算法hash密码进行存储是比较安全的。pbkdf2.Key函数解析

Key(password, salt []byte, iter, keyLen int, h func() hash.Hash)
  • 参数一,明文密码
  • 参数二,盐值
  • 参数三,迭代轮数
  • 参数四,最后的hash长度
  • 参数五,hash算法

(2)生成iv

rand.Reader是随机数流,可以从该流中读取需要长度的随机数。io.ReadFull函数用于从指定的读取器r中读取到指定的缓冲区buf中。

iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
    return nil, err
}

(3)创建Writer

block, err := aes.NewCipher(key)
return &Writer{
		w: w,
		enc: &cipher.StreamWriter{
			S: cipher.NewCFBEncrypter(block, iv),
			W: w,
		},
		key: key,
		iv:  iv,
	}, nil

最后成功创建Writer类,而且包含了加密通信所必要的几个元素

3. 实现Write函数

Write函数主要是为了协商iv以及发送加密数据

func (w *Writer) Write(p []byte) (nRet int, errRet error) {
	if w.err != nil {
		return 0, w.err
	}

	// When write is first called, iv will be written to w.w
	if !w.ivSend {
		w.ivSend = true
		_, errRet = w.w.Write(w.iv)
		if errRet != nil {
			w.err = errRet
			return
		}
	}

	nRet, errRet = w.enc.Write(p)
	if errRet != nil {
		w.err = errRet
	}
	return
}

设置了ivSend标志字段,如果是第一次通信,将会使用原生套接字发送十六字节iv值。之后使用w.enc套接字发送通信内容。

0x3 DecryptReader 设计思路

reader的设计思路可以归结为以下几点

  • 设计reader结构体,并包含原始套接字(为了接收明文数据)
  • 设计reader结构体,并包含加密套接字
  • 实现Read函数,接受随机IV

1.结构体设计

type Reader struct {
	r   io.Reader
	dec *cipher.StreamReader
	key []byte
	iv  []byte
	err error
}

2.初始化

Reader的初始化相对来说比较简单,因为密钥是根据token计算得来的,iv需要接受来自套接字另一端的writer。该部分的重点代码在Read函数的实现上。

(1)生成密钥

采用和writer端相同的密钥生成方式,保证在token相同的情况下aes key也是一样的

key = pbkdf2.Key(key, []byte(DefaultSalt), 64, aes.BlockSize, sha1.New)

(2)创建Reader

因为Reader在创建的时候知道的字段有限,所以创建时只需指定已知字段即可,创建方式如下

return &Reader{
    r:   r,
    key: key,
}

主要的参数通过和writer端协商之后进行填充,详情可参照Read函数的实现

3.实现Read函数

func (r *Reader) Read(p []byte) (nRet int, errRet error) {
	if r.err != nil {
		return 0, r.err
	}

	if r.dec == nil {
		iv := make([]byte, aes.BlockSize)
		if _, errRet = io.ReadFull(r.r, iv); errRet != nil {
			return//从套接字另一端读取iv值
		}
		r.iv = iv

		block, err := aes.NewCipher(r.key)
		if err != nil {
			errRet = err
			return
		}
		r.dec = &cipher.StreamReader{//创建加密流
			S: cipher.NewCFBDecrypter(block, iv),
			R: r.r,
		}
	}

	nRet, errRet = r.dec.Read(p)//进行数据解密,并读到缓冲区
	if errRet != nil {
		r.err = errRet
	}
	return
}

0x03 加解密代码优化

读了这部分的代码总感觉有点怪怪的,整体的感觉就是client端有read和write,server端有read和write,但他们的iv是两两配对的,比如client的read和server端的write是一个iv密钥对。明明是一个网络流还整出了两套不一样的iv向量,着实有些奇怪。为了优化该部分代码,笔者将两个结构体抽象成了一个结构体,来完成所有的操作,并增加协商方法,在套接字建立连接之后开始进行密钥协商。

0x1 结构体设计

type SecSocket struct {
	w		io.Writer
	r 		io.Reader
	iv		[]byte
	key		[]byte
	enc 	*cipher.StreamWriter
	dec 	*cipher.StreamReader
	isNego  bool
	err 	error
}

设计该结构体的初衷就是将iv及key绑定在同一套接字两端的输入输出流加解密算法上。因此在SecSocket包含了io.Writer和io.Reader两个接口,其初始化函数如下所示

func NewSecSocket(r io.Reader,w io.Writer)(*SecSocket, error){
	return &SecSocket{
		w: w,
		r: r,
		isNego: false,
		err: nil,
	},nil
}

0x2 密钥协商

和Frp中的设计理念不同的是SecSocket需要一个密钥协商过程,协商的时候绑定iv和key值。先发起连接请求的一端为NegotiateC协商函数,负责监听的一端为NegotiateS协商函数。

func (ss *SecSocket) NegotiateC()(errRet error) {
	iv := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {//随机生成iv
		return err
	}
	ss.iv = iv

	key := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, key); err != nil {//随机生成key
		return err
	}
	ss.key = key

	_,errRet = ss.w.Write(key)//使用原生socket发送key
	if errRet != nil {
		ss.err = errRet
		return
	}
	_,errRet = ss.w.Write(iv)//使用原生socket发送iv
	if errRet != nil {
		ss.err = errRet
		return
	}
	block, err := aes.NewCipher(ss.key)//初始化算法
	if err != nil {
		errRet = err
		return
	}
	ss.dec = &cipher.StreamReader{
		S: cipher.NewCFBDecrypter(block, ss.iv),//生成解密算法
		R: ss.r,
	}
	ss.enc = &cipher.StreamWriter{
		S: cipher.NewCFBEncrypter(block, ss.iv),//生成加密算法
		W: ss.w,
	}
	ss.isNego = true
	return nil
}

整体来说client端在密钥协商过程中只负责发送iv和key,和frp的协商还不太一样的地方在于key也是随机生成的并参与协商过程,这样每次建立通信连接使用的密钥也是不一样的。最后给dec和enc加密流选择算法并赋值。


反过来看下服务端,逻辑更是简单,接受来自对端的iv和key并给dec和enc加密流选择算法并赋值。

func (ss *SecSocket) NegotiateS()(errRet error){
	key := make([]byte, aes.BlockSize)
	if _, errRet = io.ReadFull(ss.r, key); errRet != nil {//接收key
		return
	}
	ss.key = key

	iv := make([]byte, aes.BlockSize)
	if _, errRet = io.ReadFull(ss.r, iv); errRet != nil {//接收iv
		return
	}
	ss.iv = iv
	block, err := aes.NewCipher(ss.key)
	if err != nil {
		errRet = err
		return
	}
	ss.dec = &cipher.StreamReader{
		S: cipher.NewCFBDecrypter(block, ss.iv),//生成解密算法
		R: ss.r,
	}
	ss.enc = &cipher.StreamWriter{
		S: cipher.NewCFBEncrypter(block, ss.iv),//生成加密算法
		W: ss.w,
	}
	ss.isNego = true
	return
}

0x3 写入读取函数

将p缓冲区写入enc加密流

func (ss *SecSocket) Write(p []byte) (nRet int, errRet error) {
	if !ss.isNego {
	}
	nRet, errRet = ss.enc.Write(p)
	if errRet != nil {
		ss.err = errRet
	}
	return
}

从dec解密流中读取字符到p缓冲区

func (ss *SecSocket) Read(p []byte) (nRet int, errRet error) {
	if ss.err != nil {
		return 0, ss.err
	}
	nRet, errRet = ss.dec.Read(p)
	if errRet != nil {
		ss.err = errRet
	}
	return
}

0x04 扩展功能设计

笔者想基于Frp的加解密功能实现一些扩展功能,列表如下:

参考链接

https://blog.csdn.net/qq_39112101/article/details/84519260


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM