目标
对 Steam 登录接口进行逆向分析。 目标网址:https://store.steampowered.com/login/
逆向分析
输入账号密码点击登录后,首先观察接口 GetPasswordRSAPublicKey/v1。从命名可以看出该接口返回 RSA 加密的公钥信息。观察请求参数,核心加密参数为 input_protobuf_encoded。
在浏览器中全局搜索该参数名,可以定位到两处调用位置。可以看到 input_protobuf_encoded 的值经过处理,其源头是 r.JQ(o),而 o 的值为 n.SerializeBody()。这里的 n 是一个包含账号信息的实例对象。
我们可以通过原型链进入构造函数进行断点调试。在 super 位置下断,发现实例化时传入了一个类定义。初始化时会检查 account_name 属性,若不存在则调用特定方法创建对象。
无论是从 n.aR 方法入手,还是追踪 account_name 的属性及其父类,最终都会指向 protobuf 协议的处理逻辑。
Protocol Buffers 基础
Protobuf 协议根据特定的语法定义数据结构,发送和接收数据前必须约定好字段格式。上文中的类正是对 account_name 字段的定义。我们可以参考 JS 代码中的格式编写自己的 .proto 文件。
Protobuf 常见的数据类型包括 int32、string、bool、bytes、message 等。
新建一个 proto 文件(需配置环境),定义 account_name 字段:
syntax = "proto3";
message CAuthenticationGetPasswordRsaPublicKeyRequest {
string account_name = 1;
}
执行命令 protoc --python_out=. xx.proto 将 proto 文件转为 Python 代码。生成的 py 文件可以直接使用。
使用示例如下:
from loguru import logger
from steam_pb2 import CAuthenticationGetPasswordRsaPublicKeyRequest
def get_rsa_public_key(username):
message = CAuthenticationGetPasswordRsaPublicKeyRequest(account_name=username)
logger.info(message.SerializeToString())
logger.info(type(message))
if __name__ == '__main__':
get_rsa_public_key("a123456789")
回到逆向流程,我们已经知道了 o 的生成方式,剩下的 r.JQ 方法通常是 Base64 编码。组合起来就生成了 input_protobuf_encoded 的值。
响应信息解析
推荐在 XHR 请求处下断点,跟踪直到看到响应信息解析的地方。l.data 即为响应信息,关键方法是 r.deserializeBinaryFromReader。单步跟踪会进入静态方法 MBF,这里判断 protobuf 数据格式是否已定义,未定义则进行定义。
查看定义的字段,例如 publickey_mod、publickey_exp、timestamp。按照 JS 代码中的字段与类型重新定义 proto 文件:
message CAuthenticationGetPasswordRsaPublicKeyResponse {
string publickey_mod = 1;
string publickey_exp = 2;
uint64 timestamp = 3;
}
完整请求代码
整合上述步骤,完整的 Python 请求代码如下:
import base64
import requests
from steam_pb2 import (
CAuthenticationGetPasswordRsaPublicKeyRequest,
CAuthenticationGetPasswordRsaPublicKeyResponse
)
headers = {
'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
def get_rsa_public_key(username):
origin = 'https://steamcommunity.com'
message = CAuthenticationGetPasswordRsaPublicKeyRequest(account_name=username)
protobuf = base64.b64encode(message.SerializeToString()).decode()
url = f'https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1'
params = {
"origin": origin,
"input_protobuf_encoded": protobuf
}
response = requests.get(url, params=params, headers=headers, timeout=3)
# 解析响应信息
response_obj = CAuthenticationGetPasswordRsaPublicKeyResponse.FromString(response.content)
print(response_obj)
return response_obj.publickey_mod, response_obj.publickey_exp, response_obj.timestamp
if __name__ == '__main__':
get_rsa_public_key("a123456789")
至此,第一个接口的请求参数与响应信息都已搞定。返回的三个参数 publickey_mod、publickey_exp、timestamp 明显用于后续 RSA 加密。
登录接口分析
接下来是真正的登录接口 BeginAuthSessionViaCredentials/v1。该接口参数只有一个 input_protobuf_encoded。同样在老地方下断,根据 t 值判断接口。
找到约定字段的地方进行改写,主要字段包括 device_friendly_name、account_name、encrypted_password 等。注意 device_details 属于自定义类型,需要单独查找其结构定义。
密码是被加密过的,加密方法推断为 RSA,使用上一步获取的 publickey_exp 和 publickey_mod 作为模数与指数。响应数据的解析方法同上,不再赘述。
至此,整个逆向流程结束。


