前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >BattlEye客户端仿真

BattlEye客户端仿真

原创
作者头像
franket
修改2020-09-03 12:58:02
1.6K0
修改2020-09-03 12:58:02
举报
文章被收录于专栏:技术杂记

流行的反作弊BattlEye被现代网络游戏广泛使用,例如Tarkov的Escape,并被许多人视为行业标准的反作弊。在本文中,我将演示过去一年中一直使用的方法,该方法使您无需安装BattlEye即可在线玩任何受BattlEye保护的游戏。

BattlEye初始化

BattlEye在启动时由相应的游戏动态加载,以初始化软件服务(“ BEService”)和内核驱动程序(“ BEDaisy”)。这两个组件对于确保游戏的完整性至关重要,但是到目前为止,最关键的组件是游戏直接与之交互的用户模式库(“ BEClient”)。该模块导出两个功能:GetVer更重要的是Init

初始化程序是游戏会叫什么,但是这个功能以前从未被记录在案,作为人主要集中在BEDaisy或他们的shellcode。在最重要的例程BEClient,包括初始化,保护和虚拟化VMProtect,我们能感谢devirtualise和逆向工程师vtil秘密俱乐部成员灿Boluk,但内部的工作BEClient是这后面的部分话题系列,所以这里是一个快速总结。

初始化及其参数具有以下定义:

代码语言:javascript
复制
// BEClient_x64!Init
__declspec(dllexport)
battleye::instance_status Init(std::uint64_t integration_version,
                               battleye::becl_game_data* game_data,
                               battleye::becl_be_data* client_data);
  
enum instance_status
{
    NONE,
    NOT_INITIALIZED,
    SUCCESSFULLY_INITIALIZED,
    DESTROYING,
    DESTROYED
};

struct becl_game_data
{
    char*         game_version;
    std::uint32_t address;
    std::uint16_t port;

    // FUNCTIONS
    using print_message_t = void(*)(char* message);
    print_message_t print_message;

    using request_restart_t = void(*)(std::uint32_t reason);
    request_restart_t request_restart;

    using send_packet_t = void(*)(void* packet, std::uint32_t length);
    send_packet_t send_packet;

    using disconnect_peer_t = void(*)(std::uint8_t* guid, std::uint32_t guid_length, char* reason);
    disconnect_peer_t disconnect_peer;
};

struct becl_be_data
{
    using exit_t = bool(*)();
    exit_t exit;

    using run_t = void(*)();
    run_t run;

    using command_t = void(*)(char* command);
    command_t command;

    using received_packet_t = void(*)(std::uint8_t* received_packet, std::uint32_t length);
    received_packet_t received_packet;

    using on_receive_auth_ticket_t = void(*)(std::uint8_t* ticket, std::uint32_t length);
    on_receive_auth_ticket_t on_receive_auth_ticket;

    using add_peer_t = void(*)(std::uint8_t* guid, std::uint32_t guid_length);
    add_peer_t add_peer;

    using remove_peer_t = void(*)(std::uint8_t* guid, std::uint32_t guid_length);
    remove_peer_t remove_peer;
};

如图所示,这些是用于游戏和BEClient之间的互操作性的非常简单的容器。becl_game_data由游戏定义,并包含BEClient需要调用的函数(例如send_packet),而becl_be_dataBEClient定义,并且包含游戏初始化后使用的回调(例如receive_packet )。请注意,这两种结构在某些具有特殊功能的游戏中略有不同,例如我们已经破解了最近在Tarkov的Escape中引入的数据包加密。较旧的BattlEye版本(DayZ,Arma等)使用功能指针交换挂钩完全不同的方法来拦截流量通信,因此这些结构不适用。

一个简单的Init实现如下所示:

代码语言:javascript
复制
// BEClient_x64!Init
__declspec(dllexport)
battleye::instance_status Init(std::uint64_t integration_version,
                               battleye::becl_game_data* game_data,
                               battleye::becl_be_data* client_data)
{
    // CACHE RELEVANT FUNCTIONS
    battleye::delegate::o_send_packet    = game_data->send_packet;

    // SETUP CLIENT STRUCTURE
    client_data->exit                   = battleye::delegate::exit;
    client_data->run                    = battleye::delegate::run;
    client_data->command                = battleye::delegate::command;
    client_data->received_packet        = battleye::delegate::received_packet;
    client_data->on_receive_auth_ticket = battleye::delegate::on_receive_auth_ticket;
    client_data->add_peer               = battleye::delegate::add_peer;
    client_data->remove_peer            = battleye::delegate::remove_peer;

    return battleye::instance_status::SUCCESSFULLY_INITIALIZED;
}

这将允许我们的自定义BattlEye客户端接收从游戏服务器的BEServer模块发送的数据包。

封包处理

该功能received_packet是迄今为止游戏中最重要的例程,因为它可以处理来自BattlEye服务器组件的传入数据包。与通信的完整性相比,BattlEye的通信非常简单。在最新版本的BattlEye中,数据包遵循相同的一般结构:

代码语言:javascript
复制
#pragma pack(push, 1)
struct be_fragment
{
    std::uint8_t count;
    std::uint8_t index;
};

struct be_packet_header
{
    std::uint8_t id;
    std::uint8_t sequence;
};

struct be_packet : be_packet_header
{
    union 
    {
        be_fragment fragment;

