首页 > 其他分享 >cs1.6 0day rce (一)

cs1.6 0day rce (一)

时间:2024-11-02 14:44:37浏览次数:1  
标签:Controllable int cs1.6 wad 调色板 v10 v5 rce 0day

1.前言

随着时间的推移,厂商开始放弃不安全的编程语言c++,php。很多内存不安全的代码使用rust重写,并且吸取了教训,也越来越少使用sql字符串拼接,动态反序列化用户数据这种不安全的开发方式,显然网安已经是没落了,许多网安公司亏损。没有啥是永恒不变的,ai在将来也会取代很多网安人员,现如今编程技术没啥用,有人问现如今搞网安如何,就这样。在不久的将来ai会取代所有程序员,继续卷已毫无意义,程序员可以转职成厂仔了,最没用的就是技术,总之没啥意思,下面进入主题。

2.介绍

cs1.6我发现这些bug:

  1. 越界读取导致拒绝服务。
  2. 内存申请失败导致拒绝服务。
  3. 两个整数溢出,其中一个会在内存池发生越界写入,可能导致远程代码执行(暂时没搞出利用)。

上面这几个bug都可通过发送网络数据包远程触发,客户端、服务端(房主)均可触发,不需要用户交互。这个游戏在20年前很流行,但是严重缺乏代码审计,可以想象那时候的安全性有多糟糕,如果此问题在20年前被发现将成为疯狂的抓鸡工具。此外使用金源引擎开发的游戏也可能受影响。

3.修复状态

该问题在8月19号提交给h1平台,感觉处理还是比较缓慢,搞src的读者应该都知道,该程序没有赏金,目前h1平台已确认问题并提交给valve,暂时没有看到valve官方的回应,所以此漏洞任处于未修复状态,此漏洞影响steam最新构建版本Exe build:12:23:38 dec 22 2023(9920)。

机翻的。。。

4.影响

老游戏了,玩的人比较少,公网服务器数量如下:

对战平台玩家数量如下:

5.过程

一开始想查找开源软件的bug,但是现如今很多开源软件经过大量自动化测试和代码审计,感觉如果查找开源软件的bug很困难。于是转向了闭源软件,在0几年的时候发现了不少的http,ftp服务器相关的二进制漏洞,但没有看到网络游戏相关报告,我感觉游戏的网络协议比http,ftp要复杂一点,说明很少人关注这类程序,所以想找找老游戏的bug。src里看到cs老版本似乎bug报告较少,如下图:

可以发现多数游戏都是客户端的漏洞,比如需要加载地图才能触发漏洞。这样需要搭建一个服务器(自建房间),然后诱导用户加入房间触发漏洞。我想寻找一个不用用户交互的漏洞,也就是服务端的漏洞。我们看一下cs1.6的服务端是如何解析客户端数据的:

程序目录下hlds.exe这个是服务器软件,在虚拟机上运行这个软件,当然也可以运行cs1.6来创建房间,但是在虚拟机运行不了。然后运行游戏进入房间我们就能看到如上图的数据包发送给服务器。可以看到这是字符串,其中有\用来字符串结尾,还有0x00用作字符串结尾。可以看到有格式name这种长度固定的字符串,我尝试了一下发送超长字符串数据包,看一下是否会出现经典缓冲区溢出,但是没有崩溃...看来得继续尝试。询问一下ai这个字符串数据包是啥意思:

可以看到原来诸如bottomcolor\6这些是一些设置,这看起来像是控制台命令。

