CreateProcessA参数型Shellcode的编码问题研究

发布时间 2021-12-22
近日,在对WebAccess/SCADA系统的漏洞研究中,启明星辰ADLab的工控安全研究员发现了一个未被广泛谈论的漏洞利用技术问题,即经由CreateProcessA参数进行传递的shellcode的编码问题。


简单来讲,该控制系统的漏洞由两个程序构成:核心程序CoreProcess和辅助程序HelpProcess,核心程序CoreProcess通过系统函数CreateProcessA来启动HelpProcess(同时传递了相关参数)。其中,CoreProcess的简化代码如下:


代码.png


显然,HelpProcess的WinMain函数存在一个经典的栈溢出漏洞。当lpCmdLine的数据长度超过400字节时,对buff的strcpy操作就会产生溢出;当长度超过404字节时,就会覆盖到eipCallerNext,从而劫持HelpProcess的程序控制流。


回溯代码可知,lpCmdLine的数据来源是CoreProcess的CreateProcessA调用,且是用户可控的。因此,该漏洞的利用看起来是简单的,只需要计算好eipCallerNext的偏移量并利用shellcode填充buff即可。该漏洞的利用链和堆栈布局如下所示:


回溯代码.png

 

在利用过程中,采用测试填充字符进行溢出时,eipCallerNext的覆盖总是正确的;但采用metasploit的shellcode来溢出时,eipCallerNext的覆盖就变得不正确。对数据进行比较后发现,shellcode在CoreProcess和HelpProcess是不一样的,即shellcode传递到HelpProcess后发生了改变。此外,通过尝试metasploit的不同shellcode,发现这种改变没有明显的规律可循。


针对这个问题,ADLab的安全研究员进行了深入的分析,弄清了CreateProcessA参数传递的shellcode的编码问题,并开发了自动化处理方法,从而兼容任意shellcode。


CreateProcessA的参数处理


Windows操作系统的内核是支持全球各种语言的,其提供统一的Unicode编码型内核态API;针对具体的国家或地域,Windows通过区域编码来实现本地语言支持,即Ansi字符串型的用户态API。这些用户态API在内部先把Ansi字符串转换为Unicode字符串,然后再调用内核态API;这个转换过程是透明的,用户编写的程序对此无感知。


在Window操作系统上,1个Unicode字符由2个字节组成,1个Ansi字符由1个字节或2个字节组成。当首字节的值是0到127时,它是1个ASCII字符,对应Unicode字符的2字节的内容就是该ASCII字符加1个填充字符0;例如,Ansi字符”A”,其对应的Unicode字符是”A\x00”。当首字节的值大于127时,则当前字节和下个字节组合起来是一个区域语言的字符,区域语言字符存在对应的Unicode字符映射表;例如,”\xce\xd2”的“\xce”不是1个合法的ASCII字符,它只能和“\xd2”联合作为1个中文字符“我”,对应的Unicode字符是”\x11\x62”。


如下所示,CreateProcessA就是一个Ansi编码型的用户态API,字符串”AAAA”会被自动转换为Unicode字符串并传递给HelpProcess,然后在调用WinMain之前又被自动还原为Ansi字符串。因此,对于Ansi字符串”AAAA”,CoreProcess和HelpProcess在程序开发上都无需做任何额外的处理。


代码.png


通常情况下,CreateProcessA参数lpCmdline的来源是可靠的,比如编译时预定义的字符串和API的返回值,此时lpCmdline都是正确的Ansi字符串。因此,CreateProcess几乎总能在Unicode和Ansi之间自由地正确转换。


实际上,对于任何一门区域语言,其Ansi字符和Unicode字符的映射都不是一一映射关系;即在2字节的全部取值空间中,Ansi字符表的有效项数总是小于Unicode字符表的有效项数。这意味着,针对无法确认是区域语言的2个字节,如果强制视作Ansi字符则转换成Unicode字符后不一定能还原为初始的Ansi字符。例如:”\xeb\x2a”是一条常规的jmp offset指令,它不是1个合法的中文字符;如果视作Ansi字符强制转换为Unicode字符则是”\x3f\x00”,再次转换为Ansi字符即是”?”,丢失了jmp offset指令的语义。


