Steam新版登录分析,深入了解Protobuf
2023-10-8 18:6:54 Author: mp.weixin.qq.com(查看原文) 阅读量:55 收藏

1、抓包

逆向第一步,必然抓包看看啦,抓的这是啥,登录只有两个包。

看看第一个包
url
https://*****/IAuthenticationService/GetPasswordRSAPublicKey/v1?origin=https:%2F%2Fstore.steampowered.com&input_protobuf_encoded=CgoxMTExMTExMTEx

掐指一算,是一串有乱码的字符串,偷瞄一眼发现rsa密钥相关标志010001
再看url=GetPasswordRSAPublicKey 百度翻译=获取rsa公钥。



知道了这个包是获取RSA公钥,那么问题来了,为什么会中间有乱码呢?


2、protobuf数据格式

引入正题,protobuf是什么,具体百度一下你就知道,简单来说protobuf是一种Google提供的高效的协议数据交换格式,就像JSON一样,有固定的格式,正向开发中需要编写。proto 文件来秒数这个数据具体都有什么信息,逆向没有这个文件,就只能根据值盲猜了,我为什么知道这个是protobuf数据格式,没人天生知道,都是踩了很多坑,查了很多资料得出的经验,所以一下能判断大概是什么数据。


https://gchq.github.io/CyberChef/,把响应数据HEx拿到这个网站解析。




成功获取解析出数据,左边可以边写。proto边查看右边解析结果。

ok第一个包完成文件编写。

syntax = "proto3";
package Protoc;
option go_package = ".";

//响应
message Rsa_Response{
string PublicKey= 1; //公钥
string MoShu= 2; //模数
int64 SuiJiShu= 3; //随机数
}

运行protoc --go_out=./__/ *.proto生成go代码。

3、第一个包取RSA公钥

顺便看一下url内的两个参数,第一个固定,第二个是base64编码后的用户名,就不细说了。

然后编写调用测试一下。

func (集 *Api) D登录_取Rsa公钥() bool {
局_网址 := "https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1"

Http请求 := 集.Http客户端.DevMode().R()

局_网址 += `?origin=https:%2F%2Fstore.steampowered.com&input_protobuf_encoded={input_protobuf_encoded}`
//SetPathParam 会自动url编码, 无需再编码
Http请求.SetPathParam(`input_protobuf_encoded`, B编码_BASE64编码([]byte(string([]byte{10, 11})+集.账号)))

局_post := map[string]string{}
Http请求.EnableForceMultipart().SetFormData(局_post)

Http请求.SetHeader("accept", " application/json, text/plain, */*")
Http请求.SetHeader("accept-language", " zh-CN,zh;q=0.9")
Http请求.SetHeader("cache-control", " no-cache")
Http请求.SetHeader("dnt", " 1")
Http请求.SetHeader("origin", " https://store.steampowered.com")
Http请求.SetHeader("pragma", " no-cache")
Http请求.SetHeader("referer", " https://store.steampowered.com/login/?snr=1_4_4__more-content-login")
Http请求.SetHeader("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")
Http请求.SetHeader("Host", "api.steampowered.com")
Http请求.SetHeader("Connection", "keep-alive")

var 局_请求结果 *req.Response
var err error
for i := 0; i < 3; i++ {
局_请求结果, err = Http请求.Get(局_网址)
if len(局_请求结果.Bytes()) > 0 || err != nil {
break
}
}

局_pb := &集.Rsa
err = proto.Unmarshal(局_请求结果.Bytes(), 局_pb)
if err != nil {
fmt.Println("取rsa公钥反序列化失败 ", err)
}
if 局_pb.PublicKey != "" {
return true
}
集.错误信息 = "RSA密钥获取失败"
fmt.Printf(局_请求结果.Dump())
return false

}

测试ok 没问题正常获取到Rsa公钥参数。


4、第二个包登录包



看看这个包的提交信息,好家伙这么长,经验判断应该是base64,解码失败看都有什么信息。




掐指一算,嗯嗯,又是明文+乱码,protobuf没跑了,转换成hex继续盲反序列化。


大概看看:
"2"应该是账号;
"3"这么长,联想到刚才获取rsa,估计就是加密后的密码了;
"4"和rsa包一起返回的随机数;
"9"可能是浏览器信息。
其他的就看不出来了。

这就是probuf逆向的麻烦事,没有proto文件不知道值什么信息,当然要求不高直接用也可以,反正也不用改变不知道的数据,咱们只需要修改"2","3","4"这几个值就行了,但是!!!经过我一顿断点操作,还真让我翻出了每个参数的名字。




