软件系统安全赛
半决赛
决赛
StudentManagement
线下赛一般不能从头开始写脚本所以的自己准备一个模板,这个模板我还要根据自己的学习程度进行改进。
1 | from pwn import * |
先对源码进行分析
1 | void __cdecl __noreturn main(int a1) |
根据菜单选项把add,delete,edit,show先注释一下
再进入delete看看没有UAF
1 | unsigned int __cdecl Delete(unsigned __int8 a1) |
再进入ADD函数分析结构体
1 | // a1 = size of description |
1 | chunk0_addr:chunk0_context |
1 | dword_804B080[byte_804B061] = chunk00_addr |
再进入edit
1 | unsigned int __cdecl Update(unsigned __int8 a1) |
1 | *(_DWORD *)dword_804B080[a1] + v2 >= (unsigned int)(dword_804B080[a1] - 4) |
1 | chunk0_addr + text_len >= chunk00_addr - 4 |
这个判断其实是有问题的,比如
add0,add1,add2对应的堆块如下
1 | chunk0 0x18 |
delete2 0
1 | chunk1 0x18 |
add 3 0x80
1 | chunk3 0x80 |
我通过chunk3的溢出就可以修改chunk11储存的指针成printf_got,然后把libc算出来,最终把free的真实地址改成system就行了。
关键代码编写
1 | printf_got = got('printf') |
1 | unsigned int __cdecl sub_80488C0(unsigned __int8 a1) |
定位漏洞所在的地方,再if那里切入汇编
1 | .text:0804892C lea eax, (aUC - 804B000h)[ebx] ; "%u%c" |
fix这个先不继续泄露,我先去深度学习一下汇编语言。
先看源码
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
delete没有出现uaf
1 | __int64 delete() |
这里存在一个单字节溢出
1 | __int64 __fastcall sub_E3A(int a1, unsigned int a2) |
先去add里面分析一下
1 | __int64 add() |
最多只能申请15个chunk,大小最大不能超过4096。calloc(v3, 1) 本质上是申请 v3 字节并把用户区清零,而 malloc(v3) 只申请内存不清零;所以这题里重新申请回来的 chunk 内容会被清零。
攻击思路就是利用单字节溢出先去泄露地址,然后用malloc_hook改成one_gadget去得到shell
exp关键部分编写
1 | add(0xe8) #chunk0 |
1 | int sub_DEA() |
delete函数存在UAF
1 | int sub_B89() |
add函数结合gdb调试可以分析出大改的结构题
1 | chunk0_control: |
delete只是把S[0] = 0,chunk_content = 0。存在UAF,因为这题每给libc解题还要利用IO来输出,所以这题放置到后面来写吧。
1 | pwndbg> vis |
Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
1 | $ hexo new "My New Post" |
More info: Writing
1 | $ hexo server |
More info: Server
1 | $ hexo generate |
More info: Generating
1 | $ hexo deploy |
More info: Deployment
Double Free 是一种常见的内存漏洞,发生在程序错误地两次释放同一块内存时。程序在使用 free() 函数释放内存后,如果不小心再次释放同一块内存,就会破坏堆内存的管理结构。
这种漏洞让攻击者可以利用程序的错误,操控堆内存的结构,进而可能控制程序的执行流程,执行恶意代码,甚至窃取敏感信息。为了避免这种情况,通常需要在释放内存后将指针设为 NULL,确保不会再次释放同一内存。
在 GNU 的 C 标准库实现 glibc 中,堆管理器 ptmalloc 会把较小的 chunk(默认 ≤ 64B)放入 fastbin。
fd 指针结构大概是:
1 | struct malloc_chunk { |
对于 fastbin 来说:
fdbk当:
1 | chunk_size <= max_fast |
并且:
1 | 该 chunk 不与 top chunk 相邻 |
则:
插入方式是:
1 | free(chunk1) |
如果我们直接
1 | free(chunk1) |
系统就会直接检测到double free。
怎么绕过我们可以
1 | free(chunk1) |
这样系统不会检测到。
那我们怎么利用它呢?

然后我接着申请就会依次申请回来chunk2 ,chunk1,第三次申请就会把malloc_hook当做一个堆块申请过来,我们就可以该他的地址里保存
的内容。
伪代码
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
add函数
1 | unsigned __int64 sub_400A3F() |
可以看到在bss段上的结构题
bss[4 * i]和bss[4 * 1]存的是size
bss[4 * 2]和bss[4 * 3]存的是chunk的地址 bss的数组一个单位是4字节,但是储存地址的时候要8字节所以占用两个单位。
delete函数:
1 | unsigned __int64 sub_400B73() |
在delete函数中可以看到存在UAF漏洞,它只把size置0了,没把chunk置0。
直接放exp,解释都放在注释里了。
1 | # coding=utf8 |
unlink 是利用 glibc 在双向链表管理空闲块时执行 fd->bk = bk; bk->fd = fd 的机制,通过伪造 chunk 的 fd 和 bk 指针,在触发 unlink 过程时实现任意地址写,从而达到控制程序执行流的经典堆利用技术。
unlink的过程分成以下几步
BK = P->bk,FD = P->fd;FD ->bk =BK;BK - fd =FD。

FD ->bk =BK

BK - fd =FD

接下来看看我们是怎么利用它的。
比如我们有三个堆块,第一个和第三个都是正在使用的,第二个是free的,如果存在堆溢出的漏洞我们就可以利用chunk0(第一个堆块)去改写chunk1(第二个堆块)的fd和bk。
此时我们把第二个堆块的的fd = &chunk1 - 3size(32位size=4,64位size=8),bk = &chunk1 - 2size。
此时我们free chunk0它是个small chunk,然后前面不是空闲的,不会向前合并;后面是空闲的,就会向后面合并。
然后就会执行unlink,执行的时候:

就可以达到
1 | chunk1 = &chunk1 -2*size |
注:为什么要设置成fd = &chunk1 - 3 * size,bk = &chunk1 - 2 * size。
因为有检查机制
1 | // fd bk |
题目:
main():
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
add_item();
这ADD函数可以看出控制堆块的指针存在bss段上&unk_6020C8
1 | __int64 add_item() |
change_item();
没有对更改的长度进行检测,存在堆溢出。
1 | unsigned __int64 change_item() |
思路:利用堆溢出,伪造出一个已经被free的堆块,在free它附件的堆块触发unlink,从而更改相对应的指针为atoi的got表,泄露出libc的基
地址,再计算出system的地址,最后把ayoi的got表指向的地址改成system_addr,
先上exp
1 | from pwn import * |
解释:
我们先创建三个堆块,chunk0和chunk1是用来构造fake_chunk的和触发unlink的
chunk2是用来防止与top chunk合并的。
1 | # 覆盖 chunk1 的 prev_size 和 size |
prev_size当上一个堆块是free的时候储存的是上一个堆块的大小,fake_chunk的大小是0x80所以覆盖成0x80。
size当上一个堆块是free状态的时候它的标志位应该是0,所以把0x91覆盖成0x90。
初始我们free掉chunk1,刚刚我们已经把fake_chunk构造成了free的状态,所以此时会触发unlink。
执行unlink和我们上面描述的一样,此时chunk0 = &chunk0 - 0x18 也就是bss - 0x18,所以我们将chunk0+0x18处储存的内容改成
atoi的got表,就相当于将chunk覆盖成了atoi的got,show的时候就会展示出atoi的真实地址。
计算出system的地址,此时由上面的分析可以此时chunk杯覆盖成了atoi的got表,我们更改chunk的内容就是改的atoi的真实地址。
我们把atoi的地址改成system的地址,发生/bin/sh就能成功了。

ELF 的全称是 Executable and Linkable Format,即可执行可链接格式。它定义了一种结构化的方式,来存储程序的各种信息,以便于操作系统进行加载、执行以及链接器进行代码和数据的链接。
ELF文件主要分为三种类型:
.o 结尾。包含代码和数据,可以与其他目标文件链接生成可执行文件或共享库。/bin/bash。.so 结尾。包含代码和数据,可以在两种情况下被使用:ELF文件从结构上可以分为两大部分:“链接视图” 和 “执行视图”。
一个典型的ELF文件布局如下所示:
)
1 | Program Header Table<-- 执行视图:描述如何创建进程映像(段信息) |
位于文件开头,是整个ELF文件的“总目录”。可以使用 readelf -h <file> 查看。

