前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【JS 逆向百例】steam 登录 Protobuf 协议详解

【JS 逆向百例】steam 登录 Protobuf 协议详解

原创
作者头像
K哥爬虫
发布2023-12-29 18:20:58
2940
发布2023-12-29 18:20:58
举报
文章被收录于专栏:Python 爬虫Python 爬虫
00
00

声明

**本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!**

**本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!**

目标

目标:steam 登录协议逆向分析

网址:aHR0cHM6Ly9zdG9yZS5zdGVhbXBvd2VyZWQuY29tL2xvZ2luLw==

逆向分析

输入账密后点击登录,首先看接口 GetPasswordRSAPublicKey/v1,看接口命名可以了解到这个这个接口应该是返回 RSA 加密的公钥信息,先不管这些,观察参数 ,很明显加密参数为 input\_protobuf\_encoded

01
01

这里直接全局搜索,可以定位到两处:

02
02

可以看到 input\_protobuf\_encoded 的值为 a ,而 a 的值为 r.JQ(o)

03
03

先看参数 o 的值,为 n.SerializeBody() ,其中 n 是一个对象,包含我们输入的账号信息:

04
04

这里 n 是一个实例对象,这里可以直接通过原型进到它的构造函数中:

05
05

进到构造函数中后,在 super 位置下断:

06
06

可以发现实例化的时候传了一个类:

07
07

进到这个类 c 中,这里需要清下缓存重新下断:

08
08

这里可以看到,在初始化的时候,会检查当前实例的 account\_name 属性,很明显这个是有关于账号的属性,如果不存在(这里可以理解为首次实例化)则会调用 c.M() 方法创建一个对象,格式如下:

代码语言:json
复制
{

    proto: c,

    fields: {

        account\_name: {

            n: 1,

            br: n.FE.readString,

            bw: n.Xc.writeString

        }

    }

}

到这里无论是从 n.aR 方法入手,还是从 account\_name 的几个属性以及这几个类统一的父类 o入手,都会进入到一个新的文件中,到这就可以引出本期的主角 protobuf 协议了:

09
09

Protocol Buffers

10
10

从第一点可以了解到, protobuf 协议根据特定的语法来定义数据结构。我们发送数据以及接收数据都需要讲数据字段约定好才能进行生成与解析。

字段定义

初步了解 protobuf 协议后就能理解上文中的代码了,上文中的类正是对 account\_name 字段进行定义。

那么我们就可以根据 JS 代码中的格式来编写我们自己的 proto 文件:

代码语言:json
复制
account\_name: {

    n: 1,

    br: n.FE.readString,

    bw: n.Xc.writeString

}

protobuf 常见的数据类型有以下几种:

| **数据类型** | **描述** |

| ----------------------- | -------------------------------------------------- |

| int32 | int64 | 32位和64位整数 |

| uint32| uint64 | 32位和64位无符号整数 |

| sint32| sint64 | 带符号的变长编码整数 |

| fixed32| fixed64 | 固定大小的32位和64位整数 |

| sfixed32| sfixed64 | 固定大小的带符号32位和64位整数 |

| float | 单精度浮点数 |

| double | 双精度浮点数 |

| bool | 布尔值 |

| string | 字符串 |

| bytes | 二进制数据 |

| enum | 枚举类型,表示一组命名整数值 |

| message | 消息类型,可以包含其他数据类型的字段,用于嵌套结构 |

| map | 映射类型,用于定义键值对的映射关系 |

| Any | 用于包装任意类型的消息 |

| repeated | 表示一个字段可以包含多个值,类似于数组或列表 |

| Timestamp | 表示时间戳,用于表示一个特定时间点 |

| Duration | 表示时间间隔,用于表示一段时间的持续 |

| Struct | Value | 用于表示动态的键值对 |

除了上述数据类型,还支持自定义类型。

这里我们新建一个 proto 文件(需配置环境),定义 account\_name 字段:

代码语言:protobuf
复制
syntax = "proto3";



message CAuthenticationGetPasswordRsaPublicKeyRequest {

    string account\_name = 1;

}

执行命令 protoc --python\_out=. xx.protoproto 文件转为 python 代码。

转成的 py 文件格式如下:

11
11

使用起来也很简单:

代码语言:python
复制
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")

"""

OUTPUT:

b'\n\na123456789'

<class 'steam\_pb2.CAuthenticationGetPasswordRsaPublicKeyRequest'>

"""

那么回到逆向流程中,我们已经知道了 o 的生成方式,那么还剩 r.JQ 方法,这里很简单,直接扣下来即可,根据经验也可以看出这是 base64 编码:

代码语言:javascript
复制
o = n.SerializeBody()

a = r.JQ(o);

到这就生成了 input\_protobuf\_encoded 的值,那么还需要解决接口返回值。

响应信息解析

这里推荐下 xhr 断点,断在请求发送的地方。一路往下跟直到看到响应信息解析的地方:

12
12

这里 l.data 就是响应信息,u.At 主要就是对响应信息格式进行处理,并且声明一些方法,做一些读写操作等。s.BinaryReader 也是类似,都是对响应信息做了一些预处理。