在看看返回包,登录失败,返回信息只有两个字节?这是什么鬼,盲反序列化也没数据。

那前端是怎么判断登录结果的呢?此事必有蹊跷。经过反复对比疑点在这里,响应协议头内x-eresult参数代表操作是否成功,1=成功,5=帐密错误,等等。




再看看成功都返回的数据:




还是一样,盲反序列化一条龙,再根据js判断键名。



嘿嘿以为登录成功了,没想到吧没还需要验证码。不过登录包的请求和响应protoc,文编编写完成了。

syntax = "proto3";
package Protoc;
option go_package = ".";

// 请求
message eMsg9804 {
string account_name = 2; // 1111111111
string encrypted_password = 3; // rsa加密密码
int64 encryption_timestamp = 4; // 570240150000
int32 set_remember_login = 5; // 1 是否记住登录
bool set_persistence = 7; // 1 坚持不懈?
string website_id = 8; // Mobile app内是 Mobile pc是 Store
repeated device_details device_details = 9; // 设备信息 手机和电脑不一样
int32 language = 11; // 6 语言,直接设置6就可以,中国人 app没有这个值 pc=6
}

message device_details {//电脑网页设备信息
string device_friendly_name = 1; //app=Pixel, pc= Dalvik/2.1.0 (Linux; U; Android 7.1.2; Pixel Build/NJH47F; Valve Steam App Version/3)
int32 platform_type = 2; // 2 平台类型 pc=2 app=3
int32 os_type = 3; //
uint32 gaming_device_type = 4; //
uint32 client_count = 5; //
bytes machine_id = 6; //
}

//响应============================================================
//CAuthentication_BeginAuthSessionViaCredentials_Response
//const {client_id: i, request_id: n, interval: a, allowed_confirmations: s, steamid: u, weak_token: d} = h.Body().toObject();
message Response_EMsg147 {
uint64 client_id = 1; //
bytes request_id= 2; // í”'*c¡²m*¨cށƒ
fixed32 interval= 3; // 1084227584
repeated allowed_confirmations allowed_confirmations= 4;
uint64 steamid= 5; /
string weak_token= 6; //
string agreement_session_url = 7; //协议会话url
string extended_error_message = 8; // {}
}

message allowed_confirmations{
int32 confirmation_type= 1;
string associated_message= 2;
}
//仅供参考
/*{
"allowedConfirmations": [
{
"confirmationType": 2,
"associatedMessage": "qq.com"
},
{
"confirmationType": 6,
"associatedMessage": ""
}
],
"clientId": 17287054.....000,
"requestId": "je2U....bSqoY96Bgw==",
"interval": 1084227584,
"steamid": 76561191...6700,
"weakToken": "eyAidHlwI...bT1kBHhTJj-Sd8yK2SYdYIxifwfQZmb-1yq3zogxU9JzDa6RLyDgFxwr4bmQeNXVBA",
"agreementSessionUrl": "",
"extendedErrorMessage": ""
}*/

密码是标准rsa加密,随便找个RSA的js加密填写上参数就可以用了。

生成go代码,编写测试:

