Boost【8】shared_library

1. 故事

听说 Boost 有一个跨平台的 shared_library 可以管理动态链接库,试试?

2. 尝试

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
#include <format>
#include <iostream>

#include <boost/dll/shared_library.hpp>

int main() {
boost::dll::fs::error_code ec;
boost::dll::shared_library ntdll(
L"ntdll.dll", boost::dll::load_mode::search_system_folders, ec);
if (ec) {
std::cerr << ec.message();
return EXIT_FAILURE;
}

std::cout << std::format("ntdll: {}\n", ntdll.location(ec).string());
bool has = ntdll.has("RtlGetNtSystemRoot");
std::cout << std::format("has RtlGetNtSystemRoot: {}\n", has);

if (has) {
std::wcout << std::format(L"RtlGetNtSystemRoot: {}\n",
ntdll.get<wchar_t*()>("RtlGetNtSystemRoot")());
}

try {
auto RtlGetNtSystemRoot = ntdll.get<wchar_t*()>("RtlGetNtSystemRoot");
if (nullptr != RtlGetNtSystemRoot) {
std::wcout << std::format(L"RtlGetNtSystemRoot: {}\n",
RtlGetNtSystemRoot());
}
} catch (const boost::system::system_error& e) {
std::cerr << e.what();
}
}

3. 思考

上面的例子显然不合格,因为它并不跨平台!动态加载 Windows 特有的 ntdll.dll 应该用 Windows Implementation Library

但作为范例,或者项目已经引入 Boost,却没有引入 wil,也是可以用用,只是它并不极致。比如说,ntdll.dll 其实并不需要 load,它必然被加载,只需要 GetModuleHandle 即可。

所以它其实还不如这个好用:https://github.com/UMU618/umu/blob/main/include/umu/module.hpp

“天道酬勤”的最初含义

转自:https://epaper.gmw.cn/wzb/html/2016-04/16/nw.D110000wzb_20160416_5-02.htm

“天道酬勤”大约是中国人最喜欢说的四个字,也是书法爱好者最爱写的词汇,经常用来励志。

事实上,从它的本义论起,现今我们对于它的理解是大错特错。它既非励志之言,更非一般百姓能用,若非君临天下,您断不可用此四字。

“天道酬勤”并非成语,由《尚书·大诰》:“天閟毖我成功,天亦惟用勤毖我民”之句引申而来。唐人孔颖达疏:“天慎劳民使成功,亦当勤劳民使安宁。”意思是:“上天啊,您咋这么好呢!您如此谨慎地护佑我的事业,确保其成功!您又是如此频繁地关照我的百姓……”这句话原是周天子向上天说的,用来感谢上天对国民无微不至的关怀。

到了近代,人们将其转引为更通俗的“天道酬勤”。字面虽是直白了,但歧义亦由此产生,变成了今天人们所理解的——同志们,要加倍努力啊,到时上天会犒赏大家的(最好发个大红包)。

“天道酬勤”之“勤”非“勤奋”之意,它只是表示次数多,即频繁。“酬勤”二字是倒装句,“勤”是状语,连起来意为“频繁地赏赐”。如此理解,便与我们平时的理解大有不同了。

“天道酬勤”与故宫太和殿上方的“建极绥猷”匾额传达的是一个意思,就是皇帝们要顺应天命,建立人间法则,安抚四方。所以,“天道酬勤”四字,本是君主们才能说的话。

八哥系列:Debian 12 的 xrdp 突然连不上!

八哥就是让你料不到!

故事

用 macOS 的 Microsoft Remote Desktop Beta 连着 Debian 12,切回 macOS 一段时间后,再回 Debian,发现卡死了。

一看 Debian 的机器,是睡眠了,立刻按电源键唤醒。但是 Microsoft Remote Desktop Beta 再也连不上 Debian。一直在连接的界面,也不报错,也不超时。

SSH 到 Debian 一顿治疗后,依然无法连接。最后重启 Microsoft Remote Desktop Beta,居然好了……