如果经常玩这个游戏的可能知道点击`按钮会弹出控制台窗口,可以进行一些设置。没想到数据包也会发送这些控制台命令,听到命令我想到了命令注入,会不会这些命令会有bug,于是在网上搜索了一下有关cs更多的命令:

这里有一个参数cl_allowupload 1感觉有点奇怪,运行用户上传自己的logo。这个logo应该是图片吧,但是玩了这么多年cs没有看到会显示玩家的logo。设置中也没有可以设置logo的选项,通过网上搜索原来这个logo指的是玩家喷漆,在游戏中可以按下t键进行喷漆:

按下t键就会显示喷漆,而且喷漆可以自定义,如下图:

出自【反恐精英】CS1.6 自定义喷涂教程 搞起来 #steam游戏 #电脑技巧 - 抖音

那么如何自定义喷漆呢?这个搜索一下就会有玩家在网上提供下载,是一个名叫tempdecal.wad文件,通过覆盖目录valve_schinese文件夹的tempdecal.wad,就会显示自己的自定义喷漆,也有软件可以把图片转换成wad格式的。但是自定义喷漆其它玩家会显示吗?答案是的。我们按下t键,其它玩家也会显示我们自定义的喷漆。也就是说tempdecal.wad会发送给其他玩家,由于cs会压缩网络数据包,我们抓包看不到wad文件的发送。所以我们可以在用x64dbg附加到hlds.exe上,给解析wad文件的函数下断点,解析wad文件的代码在idapro中swds.dll的sub_1D36CE0函数。客户端连接服务端,在x64dbg中给sub_1D36CE0函数下断点:

这是我们自定义的wad文件

可以看到在解析wad文件的函数断下来了,在内存中显示了我们的自定义wad文件,所以不仅客户端会解析wad文件,游戏服务器也会解析客户端的wad文件。并且经过测试发现cl_allowupload和sv_send_logos这些命令即使在服务器上设置参数为0,也就是禁止玩家上传喷漆,客户端依旧会发送自定义喷漆给服务器并且服务器也会解析,只是其他玩家无法显示发送的自定义喷漆而已,所以服务器通过命令关闭自定义喷漆依旧无法缓解漏洞。

6.wad文件格式

更多有关wad文件格式的细节,可以查看网址:https://www.bilibili.com/read/cv4682202/,作者是m明天灬过后。

感觉wad文件是一种3d模型文件,那么这种文件格式可能比较复杂(其实不太复杂),可能存在漏洞,上次我们分析过webp文件。我们看看作者的说明:

   首先是color palette调色板纹理,编号0x40,这种纹理只出现在tempdecal.wad中,tempdecal.wad也就是半条命/CS1.6的喷图文件。这个wad中只有一张纹理,对应了喷图,比如这就是一个tempdecal.wad中的喷图。需要注意,纹理的像素数量是有限制的(最多12288个像素),且长宽必须是16的倍数,超过后喷图无法上传到服务器,会变成一个小的lambda图标,此外,如果服务器禁止了上传喷图,或者你删除了tempdecal.wad,喷图也会变成这个图。

 

也就是上传不了太大的wad文件给服务器。还有就是可以让地图文件包含wad文件。

可以看到wad文件格式和图片格式还是很像的,首先是头部占用12个字节,前4个字节是wad3字符串标识,4-8个字节是纹理数量,8-12个字节是一个指针,指向了LumpInfo 的位置,LumpInfo必需在文件最末尾的32个字节处。

不想写了。照搬作者的解释:

lumpDataOffset 块数据偏移量:指块数据距离文件开头多少个字节。

 

compressedSize 压缩后大小:大部分文章说这是块数据压缩后的大小,但我没有找到关于wad压缩的资料…但它一般都是0,即无压缩,所以不影响解析。

 

size 原大小:块数据的原始大小。

 

type 纹理类型:即第四章中提到的四种纹理类型,分别是0x40(color palatte), 0x42(qpic), 0x43(miptex)和0x46(font)

 

cType 压缩类型:和compressedSize 压缩后大小对应,未找到相关资料。大部分软件的处理方法是无压缩,compressType为0,compressedSize等于size。

 

padding 占位:只用于占位,用于字节对齐。

 

textureName纹理名称:ASCII编码字符串。纹理名称必须以\0符号结束,这意味着纹理名称最长只有15个字符。

 

这里纹理类型似乎程序喷漆文件会忽略type ,只会按照0x43(miptex)类型处理,我没太注意看代码。

这是miptex类型的文件格式。

texName 纹理名称:和块信息里的纹理名称一致。

 

texWidth 纹理宽度:宽度以像素为单位。

 

texHeight 纹理宽度:宽度以像素为单位。

 

offset x4 偏移量:四个偏移量数据,每个占用四字节,无符号整型。分别对应四层mipmap的纹理数据距离文件开头多少个字节。

 

data x4 纹理数据:四部分纹理数据,对应四层mipmap的纹理。如果你还记得我们提到的索引颜色方法的话,应该还记得一个颜色只用一个字节表示。因此第一层data占width*height字节,第二层data占width*height/4,第三层data占width*height/16,第四层data占width*height/64。

 

usedColorNum 调色板颜色数量:字面意思,但不确定具体有什么作用,调色板的大小是固定256个颜色,因此无论颜色多与少占用的空间不变,因此这个字段的存在好像并没有意义。

 

palatte 调色板:wad以8位RGB存储颜色,因此一个颜色需要三个字节。调色板大小为256,因此调色板占用256*3字节。

 

padding 占位:占用两字节。只用于占位,用于字节对齐。 

 

        tempdecal.wad喷图文件中,调色板前255位都是空,最后一位代表喷图颜色。在使用tempdecal.wad时,会对调色板从纯黑(0x000000)到最后一位颜色进行插值。

 

 (老版的pldecal.wad里,调色板前255位不是空,而是从纯黑(0x000000)到纯白(0xffffff)的插值。

 miptex (0x43)
        接下来是Miptex(Mipmap texture),mipmap纹理,编号0x43,是最常用的纹理类型,我们制作地图使用的纹理都是miptex,并且VHE也不支持其他3种格式。那么名称中的Mipmap是什么意思呢?

       简单来说,Mipmap是图形学中的一种贴图渲染技术,可以用来加快渲染速度,并且可以减轻图像混叠问题。比如这里有一张棋盘格纹理: 

把它平铺到平面上,然后从侧面看过去,此时远处的纹理会出现因为降采样导致的摩尔纹(moiré pattern),特别是在运动的时候,摩尔纹会更加明显(注意远处形成的规则的曲线,随着运动曲线也在扭曲),这便是图像混叠的问题之一。

                                    摩尔纹 

    而mipmap的做法是,对一张贴图,事先压缩成一组不同大小的贴图,在距离近时选择大的贴图渲染,在距离远时选择小的贴图渲染,这样既能够省去远处采样大贴图时的额外损耗,也因为不会大幅度降采样,摩尔纹也得到缓解。

                                                            mipmap使用

                                                       有无mipmap对比

 一般mipmap有8层,即一组共八张贴图。如一张长宽256像素的贴图,则有长宽128像素,长宽64像素, 长宽32像素,依次往下,一共八张贴图。金源引擎使用的mipmap层级为4,即一组mipmap有四张贴图。且金源引擎额外要求mipmap纹理的长宽必须是16的整数倍。 

Bonus 调色板
        以上四种纹理都使用了调色板。当然这不是美术上的调色板,在计算机层面,调色板是指一组颜色列表。调色板一般对应索引颜色方法,相比于传统的依次存储每个像素的颜色,索引颜色方法存储了一组索引值和一个调色板。首先准备一个调色板,这个调色板存储了图像里出现的所有颜色,而原本存储像素颜色的位置,变成存储调色板内的索引,也就是颜色的编号。 

 wad使用的颜色为24位RGB,以及8位调色板。8位调色板意味着图像最多包含256种颜色,因此如果颜色多于256种,多的颜色只能在调色板内找一个颜色相近的替代。这就导致图像信息损失,质量下降,但是使用索引颜色方法可以对图像进行压缩,减少图像文件大小。

        假如一张长宽256像素,24位RGB存储的图像,以传统方式存储,每个颜色需要3字节,总共256x256个颜色,因此需要192KB来存储,而如果使用8位调色板的索引颜色方法,调色板需要存储256个8位RGB值,总计6KB,此外每个索引占一个字节,每个像素对应了一个索引,共有256*256个像素,总计64KB,图像索引和调色板一共只占70KB,压缩了空间。 

7.   代码阅读

首先用idapro打开swds.dll,解析wad文件头部的是sub_1D36CE0函数。

我把它命名为parse_wad3_header函数,其中arglist是可控参数,指向了wad文件数据。

首先代码这里if ( *ArgList == 860111191 )整数860111191 就是字符串”wad3”,如果头部不是wad3就return 0退出解码。然后int v4就是纹理数量,这里只能为1,只允许一个纹理。V5的值是lumpinfo第一个字节的位置,可以看到代码if ( v5 + 32 == a4 )这里的a4就是调用ftell函数获取的文件总大小,因为lumpinfo大小是32个字节,所以需要校验v5的值是否真的指向了文件末尾32个字节处,验证文件格式是否正确,否则退出解码。sub_1D51DD0函数就是只调用了malloc函数,然后拷贝lumpinfo给v7,这里的alignment_memcpy函数主要是调用memcpy,它会判断数据是否字节对其,不对其的话就对数据进行对其,毕竟这是20多年前的程序,那时候的电脑性能很差,所以会有很多的优化。这里的代码if ( *(_DWORD *)(v8 + 8) != v9 )就是比较compressedSize 和size是否相等,不相等退出解码。可以看到确实喷漆类型的wad文件格式不支持压缩,一开始我还想着compressedSize 值大于size值是否会溢出来着。然后代码还会继续进行校验,我们不看了。

int __cdecl parse_data(int ArgList, _DWORD *a2, int Controllable_parameters, int a4, int a5)
{
  int v5; // eax
  int *v6; // edi
  int *recv_data; // ebx
  int v8; // ecx
  int v9; // eax
  void (__cdecl *v10)(_DWORD *, int); // eax
  signed int v12; // [esp-8h] [ebp-10h]

  if ( (int)sub_1D2B990(ArgList) >= 5 )
  {
    v5 = sub_1D2BB40(ArgList + 3);
    if ( v5 < 0 || v5 >= a2[5] )
      return 0;
  }
  else
  {
    v5 = 0;
  }
  v6 = (int *)(a2[4] + 32 * v5);
  recv_data = (int *)newalloc((_DWORD *)(a5 + 64), a2[6] + v6[2] + 1, ArgList);// v6[2]  lumpsize
  if ( !recv_data )
    cs_erro(aDrawCachegetNo_0, ArgList);
  v8 = a2[6];                                   // a2[6]=24
  v12 = v6[2];                                  // v6[2]=lumpsize
  v9 = *v6;                                     // *v6=lumppointer
  *((_BYTE *)recv_data + v8 + v12) = 0;
  alignment_memcpy((unsigned int)recv_data + v8, Controllable_parameters + v9, v12);
  if ( !check_wad3_data(a2, (int)recv_data, (int)v6) )
    return 0;
  dword_2068F9C = 1;
  sub_1D2B930((int)&unk_26643A0, (int)aT);
  alignment_memcpy((unsigned int)&unk_26643A1, ArgList, 5);
  v10 = (void (__cdecl *)(_DWORD *, int))a2[7];
  byte_26643A6 = 0;
  if ( v10 )
    v10(a2, (int)recv_data);                    // parse_miptex    
  dword_2068F9C = 0;
  return 1;
}

调用完parse_wad3_header函数之后会调用parse_data函数,其中Controllable_parameters是可控参数,这里Controllable_parameters是个指针,idapro反编译的代码有些不正确,Controllable_parameters指向了wad文件的第12个字节处。这里指针v6的偏移4字节处存放了lumpsize,这里的newalloc函数将返回内存池中的一块未被使用的内存,这里的v6p[2]是lumpsize,也就是申请的内存大小,有也就是申请多少内存用户是可控的,如果是值小于0或者一个很大的值将会导致崩溃,这是一个漏洞,后面会更详细讲这个内存池。也就是说将recv_data分配内存池空间。然后将Controllable_parameters拷贝给recv_data,变量v9是lumpDataOffset,也是可控参数,那么可以构造一个大于文件总大小的大值导致越界读取。但是parse_wad3_header函数中的代码if ( v9 + *(_DWORD *)v8 > v5 )会校验lumpDataOffset+lumpsize是否大于lumpInfoOffset,那么光是一个lumpDataOffset大值是无法越界读取的。所以也可以构造一个lumpsize大值,比如0x186A0,而lumpInfoOffset的值是0xfffe7961,相加之后的值将会导致整数溢出,因为四个字节的整数最大范围是0xffffffff,0x186A0和0xfffe7961相加的会大于0xffffffff。会得到一个小值1,这将通过if ( v9 + *(_DWORD *)v8 > v5 )校验。然而lumpInfoOffset的值还是很大。会越界读取到没有分配内存页的空间导致崩溃。我们修改tempdecal.wad文件的值,然后将其复制到cstrike_schinese,然后加载游戏。

果然导致崩溃了。

但是这些都不是我想要的,我想可以rce。所以我们继续分析代码,因为Controllable_parameters拷贝给recv_data,所以recv_data是可控参数。然后调用check_wad3_data函数,传递了3个参数a2, recv_data, v6。

int __cdecl check_wad3_data(_DWORD *a1, int Controllable_parameters, int a3)
{
  int v4; // esi
  int v5; // eax
  int v6; // eax
  int v7; // edi
  unsigned int v8; // esi
  int i; // ecx
  int v10[16]; // [esp+Ch] [ebp-68h] BYREF
  int v11[10]; // [esp+4Ch] [ebp-28h] BYREF

  if ( a1[6] == 24 )
  {
    qmemcpy(v11, (const void *)(Controllable_parameters + 24), sizeof(v11));
    qmemcpy(v10, (const void *)Controllable_parameters, sizeof(v10));
    alignment_memcpy((unsigned int)v10, (unsigned int)v11, 16);
    v10[4] = retn_value(v11[4]);
    v4 = 0;
    v10[5] = retn_value(v11[5]);
    memset(&v10[6], 0, 20);
    do
    {
      v5 = retn_value(v11[v4 + 6]);
      v10[++v4 + 10] = a1[6] + v5;
    }
    while ( v4 < 4 );
    v6 = v10[4] * v10[5];
    v7 = ((v10[4] * v10[5]) >> 4) + ((v10[4] * v10[5]) >> 6) + v10[4] * v10[5] + ((v10[4] * v10[5]) >> 2);
    v8 = *(unsigned __int16 *)(v7 + Controllable_parameters + 24 + 40);
    if ( v10[4] && v10[5] && v10[4] <= 0x100u && v10[5] <= 0x100u )
    {
      for ( i = 0; i < 3; ++i )
      {
        if ( v6 + v11[i + 6] != v11[i + 7] )
        {
          console_error(aDrawValidatecu_1, *a1);
          return 0;
        }
        v6 >>= 2;
      }
      if ( v8 > 0x100 )
      {
        console_error(aDrawValidatecu_2, v8);
        return 0;
      }
      else if ( 3 * v8 + retn_value(v11[6]) + v7 + 2 <= *(_DWORD *)(a3 + 4) )
      {
        return 1;
      }
      else
      {
        console_error(aDrawValidatecu_0, *a1);
        return 0;
      }
    }
    else
    {
      console_error(aDrawValidatecu_3, *a1);
      return 0;
    }
  }
  else
  {
    console_error(aDrawValidatecu, *a1);
    return 0;
  }
}

我们再解释一下调色板,调色板在图片格式中如gif,bmp中也有使用。调色板就是保存常见的rgb值,因为一个rgb值占用3个字节,为了节省图片的大小,图片中本来应该存放rgb值,如果使用了调色板图片中只有占用一个字节的索引值,要显示图片的时候在根据这个索引值取出调色板占用3个字节rgb值,相当于压缩,这当然会导致画质的损失。在wad文件中并不直接存放调色板,而是在解析的时候根据声明的调色板大小将调色板存放在mipmap后面。v10[4]和v10[5]就是高度和宽度,v10[4] v10[5]相乘的像素值总共大小也就是v6。代码这里v7 = ((v10[4] * v10[5]) >> 4) + ((v10[4] * v10[5]) >> 6) + v10[4] * v10[5] + ((v10[4] * v10[5]) >> 2);这里为啥这样写,其实这里是mipmap总共大小,这个在文件格式篇章有介绍,我们再解释一下mipmap,mimap可以说是一张图片文件中包含多个不同分辨率的图片,因为在3d游戏显示一张图片(纹理)如果其分辨率越高,那么对性能消耗越大。解决办法是如果角色镜头离图片越远显示的分辨率越低,因为离镜头远了细节看的不会很清楚,所以降低分辨率画质损失不是很大,如果图片离镜头近了则切换为高分辨率图片。这里代码分辨率右移2位就是除以4,右移4位就是除以16,以此类推,这与文件格式说明相对应,可以猜测金源引擎生成的wad文件分辨率得是2的n次方,不然不会用位移运算替代除法运算。所以这里mipmap的数量是固定的,也就是有四张不同分辨率的图片。if ( v10[4] && v10[5] && v10[4] <= 0x100u && v10[5] <= 0x100u )是有检查的,但是发生检查之前,v7 = ((v10[4] * v10[5]) >> 4) + ((v10[4] * v10[5]) >> 6) + v10[4] * v10[5] + ((v10[4] * v10[5]) >> 2);这里是高度乘以宽度也就是分辨率,在把分辨率右移6到2相得到mipmap总大小,看文件格式说明我们知道调色板就是在mipmap后面,所以代码这样写取出调色板大小。很显然这里的代码没有检查,高度和宽度都是我们可以控制的值,这导致越界读取。

再到v8取出调色板大小。

else if ( 3 * v8 + retn_value(v11[6]) + v7 + 2 <= *(_DWORD *)(a3 + 4) )这里代码3*调色板大小,再和v7和可控参数的四个字节变量offset1相加,*(_DWORD *)(a3 + 4) )这里是lumpsize。显然这里又是一次校验是否会缓冲区溢出,注意一下这里的代码。我们分析下一个函数。结束完check_wad3_data后会调用函数指针v10,传递了两个参数a2,recv_data。v10所指向的函数是parse_miptex。

8.整数溢出导致基于越界写入的缓冲区溢出

char __cdecl parse_miptex(int a1, unsigned int *Controllable_parameters)
{
  int v3; // esi
  unsigned int *v4; // edi
  int v5; // eax
  signed int v6; // eax
  int v7; // eax
  unsigned int v8; // esi
  int v9; // edi
  char *v10; // eax
  int v11; // ecx
  char v12; // dl
  int v13; // ecx
  char v14; // dl
  int v15; // ecx
  int v17[10]; // [esp+Ch] [ebp-28h] BYREF
  char *Controllable_parametersa; // [esp+40h] [ebp+Ch]

  if ( *(_DWORD *)(a1 + 24) != 24 )
    cs_erro("Draw_MiptexTexture: Bad cached wad %s\n", *(const char **)a1);
  Controllable_parametersa = (char *)Controllable_parameters + *(_DWORD *)(a1 + 24);
  qmemcpy(v17, Controllable_parametersa, sizeof(v17));
  alignment_memcpy((unsigned int)Controllable_parameters, (unsigned int)v17, 16);
  Controllable_parameters[4] = retn_value(v17[4]);
  v3 = 0;
  Controllable_parameters[5] = retn_value(v17[5]);
  Controllable_parameters[8] = 0;
  Controllable_parameters[7] = 0;
  Controllable_parameters[6] = 0;
  Controllable_parameters[10] = 0;
  Controllable_parameters[9] = 0;
  v4 = Controllable_parameters + 11;
  do
  {
    v5 = retn_value(v17[v3 + 6]);
    ++v4;
    ++v3;
    *(v4 - 1) = *(_DWORD *)(a1 + 24) + v5;
  }
  while ( v3 < 4 );
  HIWORD(v9) = 0;
  v6 = Controllable_parameters[4] * Controllable_parameters[5];
  v7 = v6 + (v6 >> 2) + (v6 >> 4) + (v6 >> 6);
  v8 = Controllable_parameters[11] + v7 + 2;    // Controllable_parameters[11]=0x40
  Controllable_parameters[15] = v8;
  LOWORD(v9) = *(_WORD *)&Controllable_parametersa[v7 + 40];
  if ( dword_2068F9C )
  {
    sub_1D2B960(Controllable_parameters, &unk_26643A0, 15);
    *((_BYTE *)Controllable_parameters + 15) = 0;
  }
  LOBYTE(v10) = *((_BYTE *)Controllable_parameters + v8 + 765);
  if ( (_BYTE)v10
    || (LOBYTE(v10) = *((_BYTE *)Controllable_parameters + v8 + 766), (_BYTE)v10)
    || *((_BYTE *)Controllable_parameters + v8 + 767) != 0xFF )
  {
    *(_BYTE *)Controllable_parameters = 125;
  }
  else
  {
    *(_BYTE *)Controllable_parameters = 123;
  }
  if ( v9 > 0 )
  {
    v10 = (char *)Controllable_parameters + v8 + 2;
    do
    {
      v11 = (unsigned __int8)*(v10 - 2);
      v10 += 3;
      v12 = byte_216A000[v11];
      v13 = (unsigned __int8)*(v10 - 4);
      *(v10 - 5) = v12;
      v14 = byte_216A000[v13];
      v15 = (unsigned __int8)*(v10 - 3);
      *(v10 - 4) = v14;
      --v9;
      *(v10 - 3) = byte_216A000[v15];
    }
    while ( v9 );
  }
  return (char)v10;
}

这里的逻辑和check_wad3_data很像,但是check_wad3_data是校验文件格式是否正确。parse_miptex多了赋值。   其中代码:do

    {

      v11 = (unsigned __int8)*(v10 - 2);

      v10 += 3;

      v12 = byte_216A000[v11];

      v13 = (unsigned __int8)*(v10 - 4);

      *(v10 - 5) = v12;

      v14 = byte_216A000[v13];

      v15 = (unsigned __int8)*(v10 - 3);

      *(v10 - 4) = v14;

      --v9;

      *(v10 - 3) = byte_216A000[v15];

    }

while ( v9 );

这里的代码开始根据索引值取出调色板占用3个字节的rgb值。可以看到v10=+3每取出一个rgb值指针加3。全局变量byte_216A000就是保存了调色板的值,三次取出byte_216A000就是取出rgb。我们看一下指针v10的值和整数v9的值是怎么来的。首先v10 = (char *)Controllable_parameters + v8 + 2;Controllable_parameters 就是指向了我们自定义wad文件的缓冲区,然后 v8 = Controllable_parameters[11] + v7 + 2; v7的值就是上面我们讲的调色板大小索引。V9的值就是调色板大小,由于check_wad3_data函数里的 if ( v8 > 0x100 ),v8是调色板大小,所以这里V9的同样不能够大于0x100(255)。这时候我们就能够意识到有问题了,因为调色板的rgb值也是存放在我们自定义wad文件缓冲区,而且这个缓冲区的大小我们是可以控制的(lumpsize),这样的话我们让v9的值达到限制最大大小255,255*3=765个字节,然后声明lumpsize的小于765个字节,这就超出内存池分配的大小写入数据。第一次尝试的时候发现并没有崩溃。因为在check_wad3_data函数中if( 3 * v8 + retn_value(v11[6]) + v7 + 2 <= *(_DWORD *)(a3 + 4) )这里会校验要写入的调色板大小是否将大于lumpsize,但是这里变量v7是offset1,该参数是可控的,可以声明v7的值为0xfffffd00,这将导致整数溢出通过if( 3 * v8 + retn_value(v11[6]) + v7 + 2 <= *(_DWORD *)(a3 + 4) )校验,很好,这时候我们可以越界写入了。构造好tempdecal.wad,将x64dbg附加到启动的游戏hl.exe,不用下断点,触发异常x64dbg会中断的。我们观察崩溃:

因为异常导致断下来了,mov dword ptr ds:[eax+0x50],ecx,这里[eax+0x50]是一个内存地址,越界写入了。可以看看eax所指向的地址的属性:

该内存页没有任何属性,所以导致崩溃。继续运行,出现错误框:

下图是服务器崩溃演示,服务器没有出现错误框,但是x64dbg异常断下来了。

9.利用的可能性

出于性能考虑,存放wad文件使用内存池而没有使用malloc。tempdecal.wad将分配到内存池空间。为什么只有崩溃的演示,而没有我声称的rce,因为利用是确实需要时间。因为cs是老游戏,所以没有aslr,stackcookie等安全机制。如果读者没有了解过malloc原理,可能对内存池的代码理解会比较困难,建议没了解过malloc的读者看看malloc的原理。我们分析一下内存池布局看看有哪些可能可以rce:

内存池拿一块全局变量区来使用,地址是0x10426000,大小是0x02801000。代码实现如下:

int __cdecl newalloc(_DWORD *a1, int ArgList, int a3)
{
  int *v3; // esi
  char v5; // [esp+0h] [ebp-Ch]

  if ( *a1 )
    cs_erro(aCacheAllocAlre, v5);
  if ( ArgList <= 0 )
    cs_erro(aCacheAllocSize, ArgList);
  while ( 1 )
  {
    v3 = alloc_pool((ArgList + 103) & 0xFFFFFFF0, 0);
    if ( v3 )
      break;
    if ( (_UNKNOWN *)dword_2168BD0 == &unk_2168B80 )
      cs_erro(aCacheAllocOutO, v5);
    sub_1DBA3F0(*(_DWORD *)(dword_2168BD0 + 4));
  }
  sub_1D2B960(v3 + 2, a3, 63);
  *((_BYTE *)v3 + 71) = 0;
  *a1 = v3 + 22;
  v3[1] = (int)a1;
  return sub_1DBA460(a1);
}

从代码中可以看到主要调用alloc_pool,进alloc_pool看看:

int *__cdecl alloc_pool(int ArgList, int a2)
{
  int *v2; // esi
  int *v4; // esi
  int v5; // edi
  int v6; // eax

  if ( a2 || (_UNKNOWN *)dword_2168BC8 != &unk_2168B80 )
  {
    v4 = (int *)(dword_2168B5C + dword_2168B68);
    v5 = dword_2168BCC;
    do
    {
      if ( (!a2 || v5 != dword_2168BCC) && v5 - (int)v4 >= ArgList )
      {
        sub_1D2B830(v4, 0, 88);
        *v4 = ArgList;
        v4[19] = v5;
        v4[18] = *(_DWORD *)(v5 + 72);
        *(_DWORD *)(*(_DWORD *)(v5 + 72) + 76) = v4;
        *(_DWORD *)(v5 + 72) = v4;
        sub_1DB9F90(v4);
        return v4;
      }
      v4 = (int *)(v5 + *(_DWORD *)v5);
      v5 = *(_DWORD *)(v5 + 76);
    }
    while ( (_UNKNOWN *)v5 != &unk_2168B80 );
    if ( dword_2168B6C + dword_2168B5C - dword_2168B70 - (int)v4 < ArgList )
    {
      return 0;
    }
    else
    {
      sub_1D2B830(v4, 0, 88);
      v6 = dword_2168BC8;
      *v4 = ArgList;
      v4[19] = (int)&unk_2168B80;
      v4[18] = v6;
      *(_DWORD *)(v6 + 76) = v4;
      dword_2168BC8 = (int)v4;
      sub_1DB9F90(v4);
      return v4;
    }
  }
  else
  {
    if ( dword_2168B6C - dword_2168B68 - dword_2168B70 < ArgList )
      cs_erro(aCacheTryallocI, ArgList);
    v2 = (int *)(dword_2168B5C + dword_2168B68);
    sub_1D2B830(dword_2168B5C + dword_2168B68, 0, 88);
    *v2 = ArgList;
    dword_2168BCC = (int)v2;
    dword_2168BC8 = (int)v2;
    v2[19] = (int)&unk_2168B80;
    v2[18] = (int)&unk_2168B80;
    sub_1DB9F90(v2);
    return v2;
  }
}

中参数arglist就是我们需要分配内存的大小,可以看到代码中有很很多全局变量地址,例如dword_2168B5C,我们需要动态调试来看看这全局变量地址存放着什么:

从dword_2168B5C取出的值是0x10426020,内存池的开始地址是0x10426000,只相差0x20个字节,所以dword_2168B5C存放的值是内存池底部区域,而dword_2168B68的值是0x00BBA798,代码把它和0x10426020相加得到一个新的指针,所以dword_2168B68存放的值应该是一个大小,和0x10426020相加的值指向一块空闲内存区域。经过相加v4的值是0x10FE07B8。所以可以猜测一块内存被释放可能会更新这些全局变量的值,以便指向一块空闲内存。

dword_2168BCC存放的值是0x10FE96B0,值和v4很相近,将0x10FE96B0赋值给v5。0x10FE96B0就是0x10FE07B8顶部区域,也就是0x10FE07B8加上0x10FE07B8的内存大小就是0x10FE96B0。代码if ( (!a2 || v5 != dword_2168BCC) && v5 - (int)v4 >= ArgList )正好跟这个逻辑相对于,v5-v4就是空闲内存池的大小。如果空闲内存池的大小等于或大于ArgList,就返回v4。如果该空闲的内存池大小小于需要分配的内存,则执行v4 = (int *)(v5 + *(_DWORD *)v5); v5 = *(_DWORD *)(v5 + 76);这个代码,寻找下一个空闲的内存,v5的地址就是下个空闲的内存池,代码v5和v5的索引相加,所以v5的首地址应该存放距离下一个空闲内存池的大小,因为v5不一定是空闲内存,所以不能直接v5赋值给v4。然后空闲内存偏移76个字节处就是空闲内存的顶部的地址。while ( (_UNKNOWN *)v5 != &unk_2168B80 )这里代码会一直遍历空闲内存直到找到一块大小合适的内存,unk_2168B80这里存放的是0x10426000顶部区域的地址,如果v5的值到达顶部区域则结束循环。到if ( (!a2 || v5 != dword_2168BCC) && v5 - (int)v4 >= ArgList )内的代码会对前v4前几十个字节赋值,这和malloc的chunk类似,然后返回v4。这和malloc函数空闲双向链表很像,只不过内存池指向下一块空闲内存是偏移大小,malloc是指针。看到这里利用的思路是第一次溢出的时候先覆盖一块内存池的chunk,特别是覆盖首地址的成员,也就是(v5 + *(_DWORD *)v5)这里的*v5,让*v5的值很大,以至于和指针相加后的值指向栈区域,如果栈的地址低于内存池,则让其整数溢出指向栈区域。可以声明顶部地址比如值较小,这可以让其它数据分配不到这块内存已增加漏洞利用稳定性。然后断开连接再次发送tempdecal.wad,第二次发送tempdecal.wad的lumpsize和修改的空闲内存池大小相对应。但是返回的内存可能不是修改的空闲内存池,因为可能会返回一块更大的内存,如果第二次发送没有成功那么断开连接多次发送,这样我们可以获得任意地址写,让其覆盖栈区域的函数返回地址构建rop利用链,这里的rop可以选择一个没经过更新的dll,以应对不同版本的cs。可以rop调用CreateProcess或者VirtualAlloc来getshell。当然大概是这样,实际应该会遇到一些问题。

内存池还有更多的代码先介绍到这里。还有如果针对游戏服务器软件利用的话还需要考虑对方是否是windows或linux版本,因为windows和linux版本的api和地址偏移不一样。这可以让nmap扫描来探测对方操作系统。下一篇文章我可能会描述如何具体利用。

10.总结

为何这个漏洞20多年没有被发现,要发现这些漏洞并不难。说明那时候安全相关的程序员太少了,那时候一些安全圈子的名人可能也没有宣传的那么厉害。只不过到现在安全也凉了,不用纠结太多。下一篇文章继续分析。

标签:Controllable,int,cs1.6,wad,调色板,v10,v5,rce,0day
From: https://www.cnblogs.com/Binarysecurity/p/18521760

相关文章

  • Codeforces Round 983 (Div. 2)
    A最坏的情况就是所有开着的开关尽可能配对最好的情况就是所有开着的开关尽可能不配对#include<bits/stdc++.h>usingnamespacestd;typedeflonglongll;typedefpair<int,int>PII;constintN=1e6+10;constintmod=998244353;constintINF=0x3f3f3f3f;constllI......
  • Codeforces Round 983 div2 个人题解(A~D)
    CodeforcesRound983div2个人题解(A~D)Dashboard-CodeforcesRound983(Div.2)-Codeforces火车头#define_CRT_SECURE_NO_WARNINGS1#include<algorithm>#include<array>#include<bitset>#include<cassert>#include<cmath>#in......
  • CodeForces Dora and C++ (2007C) 题解
    CodeForcesDoraandC++(2007C)题解题意给一个数组\(c_{1...n}\),定义数组的\(range\)是最大值减最小值,即极差。给出两个值\(a\)和\(b\),每步操作中,可给数组中任一一个数增大\(a\)或增大\(b\)。问任意步操作后(可以是\(0\)步),极差的最小值。思路(要直接看答案可以跳......
  • Codeforces Round 982 (Div. 2)解题报告
    CodeforcesRound982(Div.2)解题报告A显然答案不会小于\(2(\maxw+\maxh)\)。构造方案学习样例一,挺明显的。B有个小性质(好像没用):一旦能通过操作变成non-increasing,再对整个序列操作一次必然变为同一个数字。我们把一开始remove的数字记为A类,通过操作删掉的记为B类......
  • Educational Codeforces Round 20 E. Roma and Poker
    差分约束我们记W表示\(1\),L表示\(-1\),D表示\(0\),然后记前\(i\)位的前缀和是\(dis[i]\)。则我们可以根据题面得到如下约束。当前位是W,则有\[\left\{\begin{matrix}dis[i]-dis[i-1]\le1\\dis[i-1]-dis[i]\le-1\end{matrix}\right.\]当前位是L,则有\[\left\{\begin{m......
  • Codeforces Round 977 (Div. 2, based on COMPFEST 16 - Final Round)
    Preface这场其实是上周四VP的,因为当时马上要出发打济南站了,但因为挺久没写代码了所以打算临阵磨枪一下好家伙结果Div.2D不会做直接给人整麻了,不过好在看了眼榜把E2写了,后面发现这个D想到了就不难A.MeaningMean容易发现从小到大操作一定最优#include<cstdio>#inc......
  • Educational Codeforces Round 171 (Rated for Div. 2)B-D
    B.BlackCells题目:思路:首先我们发现要分奇偶讨论。偶数:很简单,取a[2]-a[1],a[4]-a[3],.........a[n]-[n-1]的最大值。奇数:我只需要知道假如删除当前的这个数剩下的数最大的间隔值,注意只能删除1,3,等奇数位,因为要保证删除后左右的数为偶数。(我的代码里面是偶数位因......
  • Codeforces Round 978(div.2) D1
    #感觉挺有意思的一道题题目:思路:观察发现对于两个人a,b如果两个人里面没有Impostor那么,你得到的回答是一样的,如果有impostor那么结果不同,那么我们就可以两个两个的询问,先找到2个人里面有impostor然后在找另外一个人来询问就行了。代码:#include<bits/stdc++.h>usin......
  • Codeforces Global Round 27,D. Yet Another Real Number Problem 题解
    单调栈+贪心题意:给定一个序列从左往右对于每个索引iii,求出其前缀的数组可以进行任意次以下操作的条件下:选择......
  • Codeforces Round 981 (Div. 3) ABCDE
    CodeforcesRound981(Div.3)ABCDEA.SakurakoandKosuke藕是看样例直接猜了结论......