// 本命令由自动生成,请配合[ go get -u gitee.com/anyueyinluo/Efunc ]库使用。
func (集 *Api) D登录_登录() bool {
局_网址 := `https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1`
Http请求 := 集.Http客户端.EnableDumpAll().R()
Http请求.SetHeader(`Accept`, `application/json, text/plain, */*`)
Http请求.SetHeader(`Accept-Language`, `zh-CN,zh;q=0.9`)
Http请求.SetHeader(`Cache-Control`, `no-cache`)
Http请求.SetHeader(`Content-Type`, `multipart/form-data; boundary=----WebKitFormBoundarybxBPD59cJtdK0xKS`)
Http请求.SetHeader(`Origin`, `https://store.steampowered.com`)
Http请求.SetHeader(`Pragma`, `no-cache`)
Http请求.SetHeader(`Referer`, `https://store.steampowered.com/login/?redir=&redir_ssl=1&snr=1_4_4__global-header`)
局_设备信息 := []*__.DeviceDetails{{
DeviceFriendlyName: "Pixel",
PlatformType: 3,
}}

局_pb := __.EMsg9804{
AccountName: 集.账号,
EncryptedPassword: 密码加密(集.密码, 集.Rsa.PublicKey, 集.Rsa.MoShu),
EncryptionTimestamp: 集.Rsa.SuiJiShu,
SetPersistence: true,
SetRememberLogin: 1,
WebsiteId: "Mobile",
Language: 6,
DeviceDetails: 局_设备信息,
}

局_pb字节集, err := proto.Marshal(&局_pb)
if err != nil {
集.错误信息 = "登录信息序列化失败:" + err.Error()
return false
}

局_post := map[string]string{
"input_protobuf_encoded": base64.StdEncoding.EncodeToString(局_pb字节集),
}
Http请求.EnableForceMultipart().SetFormData(局_post)

var 局_请求结果 *req.Response
for i := 0; i < 3; i++ { // 重试三次防止意外
局_请求结果, err = Http请求.Post(局_网址)
if len(局_请求结果.Bytes()) > 0 || err != nil {
break
}
}

fmt.Printf("X-eresult:%s\n", 局_请求结果.GetHeader("X-eresult"))
if 局_请求结果.GetHeader("X-eresult") != "1" {
i, _ := strconv.Atoi(局_请求结果.GetHeader("X-eresult"))
switch i {
default:
集.错误信息 = 局_请求结果.GetHeader("X-eresult") + 局_请求结果.String()
fmt.Print("\n", 局_请求结果.Dump())
case 2:
集.错误信息 = "#Login_RefreshReason_Generic"
集.错误信息 = "#请再次登录"
case 7:
集.错误信息 = "#Login_RefreshReason_Generic"
集.错误信息 = "#请再次登录"
case 6:
集.错误信息 = "#Login_RefreshReason_LoggedInElsewhere"
集.错误信息 = "#此帐户已在另一台计算机上登录"
case 34:
集.错误信息 = "#Login_RefreshReason_LogonSessionReplaced"
集.错误信息 = "#此帐户已在别处登录"
case 5:
集.错误信息 = "#Login_RefreshReason_InvalidPassword"
集.错误信息 = "您的帐户凭据已更改,密码错误"
case 26:
集.错误信息 = "#Login_RefreshReason_Revoked"
集.错误信息 = "#您的会话已结束"
case 27:
集.错误信息 = "#Login_RefreshReason_Expired"
集.错误信息 = "#您的会话已过期"
case 49:
集.错误信息 = "#Login_RefreshReason_PasswordRequiredToKickSession"
集.错误信息 = "#确认您的凭据以从另一台计算机退出"
case 43:
集.错误信息 = "#Login_RefreshReason_AccountDisabled"
集.错误信息 = "#您的帐户已被禁用"
case 69:
集.错误信息 = "#Login_RefreshReason_ParentalControlRestricted"
集.错误信息 = "#您帐户的家长控制要求您确认凭据"
case 84:
集.错误信息 = "#Login_Error_RateLimit_Description"
集.错误信息 = "#短期内来自您所在位置的失败登录过多。请稍后再试。"
}
return false
}

fmt.Printf("登录响应Hex:%s\n", hex.EncodeToString(局_请求结果.Bytes()))

err = proto.Unmarshal(局_请求结果.Bytes(), &集.Response_EMsg147)
if err != nil {
集.错误信息 = "登录结果反序列化失败:" + err.Error()
return false
}
fmt.Print("\n", 局_请求结果.Dump())
if 集.Response_EMsg147.Steamid == 0 {
return false
}
return true
}

5、提交邮件验证码包

既然要验证码,那也没办法继续吧。这个包就很简单了,需要的信息上边包都返回了,至直接使用提交即可。

x-eresult: 65=代码错误 1=成功。




proto文件

syntax = "proto3";
package Protoc;
option go_package = ".";
/*
i.SetEMsg(9804),
i.Body().set_client_id(this.m_strClientID),
i.Body().set_steamid(this.m_steamid),
i.Body().set_code(e),
i.Body().set_code_type(r ? 2 : 3);
*/

//Authentication.UpdateAuthSessionWithSteamGuardCode#1
message UpdateAuthSessionWithSteamGuardCode {
uint64 client_id = 1; //47388000 登录返回的信息
fixed64 steamId= 2; // 10168
string code= 3; //"" 验证码
int64 code_type= 4; // 2 2是邮件验证码 3可能是动态码
}

6、刷新状态包

但是有个问题啊,为什么提交完验证码没返回登录凭据呢acctoken。


原来还有一个包,在这了获取,因为页面跳转浏览器不会缓存这个返回数据,我用花瓶查看到的。


qing




返回信息就在这里。

proto文件编写:

syntax = "proto3";
package Protoc;
option go_package = ".";

message PollAuthSessionStatus_Request {
uint64 ClientID = 1;
bytes request_id= 2; //登录包返回
}

