碎碎念
刚进黑灯模式就出了flag,看到还是 0 解题以为可以拿全场唯一解光荣退役了,结果平台的flag配置是烂的,以为 0 解的原因是平台烂直接找运维对线,拉扯了一个小时左右赛后说 pwsh 需要人工验证flag以为其实大家都会做只是没交上,第二天起床一看还真是一血全场只有 2 解。也算光荣退役了?
但是比赛结果一般,打ctf不如闲鱼买flag,整场比赛充斥着国粹这种脑瘫题,不过打不过闲鱼哥还是我太菜了。
Writeups
初步分析
题目备份:https://small.fileditch.ch/s3/ssQpUZIxaIRvkfVeChBp.ps1
一个powershell
脚本,先base64后解密再执行,先上手解密base64串
1
2
|
with open("p4.ps1", "w") as f:
f.write(base64.b64decode(code).decode())
|
1
2
3
4
|
$M=@{129693309641433576095262078804193086780='(New-ObJecT sysTEm.Io.COMpResSIoN.DEflATesTReAm( [SyStEM.iO.meMORYsTreaM] [sYSTEm.CoNVErT]::FRoMbASe64StrIng( "1VhNaxxHED3Xv+iDYXZBWuwEArHJwQYTy8gS2CKHiCUHIX/koDiKyMXWf89G8s7Ux6ue7hpN7zqBZKa7qvrVq1fVs+rSt3/o7j/0/wNtH+5eaLt7/ySWktmmbThuTP06bfe1pY0grNUi9fY6tPHOBLYnkec17FuLnpHEY2RyVgsqS5T0kLM+yiyA4CTXSa3yOoO4EoWLS+U6mjtxF2K75DKYVx5LkgBWtpCjUBYE+GdMvFpoCXFswiShhd3ABXQR9mqKxxdje1RGgPmqqWXTICCR7fTEbe1HV7FJv+2yAUYpYq2Pjj....'
$t=Read-Host -Prompt "Enter your flag";
[System.Collections.Queue]$tt=([byte[]][char[]]$t|%{$_ -shr 4;$_%16});
iex($M[129693309641433576095262078804193086780])
|
M太长了这里就不放了,可以看到M是一个字典,对应的是混淆过的powershell脚本,每个key
(一串数字)分别对应了一段混淆过的powershell的脚本,尝试解混淆。
混淆分析
混淆方式类似如下,这里给出几个样本:
1
2
3
4
5
6
7
|
(New-ObJecT sysTEm.Io.COMpResSIoN.DEflATesTReAm( [SyStEM.iO.meMORYsTreaM] [sYSTEm.CoNVErT]::FRoMbASe64StrIng( "....." ) ,[SYSTeM.io.COMpRESSIoN.COmPressiOnmODe]::dECoMprEss ) | FoREaCH-objEct{New-ObJecT sySTem.Io.StReamreAder( $_ , [TexT.enCOdING]::ascII) }|FOrEacH-obJECt{$_.REAdtoend( )} )|. ( $sHElLId[1]+$Shellid[13]+"X");
&( $SHEllID[1]+$SHEllid[13]+"X") ( nEw-obJEcT System.io.STREAMrEADeR((nEw-obJEcT IO.cOMPreSsiOn.deFLaTeSTreAm([system.io.MeMoRYStReam][sYsTem.COnVERt]::fROMBAsE64sTRiNG(" ...") ,
[sYsTEm.io.COmPREsSIoN.coMpRessionmoDE]::deComPResS )|% { nEw-objEcT iO.StREamREAdEr( $_ ,[TExt.EncOdING]::asCii )}|%{ $_.ReadtoeND( ) })|& ( $EnV:cOMspEc[4,15,25]-joiN"");
( New-OBjecT system.iO.STreAmReAdeR(( New-OBjecT SYstEM.IO.cOMprESsion.DeFlATeStrEAm( [io.meMOrySTReAm][cONVert]::FrOmBASE64sTrInG( "..."),[SysTEm.IO.coMPreSsIon.coMpreSsIonMODE]::decOmPress )),[sYsTeM.TEXT.ENcOdiNg]::aSciI) ).REAdtoeND()| .( $pShOmE[21]+$pShoME[34]+"x");
|
可以归纳为以下的模式:
iex的变种
(混淆过的字符串和对应的解密代码
)
混淆过的字符串和对应的解密代码
| iex的变种
其中 iex 是 powershell 的一个 cmdlet 指令,全名 Invoke-Expression
,作用是执行表达式(类似eval()
)。
在上面的例子中,混淆过的字符串和对应的解密代码都是base64和压缩,但在嵌套的更深层次的混淆中,存在很多种加密或编码方式。
而iex
的变种是如下几种:
1
2
3
|
( $SHEllID[1]+$SHEllid[13]+"X")
( $EnV:cOMspEc[4,15,25]-joiN"")
.( $pShOmE[21]+$pShoME[34]+"x")
|
在嵌套的更深层次的混淆中同样存在非常多种iex的变种,这里只是其中几种。
(除此之外还有随机大小写混淆但是不关注)
静态解混淆尝试
一个直观的思路是去掉代码中的iex变种变成字符串解密操作,直接运行得到解密后的代码。但是混淆后的代码多达1000+个,每个都有5~6层混淆嵌套,手动非常的不现实。
本来想当正则大师解决,但是iex变种有20+种难以收集,且存在混淆,执行方式也不一样。
另外这里有个对本题没有帮助的仓库。https://github.com/pan-unit42/public_tools/tree/master/powershellprofiler
动态解混淆
很明显我们的目的是找到一种自动化解混淆的方式。如果我们不考虑静态分析,而是考虑 Hook Powershell里的iex
函数,让它每次被调用的时候都改为输出参数的值然后直接运行脚本呢?
查一查可以找到在Powershell
中命令默认的调用顺序:
- Alias
- Function
- Cmdlet
- 本机 Windows 命令
Powershell 会按照上面的顺序寻找,并且调用第一个被寻找到的方法
可以发现,对 Function 的调用优先于 Cmdlet。也就是说如果我们定义一个名为Invoke-Expression
的函数,在iex
被调用时将会调用我们的自定义函数而不是iex
。
编写如下函数重载iex
.
1
|
function Invoke-Expression{$input|%{$_;};$args|%{$_;}};
|
作用是将input
(用于管道执行方法)或args
(用于调用执行方法)直接输出。
现在我们可以自动化解混淆,请一个 python
大师来完成。
注意到代码中包含一些干扰直接执行的代码比如while($true){echo Warning!};
和(sleep 10000);
会导致程序卡死,但是情况不多(就这两种)特判一下即可。
Script by Timlzh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import subprocess
from threading import Thread
from json import loads, dumps
with open('hash.json', 'r') as f:
hash_dict = eval(f.read())
redir = "function Invoke-Expression{$input|%{$_;};$args|%{$_;}};"
cmd = 'powershell.exe'
result = {}
cnt = 0
def exp(line):
try:
script = redir + line.strip('\n') + '\n'
p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = p.communicate(script.encode('utf-8'))
res = out.split(b'\n')[6].strip().decode('gbk')
if('while' in res or 'sleep' in res):
return res
if(res == '' or 'F:\\CNSS>' in res or '无法将' in res or '无效的' in res or '然后' in res or '不能' in res):
raise Exception('error')
return exp(res)
except Exception as e:
return line
def run(key, cnt):
result[key] = []
for x, line in enumerate(hash_dict[key].split(';')):
print(cnt, x)
result[key].append(exp(line))
t = []
for key in hash_dict.keys():
t.append(Thread(target=run, args=(key,cnt,)))
t[-1].start()
cnt += 1
if cnt % 10 == 0:
for x in t:
x.join()
t = []
print(cnt)
with open('result.json', 'w') as f:
f.write(dumps(result))
|
~好羡慕你们坚实的代码基础啊~
逻辑分析
在反混淆后的对应代码中,大部分都是恶意代码,但是可以发现如下形式的代码:
1
|
iex($M[@(103029045898495106128140488393279490731,283962955136589324103060025895380071615,100347917729209686340069745617547349730,238405316955808243927606563710399446649,218698919069890324731296928086576483285,13940646367990748425698245588557690704,233558059221060425999304675733838234320,129692624346663502335241542240082220070,35082001525670982399453018046361223701,328520292299326032765832843219896353365,22937617477989053507015679147587112829,83375175731001160297499272972594268643,181113447597478873176042023659491087625,133078815375884196401153883379379993112,222768486184364264889018742183865405471,180102119900859063039430119683397492538)[$tt.Dequeue()]]);
|
结合之前对输入的处理:
1
2
|
[System.Collections.Queue]$tt=([byte[]][char[]]$t|%{$_ -shr 4;$_%16});
iex($M[129693309641433576095262078804193086780])
|
不难发现就是把flag每一位按照高四位和低四位拆开作为每次跳转的索引,恢复出每个key
对应的跳转目标找到跳转路径即可。
可以发现93920216895015486878992607137045760151对应的代码是输出Congratulations! you get the correct flag
字符串,程序的入口是129693309641433576095262078804193086780
,反向寻找路径并组合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import json
with open("result.json", "r") as f:
dic = json.load(f)
now = dict()
for key in dic:
for x in dic[key]:
if "$tt.Dequeue()" in x:
now[key] = x[9:-19].split(",")
start = '93920216895015486878992607137045760151'
fin = '129693309641433576095262078804193086780'
flag = []
while start != fin:
for key in now.keys():
if start in now[key]:
for i in range(len(now[key])):
if now[key][i] == start:
flag.append(i)
break
start = key
break
print(flag)
i = len(flag)-1
while i >= 0:
c = (flag[i] << 4) + flag[i-1]
print(chr(c), end="")
i -= 2
|
总结
虽然看起来是狗屎但是其实做起来还好(如果有思路的话),但是这平台是烂的我不好说。