众所周知,steam里骗子猖狂。以下页面是骗子私信发给我的一个仿冒的steam钓鱼网站。
如果点击 接受礼物 就会跳转至虚假的steam登录界面。
如果毫无防备,通过手机steam客户端扫描右边的二维码进行登录,steam会进行登录异常的警告,一般来说要通过这一步还是很麻烦的。如果强行继续,那么steam账号除了会被劫持api key以外,大概率还会被洗库存,比如一些便宜的卡片和库存里的小件都会被卖出,而不会触发steam手机令牌的验证。
那么骗子是如何制作这些钓鱼网站的?steam对于自身系统的保护究竟做的怎么样?
这些都绕不开对于steam的逆向分析。
开始逆向
抓包观察了一会steam登录界面后,就已经捋清了整个登录站点的逻辑。
在最开始会连接/BeginAuthStatusViaQRCode
获取登录二维码,之后每隔一段时间(5s)就会请求 PollAuthSessionStatus/v1
以更新会话状态,如果当前页面的二维码已经过期,则会刷新。
如果先点击登录按钮,会请求/GetPasswordRSAPublicKey/v1
,之后是 /BeginAuthSessionStatusViaCredentials
。
可以很明显的看到用的是 protobuf 协议。
查看这个网络请求的来源:
继续追踪这个API的来源。在js文件顶部可以看到这是用webpack打包的:
找到来自Send的方法调用比较可疑:
这里的 r
是一个对象,查看一下它的构造方法:
已经能看到proto的结构了。
构建出用于请求GetPasswordRSAPublicKey/v1
的proto:
syntax = "proto3";
package steam;
option go_package = "proto/steam"
message GetPasswordRSAPublicKey_Request {
string account_name = 1;
}
message GetPasswordRSAPublicKey_Response {
string public_key_mod = 1;
string public_key_exp = 2;
uint64 timestamp = 3;
}
编译proto:
$ protoc --go_out=. --go-grpc_out=. steam.proto
安装golang的protobuf包:
$ go get google.golang.org/protobuf
发起请求代码,grpc是基于http2协议的,所以直接http get就行:
import (
"encoding/base64"
"io"
"log"
"net/http"
"net/url"
pb "steam/api/steam"
"google.golang.org/protobuf/proto"
)
func main() {
api := "/GetPasswordRSAPublicKey/v1"
p_req := &pb.CAuthentication_GetPasswordRSAPublicKey_Request{
AccountName: "hello",
}
byte_arr, err := proto.Marshal(p_req)
if err != nil {
log.Fatal("err marshaling: ", err)
}
encoded := base64.StdEncoding.EncodeToString(byte_arr)
params := url.Values{}
params.Add("origin", "https://steamcommunity.com")
params.Add("input_protobuf_encoded", encoded)
req, _ := http.NewRequest("GET", api, nil)
req.URL.RawQuery = params.Encode()
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3")
c := &http.Client{}
resp, err := c.Do(req)
if err != nil {
log.Fatal("err when http req: ", err)
}
defer resp.Body.Close()
handle_response(resp.Body)
发起grpc请求已经写完了,接下来要处理它的返回。将proto buffers反序列化就能得到steam服务器分发的RSAkey了:
func handle_response(r io.Reader) {
byte_arr, err := io.ReadAll(r)
if err != nil {
log.Fatal("err to read response: ", err)
}
resp := &pb.CAuthentication_GetPasswordRSAPublicKey_Response{}
proto.Unmarshal(byte_arr, resp)
log.Println(resp)
}
拿到了RSAKey后,才可以开始真正地访问登录接口/BeginAuthSessionStatusViaCredentials
。
找到了BeginAuthSessionReq的proto定义:
之后找到如何打包请求体:
进行加密的方法就是m.P8
, 其中o是密码明文,t是公钥。
const l = m.P8(o, t), c = F.w.Init(A.iP);
m.PB 函数的构造:
开始构建proto文件,需要注意的是 platform_type
和 persisteance
是两个枚举类型,persistance
字段其实就是remember_login
,当后者为true时前者为1,反之亦然。
message BeginAuthSessionViaCredentials_Request {
string device_friendly_name = 1;
string account_name = 2;
string encrypted_password = 3;
string encryption_timestamp = 4;
bool remember_login = 5;
platformType platform_type = 6;
persistence persistence = 7;
string website_id = 8;
DeviceDetails device_details = 9;
string guard_data = 10;
int32 language = 11;
int32 app_level = 12;
}
而里面的 DeviceDetails
是一个结构体,device_friendly_name
其实就是UA,platform_type
在 Windows 下是2。
message DeviceDetails {
string device_friendly_name = 1;
string platform_type = 2;
string os_type = 3;
string gaming_device_type = 4;
string client_count = 5;
string machine_id = 6;
string app_type = 7;
}
而对于QR code的请求api是
/BeginAuthSessionStatusViaQR
message CAuthentication_DeviceDetails {
string device_friendly_name = 1;
string platform_type = 2;
string os_type = 3;
string gaming_device_type = 4;
string client_count = 5;
string machine_id = 6;
string app_type = 7;
}
message CAuthentication_BeginAuthSessionViaQR_Request {
string device_friendly_name = 1;
string platform_type = 2;
CAuthentication_DeviceDetails device_details = 3;
string website_id = 4;
}
message CAuthentication_BeginAuthSessionViaQR_Response {
string client_id = 1;
string challenge_url = 2;
bytes request_id = 3;
float interval = 4;
repeated AllowedConfirmation allowed_confirmations = 5;
int32 version = 6;
w}
然而,QRCode的返回是json格式的:
拿到了challenge_url,至此我的目标已经完成了。后续对于使用密码登录的令牌验证过程,以及登录成功后的回调等以后再看看吧。
(the place send grpc req)
比较有意思的是steam官网里留存了一个没有经过混淆加密的login.js,就连注释也在,应该是很很久以前留下来的代码。
在2019年的时候,这些api还有效,Github上的这个脚本就是逆向了这些api。但时至今日,这些API已经被弃用了。
目前能看到这个 LoginManager 是会初始化的,但里面的方法却都不会被调用。
$J.post( this.m_strBaseURL + 'getrsakey/', this.GetParameters( { username: username } ) )
.done( $J.proxy( this.OnRSAKeyResponse, this ) )
.fail( function () {
ShowAlertDialog( '错误', '与 Steam 服务器通信时出现问题。请稍后重试。' );
$J('#login_btn_signin').show();
$J('#login_btn_wait').hide();
_this.m_bLoginInFlight = false;
});