折腾记录

八哥一:为什么睡眠了?

因为每次 RDP 进入 KDE 后,都弹出一个密码输入框,上面想着“挂起系统需要身份验证”,于是稣做了如下操作,把它去掉。

sudo vim /etc/polkit-1/rules.d/85-suspend.rules,输入:

1
2
3
4
5
6
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.login1.suspend" &&
subject.isInGroup("tsusers")) {
return polkit.Result.YES;
}
});
1
2
sudo chmod 755 /etc/polkit-1/rules.d
sudo chmod 644 /etc/polkit-1/rules.d/85-suspend.rules

结果——机器就能自动睡眠了。这是不符合预期的……

八哥二:SSH 好好的,唯独 XRDP 坏了?

确实是稣的一大失算!因为期间在 Debian 本地使用了 wayland,而 xrdp 用的是 xorg,怀疑这可能把 xrdp 弄坏,就把目光都集中在 xrdp 上,各种修复,甚至重新安装、配置 xrdp,也无济于事。

另外一个误导因素是:刚用上 京东京造的 SSD,期间摸了摸,觉得烫得不行,担心是 SSD 异常,导致 xrdp 的文件被破坏。事实上,这个破硬盘也确实因为过热自动写保护两次,最后都无法启动系统了。

八哥三:Windows 连 Debian 居然没问题……

这才意识到是 macOS 的 Microsoft Remote Desktop Beta 有问题!连忙重启这个 App,终于真相大白!现在看着这名字末尾的 Beta 陷入沉思。

用 TraceView 取代 DbgView

1. 故事

  • 开发虚拟显示器驱动时,打太多日志到 DbgView,结果导致驱动被底层主动杀死。

  • 有一天,Linux 和 Windows 驱动都精通的钧叔,突然和稣吐槽,WPP 太擸𢶍。

2. 问题

  • OutputDebugStringA 太慢了!

  • WPP 太乱了!

3. 相关知识

  • ETW(Event Tracing for Windows)是 Windows 操作系统中的一种事件跟踪技术,可以用于记录系统和应用程序生成的事件。ETW 的优点就是性能好,并且同时具备内核态和用户态 API。

  • WPP(Windows Software Trace Preprocessor)是一种用于 Windows 软件跟踪的预处理器,可以帮助开发人员在代码中插入跟踪语句,并生成可用于 ETW 的跟踪消息。即,WPP 基于 ETW,只是做了层封装。

  • TraceView is a trace controller and a trace consumer. 类似于用 DbgView 看 OutputDebugStringA 产生的消息。ETW 产生的消息用 TraceView 来看。

4. 解决

慢的,不用就行。乱的,换个用法。

既然已经有 TraceView,那么我们只需要把 OutputDebugStringA 替换为 ETW 的 API 不就完事了吗?说干就干,先写个简单的 Trace Provider 代码:

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
#include <Windows.h>

#include <evntprov.h>

#include <cassert>
#include <iostream>
#include <string_view>

class EtwLog {
public:
EtwLog() = default;
~EtwLog() {
if (0 != event_handle_) {
EventUnregister(event_handle_);
}
}

ULONG Initialize(const GUID& guid) noexcept {
ULONG ec = EventRegister(&guid, nullptr, nullptr, &event_handle_);
assert(ERROR_SUCCESS == ec);
return ec;
}

ULONG Log(std::wstring_view message,
std::uint8_t level = 1,
std::uint64_t keyword = 1) noexcept {
assert(0 != event_handle_);
EVENT_DESCRIPTOR event_descriptor;
EVENT_DATA_DESCRIPTOR data_descriptor;
EventDescCreate(&event_descriptor, 0, 0, 0, level, 0, 0, keyword);
EventDataDescCreate(
&data_descriptor, message.data(),
static_cast<ULONG>((message.size() + 1) *
sizeof(std::wstring_view::value_type)));
return EventWrite(event_handle_, &event_descriptor, 1, &data_descriptor);
}

private:
REGHANDLE event_handle_{};
};