关键看 r.deserializeBinaryFromReader ,单步跟,会进入到一个 MBF 静态方法中:

13
13

这个很像上文中类 c 构造方法中的一段代码,都是判断 protobuf 数据格式是否定义,如果没有定义的话会进行定义,那么这里与上文也一样,进到 l.M() 中就可以看到定义的字段:

代码语言:javascript
复制
static M() {

    return l.sm\_m || (l.sm\_m = {

        proto: l,

        fields: {

            publickey\_mod: {

                n: 1,

                br: n.FE.readString,

                bw: n.Xc.writeString

                },

            publickey\_exp: {

                n: 2,

                br: n.FE.readString,

                bw: n.Xc.writeString

                    },

            timestamp: {

                n: 3,

                br: n.FE.readUint64String,

                bw: n.Xc.writeUint64String

               }

            }

        }),

    l.sm\_m

    }

那么又显而易见了,按照 JS 代码中的字段与类型进行定义即可:

代码语言:protobuf
复制
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 = CAuthenticationGetPasswordRsaPublicKeyResponse.FromString(response.content)

    print(response)





if \_\_name\_\_ == '\_\_main\_\_':

    get\_rsa\_public\_key("a123456789")

"""

OUTPUT:

publickey\_mod: "a2fdc8f523c87c6c27e904c89c91ecb56c1199dfcfa2c0fc34c4977c3582aa0f49a3f8fe33cffbd780cc71cfc61d3b7a6f98efc8a14d21174792ef47a8e0b8a6a21c35271ebe384196e60d5d26f010e2539db9b8112873e2bfd08fe73d27f0f15457028ad5da27db4fffb4e17702191f1a7d7f96e60d172835333fea40daf707b38e2030f143b518173453bb5c9e9bf1cbe946e2b4b00d037c9691c2ae9608c4f63263306663f2d8066674d870eb2f142e7c9819416d0499cdc1cc76d47b689ae753648a29cd4d82f6c8f18374ab38c6cb2338652ef5214d620e986e8e7c399e4ef6739485eaccd8cea56d14d61dcd7e8e4f51be82803cea77c7be522e2cfebd"

publickey\_exp: "010001"

timestamp: 127222000000

"""

到这里第一个接口的请求参数与响应信息我们就都搞定了,这里返回了三个参数:publickey\_modpublickey\_exptimestamp,很明显是用于进行 RSA 加密的,那么看下一个接口:

14
14

这个接口为登录接口,会返回账号的登录结果信息。该接口参数只有一个 input\_protobuf\_encoded,那么依旧在老地方下断,根据 t 值来判断接口:

15
15

那么还是一样的操作,找到约定字段的地方进行改写:

代码语言:json
复制
fields: {

    device\_friendly\_name: {

        n: 1,

        br: n.FE.readString,

        bw: n.Xc.writeString

    },

    account\_name: {

        n: 2,

        br: n.FE.readString,

        bw: n.Xc.writeString

    },

    encrypted\_password: {

        n: 3,

        br: n.FE.readString,

        bw: n.Xc.writeString

    },

    encryption\_timestamp: {

        n: 4,

        br: n.FE.readUint64String,

        bw: n.Xc.writeUint64String

    },

    remember\_login: {

        n: 5,

        br: n.FE.readBool,

        bw: n.Xc.writeBool

    },

    platform\_type: {

        n: 6,

        br: n.FE.readEnum,

        bw: n.Xc.writeEnum

    },

    persistence: {

        n: 7,

        d: 1,

        br: n.FE.readEnum,

        bw: n.Xc.writeEnum

    },

    website\_id: {

        n: 8,

        d: "Unknown",

        br: n.FE.readString,

        bw: n.Xc.writeString

    },

    device\_details: {

        n: 9,

        c: u

    },

    guard\_data: {

        n: 10,

        br: n.FE.readString,

        bw: n.Xc.writeString

    },

    language: {

        n: 11,

        br: n.FE.readUint32,

        bw: n.Xc.writeUint32

    },

    qos\_level: {

        n: 12,

        d: 2,

        br: n.FE.readInt32,

        bw: n.Xc.writeInt32

    }

}

这里需要注意的是 device\_details ,可以看到这里这个字段并没有声明类型,这种就属于自定义类型,u 就是它的类型:

16
16

结构定义好后可以继续往下跟,找到传输的数据字段:

17
17

这里密码是被加密过的,加密方法为 h.IC(a, t),这里根据上一个接口的明文规范可以直接推断出为 RSA 加密。publickey\_exppublickey\_mod 为模数与指数,用于生成公钥:

18
18

密码生成后,登录接口 BeginAuthSessionViaCredentials/v1 的参数就解决了。至于响应数据的解析依旧是按上文中的方法,这里就不再赘述。

至此,整个逆向流程就结束了。

结果验证

19
19

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 声明
  • 目标
  • 逆向分析
  • Protocol Buffers
  • 字段定义
  • 响应信息解析
  • 结果验证
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档