因此,通过CreateProcessA的cmdline参数进行shellcode传递,必须要考虑区域语言的Ansi字符和Unicode字符相互转换的问题。


在本文的漏洞利用案例中,本地区域的语言是中文简体,对应Ansi编码表是GBK。因此,必须要对metasploit的shellcode进行GBK编码,确保其是正确的Ansi字符串。


GBK表的编码在2字节取值空间的范围是8140-FEFE,即第1字节的取值范围是0x81到0xFE,第2字节的取值是0x40到0xFE,如下所示:


 字节.png


此外,第2字节的实际有效取值还有更多约束。比如,第2字节不能为0X7F。针对某些取值的字节,第2字节的取值比[0x40, 0xFE]的空间更小。如下图所示,有的只能取该空间的后半部分,有的则只能取前半部分。


对于shellcode来讲,其每个字节的取值在0到255之间都是完全合法的。因此,本文的漏洞利用要实现shellcode的随意替换,必须要有一种方法来对shellcode中违背GBK编码的字节进行处理,从而避免Ansi字符和Unicode字符间转换导致的shellcode字符被改变的问题。一个基本的方法是按照如下的流程对shellcode进行处理,其关键是对GBK表进行查表并修正汇编指令。


 字节调整.png


以如下的shellcode为例,在扫描到字节0xEB时,发现是非ASCII字符且查表GBK结果是不存在,需要进行转换;查询GBK表后发现,在0xEB之前插入0x90可以使得90 EB是一个合法的GBK字符,同时90EB 38又不改变原来的汇编语义,转换成功。同理,继续扫描到下一个字节0XEB时,再做同样的转换就可以。但是,第2次的转换插入了新的字节0x90,导致了原始lab1对应的偏移量发生了改变;原始lab的指令实际位于转后的lab+1位置,使得第一个0XEB的语义非法了。因此,转换过程还要求跟踪指令区块的长度变化。


转换汇编.png


除了指令区块的长度改变外,还有其它兼容性问题。比如,shellcode中特殊取值(典型有0)的字节处理问题,对shellcode的内嵌参数修改问题等。因此,尽管查表转换是最根本的办法,但全表查询的空间大,限制了shellcode的灵活性。为了解决该问题,ADLab的安全研究员提出了一种基于计算的shellcode编码方法。


Shellcode计算转换


首先,我们把shellcode分为两部分:头部的固定decoder和尾部的多变payload。然后,采用查表方式进行手工编写符合GBK编码的汇编代码。其中,decoder的长度很有限,决定了这个编写的代价不大;同时,多变payload是没有额外限制的,通过编写对应的encoder来编码payload使其不违反GBK编码,又可以被decoder还原。通过这种方式,对原始shellcode的选择和改变就完全不用关心GBK编码问题,使得该漏洞的利用更加丰富。


为了减少decoder的体积,我们设计了一种计算方法来编码和解码,这样就不需要存储GBK字符表或者复杂的规则。原始shellcode编码时的计算规则如下:


遇到字节是ASCII、0x80和0xff,直接保留。


遇到字节是\x00,转换成加法运算符\x90和2个计算数符\x80和\x80。


遇到字节是\x90,转换成加法运算符\x90和2个计算数符\x48和\x48。


遇到2个字节可以转换为unicode字符,直接保留这2个字节。


遇到前面都不能处理的字节,直接转换成加法运算符\x90和2个计算数符,第1个是\x80,第2个是差值。


采用上述的编码方法后,任何shellcode都可以被转换为合法GBK字符串,并且decoder对payload的解码计算也十分简单,只需要如下的1条规则:


遇到字符是\x90,直接对后2个字符进行加法计算,并用结果替换字符\x90。 


至此,CreateProcessA参数传递的shellcode的编码问题就全部被约束在了只有一条规则的decoder代码中,很显然这是一个边界十分明确的局部问题,因此很容易就解决了。采用这种方法,本文的漏洞利用可以随意调用metasploit中的shellcode,无需再担心它们的指令内部细节。


在多语言环境下,shellcode如果不是直接的内存传递,则可能会被系统API函数所转换,从而导致其因在获得执行权之前发生内容改变而无效。因此,在漏洞利用过程中,需要注意shellcode是否受到多语言版本的API影响。