int main() {
std::cout << "Hello ETW!\n";

EtwLog etw;
// {aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}
static const GUID guid = {0xaaaaaaaa,
0xaaaa,
0xaaaa,
{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}};
ULONG ec = etw.Initialize(guid);
if (ERROR_SUCCESS != ec) {
std::cerr << "Failed to Initialize ETW!\n";
return ec;
}
etw.Log(L"Hello ETW!"); // View via TraceView
}

打开 TraceView,做好基本配置:

采用 GUID 方式

没有 TMF,先 Auto,等八哥

然后,运行以上 C++ 代码,回到 TraceView 界面,能看到捕获到信息,但并没有“Hello ETW!”,而是写着“解码错误 1168”。

解码错误

到此,恍然大悟,原来 ETW 太底层,所以才有 WPP 定义一系列规范来使用 ETW,只不过 WPP 太老,不好用了。有没有一种不需要 pdb/man/tmf 的使用 ETW 的方式?

有的。它就是 Windows 10 新增的 TraceLogging

Windows 10 introduces TraceLogging which builds on ETW and provides a simplified way to instrument code for native, .NET and WinRT developers.

TraceLogging is a system for logging events that can be decoded without a manifest. On Windows, TraceLogging is used in user-mode and kernel-mode to generate Event Tracing for Windows (ETW) events. TraceLogging builds on Event Tracing for Windows (ETW) and provides a simplified way to instrument code.

参考微软给的例子就很容易理解并上手:C/C++ TraceLogging Examples

诗盗·如梦令·挨踢垂暮

《诗盗·如梦令·挨踢垂暮》

常记挨踢垂暮,沉重不知归路。

知识宛成咒,误入内卷深处。

要猝,要猝,惊起一滩社畜。

注解

改自宋代李清照的《如梦令·常记溪亭日暮》:

常记溪亭日暮,沉醉不知归路。
兴尽晚回舟,误入藕花深处。
争渡,争渡,惊起一滩鸥鹭。

导出标记为不可导出的证书

故事

稣于 2022-10-16 公布自己的签名证书。当时是在自己的笔记本上用以下 PowerShell 命令生成的:

1
New-SelfSignedCertificate -DnsName "umu618.com" -CertStoreLocation "Cert:\CurrentUser\My" -HashAlgorithm sha512 -KeyLength 4096 -Type CodeSigningCert -FriendlyName UMU618 -NotAfter 2049-11-10

因为 PowerEconomizer 已经使用它签名,所以私钥的存储安全也就正规处理——导出到 pfx 文件加密并分布式保管。

后来,因为很久没有再验证过私钥的密码,居然,忘记了……想从笔记本再次导出一份,却发现无法导出!(此处脑补:大概是自己导出后,就删除系统里的私钥,然后又导入一次,并设置为不可导出!)不愧是重视安全的稣,连自己都要防!回头又对备份的 pfx 尝试上百次密码,安全性依然牢不可摧,只能放弃!

破解思路

想想其它办法吧!理论上,系统里一定是有私钥的,“不可导出”只是个标志而已,无视它即可。

PrivateKey

  1. 从系统自己读取私钥。这需要了解 Windows 对私钥的存储方式,包括保存位置,怎么加密保护的,文件格式怎么解析……按照微软的习性,这肯定需要大量逆向,太难了!

  2. 使用系统的 API 导出,但对关键函数进行 Hook,在内存里修改标志位,骗过 API 这是可以导出的。

具体步骤

1. 拿来主义

主要思路放在第二种,进行一番搜索后发现 mimikatz 疑似有稣想要的功能。

然而实际测试发现,稣的系统太新,是 Windows 11 Build 22621,而 mimikatz 已经年久失修,无能为力。

2. 缝缝补补

