进入部分场景的本质其实是客户端发送协议后,接收并响应服务端的包,所以,对于这一部分前置的动作可以通过主动发送协议完成。
例如,对于对局的测试,创房间->匹配->确认对局->选择英雄是前置条件,则都可以通过发送协议来完成。
上图中,主动发送了进入房间的包和开启匹配的包,客户端虽然没有发送包,但会响应服务端返回的包。
因为客户端能响应服务端包的特性,也带来其他的一些问题,比如对于新手账号,进入房间时会触发新手引导,导致部分区域被遮盖,影响测试流程。
在一些场景下,协议测试也可以作为测试的一部分。
利用代理进行 MITM 攻击是此中间件的原理。而游戏大多是 tcp/websocket 等长连接通信,所以需要使用 socks5 代理,普通的 http 代理只能转发 http 数据。
代理在接到客户端的连接服务端请求后,会和客户端保持通信的同时,和服务端也建立连接,再作为中间人的身份传话。
client <-> proxy <-> server
可以理解为该中间件可以控制代理传假话或者不传话。
为了控制代理,需要引入消息队列来处理代理接收到的数据。对应不同功能,可以简单表述为听和说两种。
对应的是将收到的客户端数据和服务端数据放入消息队列中,让消费者处理。
client/server -> proxy -> MQ -> 消费者进程
此过程还有一个细节,对于收到的数据是否要再转给对端,这是拦截功能的要点。
说对应的就是向两端发送数据,生产者进程将数据放入消息队列,代理收到后发送给 client/server。
生产者进程 -> MQ -> proxy -> client/server
为了能承载更多设备,代理部分使用 go 语言开发的go-mitmproxy,但项目目前还不支持 socks5 代理,所以我在其基础上增加了 socks5 模式,但处理数据时没有区分 http 流量,还需要后续改进。
仿照了 mitmproxy 的做法,在 tcp 连接的三个过程 (tcp_start、tcp_message、tcp_end) 中加入了 hook,只需要定义 Addon 即可控制连接。
在 tcp 开始连接时,需要建立两个插入数据的消息队列,我使用了 Redis 的 Pub/Sub 模式,所以需要订阅插入 client/server 的频道。
type AddHeader struct {
addon.Base
client *redis.Client
context context.Context
mode string
}
func (a *AddHeader) TCPStart(f *flow.Flow){
connName := f.ClientConn.RemoteAddr().String() + "<->" + f.ServerConn.RemoteAddr().String()
insertToServerChannel := "InsertServer:" + connName
insertToClientChannel := "InsertClient:" + connName
// a.client是redis连接client,将同一个client的连接放入一个set中。
a.client.SAdd(a.context, strings.Split(f.ClientConn.RemoteAddr().String(), ":")[0], connName)
// 两个消息队列在Goroutine中进行
go a.InsertMessage(f.ClientConn, insertToClientChannel)
go a.InsertMessage(f.ServerConn, insertToServerChannel)
}
// 订阅channel以监听插入事件
func (a *AddHeader) InsertMessage(conn net.Conn,channel string) {
pubSub := a.client.Subscribe(a.context, channel)
defer pubSub.Close()
for {
msg, err := pubSub.ReceiveMessage(a.context)
if err != nil {
panic(err)
}
msgHex := msg.Payload
// 设置终止符以退出订阅
if msgHex == TERMINATOR{
fmt.Println(TERMINATOR)
break
// 更改模式
}else if strings.HasPrefix(msgHex, CHANGE_MODE){
switch msgHex {
case MODE_FREE:
fallthrough
case MODE_HOLD_UP_CLIENT:
fallthrough
case MODE_HOLD_UP_SERVER:
fallthrough
case MODE_HOLD_UP_ALL:
a.mode = msgHex
default:
// error mode
continue
}
}
b, e := hex.DecodeString(msgHex)
if e != nil{
break
}
_, e = conn.Write(b)
if e != nil{
break
}
}
}
和 tcp_start 和 tcp_end 不同的是,这个方法会触发多次,主要用于将接收到的数据放入消息队列,以及拦截数据。
func (a *AddHeader) TCPMessage(f *flow.Flow) {
msg := *f.Message
connName := f.ClientConn.RemoteAddr().String() + "<->" + f.ServerConn.RemoteAddr().String()
clientSendMsgChannel := "Client:" + connName
serverSendMsgChannel := "Server:" + connName
if msg.FromClient == true{
a.client.LPush(a.context, clientSendMsgChannel, msg.Content, 10*time.Second)
}else {
a.client.LPush(a.context, serverSendMsgChannel, msg.Content, 10*time.Second)
}
// 决定是否拦截(清空数据)
switch a.mode {
case MODE_HOLD_UP_SERVER:
if f.Message.FromClient{
f.Message.Content = nil
}
case MODE_HOLD_UP_CLIENT:
if !f.Message.FromClient{
f.Message.Content = nil
}
case MODE_HOLD_UP_ALL:
f.Message.Content = nil
default:
// do nothing
}
}
func (a *AddHeader) TCPEnd(f *flow.Flow){
connName := f.ClientConn.RemoteAddr().String() + "<->" + f.ServerConn.RemoteAddr().String()
insertToServerChannel := "InsertServer:" + connName
insertToClientChannel := "InsertClient:" + connName
// 通知退出订阅
serverErr := a.client.Publish(a.context, insertToServerChannel, TERMINATOR).Err()
clientErr := a.client.Publish(a.context, insertToClientChannel, TERMINATOR).Err()
if serverErr != nil {
fmt.Print(serverErr)
}
if clientErr != nil {
fmt.Print(clientErr)
}
// 清除此次连接记录
a.client.SRem(a.context, strings.Split(f.ClientConn.RemoteAddr().String(), ":")[0], connName)
}
1、将该代理部署在服务器上。
2、待测手机使用 Postern 或 ProxyDroid 等支持 socks5 代理软件,代理到对应服务器。
3、在自动化脚本中通过和 redis 交互,插入或获取包。