主要包含以下信息:
0x7F 和字符串 ELF,用于标识这是一个ELF文件。ELF32)还是64位(ELF64)文件。1。1 | /* The ELF file header. This appears at the start of every ELF file. */ |

红色区域:从左到右
EI_MAG0 ~3、EI_CLASS 、EI_DATA、EI_VERSION、EI_OSABI、EI_ABIVERSION、EI_PAD
EI_MAG0 ~3:0X7F ELF 文件标识
EI_CLASS:0x02 当取值为0时,是非法类别,1是32位的目标,2是64位的目标。
EI_DATA:0x01 表示数据的编码,当为0时,表示非法数据编码,1表示高位在前,2表示低位在前。
EI_VERSION:0x01 ELF 版本:01 = 当前版本
EI_ABIVERSION:0x00 ABI 版本
EI_PAD:0x00 填充字节 (共7个字节)
黄色区域:从左到右,从上到下
e_type 、e_machine 、e_version、e_entry
e_phoff、 e_shoff
00 30(小端序)
1 | 值(十六进制) 宏定义 描述 |
00 3E
1 | /* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ |
00 00 00 01
EV_NONE:00
EV_CURRENT:01
0表示非法版本,1表示当前版本。
F0 10 00 00 00 00 00 00 0x00000000000010F0 (入口点地址)
40 00 00 00 00 00 00 00 0x0000000000000040 (程序头表偏移)
E0 39 00 00 00 00 00 00 0x00000000000039E0 (节头表偏移)
程序头表是一个由 Elf*_Phdr 结构体组成的数组,用于描述 ELF 文件中的段(Segment)信息,这些信息指明了操作系统应如何将这些段装载到内存中并执行。因此,只有可执行文件和共享库包含程序头表,而目标文件则没有。
1 | typedef struct |
p_type:段的类型,用于区分该段是代码段、数据段、动态链接信息段还是其他特殊类型的段。
p_offset:段内容在ELF文件内的起始偏移量,指示了从文件何处开始读取该段。
p_vaddr:段在进程虚拟内存空间中的起始地址,即该段应该被加载到的虚拟地址。
p_paddr:段在物理内存中的起始地址。此字段通常被保留,在现代操作系统中由于使用虚拟内存,其值通常与 p_vaddr 相同。
p_filesz:段在ELF文件中所占的大小。某些段(如 .bss)在文件中可能不占空间,此时此值会小于 p_memsz。
p_memsz:段在内存中所占的大小。如果该段包含未初始化的数据(如 .bss),其在内存中的大小会大于在文件中的大小。
是 ELF 文件中ELF文件的节区按功能划分的各个部分,其信息由节区头部表统一描述,可视为节区的 “目录”。可以使用 readelf -S
<file> 查看

1 | typedef struct |
sh_name:节名称在字符串表(.shstrtab 节)中的索引。通过此索引可以找到表示该节名称的字符串。
sh_type:节的类型,定义了节的内容和语义。常见类型包括:
SHT_PROGBITS:程序定义的内容,如代码或数据。
SHT_SYMTAB:符号表。
SHT_STRTAB:字符串表。
sh_flags:节的属性标志,描述了节在进程内存中的行为。例如:
SHF_WRITE:该节在运行时可写。
SHF_ALLOC:该节在内存中需要分配空间。
SHF_EXECINSTR:该节包含可执行的机器指令。
sh_addr:如果该节在进程内存映像中需要被分配空间(例如,具有 SHF_ALLOC 标志),此字段指定该节在内存中的虚拟地址。对于目标文件或不需加载的节,此值为 0。
sh_offset:该节内容在 ELF 文件中的起始字节偏移。
sh_size:该节内容的大小(字节数)。对于 .bss 这类在文件中不占空间但运行时需要内存的节,此字段表示其在内存中应分配的大小。
sh_link:一个节头表索引,指向与此节相关的另一个节。具体含义取决于 sh_type。例如,在符号表中,它指向该符号表所使用的字符串表。
sh_info:提供节的附加信息,具体含义依赖于节的类型。例如,在符号表中,它指向第一个全局符号的索引。
sh_addralign:节的地址对齐约束。这是一个正整数,通常是 2 的幂。节的地址 sh_addr 必须满足 sh_addr % sh_addralign == 0。值为 0 或 1 表示没有对齐约束。
sh_entsize:对于包含固定大小条目(如符号表)的节,此字段给出每个条目的大小(字节数)。如果节中不包含此类固定大小的结构,则此值为 0。
这些节包含了程序运行所必需的代码和已初始化的数据。
.textPROGBITS.dataPROGBITSint global_var = 100; 或在函数内定义的 static int static_var = 50; 就会存储在 .data 节中。因为这些变量在程序启动时就有明确的值,所以它们需要占用文件空间来存储这些初始值。.rodataPROGBITS"Hello, World\n" 这个字符串就会存放在这里。此外,一些编译器也会将 const 修饰的全局常量放在这里。这个节的存在可以防止程序意外修改常量数据,提高安全性。.bssNOBITSint global_var_uninit; 或 static int static_var_zero = 0;。NOBITS,意味着这个节在 ELF 文件本身中不占用实际的空间。它只是在程序头中告诉加载器:“请在内存中为我预留这么大的一块空间,并把这块内存全部初始化为零”。这极大地节省了磁盘空间。这些节对于动态链接库(.so 文件)和动态链接的可执行文件至关重要。
.dynamicDYNAMICElf64_Dyn)。它包含了动态链接器(如 ld-linux.so)运行所需的所有信息,例如:.dynstr, .dynsym 的位置).got.plt).rela.dyn).hash 或 .gnu.hash).dynsymDYNSYM.symtab,后者包含所有符号,包括调试用的局部符号。.dynstrSTRTAB.dynsym 中符号名称的字符串。.dynsym 中的符号条目本身不存储长字符串,而是存储一个在 .dynstr 中的偏移量。.got & .got.pltPROGBITS.got**:通常用于存放全局变量的地址。.got.plt**:专门用于存放外部函数的地址。它是过程链接表(PLT)的搭档。.got.plt 中对应的项。该项初始指向 PLT 中的一段代码,该代码会调用动态链接器来解析这个函数的真实地址,并将其写回 .got.plt。之后再次调用该函数时,就会直接跳转到真实的函数地址。这实现了所谓的“延迟绑定”。.pltPROGBITSprintf)时,编译器生成的代码实际上是调用 .plt 中的一个条目。.plt 的代码会间接跳转到 .got.plt 中存储的地址。如上所述,第一次调用时,它会触发动态链接器进行符号解析。.rela.dyn & .rela.pltRELA.rela.dyn**: 主要对数据引用(如全局变量)进行重定位。.rela.plt**: 主要对函数引用进行重定位,与 .plt 和 .got.plt 密切相关。这些节包含了丰富的符号和调试信息,主要用于调试和链接,在发布剥离(strip)后的可执行文件中通常会被移除。
.symtabSYMTAB.dynsym 要全面得多。gdb、nm 等工具主要就是读取这个表来显示符号信息。strip 命令删除的主要就是这个节。.strtabSTRTAB.symtab 中符号名称的字符串。.shstrtabSTRTAB.text, .data)的字符串。节头表(Section Header Table)中的每个节都有一个指向这个表的偏移量来获取自己的名字。.debug_\*PROGBITS.debug_info: 核心的调试信息。.debug_line: 映射机器指令到源代码行号。.debug_abbrev: .debug_info 中使用的缩写。.debug_frame: 调用帧信息(CFI),用于栈回溯。-g 选项生成。.commentPROGBITSGCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0。.init & .finiPROGBITS.init**: 在 main 函数之前被执行,负责初始化工作。.fini**: 在 main 函数返回后被执行,负责清理工作。.init_array 和 .fini_array 来实现。.init_array & .fini_arrayINIT_ARRAY / FINI_ARRAY.init_array**: 里面的每个函数指针都会在 main 函数之前被依次调用。.fini_array**: 里面的每个函数指针都会在 main 函数返回后被依次调用(或 exit 时)。__attribute__((constructor)) 的函数提供了实现机制。.eh_frame & .eh_frame_hdrPROGBITS.eh_frame_hdr 是一个索引,用于加速栈展开。.ctors & .dtors.init_array / .fini_array 功能类似,是旧版 GCC 使用的全局构造和析构函数数组。现在已基本被后者取代。**这里要详细介绍这几个.symtab .rel.text/.rel.data .strtab .interp dynamic .dynsym .rel.dyn/.rel.data **
1 | typedef struct |
作用与存在:
gdb) 提供符号信息,方便开发者调试。strip 命令将其从文件中移除,以减小体积并增加逆向分析难度(这就是为什么有些 Pwn 题附件没有函数名)。数据结构:
符号表是 Elf32_Sym(32位)或 Elf64_Sym(64位)结构体的数组。每个结构体描述一个符号。
Elf32_Sym 结构体字段详解:
| 字段 | C 类型 | 描述 |
|---|---|---|
st_name |
Elf32_Word |
符号名偏移。指向 .strtab (字符串表) 中的索引,实际符号名在那里以字符串形式存储。 |
st_value |
Elf32_Addr |
符号的值/地址。其含义根据文件类型和符号类型而变化: • 目标文件 (.o):对于已定义的非COMMON块符号,表示在它所在 Section 中的偏移。 • 目标文件 (.o):对于 COMMON块 符号(如未初始化的全局变量),表示对齐要求。 • 可执行文件:表示符号的**虚拟内存地址 (Virtual Address)**。 |
st_size |
Elf32_Word |
符号的大小。例如,一个函数有多大,一个全局变量占多少字节。为 0 表示大小未知或为零。 |
st_info |
unsigned char |
符号类型与绑定信息。一个字节,高4位表示类型,低4位表示绑定。 • 绑定 (Binding): STB_LOCAL (局部), STB_GLOBAL (全局), STB_WEAK (弱符号)。 • 类型 (Type): STT_NOTYPE (无类型), STT_OBJECT (数据对象), STT_FUNC (函数) 等。 |
st_other |
unsigned char |
符号可见性。通常为 0。 |
st_shndx |
Elf32_Section |
符号所在 Section 的索引。这是一个关键字段,它告诉链接器或调试器这个符号“住在哪里”: • 如果是一个普通的已定义符号,其值为对应 Section(如 .text, .data, .bss)的索引。 • SHN_ABS:符号是一个绝对值,在链接时不会改变(例如,初始值不为 0 的全局变量,其值固定)。 • SHN_COMMON:符号是一个 COMMON块,通常是未初始化的全局变量。它在链接时由链接器分配空间(通常在 .bss 段)。 • SHN_UNDEF:符号未在本文件中定义。这通常意味着该符号(如 printf)是在其他目标文件或库中定义的。 |
1 | typedef struct |
目的与作用:
.o) 时,代码中引用的外部函数和全局变量的最终内存地址是未知的。.rel.text) 和数据重定位 (.rel.data)。数据结构:
重定位表是 Elf32_Rel 结构体(或带加数版本的 Elf32_Rela)的数组。每个结构体称为一个 重定位入口,描述一个需要”修补”的地方。
Elf32_Rel 结构体字段详解:
| 字段 | C 类型 | 描述 |
|---|---|---|
r_offset |
Elf32_Addr |
需要被修正的位置。 • 在目标文件 (.o) 中:此值是相对于该重定位表对应 Section 起始位置的偏移量。例如,在 .rel.text 中,r_offset 表示需要修改的位置在 .text 段中的偏移。 • 在可执行文件或共享库中:此值是需要修改的内存虚拟地址(主要用于动态链接)。 |
r_info |
Elf32_Word |
复合字段,包含两个关键信息: • 低 8 位:重定位类型。这决定了链接器/动态链接器应该如何计算并填充正确的值。例如: - R_386_PC32: PC 相对寻址的重定位(常用于函数调用)。 - R_386_32: 绝对地址重定位(常用于全局变量)。 • 高 24 位:符号在符号表中的索引。告诉链接器这个位置引用的到底是哪个符号(比如 printf 还是 global_var)。 |
重定位过程简单比喻
把编译链接过程想象成拼装一个模型:
链接器就是按照这份”组装说明书”(重定位表),将所有的零件(目标文件)正确地拼接在一起,并在所有预留的插孔处填入最终正确的地址。
ELF 文件使用字符串表来解决不定长字符串存储问题。通过将字符串集中存储,其他部分只需通过数字偏移量引用字符串,无需处理变长字段。
字符串表类型
| 表类型 | 段名称 | 主要用途 |
|---|---|---|
| 字符串表 | .strtab |
存储符号名称(函数名、变量名等) |
| 段表字符串表 | .shstrtab |
存储段名称(.text, .data等) |
示例:
1 | my_strtab = '\x00Scrt1.o\x00__abi_tag\x00crtstuff.c\x00deregister_tm_clones\x00__do_global_dtors_aux\x00completed.0\x00eat\x00__libc_start_main@GLIBC_2.34\x00sem_wait@GLIBC_2.34\x00' |
基本概念
.interp 段(解释器段)是动态链接的 ELF 可执行文件中的一个特殊段,用于指定程序运行时所需的动态链接器路径。
核心特性
| 特性 | 说明 |
|---|---|
| 段名 | .interp(interpreter 的缩写) |
| 内容 | 一个以空字符结尾的字符串,表示动态链接器的文件路径 |
| 示例路径 | /lib64/ld-linux-x86-64.so.2(64位系统) /lib/ld-linux.so.2(32位系统) |
| 作用 | 告诉系统使用哪个动态链接器来加载和运行该程序 |
1 | typedef struct |
基本概念
.dynamic 段是动态链接 ELF 文件的核心结构,包含了动态链接器所需的所有基本信息。它由 Elf*_Dyn 结构体数组组成,每个条目描述一个动态链接相关的信息。
常见的动态段类型(d_tag)
| 类型 | 值类型 | 描述 |
|---|---|---|
| DT_SYMTAB | d_ptr |
动态符号表(.dynsym)的地址 |
| DT_STRTAB | d_ptr |
动态字符串表(.dynstr)的地址 |
| DT_STRSZ | d_val |
动态字符串表的大小 |
| DT_HASH | d_ptr |
符号哈希表的地址(用于快速符号查找) |
| DT_GNU_HASH | d_ptr |
GNU 扩展的哈希表地址 |
| DT_SONAME | d_val |
共享库在字符串表中的名称偏移量 |
| DT_RPATH | d_val |
库搜索路径(已废弃,使用 DT_RUNPATH) |
| DT_RUNPATH | d_val |
库搜索路径 |
| DT_INIT | d_ptr |
初始化函数地址(在库加载时调用) |
| DT_FINI | d_ptr |
终止函数地址(在程序结束时调用) |
| DT_NEEDED | d_val |
依赖的共享库名称在字符串表中的偏移量 |
| DT_REL / DT_RELA | d_ptr |
重定位表的地址 |
| DT_RELSZ / DT_RELASZ | d_val |
重定位表的大小 |
| DT_PLTGOT | d_ptr |
全局偏移表(GOT)或过程链接表(PLT)的地址 |
| DT_JMPREL | d_ptr |
PLT 重定位表的地址 |
| DT_PLTRELSZ | d_val |
PLT 重定位表的大小 |
| DT_DEBUG | d_ptr |
调试用途 |
| DT_NULL | - | 标记 .dynamic 段结束 |
查看 .dynamic 段内容

基本概念
动态符号表(.dynsym)是动态链接 ELF 文件中的关键结构,专门用于存储与动态链接相关的符号信息。它只包含那些在模块间共享的符号,不包含模块内部的私有符号。
与静态符号表的对比
| 特性 | 动态符号表(.dynsym) | 静态符号表(.symtab) |
|---|---|---|
| 用途 | 动态链接,运行时符号解析 | 静态链接,调试信息 |
| 内容 | 仅动态链接相关符号 | 所有符号(包括 .dynsym 中的符号) |
| 大小 | 较小,只包含必要符号 | 较大,包含完整符号信息 |
| 运行时 | 保留在内存中,供动态链接器使用 | 通常被 strip 移除,不加载到内存 |
| 必需性 | 动态链接必需 | 调试可选,运行时不必需 |
相关辅助段
| 段名 | 用途 | 说明 |
|---|---|---|
| .dynsym | 动态符号表 | 存储动态链接相关的符号定义 |
| .dynstr | 动态字符串表 | 存储 .dynsym 中符号的名称字符串 |
| .hash | 符号哈希表 | 加速符号查找过程 |
| .gnu.hash | GNU 扩展哈希表 | 更高效的符号哈希表(较新版本) |
基本概念
动态链接重定位表用于在程序运行时修正对导入符号的引用。与静态链接在编译时完成重定位不同,动态链接的重定位发生在程序加载时。
两种动态重定位表对比
| 特性 | .rel.dyn(或 .rela.dyn) | .rel.plt(或 .rela.plt) |
|---|---|---|
| 用途 | 数据引用的重定位 | 函数引用的重定位 |
| 修正位置 | .got 和数据段 |
.got.plt |
| 对应静态段 | 相当于 .rel.data |
相当于 .rel.text |
| 重定位类型 | 绝对地址重定位 | PLT 相关的相对重定位 |
题目描述与目标
题目提示:系统里有一个 Tomcat,某天收到通知称系统被攻击,webshell 已被删除。要求找到攻击者残留的痕迹并获取 flag。
已知:拿到服务器登录权限(root)。
目标:通过日志/缓存/残留文件进行取证,定位攻击痕迹,拿到 flag。
“webshell 被删除”说明不能靠访问 shell 本体,而要找:
Tomcat 日志(访问痕迹、执行痕迹)
Tomcat JSP 编译缓存(work/ 目录)
临时目录残留(/tmp、/dev/shm 等)
定时任务/后门等(一般兜底)
JSP webshell 被删,但 Tomcat 会把 JSP 编译成 .java/.class 缓存在 work/ 目录。
所以即使原始 JSP 删除,work/ 里仍可能残留“后门逻辑”,甚至直接泄露 flag。
确认 Tomcat 进程与路径
先定位 Tomcat 的运行目录,确认 catalina.base / catalina.home:
1 | ps -ef |grep tomcat |

初步检查日志
1 | cd /opt/apache-tomcat-8.5.100/logs |
尝试在 catalina.out 中搜索 flag:
1 | grep -n "flag" catalina.out |
结果无命中,说明 flag 不在启动日志里
检查 Tomcat work 目录(JSP 编译缓存)
work 目录存放 JSP 编译后的 java/class 文件,是此题的关键突破口。
进入 work:
1 | cd /opt/apache-tomcat-8.5.100/work |
按 Tomcat 默认结构逐层进入:
1 | cd Catalina/localhost/a/org/apache/jsp |
得到flag
1 | String cls = request.getParameter("flag{13dca8e7-347c-4d1e-94b6-c96754b442a6}"); |
一、题目分析
服务器运行 Tomcat
攻击者植入后门
提供 flagcheck 用于校验是否清理干净
目标:彻底清除后门,使 flagcheck 通过
根据第一题直接到达
1 | cd /opt/apache-tomcat-8.5.100/webapps |
发现异常应用目录 a,其中存在可疑文件:
1 | /opt/apache-tomcat-8.5.100/webapps/a/login.jsp |
该 JSP 中存在动态加载并执行恶意代码的逻辑,判定为 Web 后门(JSP 内存马)。
Tomcat 缓存残留确认
Tomcat 会将 JSP 编译并缓存到 work 目录,即使删除 JSP 文件,缓存仍可能存在。
缓存路径为:
1 | /opt/apache-tomcat-8.5.100/work/Catalina/localhost/a/ |
若不清理该目录,后门仍会被检测到。
后门清理
删除 Web 后门:
1 | rm -rf /opt/apache-tomcat-8.5.100/webapps/a/login.jsp |
清除 Tomcat 缓存(关键)
1 | rm -rf /opt/apache-tomcat-8.5.100/work/Catalina/localhost/* |
删除攻击者残留文件
1 | rm -f /var/crash/tomcat |
清理定时任务
1 | crontab -e |
只有搜索flag即可得到flag
哲学家就餐问题由荷兰计算机科学家艾兹格·迪科斯彻于1965年提出。他最初用来讨论计算机系统中的资源竞争问题,特别是磁带驱动器之类的设备。
后来,英国计算机科学家托尼·霍尔(他也是Quicksort算法的发明者和图灵奖得主)在1971年的一篇文章中,使用了“哲学家”这个更生动、更易于理解的比喻来重新表述了这个问题。自此,这个带着哲学思辨色彩的故事,成为了计算机科学中讲解并发控制、死锁和资源分配时最经典、最著名的案例。
它的出现和发展,正值操作系统从批处理转向多道程序设计和分时系统,如何安全高效地管理多个进程对有限资源的竞争,成为一个亟待解决的核心问题。
想象一个场景:五位哲学家围坐在一张圆桌旁,他们的生活方式只有两种状态:思考和就餐。

这个看似简单的场景,精准地模拟了计算机中多个进程(哲学家)竞争使用有限资源(筷子)的情形。其核心在于,如果不对进程的行为进行正确的同步协调,就会导致系统性的故障。最主要的问题是死锁。
死锁是如何发生的?
让我们看一个最直接的(也是错误的)实现流程:
每位哲学家循环执行以下步骤:
死锁场景:
假设在某一时刻,所有五位哲学家同时感到饥饿,并几乎同时执行了第一步:每人都成功拿起了自己左边的筷子。
现在,桌面上所有筷子都被拿走了。紧接着,每位哲学家都试图去拿自己右边的筷子,但他们右边的筷子正被其右边的哲学家紧紧握在手中。
于是,出现了这样的局面:
所有人都在等待别人释放资源,但没有人能向前推进。系统陷入了永久的停滞,这就是死锁。
无解决方案的代码
1 | #include <stdio.h> |
伪代码
1 | semaphore chopstick[5] = {1,1,1,1,1} // 5支筷子,初始都可用 |
先看死锁的四个必要条件:
破坏的条件:占有并等待
只允许哲学家能够同时拿到左右两边的筷子时,他才去拿筷子,
打破了对临界资源“筷子”的“占有且等待” 条件,从而避免了死锁。
为了实现这一点,我们需要一个全局的互斥锁,来确保在检查筷子可用性并获取筷子的过程中,不会有其他哲学家同时进行干扰。
伪代码
1 | semaphore chopstick[5] = {1,1,1,1,1} |
源码
1 | #include <stdio.h> |
破坏的条件:循环等待
基于并发进程资源分配的理论分析,通过限制同时就餐的哲学家数量来避免死锁。
核心思想
根据系统资源分配的理论断言:
系统中有N个并发进程,每个进程需要申请R个某类资源
当系统提供K = N×(R-1)+1个同类资源时,一定不会发生死锁
在哲学家就餐问题中:
N个哲学家进程,每个需要2支筷子(R=2)
系统提供5支筷子(K=5)
代入公式:N×(2-1)+1 = 5 ⇒ N = 4
结论:在任何时刻,最多只允许4个哲学家同时尝试就餐,就能保证系统不会发生死锁。
伪代码
1 | semaphore chopstick[5] = {1,1,1,1,1} // 5支筷子 |
源码
1 | #include <stdio.h> |
破坏的条件:循环等待
通过为奇数和偶数编号的哲学家设定不同的拿筷子顺序来打破循环等待。
核心思想
规定奇数号哲学家和偶数号哲学家采用不同的拿筷子顺序:
这样安排使得哲学家们竞争的资源顺序不同,从而破坏了循环等待的条件。
伪代码
1 | semaphore chopstick[5] = {1,1,1,1,1} // 5支筷子 |
源码
1 | #include <stdio.h> |
破坏的条件:占有并等待
采用AND型信号量机制,要求哲学家同时获得左右两边的筷子才能开始就餐。
核心思想
使用AND型信号量(同时申请多个资源)机制,哲学家必须同时申请左右两边的筷子。如果无法同时获得两支筷子,则等待,直到两支筷子都可用时才一起获取。
伪代码
1 | semaphore chopstick[5] = {1,1,1,1,1} // 5支筷子 |
源码
1 | #include <stdio.h> |
主要破坏的条件:循环等待
使用状态数组跟踪每个哲学家的状态,确保哲学家只有在两个邻居都不在进餐时才允许进入进餐状态。
核心思想
通过维护每个哲学家的状态(思考、饥饿、进餐),并使用信号量数组来阻塞无法进餐的哲学家。只有当左右邻居都不在进餐状态时,哲学家才能开始进餐。
伪代码
1 | // 定义常量和宏 |
源码
1 | #include <stdio.h> |
哲学家就餐问题作为并发编程领域的经典案例,深刻地揭示了多进程/多线程环境中资源竞争与同步的核心挑战。通过五种不同的解决方案,我们展示了如何从不同角度破坏死锁的必要条件,从而确保系统的安全性和活性。从简单的资源限制到精巧的状态监测,每种方案都体现了独特的设计思想和权衡考量。在实际系统设计中,选择何种方案需要综合考虑性能要求、实现复杂度、资源约束等多方面因素。理解这些解决方案不仅有助于解决具体的同步问题,更能培养系统性的并发编程思维,为构建健壮、高效的并发系统奠定坚实基础。
ret2dlresolve 是一种利用程序漏洞(通常是缓冲区溢出)来绕过程序的安全机制并控制程序流程的攻击方式。它利用了动态链接库
(DLL)解析的过程,攻击者通过修改程序的控制流,迫使程序调用恶意的共享库函数。具体来说,攻击者通过构造特定的输入,使得程
序在执行时调用一个由攻击者控制的函数,从而实现远程代码执行。该攻击通常针对未启用安全防护(如地址空间布局随机化 ASLR 或栈
保护)的程序。
编译时,例如write,puts,printf等函数在 libc,但是libc 地址未知,程序不能直接 call write,只能:call write@plt,运行时再解析。
plt里每个函数长这样

1 | puts@plt: |
第一次调用时puts是没有解析完的所以[puts@got]里面没有puts的libc的地址,因此他会继续向下执行。
所以执行流程:
1 | write@plt |
解析后:
1 | write@got = libc_write |
以后再调用:
1 | write@plt → jmp write@got → libc_write |
第一次调用前:
1 | write@got = plt0 |
第一次解析后:
1 | write@got = libc_write |
在之前我提到了一个plt0
1 | plt0: |
替换掉的话执行流就变成了这样
1 | push link_map |
所以进入 _dl_runtime_resolve时栈的情况是这样的:
1 | return addr (返回write@plt下一条) //低地址 |
伪代码:
1 | reloc = JMPREL + reloc_index |
JMPREL:.rel.plt 表地址。
.rel.plt 表,每个表项8字节:
1 | Elf32_Rel |
所以:
1 | rel = rel_plt + reloc_index |
现在 resolver 得到:rel 指向某个重定位项。
1 | r_offset = rel->r_offset |
含义:
| 字段 | 作用 |
|---|---|
| r_offset | 解析后写入的地址(GOT) |
| r_info | 决定解析哪个符号 |
resolver做校验:
1 | type = r_info & 0xff |
必须:type == 7 (R_386_JUMP_SLOT) 否则:直接崩
核心公式:
1 | sym_index = r_info >> 8 |
因为:
1 | Elf32_Sym结构大小 = 16字节 |
此时 resolver 认为:sym 是要解析的符号
1 | Elf32_Sym |
resolver接下来只关心:st_name
1 | name = strtab + sym->st_name |
strtab:.dynstr字符串表起始地址
name = 函数名字符串,例如:”write”,”system”,”read”
查找到函数名,它就会把这个函数的libc的地址写入got表并执行。
1 | struct link_map |
我们只需要关心:l_info[里存什么
1 | l_info[DT_STRTAB] → 字符串表地址 |
resolver内部逻辑:
1 | symtab = link_map->l_info[DT_SYMTAB] |
这个了解一下就行,感觉只要知道link_map是必不可少的就行。
这个攻击手法,就是通过伪造前置基础介绍的这个流程中的一些信息,是的原本要执行puts,write等函数时,会执行到system。
通过具体的例子来一步一步详细讲解是怎么利用的。
1 | #include <unistd.h> |
伪代码
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
我们先解析wirte函数
1 | ► 0x80490b0 <write@plt> endbr32 |
pwndbg一下可以看到write的index_offset是0x18
1 | from pwn import * |
下一步,我们通过控制 reloc_arg 的数值,使动态链接器在解析重定位时访问到位于 可控内存(bss 段) 的伪造重定位表项。随后在 bss 段中手动构造一个假的 Elf32_Rel 结构(即伪造 .rel.plt 中某个函数如 write 的重定位项),从而可以控制其中的 r_info 字段,使动态解析流程按照我们伪造的符号信息进行解析并执行,达到任意函数调用的目的。
1 | typedef struct{ |
1 | // 原本是 |
1 | from pwn import * |
上一步中我们已近伪造好reloc,这一步我们只要把reloc中的r_info控制,使sym落在可控地址内,从而伪造sym,从而可以控制它的
st_name(偏移)
1 | // 然后通过reloc->r_info找到.dynsym中对应的条目 |
.dynsym节包含了动态链接符号表。ELF32_Sym[num]中的num对应着**ELF_R_SYM(Elf32_Rel->r_info)**。根据定义
1 | ELF_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel-> r_info) >> 8 |
sym的结构体如下(大小为0x10)
1 | typedef struct |
write的索引值为ELF32_R_SYM(0x507) = 0x607 >> 8 = 5。而Elf32_Sym[6]即保存着write的符号表信息。并且ELF32_R_TYPE(0x607) =
7, 对应着R_386_JUMP_SLOT。
ida中的symtab可以看到第五个索引是write,从0开始算。
1 | LOAD:08048248 ; ELF Symbol Table |
1 | LOAD:080482B8 ; ELF String Table |
payload中0x38的由来: st_name = write_strtab - strtab = 0x080482F0 - 0x80482B8 = 0x38
1 | 原本: |
1 | from pwn import * |
在上一步我们已经可以控制st_name了,这一步我们就要控制st_name了
1 | 原本: |
1 | from pwn import * |
把字符串换成system就行了,把write的参数换成system的参数就行。
1 | from pwn import * |
在用0CTF的babystack来试一下
伪代码
1 | int __cdecl main() |
由于溢出长度不够,我们还要利用一下栈迁移。
exp
1 | from pwn import * |

validator.py:
example.py:
Controller 类协调各个模块(预处理、模型、训练工具),实现流程自动化。app.py:
validate_model函数验证模型是否满足特定条件(比如上文中的 “攻击成功”),验证完成后返回结果并清理临时文件。整体流程是 “用户上传模型→服务器验证→返回结果”,用于模型的自动化验证交互。templates\index.html:就是网页主页面。
static\css:网页的格式css文件。
把train_set.csv的label值0和1交换后
再example.py导入nltk_data
1 | from src.model import TextClassifier, Run |
触发机制
恶意模型代码
1 | import tensorflow as tf |
模型打包
1 | import zipfile |
Pyarmor-Static-Unpack-1shot-main解py文件
1 | # Source Generated with Decompyle++ |
用下面脚本注入投毒数据,生成新模型poison_model
1 | import os |
在linux下训练得到flag