开始对 mimikatz/modules/crypto/kuhl_m_crypto_patch.c 进行改进,关键点在于 CPExportKey_4000 和 CPExportKey_4001 的入口特征,需要逆向获得。于是用 IDA 简单看看 rsaenh.dll,顺利获得入口处的汇编指令,换上后依然失败。后来发现不换,其实也能找到入口,旧版本用更短的前缀一样可以找到。

看来是后面的处理不对,跟踪到 kuhl_m_crypto_extractor_capi64 函数发现一个魔法数字 RSAENH_KEY_64 被使用了两次,感觉是突破口。

1
#define RSAENH_KEY_64	0xe35a172cd96214a0

果断在 GitHub 上搜一下,结果找到另一个基于 EasyHook 的实现 jailbreak,稣毕竟是 EasyHook 代码贡献者,当然是切换到 jailbreak 尝试,结果令人愉悦!在虚拟机里实践成功。

IDA 1
IDA 2
IDA 3
IDA 4

3. 惨遭打脸

回到稣的笔记本 jailbreak 尝试却失败了!怎么回事?难道稣的笔记本有其它保护?通常都是稣被打脸的,所以这次是 jailbreak 被稣的笔记本打脸?

仔细对比,发现以下提示是不同的!

笔记本上说的是“找不到私钥”

虚拟机上说的是“私钥不可导出”

4. 痛苦地回忆

稣的系统里明明有私钥,要导出时却说找不到私钥?有没有可能是因为系统密码修改过,导致无法解密私钥?还真有可能!立刻在虚拟机里实验,果然修改用户密码,并重新登录后,出现和笔记本一样的“找不到私钥”!

然后就是痛苦地回忆……上次改密码,那可是半年前……咳咳,闭环了,又绕回密码安全问题,所以——千万不要忘记密码!

总结

虽然学到很多,但没有赚到钱。

现代 C++【4】std::chrono::zoned_time

问题

最近经历了一次半夜提交代码,却发现单元测试无法通过,而无法合并到主线的小事故。经过检查,是一个日志清理模块的实现有问题,一会儿使用 UTC,一会儿使用本地时间(东八区),导致只要在 [0:00, 8:00) 提交代码就无法通过单元测试!而平时都是 10 点上班,所以没长期发现。

在纠正实现的时候,首先想到可以用 _get_timezone 来修正时间,但它是个 CRT 函数,显得不够现代,所以打算用 C++ 20 来实现。

解决

先来看 C 和 C++ 混合的解决方式:

1
2
3
long tz{};
_get_timezone(&tz);
auto local_now = std::chrono::system_clock::now() - std::chrono::seconds(tz);

这个代码除了不够现代,它还是 MS 特有的(Microsoft Specific),文档都埋坑(见文末)……C++ 20 里有跨平台的封装:std::chrono::zoned_time,下面用它来实现:

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
#include <chrono>
#include <iostream>

int main() {
// QPC 时间,非人类历法时间
auto now = std::chrono::steady_clock::now();
std::cout << "System boot time: " << now.time_since_epoch() << '\n';

// 以下是人类历法时间
auto utc_now = std::chrono::system_clock::now();
std::cout << "UTC time: " << utc_now
<< ", timestamp: " << utc_now.time_since_epoch() << '\n';

auto current_zone = std::chrono::current_zone();
std::cout << current_zone->name()
<< " time: " << current_zone->to_local(utc_now) << ", timestamp: "
<< current_zone->to_local(utc_now).time_since_epoch() << '\n';

std::chrono::zoned_time<std::chrono::system_clock::duration> local_time{
std::chrono::current_zone(), utc_now};
std::cout << "Local time: " << local_time.get_local_time()
<< ", timestamp: " << local_time.get_local_time().time_since_epoch()
<< '\n';

std::cout << "Timezone: "
<< (utc_now.time_since_epoch() -
local_time.get_local_time().time_since_epoch())
<< '\n';
}

可能的输出:

