无银第八哥【1】条件断点

问题

使用“无银第八哥”给注册表 API 下断点,结果调用极其频繁,如果一个个人工去看,容易逐渐失去耐心。毕竟,挨踢太卷了!

解决

您只需要“条件断点”!但是怎么写“条件”成为拦路虎。

好在,“无银第八哥”自带了一份很简要的学习材料,您可以在帮助菜单打开,或按 F1,或在命令窗口输入 .hh 打开,然后输入“conditional breakpoints”,将进入一篇名为《Conditional breakpoints in WinDbg and other Windows debuggers》的帮助文档。

“条件断点”建议的使用方式是,把条件写到文件里,方便复用。指定一个断点的条件为某个文件内容的语法是:

1
bp function "$$<C:\\commands.txt"

当然,文件的内容才是重点,将在后面的实践例子里讲解。

实践

需求

winver 显示的 Windows 的 Relase 版本信息,比如“22H2”,是从注册表里读的,想断下这个读取。

winver

实现

以下在 Windows 11 x64 下进行。

  1. 下断点前,需要先知道这个 Release 信息保存的注册表位置:
  • 主键:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
  • 值:DisplayVersion
  • 数据:即是需要获取的 Release 信息
  1. 但是 winver.exe 的导入表里并没有任何 Reg API,反而这个程序的核心就是调用 SHELL32!ShellAboutW 而已。

  2. 查看 SHELL32.dll 的导入表,发现有 api-ms-win-core-registry-l1-1-0.dll,但 Reg API 里有两个可以读值,需要做个基本排序,按兼容性推测,使用 RegQueryValueExW 几率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 老 API
LSTATUS RegQueryValueExW(
[in] HKEY hKey,
[in, optional] LPCWSTR lpValueName,
LPDWORD lpReserved,
[out, optional] LPDWORD lpType,
[out, optional] LPBYTE lpData,
[in, out, optional] LPDWORD lpcbData
);

// 新 API
LSTATUS RegGetValueW(
[in] HKEY hkey,
[in, optional] LPCWSTR lpSubKey,
[in, optional] LPCWSTR lpValue,
[in, optional] DWORD dwFlags,
[out, optional] LPDWORD pdwType,
[out, optional] PVOID pvData,
[in, out, optional] LPDWORD pcbData
);
  1. 打开“无银第八哥”,按 Ctrl+E,打开 C:\Windows\System32\winver.exe

  2. 第一个需要尝试的断点是 RegQueryValueExW,输入 bm KERNELBASE!RegQueryValueExW,然后 g,发现可以断下。

  3. 开始考虑“条件断点”,需要针对 lpValueName 判断是否为 “DisplayVersion”。lpValueName 为第二个参数,按照 x64 call,即为 rdx,所以编写 C:\devel\windbg\RegQueryValueExW.txt 代码如下:

1
2
.if (@rdx != 0) { as /mu ${/v:ValueName} @rdx } .else { ad /q ${/v:ValueName} }
.if ($scmp(@"${ValueName}", "DisplayVersion") == 0) { .echo ValueName } .else { .echo ValueName; gc }

然后,输入 bm KERNELBASE!RegQueryValueExW "$$<C:\\devel\\windbg\\RegQueryValueExW.txt",再 g,发现能断下:

1
2
3
DisplayVersion
KERNELBASE!RegQueryValueExW:
00007ffc`2bd79ff0 48895c2410 mov qword ptr [rsp+10h],rbx ss:0000006c`ef69e768=00007ffc2e6731dc
  1. 额外地,可以返回(gu)看看(ub)调用方长啥样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> gu
shcore!_SHRegQueryValueW+0x8d:
00007ffc`2e53e6bd 0f1f440000 nop dword ptr [rax+rax]
0:000> ub
shcore!_SHRegQueryValueW+0x6b:
00007ffc`2e53e69b 44897db0 mov dword ptr [rbp-50h],r15d
00007ffc`2e53e69f 48894c2428 mov qword ptr [rsp+28h],rcx
00007ffc`2e53e6a4 4c8d4db8 lea r9,[rbp-48h]
00007ffc`2e53e6a8 498bcd mov rcx,r13
00007ffc`2e53e6ab 48897c2420 mov qword ptr [rsp+20h],rdi
00007ffc`2e53e6b0 4533c0 xor r8d,r8d
00007ffc`2e53e6b3 488bd0 mov rdx,rax
00007ffc`2e53e6b6 48ff15d3750a00 call qword ptr [shcore!_imp_RegQueryValueExW (00007ffc`2e5e5c90)]

GetEnvironmentStrings 函数的八哥史

故事

故事总由八哥开始!今天稣看到一个 buggy 的 API 声明:

1
2
3
4
5
#ifdef UNICODE
#define GetEnvironmentStrings GetEnvironmentStringsW
#else
#define GetEnvironmentStringsA GetEnvironmentStrings
#endif // !UNICODE

GetEnvironmentStrings

但是经历过 OutputDebugString 逆向的稣十分淡定地推测,这一定是故意的!毕竟,微软为了兼容性,啥都干得出来。

稣的逆向经验:一般 API 都是 A 的版本调用 W,而 OutputDebugString 是例外,OutputDebugStringW 调用 OutputDebugStringA。

搜索知识

找到 Raymond Chen 写的《A brief history of the GetEnvironmentStrings functions》。原来,这个 API 早在 Windows NT 3.1 时就烙下八哥!

The Get­Environment­Strings function has a long and troubled history.

The first bit of confusion is that the day it was introduced in Windows NT 3.1, it was exported funny. The UNICODE version was exported under the name Get­Environment­StringsW, but the ANSI version was exported under the name Get­Environment­Strings without the usual A suffix.

A mistake we have been living with for over two decades.

虽然后来可以解决这个例外,但微软选择保留此例外。

结论

大家可以不必担心相关的可能问题,因为现代的 Windows 会同时导出 GetEnvironmentStrings 和 GetEnvironmentStringsA。

GetEnvironmentStrings functions

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