Ladon下的Powershell混淆还原

前言

Ladon为大型内网渗透工具,可PowerShell模块化、可CS插件化、可内存加载,无文件扫描。含端口扫描、服务识别、网络资产探测、密码审计、高危漏洞检测、漏洞利用、密码读取以及一键GetShell,支持批量A段/B段/C段以及跨网段扫描,支持URL、主机、域名列表扫描等。

此次我们着重于对PowerShell的混淆进行简单的分析,其对应混淆的方式是将Powershell脚本通过只包含特殊符号来进行,如下图:

还原混淆

我们此处以2023 *CTF中的ez_code为例

我们可以看到其对应的代码由符号组成(代码太长了此处就不贴完了)

1
('('  | % { ${-``} = + $() } { ${]} = ${-``} } { ${!;*} = ++  ${-``} } { ${*@ } = (  ${-``} = ${-``} + ${!;*}  ) } { ${=$``} = (  ${-``} = ${-``} + ${!;*}  ) } { ${ ]} = (${-``} = ${-``} + ${!;*}  ) } { ${!} = (${-``} = ${-``} + ${!;*}  ) } { ${#.} = (${-``} = ${-``} + ${!;*}  ) } { ${(} = (${-``} = ${-``} + ${!;*}  ) } { ${)``} = (${-``} = ${-``} + ${!;*}  ) } { ${``*%} = (${-``} = ${-``} + ${!;*}) } { ${$%} = "[" + "$(@{  })"[${(}  ] + "$(@{  })"[  "${!;*}${``*%}"  ] + "$(@{  }  )  "[  "${*@ }${]}"  ] + "$?"[${!;*}  ] + "]" } { ${-``} = "".("$(  @{}  )  "[  "${!;*}${ ]}"  ] + "$(@{})  "[  "${!;*}${#.}"] + "$(  @{  }  )"[${]}] + "$(@{}  )"[${ ]}] + "$?  "[  ${!;*}] + "$(  @{})"[${=$``}  ]) } { ${-``} = "$(@{  }  )"[  "${!;*}" + "${ ]}"] + "$(@{  })  "[${ ]}  ] + "${-``}"["${*@ }" + "${(}"  ] }  )  ; ${@*} = "${$%}${``*%}${``*%}+${$%}${!;*}${]}${)``}+${$%}${``*%}${(}+${$%}${!;*}${!;*}${!}+${$%}${!;*}${!;*}${!}+${$%}${=$``}${*@ }+${$%}${``*%}${``*%}+${$%}${!;*}${]}${ ]}+${$%}${!;*}${]}${!}+${$%}${!;*}${!;*}${*@ }+${$%}${!;*}${]}${!;*}+${$%}${!;*}${!;*}${ ]}+${$%}${ ]}${]}+${$%}${ ]}${!;*}+${$%}${!}${)``}+${$%}${!;*}${]}+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${!;*}${]}${]}+${$%}${!;*}${]}${!;*}+${$%}${!;*}${]}${*@ }+${$%}${=$``}${*@ }+${$%}${``*%}${!}+${$%}${``*%}${!}+${$%}${!;*}${]}${!}+${$%}${!;*}${!;*}${]}+${$%}${!;*}${]}${!}+${$%}${!;*}${!;*}${#.}+${$%}${``*%}${!}+${$%}${``*%}${!}+${$%}${ ]}${]}+${$%}${!;*}${!;*}${!}+${$%}${!;*}${]}${!;*}+${$%}${!;*}${]}${)``}+${$%}${!;*}${]}${*@ }+${$%}${ ]}${!;*}+${$%}${!}${)``}+${$%}${!;*}${]}+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${=$``}${*@ }+${$%}${!;*}${!;*}${!}+${$%}${!;*}${]}${!;*}+${$%}${!;*}${]}${)``}+${$%}${!;*}${]}${*@ }+${$%}${ ]}${#.}+${$%}${!;*}${]}${]}+${$%}${=$``}${*@ }+${$%}${#.}${!;*}+${$%}${=$``}${*@ }+${$%}${ ]}${)``}+${$%}${!;*}${*@ }${]}+${$%}${!}${#.}+${$%}${!}${!}+${$%}${!}${ ]}+${$%}${!}${=$``}+${$%}${!}${*@  ....

看上去让人挺无从下手的不是吗?我们首先找到分号以及大括号来将其进行简单的划分,我们将分号前的作为第一部分,分号后的作为第二部分

第一部分

1
('('  | % { ${-``} = + $() } { ${]} = ${-``} } { ${!;*} = ++  ${-``} } { ${*@ } = (  ${-``} = ${-``} + ${!;*}  ) } { ${=$``} = (  ${-``} = ${-``} + ${!;*}  ) } { ${ ]} = (${-``} = ${-``} + ${!;*}  ) } { ${!} = (${-``} = ${-``} + ${!;*}  ) } { ${#.} = (${-``} = ${-``} + ${!;*}  ) } { ${(} = (${-``} = ${-``} + ${!;*}  ) } { ${)``} = (${-``} = ${-``} + ${!;*}  ) } { ${``*%} = (${-``} = ${-``} + ${!;*}) } { ${$%} = "[" + "$(@{  })"[${(}  ] + "$(@{  })"[  "${!;*}${``*%}"  ] + "$(@{  }  )  "[  "${*@ }${]}"  ] + "$?"[${!;*}  ] + "]" } { ${-``} = "".("$(  @{}  )  "[  "${!;*}${ ]}"  ] + "$(@{})  "[  "${!;*}${#.}"] + "$(  @{  }  )"[${]}] + "$(@{}  )"[${ ]}] + "$?  "[  ${!;*}] + "$(  @{})"[${=$``}  ]) } { ${-``} = "$(@{  }  )"[  "${!;*}" + "${ ]}"] + "$(@{  })  "[${ ]}  ] + "${-``}"["${*@ }" + "${(}"  ] }  )  ;

在powershell中我们可以使用${变量名}的方式来定义任意变量,我们先看上面部分,$()为空子表达式等价于$null当我们在前面加上一个+时会将其转换为数值 0,所以上述中的混淆中第一个大括号{ ${-``} = + $() }为定义了${-``}变量值为 0

利用上述规则我们可以将部分变量进行表达出来,其实还一个最简单的方式获取对应变量代表的值,我们可以使用echo来打印对应变量的值

在输出时我们同样以大括号拆开后来辨别对应变量名,之后将其进行输出即可快速得到对应的值

在翻译过程我们可以注意到有@{}的出现,其代表着一个空的哈希表,当我们将其放在子表达式$()中,并转换为字符串即"$(@{})"时,我们便可以得到字符串System.Collections.Hashtable此时我们可以按照下述索引进行替换

1
2
3
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 
S y s t e m . C o l l e c t i o n s . H a s h t 24 25 26 27
a b l e

同样的在翻译工程中我们还会遇到$?,其一个自动变量指示的是我们上一条语句的执行情况,如果执行成功为True反之为False,我们同样可以利用索引来取对应字符

翻译出来后我们可以发现有"".inSert字样,Insert()System.String中的一个方法名,原型如下:

1
string Insert(int startIndex, string value)

我们使用<Object>.Method name可以得到上述一个方法对象

对于"".inSert我们同样可以创建一个索引表如下:

1
2
3
00 01 02 03 04 05 06 07 08 09 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
s t r i n g I n s e r t ( i n t s t a r t I n d e x , s t r i n g v a 39 40 41 42
l u e )

经过上述转换后我们可以得到对应的变量名与其值为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{ ${-``} = + $() } # 0
{ ${]} = ${-``} } # 0
{ ${!;*} = ++ ${-``} } # 1
{ ${*@ } = ( ${-``} = ${-``} + ${!;*} ) } # 2
{ ${=$``} = ( ${-``} = ${-``} + ${!;*} ) } # 3
{ ${ ]} = (${-``} = ${-``} + ${!;*} ) } # 4
{ ${!} = (${-``} = ${-``} + ${!;*} ) } # 5
{ ${#.} = (${-``} = ${-``} + ${!;*} ) } # 6
{ ${(} = (${-``} = ${-``} + ${!;*} ) } # 7
{ ${)``} = (${-``} = ${-``} + ${!;*} ) } # 8
{ ${``*%} = (${-``} = ${-``} + ${!;*}) } # 9
{ ${$%} = "[" + "$(@{ })"[7] + "$(@{ })"[19] + "$(@{ })"[24] + "$?"[1] + "]" } # CHar
{ ${-``} = "".("$(@{} )"[14] + "$(@{}) "[16] + "$( @{ } )"[0] + "$(@{})"[4] + "$?"[1] + "$( @{})"[3]) } # "".inSert
{ ${-``} = "$(@{ } )"[14] + "$(@{ }) "[4] + "${-``}"[27] } # iex

第二部分

之后我们将其进行批量替换,用对应数字或者字符串来替换第二部分中的变量。在这之后我们可以大致得到一串由[CHar]xx+[CHar]xx组成的串

xx为某些数字,由我们之前的字符串替换得到

解密

我们此时可以将其直接替换掉[CHar]以及+可以直接将其丢进Cyberchef中进行转换,之后我们便可以得到对应的源代码程序

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from ctypes import c_uint32
class chiper():
def __init__(self):
self.d = 0x87654321
k0 = 0x67452301
k1 = 0xefcdab89
k2 = 0x98badcfe
k3 = 0x10325476
self.k = [k0, k1, k2, k3]

def e(self, n, v):
def MX(z, y, total, key, p, e):
temp1 = (z.value >> 6 ^ y.value << 4) + (y.value >> 2 ^ z.value << 5)
temp2 = (total.value ^ y.value) + (key[(p & 3) ^ e.value] ^ z.value)
return c_uint32(temp1 ^ temp2)
key = self.k
delta = self.d
rounds = 6 + 52//n
total = c_uint32(0)
z = c_uint32(v[n-1])
e = c_uint32(0)

while rounds > 0:
total.value += delta
e.value = (total.value >> 2) & 3
for p in range(n-1):
y = c_uint32(v[p+1])
v[p] = c_uint32(v[p] + MX(z, y, total, key, p, e).value).value
z.value = v[p]
y = c_uint32(v[0])
v[n-1] = c_uint32(v[n-1] + MX(z, y, total,
key, n-1, e).value).value
z.value = v[n-1]
rounds -= 1
return v

def bytes2ints(self,cs:bytes)->list:
new_length=len(cs)+(8-len(cs)%8)%8
barray=cs.ljust(new_length,b'\x00')
i=0
v=[]
while i < new_length:
v0 = int.from_bytes(barray[i:i+4], 'little')
v1 = int.from_bytes(barray[i+4:i+8], 'little')
v.append(v0)
v.append(v1)
i += 8
return v

def check(instr:str,checklist:list)->int:
length=len(instr)
if length%8:
print("Incorrect format.")
exit(1)
c=chiper()
v = c.bytes2ints(instr.encode())
output=list(c.e(len(v),v))
i=0
while(i<len(checklist)):
if i<len(output) and output[i]==checklist[i]:
i+=1
else:
break
if i==len(checklist):
return 1
return 0

if __name__=="__main__":
ans=[1374278842, 2136006540, 4191056815, 3248881376]
# generateRes()
flag=input('Please input flag:')
res=check(flag,ans)
if res:
print("Congratulations, you've got the flag!")
print("Flag is *ctf{your_input}!")
exit(0)
else:
print('Nope,try again!')

不难看出是一个XXTEA,我们写出对应解密脚本将其进行解密即可

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from ctypes import *


def MX(z, y, total, key, p, e):
temp1 = (z.value >> 6 ^ y.value << 4) + \
(y.value >> 2 ^ z.value << 5)
temp2 = (total.value ^ y.value) + \
(key[(p & 3) ^ e.value] ^ z.value)
return c_uint32(temp1 ^ temp2)


def encrypt(n, v, key):
delta = 0x87654321
rounds = 6 + 52 // n

total = c_uint32(0)
z = c_uint32(v[n - 1])
e = c_uint32(0)

while rounds > 0:
total.value += delta
e.value = (total.value >> 2) & 3
for p in range(n - 1):
y = c_uint32(v[p + 1])
v[p] = c_uint32(v[p] + MX(z, y, total, key, p, e).value).value
z.value = v[p]
y = c_uint32(v[0])
v[n - 1] = c_uint32(v[n - 1] + MX(z, y, total, key, n - 1, e).value).value
z.value = v[n - 1]
rounds -= 1

return v


def decrypt(n, v, key):
delta = 0x87654321
rounds = 6 + 52 // n

total = c_uint32(rounds * delta)
y = c_uint32(v[0])
e = c_uint32(0)

while rounds > 0:
e.value = (total.value >> 2) & 3
for p in range(n - 1, 0, -1):
z = c_uint32(v[p - 1])
v[p] = c_uint32((v[p] - MX(z, y, total, key, p, e).value)).value
y.value = v[p]
z = c_uint32(v[n - 1])
v[0] = c_uint32(v[0] - MX(z, y, total, key, 0, e).value).value
y.value = v[0]
total.value -= delta
rounds -= 1

return v


# test
if __name__ == "__main__":
# 该算法中每次可加密不只 64bit 的数据,并且加密的轮数由加密数据长度决定
v = [1374278842, 2136006540, 4191056815, 3248881376]
k = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
n = 4

res = decrypt(n, v, k)
flag = b''
for i in res:
flag += i.to_bytes(4, 'little')
print(flag) # yOUar3g0oD@tPw5H

Ladon下的Powershell混淆还原
https://equinox-shame.github.io/2023/08/12/Ladon下的Powershell混淆还原/
作者
梓曰
发布于
2023年8月12日
许可协议