1
2
3
4
5
System boot time: 963185693626400ns
UTC time: 2023-05-28 07:59:21.9329686, timestamp: 16852607619329686[1/10000000]s
Asia/Shanghai time: 2023-05-28 15:59:21.9329686, timestamp: 16852895619329686[1/10000000]s
Local time: 2023-05-28 15:59:21.9329686, timestamp: 16852895619329686[1/10000000]s
Timezone: -288000000000[1/10000000]s

PS: 目前为止,g++ 对 C++ 20 支持不好,请用 MSVC 测试。

注意事项std::chrono::zoned_time may throw if location is not in the time zone database. 需要 catch 类型为 std::chrono::nonexistent_local_time 的异常。

_get_timezone 的坑

_get_timezone 的返回值的含义是 UTC 和 localtime 的差值,单位为秒,比如东八区是 -28800。它的实现是这样的:

1
2
3
4
5
6
7
8
9
extern "C" errno_t __cdecl _get_timezone(long* result)
{
_VALIDATE_RETURN_ERRCODE(result != nullptr, EINVAL);

// This variable is correctly inited at startup, so no need to check if
// CRT init finished.
*result = _timezone.value();
return 0;
}

目前它的文档里并没有提到需要“前置调用”……如果直接使用,可能得到一个错误的默认值 28800,这是“西八区”的意思!正确的做法是调用 _tzsetgmtimelocaltime 等函数后,再调用 _get_timezone。

参考

  1. _get_timezone
  2. std::chrono::zoned_time: https://en.cppreference.com/w/cpp/chrono/zoned_time

DXGI 类工厂缓存

1. 问题

  • CreateDXGIFactory1 太慢!

  • CreateDXGIFactory2 太慢!

2. 验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
// Do NOT use CreateDXGIFactory + IDXGIFactory
// CreateDXGIFactory1 + IDXGIFactory1: Windows 7
// CreateDXGIFactory1 + IDXGIFactory2: Windows 8 and Platform Update for Windows 7
// CreateDXGIFactory2 + IDXGIFactory3: Windows 8.1
// CreateDXGIFactory2 + IDXGIFactory4: Windows 10
// CreateDXGIFactory2 + IDXGIFactory5: Windows 10
// CreateDXGIFactory2 + IDXGIFactory6: Windows 10 1803
// CreateDXGIFactory2 + IDXGIFactory7: Windows 10 1809
auto now = std::chrono::steady_clock::now();
CComPtr<IDXGIFactory7> dxgi_factory_;
HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory_));
// HRESULT hr = CreateDXGIFactory2(0, IID_PPV_ARGS(&dxgi_factory_));
if (FAILED(hr)) {
std::cerr << std::format("!CreateDXGIFactory1(), #0x{:08X}\n", hr);
return hr;
}
auto diff = std::chrono::steady_clock::now() - now;
std::cout << std::format("CreateDXGIFactory1() costs {}\n", diff);
}

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CPU: Intel(R) N100
// GPU: Intel(R) UHD Graphics
CreateDXGIFactory1() costs 14795500ns
CreateDXGIFactory1() costs 12253500ns
CreateDXGIFactory1() costs 7063000ns

CreateDXGIFactory2() costs 13026300ns
CreateDXGIFactory2() costs 8201400ns
CreateDXGIFactory2() costs 8535500ns

// CPU: 11th Gen Intel(R) Core(TM) i7-11700F @ 2.50GHz
// GPU: NVIDIA GeForce RTX 3060
CreateDXGIFactory2() costs 11'770'500ns
CreateDXGIFactory2() costs 13'821'500ns
CreateDXGIFactory2() costs 11'671'900ns

居然平均 10ms 以上,确实太慢!

3. 解决

既然创建类工厂慢,就不要频繁创建,创建后把它缓存起来。

新问题:缓存失效怎么办?

There are only two hard things in Computer Science: cache invalidation and naming things.
– Phil Karlton

Phil Karlton:计算机科学领域有两个难题:一个是缓存失效,另一个就是命名。

答案:IDXGIFactory1::IsCurrent

以下是 MSDN 说的:

Informs an application of the possible need to re-create the factory and re-enumerate adapters.