        // DATA STARTS AT body[1] IF PACKET IS FRAGMENTED
        struct
        {
            std::uint8_t no_fragmentation_flag;
            std::uint8_t body[0];
        };
    };
    inline bool fragmented()
    {
        return this->fragment.count != 0x00;
    }
};
#pragma pack(pop)

所有数据包都有一个标识符和一个序列号(由请求/响应通信和心跳使用)。请求和响应具有分段模式,该模式允许BEServerBEClient0x400字节块(看似任意)发送数据包,而不是发送一个大数据包。

在BattlEye的当前迭代中,以下数据包用于通信:

初始化(00

与游戏服务器建立连接后,此数据包将立即发送到BEClient模块。该数据包仅发送一次,除了数据包ID之外,不包含任何数据00,对此数据包的响应很简单00 05

开始('02')

该数据包在交换“ INIT”数据包之后立即发送,并包含服务器生成的客户端GUID。该数据包的响应只是报头:02 00

请求(04)/响应(05

此类数据包从BEServer发送到BEClient以请求(在极少数情况下,只需传输)数据,BEClient将使用该RESPONSE数据包类型发送回该请求的数据。

第一个请求包含重要信息,例如服务和集成版本,不响应它会使游戏服务器断开连接。之后,请求是特定于游戏的。

心跳(09

BEServer模块使用此类型的数据包来确保未断开连接。它使用顺序索引每30秒发送一次,并且如果客户端未使用相同的数据包进行响应,则客户端将与游戏服务器断开连接。此心跳包只有三个字节长,用于同步的顺序索引是递增的,因此易于仿真。示例心跳可能是:09 01 00,这是传输的第二个心跳(序列从零开始)。

仿真

有了这些知识,就可以仅使用两个专有数据来模拟整个BattlEye的反作弊行为:对请求序列1和2的响应。可以使用诸如Wireshark之​​类的工具拦截这些内容,并根据各自游戏的需要将其重播多次,因为BattlEye使用的数据包加密是静态且无上下文的。

如前所述,模拟INIT数据包只需使用序列号5即可:

代码语言:javascript
复制
case battleye::packet_id::INIT:
{
    auto info_packet = battleye::be_packet{};
    info_packet.id       = battleye::packet_id::INIT;
    info_packet.sequence = 0x05;

    battleye::delegate::o_send_packet(&info_packet, sizeof(info_packet));
    break;
}

START通过响应收到的数据包的标头来模拟数据包:

代码语言:javascript
复制
case battleye::packet_id::START:
{
    battleye::delegate::o_send_packet(received_packet, sizeof(battleye::be_packet_header));
    break;
}

HEARTBEAT通过响应收到的数据包来模拟数据包:

代码语言:javascript
复制
case battleye::packet_id::HEARTBEAT:    
{
    battleye::delegate::o_send_packet(received_packet, length);
    break;
}

REQUEST可以通过重放以前生成的响应来完成对数据包的仿真,可以使用代码钩子或中间人软件将其记录下来。这些数据包是特定于游戏的,某些游戏可能会因不处理特定请求而断开您的连接,但是大多数游戏只需要处理前两个请求,之后仅用数据包头进行答复就足以不被游戏服务器断开连接。重要的是要注意所有REQUEST数据包都将立即以标头响应,以使服务器知道客户端知道该请求。这是BottlEye模仿它们的方式:

代码语言:javascript
复制
case battleye::packet_id::REQUEST:
{
    // IF NOT FRAGMENTED RESPOND IMMEDIATELY, ELSE ONLY RESPOND TO THE LAST FRAGMENT
    const auto respond = 
        !header->fragmented() || 
        (header->fragment.index == header->fragment.count - 1);

    if (!respond)
        return;

    // SEND BACK HEADER
    battleye::delegate::o_send_packet(received_packet, sizeof(battleye::be_packet_header));

    switch (header->sequence)
    {
    case 0x01:
    {
        battleye::delegate::respond(header->sequence,
            {
                // REDACTED BUFFER
            });
        break;
    }
    case 0x02:
    {
        battleye::delegate::respond(header->sequence, 
            {    
                // REDACTED BUFFER
            });
        break;
    }
    default:
        break;
    }
    break;
}

它使用以下帮助函数进行响应:

代码语言:javascript
复制
void battleye::delegate::respond(
    std::uint8_t response_index, 
    std::initializer_list<std::uint8_t> data)
{
    // SETUP RESPONSE PACKET WITH TWO-BYTE HEADER + NO-FRAGMENTATION TOGGLE

    const auto size = sizeof(battleye::be_packet_header) + 
                      sizeof(battleye::be_fragment::count) + 
                      data.size();

    auto packet = std::make_unique<std::uint8_t[]>(size);
    auto packet_buffer = packet.get();

    packet_buffer[0] = (battleye::packet_id::RESPONSE); // PACKET ID
    packet_buffer[1] = (response_index - 1);            // RESPONSE INDEX
    packet_buffer[2] = (0x00);                          // FRAGMENTATION DISABLED


    for (size_t i = 0; i < data.size(); i++)
    {
        packet_buffer[3 + i] = data.begin()[i];
    }

    battleye::delegate::o_send_packet(packet_buffer, size);
}

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

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

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

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

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • BattlEye初始化
  • 封包处理
    • 初始化(00)
      • 开始('02')
        • 请求(04)/响应(05)
          • 心跳(09)
          • 仿真
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档