如前所述,服务器管理员可以创建具有任意数量自定义的服务器,包括自定义地图和声音。每当玩家加入具有此类自定义设置的服务器时,需要传输自定义设置背后的文件。服务器管理员可以为服务器播放列表中的每个地图创建需要下载的文件列表。
在连接阶段,服务器向客户端发送 HTTP 服务器的 URL,从这里下载必要的文件。对于每个自定义文件,都会创建一个 cURL 请求。为每个请求设置的两个选项引起了我们的兴趣:CURLOPT_HEADERFUNCTION和CURLOPT_WRITEFUNCTION. 前者允许注册为 HTTP 响应中的每个 HTTP 标头调用的回调。后者允许注册每当接收到正文数据时触发的回调。
以下屏幕截图显示了如何设置这些选项:

我们有兴趣了解 Valve 开发人员如何处理传入的 HTTP 标头以及对我们命名为 的函数进行逆向工程CurlHeaderCallback()。
事实证明,它CurlHeaderCallback()只是解析了Content-LengthHTTP 标头并Content-Length相应地在堆上分配了一个未初始化的缓冲区,因为它应该对应于应该下载的文件的大小。
然后CurlWriteCallback()将简单地将接收到的数据写入该缓冲区。
最后,一旦 HTTP 请求完成且不再接收数据,缓冲区将写入磁盘。
我们立即注意到 HTTP 标头解析中的一个缺陷Content-Length:如下面的屏幕截图所示,进行了区分大小写的比较。

区分大小写的Content-Length标题搜索。
这种比较是有缺陷的,因为 HTTP 标头也可以是小写的。这仅适用于 Linux 客户端,因为它们使用 cURL 然后进行比较。在 Windows 上,客户端只假设 Windows API 返回的值是正确的。这会产生相同的错误,因为我们可以只发送一个Content-Length带有小的响应主体的任意标头。
我们使用 Python 脚本设置了一个 HTTP 服务器,并使用了一些 HTTP 标头值。最后,我们想出了一个触发错误的 HTTP 响应:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 1337
content-length: 0
Connection: closed当客户端收到这样一个文件下载的 HTTP 响应时,它会识别第一个Content-Length标头并分配一个大小为 的缓冲区1337。但是,接下来是content-length具有大小的第二个标头0。尽管 CS:GO 代码Content-Length由于其区分大小写的搜索而错过了第二个标头,并且仍然需要1337正文数据字节,但 cURL 使用最后一个标头并立即完成请求。
在 Windows 上,即使响应格式错误,API 也只会返回第一个标头值。CS:GO 代码然后将分配的缓冲区以及缓冲区中包含的所有未初始化的内存内容(包括指针)写入磁盘。
尽管 CS:GO 似乎使用 Windows API 来处理 Windows 上的 HTTP 下载,但完全相同的 HTTP 响应起作用并允许我们在玩家的机器上创建包含未初始化内存内容的任意大小的文件。
然后服务器可以通过CNETMsg_File消息请求这些文件。当客户端收到此消息时,他们会将请求的文件上传到服务器。它的定义如下:
message CNETMsg_File {
optional int32 transfer_id = 1;
optional string file_name = 2;
optional bool is_replay_demo_file = 3;
optional bool deny = 4;
}文件上传后,攻击者控制的服务器可以搜索文件的内容以查找指向engine.dll或堆指针以破坏 ASLR。我们在附录部分详细描述了这一步Breaking ASLR。
为了进一步实现游戏的定制,服务器和客户端交换ConVars,它们本质上是配置选项。
每个 ConVar 都由一个全局对象管理,存储在engine.dll. 以下代码片段显示了此类对象的简化定义,用于解释为什么 ConVars 成为帮助利用 OOB 访问的强大小工具:
struct ConVar {
char *convar_name;
int data_len;
void *convar_data;
int color_value;
};社区服务器可以ConVar在比赛期间更新其值并通过发送CNETMsg_SetConVar消息通知客户端:
message CMsg_CVars {
message CVar {
optional string name = 1;
optional string value = 2;
optional uint32 dictionary_name = 3;
}
repeated .CMsg_CVars.CVar cvars = 1;
}
message CNETMsg_SetConVar {
optional .CMsg_CVars convars = 1;
}这些消息由一个简单的键/值结构组成。当将消息定义与struct ConVar定义进行比较时,假设valueConVar 消息的完全攻击者可控制的字段被复制到客户端的堆中并且指向它的指针存储在对象的convar_value字段中是正确的ConVar。
正如我们之前讨论的,OOB 访问CSVCMsg_SplitScreen发生在指向对象的指针数组中。下面是OOB访问发生的代码的反编译作为提醒:

由于数组和所有ConVars都位于 的.data部分engine.dll,我们可以可靠地设置player_slot参数,使得ptr_to_object指向ConVar我们之前设置的值。这可以说明如下:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。