FALSE, if a new adapter is becoming available or the current adapter is going away. TRUE, no adapter changes.

IsCurrent returns FALSE to inform the calling application to re-enumerate adapters.

这就很含糊了,IsCurrent 返回 FALSE 时,究竟该 re-enumerate adapters,还是 re-create the factory?

假设稣需要反复调用 EnumAdapterByLuid,如果只 re-enumerate adapters,那么稣每次直接用 EnumAdapterByLuid 即可,根本不需要理会 IsCurrent。所以这显然是错的,真正需要的是 re-create the factory,再 re-enumerate adapters。

这点可以在类工厂创建后,禁用和启用显示适配器来验证。根据稣的测试,类工厂创建后,禁用显示适配器,此时 IsCurrent 返回 FALSE,依然可以枚举出被禁用的显示适配器。只有重建类工厂,才能得到当前正确工作的显示适配器。

八哥之神后传【9】

床上

圣小开:周老师!

周易:还睡不?

圣小开:稣不在学校的时间,身体怎么办?

周易:已经请你未来的妻子帮你打理。

圣小开:啥?女的!

周易:有问题?

圣小开:完了,完了,等稣回去,目测 11 个潜在的女朋友全没啦!

周易:放心,她们死不了。

圣小开:明早咱们就回去?

周易:是我回去,你已经死了,就留在这里,吧!

圣小开:不是吧?稣明明就死了,为啥身体被一个女人冒名顶替?

周易:睡吧!死都死了,还要管这么多,当初怎么不别死?

圣小开:哦,好有道理。稣是量子化的,没有中间状态,没有帅气的动作,只有脸瘫。一切都是这么淡定。

元至正八年,鹿邑

朱重八:敢问恩公尊姓大名?来日必当报此大恩!

圣仙山:天机不可泄露。吾观汝有龙相,将来打下天下,定国号为明可好?

朱重八:观恩公一身仙家道行,不似信口开河,然俺乃一介草根,如何能打下天下?

圣仙山语重心长地说:重八哥,也是个八哥啊!你看!

朱重八:妙啊!太妙哩,世间竟真有如此仙法。斗胆再问恩公法号,将来重八定当为恩公修宫建观!

圣仙山:呵呵。将来大明有人立功,汝可赐姓为郑。

朱重八:大明王朝,赐姓郑……莫非……恩公就是……

圣仙山:以后多收义子,还有不准再叫重八!哈哈哈。

朱重八:em?恩公怎么突然没了,神仙呐!

吓醒

圣小开:周老师真的不见了?

胡连玉:什么周老师?

圣小开:和稣一起来的那位?

胡连玉:李伯阳呀,他已经被县里的人接走了。

圣小开:那他有留下什么钱给稣吗?

胡连玉:没有呢,他说你很聪明,会自力更生。

圣小开:这里连电都没有,不得先发明个电池……太累了,稣打算走捷径,入赘到你家,快,带稣去你娘家。

胡连玉:又发烧了?

圣小开:当然没有。你妹妹是读书人,说明你娘家的家境还行。你妹还未嫁,以稣的智慧,嘿嘿,大胆推测,你们家缺男丁,就算不缺,多个上门女婿也是美哉。

胡连玉:你都没见过我妹!

圣小开:看你就知道个大概了。

胡连玉:我看你特别有诚意。

圣小开:是的。

胡连玉:不过去我娘家需要走七天七夜。我给你地图和一封信,你自己走过去吧!

圣小开:那不得饿死在路上?稣就是没带钱才想就地傍富婆的!这……

胡连玉:公子真爱说笑。你是大官李伯阳的学生,肯定也是富贵人家……

圣小开:穷!真穷!特别穷!

胡连玉写信中:圣仙山?

圣小开:啥?

胡连玉:你叫圣仙山?

圣小开:稣叫圣小开,是圣先生,但不……

胡连玉:李伯阳说你大名圣仙山,字小开,那没错!

圣小开:嗯?