//======Response
//
message PollAuthSessionStatus_Response {
uint64 ClientID = 1;
string new_challenge_url = 2;
string refresh_token = 3;
string access_token= 4;
bool had_remote_interaction= 5;
string account_name= 6;
string new_guard_data= 7;
string agreement_session_url= 8;
}

写代码测试:

// 本命令由自动生成,请配合[ go get -u gitee.com/anyueyinluo/Efunc ]库使用。
func (集 *Api) D登录_刷新状态() bool {
局_网址 := `https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1`
Http请求 := 集.Http客户端.R()
var 局_PB __.PollAuthSessionStatus_Request
局_PB.ClientID = 集.Response_EMsg147.ClientId
局_PB.RequestId = 集.Response_EMsg147.RequestId

局_PB_字节集, _ := proto.Marshal(&局_PB)

局_post := map[string]string{
"input_protobuf_encoded": B编码_BASE64编码(局_PB_字节集),
}
Http请求.EnableForceMultipart().SetFormData(局_post)
Http请求.SetHeader(`accept`, `application/json, text/plain, */*`)
Http请求.SetHeader(`accept-language`, `zh-CN,zh;q=0.9`)
Http请求.SetHeader(`cache-control`, `no-cache`)
Http请求.SetHeader(`content-type`, `multipart/form-data; boundary=----WebKitFormBoundaryuudBrPBCeV3jLjkZ`)
Http请求.SetHeader(`dnt`, `1`)
Http请求.SetHeader(`origin`, `https://store.steampowered.com`)
Http请求.SetHeader(`pragma`, `no-cache`)
Http请求.SetHeader(`referer`, `https://store.steampowered.com/login/?redir=%3Fsnr%3D1_60_4__global-responsive-menu&redir_ssl=1&snr=1_4_4__global-header`)
Http请求.SetHeader(`user-agent`, `Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1`)
var 局_请求结果 *req.Response
var err error
for i := 0; i < 3; i++ { // 重试三次防止意外
局_请求结果, err = Http请求.Post(局_网址)
if len(局_请求结果.Bytes()) > 0 || err != nil {
break
}
}
if 局_请求结果.GetHeader("X-eresult") != "1" {
i, _ := strconv.Atoi(局_请求结果.GetHeader("X-eresult"))
switch i {
default:
集.错误信息 = 局_请求结果.GetHeader("X-eresult") + 局_请求结果.String()
fmt.Print("\n", 局_请求结果.Dump())
case 2:
集.错误信息 = "#Login_Error_Expired_Description"
集.错误信息 = "#登录请求已失效"
case 3:
集.错误信息 = "#Login_Error_Network_Description"
集.错误信息 = "#与 Steam 通信时出现问题。请稍后重试。"
case 4:
集.错误信息 = "#Login_Error_MoveAuthenticator_Description"
集.错误信息 = "#在移动您的验证器时出现问题,请稍后再试。"
case 5:
集.错误信息 = "#Login_Error_RateLimit_Description"
集.错误信息 = "#短期内来自您所在位置的失败登录过多。请稍后再试。"
case 6:
集.错误信息 = "#网页端和桌面版 Steam 客户端均不支持匿名登录;仅 steamcmd 支持匿名登陆。"
集.错误信息 = "#短期内来自您所在位置的失败登录过多。请稍后再试。"
}
return false
}
fmt.Printf("刷新状态提交响应Hex:%s\n", hex.EncodeToString(局_请求结果.Bytes()))
var 局_pb __.PollAuthSessionStatus_Response
err = proto.Unmarshal(局_请求结果.Bytes(), &局_pb)
if err != nil {
集.错误信息 = "刷新状态结果反序列化失败:" + err.Error()
return false
}
fmt.Print(json.Marshal(局_pb))
fmt.Printf("%v\n", 局_pb)
return true
}

测试ok成功获取到acctoken。

7、结语

这个网站其实没什么加密,只是使用了protobuf格式的数据,所以导致读取比较麻烦,尤其是某些语言真的很不方便。我也是没办法才改使用go来操作,go语言还是挺方便的。

看雪ID:暗月隐落

https://bbs.kanxue.com/user-home-895862.htm

*本文为看雪论坛优秀文章,由 暗月隐落 原创,转载请注明来自看雪社区

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复

球分享

球点赞

球在看


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458521120&idx=2&sn=b84f45dd6d51667a395fed15a00d77e4&chksm=b18d3eaa86fab7bcb13f5ab08da3ed6a35c9e30ef328eddd193cf81114821bbcdf2443b0dfbe&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh