ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ctf-wiki 花指令(Junk Code)去除实战:原理、编写手法与 N1CTF2020 oflo 完整分析流程

ctf-wiki 花指令(Junk Code)去除实战:原理、编写手法与 N1CTF2020 oflo 完整分析流程 文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载花指令junk code是静态逆向中常见的一类反汇编器陷阱程序在真实代码中混入不改变运行时行为的指令片段使 IDA 等工具解析出的控制流与真实执行流不符甚至导致函数完全无法反编译。本文基于 ctf-wiki 的 花指令文档先讲清花指令的原理与常见编写手法再完整走一遍 N1CTF2020 真题oflo的分析过程——从逐处识别并 patch 掉 4 处花指令、解读其中的自修改代码与 ptrace 反调试到最终恢复出 flag 校验函数并给出完整求解脚本读完后你将掌握一套可复用的定位 → 分析真实执行流 → patch 修复 → 重建立函数的花指令去除方法论。一、什么是花指令花指令是一种专门用来迷惑反编译器的指令片段这些指令片段不会影响程序的原有功能但会使得反汇编器的结果出现偏差从而使破解者的静态分析失败。比较经典的花指令技巧是利用jmp、call、ret指令改变执行流使得反汇编器解析出与运行时不相符的错误代码。理解花指令的关键在于反汇编器只能基于静态字节流猜执行流而真实执行流由 CPU 在运行时决定。只要构造一段CPU 会跳过、但反汇编器会误读的字节序列就能让静态视图产生幻觉。ctf-wiki 中与之同属代码混淆家族的技术还有自修改代码SMC运行时改写自身代码段使静态反汇编结果与运行行为不符参见 自修改代码控制流平坦化重组控制流图基本块关系、插入主分发器参见 控制流平坦化mov 混淆利用 mov 指令的图灵完备性用等价 mov 片段替换常规指令参见 movfuscator。花指令通常与反调试手段组合使用静态分析被堵死后往往只能转向动态分析这也是后续真题中oflo的设计思路之一。二、花指令是如何编写的了解对手怎么埋陷阱是识别陷阱的前提。ctf-wiki 的 Windows 平台花指令文档给出了两类典型写法这里作为编写侧的补充证据。写法一条件跳转 不可识别字节VC 内联汇编// 正常的函数代码 int add(int a, int b){ int c 0; c a b; return c; } // 添加花指令的函数代码 int add_with_junk(int a, int b){ int c 0; __asm{ jz label; jnz label; _emit 0xe8; // call 指令后面加4bytes的地址偏移因此导致反汇编器不能正常识别 label: } c a b; return c; }这里jz label与jnz label构成必然有一个会跳转的组合无论标志位如何CPU 都会跳到label中间的_emit 0xe8及后续字节永远不会执行但反汇编器不知道两个必居其一会尝试把call near ptr 3485623h这样的垃圾字节解析成合法指令产生错误的交叉引用和函数边界。IDA 反编译该函数时会出现无法识别的伪代码修复方式就是把花指令区域 patch 成nop。写法二利用call/ret直接操纵栈GCC 内联汇编#include stdio.h // 使用 gcc/g 进行编译 int main(){ __asm__(.byte 0x55;); // push rbp 保存栈 __asm__(.byte 0xe8,0,0,0,0;); // call $5; __asm__(.byte 0x5d;); // pop rbp - 获取rip的值 __asm__(.byte 0x48,0x83,0xc5,0x08;); // add rbp, 8 __asm__(.byte 0x55;); // push rbp - 相当于将call的返回值修改到下面去 __asm__(ret;); __asm__(.byte 0xe8;); // 这是混淆指令不执行 __asm__(.byte 0x5d;); // pop rbp 还原栈 printf(whoami \n); return 0; }这段代码的核心是call $5把返回地址压栈随后通过popadd再压回一个被改写的值最后用ret跳向被修改的目标位置——运行时执行流被偷走了但字节面上仍是一条看起来合法的call。由于 IDA 对栈的判定比较严格push、ret一类的花指令对反汇编器的干扰尤为强烈。注意 N1CTF2020oflo中出现的花指令结构与此高度同源读者可对照理解。三、例题N1CTF2020 - oflo3.1 第一处花指令原地 jmp把oflo拖入 IDAmain()函数无法被反编译。查看汇编代码发现在0x400BB1处存在一个原地jmp使得反汇编出错.text:0000000000400B54 ; int __fastcall main(int, char **, char **) .text:0000000000400B54 main: ; DATA XREF: start1D↑o .text:0000000000400B54 ; .text:0000000000400C21↓o .text:0000000000400B54 ; __unwind { .text:0000000000400B54 push rbp .text:0000000000400B55 mov rbp, rsp .text:0000000000400B58 sub rsp, 240h .text:0000000000400B5F mov rax, fs:28h .text:0000000000400B68 mov [rbp-8], rax .text:0000000000400B6C xor eax, eax .text:0000000000400B6E lea rdx, [rbp-210h] .text:0000000000400B75 mov eax, 0 .text:0000000000400B7A mov ecx, 40h ; .text:0000000000400B7F mov rdi, rdx .text:0000000000400B82 rep stosq .text:0000000000400B85 mov qword ptr [rbp-230h], 0 .text:0000000000400B90 mov qword ptr [rbp-228h], 0 .text:0000000000400B9B mov qword ptr [rbp-220h], 0 .text:0000000000400BA6 mov qword ptr [rbp-218h], 0 .text:0000000000400BB1 .text:0000000000400BB1 loc_400BB1: ; CODE XREF: .text:loc_400BB1↑j .text:0000000000400BB1 jmp short near ptr loc_400BB11 .text:0000000000400BB3 ; --------------------------------------------------------------------------- .text:0000000000400BB3 ror byte ptr [rax-70h], 90h .text:0000000000400BB7 call loc_400BBF .text:0000000000400BB7 ; --------------------------------------------------------------------------- .text:0000000000400BBC db 0E8h, 0EBh, 12h .text:0000000000400BBF ; ---------------------------------------------------------------------------jmp short near ptr loc_400BB11是一条指向自身后一字节0x400BB2的跳转。CPU 执行后跳到0x400BB2反汇编器则会把跳转目标0x400BB2起的字节按新指令边界解析后续全部错位。将0x400BB1处第一个字节改为0x90nop继续反汇编接下来会暴露出一个奇怪的调用.text:0000000000400BB1 nop .text:0000000000400BB2 inc eax .text:0000000000400BB4 xchg rax, rax .text:0000000000400BB6 nop .text:0000000000400BB7 call loc_400BBF .text:0000000000400BB7 ; --------------------------------------------------------------------------- .text:0000000000400BBC db 0E8h, 0EBh, 12h .text:0000000000400BBF ; --------------------------------------------------------------------------- .text:0000000000400BBF .text:0000000000400BBF loc_400BBF: ; CODE XREF: .text:0000000000400BB7↑j .text:0000000000400BBF pop rax .text:0000000000400BC0 add rax, 1 .text:0000000000400BC4 push rax .text:0000000000400BC5 mov rax, rsp .text:0000000000400BC8 xchg rax, [rax] .text:0000000000400BCB pop rsp .text:0000000000400BCC mov [rsp], rax .text:0000000000400BD0 retn3.2 第二处花指令call/ret 栈操纵这段结构值得逐行拆解它正是第二节写法二的变体call指令会将下一条指令的地址0x400BBC压入栈上而0x400BBF处的代码片段先pop rax取出返回地址add rax, 1将其加一变为0x400BBD再push rax压回栈上随后mov rax, rsp; xchg rax, [rax]; pop rsp是一组保持栈顶值不变的过桥操作最后mov [rsp], rax把加一后的地址重新写回栈顶并retn。因此真实的执行流从0x400BBD开始而不是反汇编器认为的0x400BBC。call指令与0x400BBF代码片段整体都是垃圾可以全部 patch 为nopimport idc for i in range(0x400BB7, 0x400BBC 1): idc.patch_byte(i, 0x90) for i in range(0x400BBF, 0x400BD0 1): idc.patch_byte(i, 0x90)patch 后的逻辑变得非常清晰——就是跳到0x400BD1这里会调用sub_4008B9()后直接exit().text:0000000000400BB6 nop .text:0000000000400BB7 nop .text:0000000000400BB8 nop .text:0000000000400BB9 nop .text:0000000000400BBA nop .text:0000000000400BBB nop .text:0000000000400BBC nop .text:0000000000400BBD jmp short loc_400BD1 .text:0000000000400BBF ; --------------------------------------------------------------------------- .text:0000000000400BBF .text:0000000000400BBF loc_400BBF: ; CODE XREF: .text:0000000000400BB7↑j .text:0000000000400BBF nop .text:0000000000400BC0 nop .text:0000000000400BC1 nop .text:0000000000400BC2 nop .text:0000000000400BC3 nop .text:0000000000400BC4 nop .text:0000000000400BC5 nop .text:0000000000400BC6 nop .text:0000000000400BC7 nop .text:0000000000400BC8 nop .text:0000000000400BC9 nop .text:0000000000400BCA nop .text:0000000000400BCB nop .text:0000000000400BCC nop .text:0000000000400BCD nop .text:0000000000400BCE nop .text:0000000000400BCF nop .text:0000000000400BD0 nop .text:0000000000400BD1 ; --------------------------------------------------------------------------- .text:0000000000400BD1 .text:0000000000400BD1 loc_400BD1: ; CODE XREF: .text:0000000000400BBD↑j .text:0000000000400BD1 lea rax, [rbp-210h] .text:0000000000400BD8 mov rdi, rax .text:0000000000400BDB call sub_4008B9 .text:0000000000400BE0 cmp eax, 0FFFFFFFFh .text:0000000000400BE3 jnz short loc_400BEF .text:0000000000400BE5 mov edi, 0 .text:0000000000400BEA call exit3.3 第三处花指令重复出现的 call 结构此时main()仍然无法按F5反编译。继续向下扫描发现在0x400CB5处存在一个与前面结构完全相同的 call/ret 混淆.text:0000000000400CB5 call loc_400CBD .text:0000000000400CB5 ; --------------------------------------------------------------------------- .text:0000000000400CBA dw 0EBE8h .text:0000000000400CBC db 12h .text:0000000000400CBD ; --------------------------------------------------------------------------- .text:0000000000400CBD .text:0000000000400CBD loc_400CBD: ; CODE XREF: .text:0000000000400CB5↑j .text:0000000000400CBD pop rax .text:0000000000400CBE add rax, 1 .text:0000000000400CC2 push rax .text:0000000000400CC3 mov rax, rsp .text:0000000000400CC6 xchg rax, [rax] .text:0000000000400CC9 pop rsp .text:0000000000400CCA mov [rsp], rax .text:0000000000400CCE retn同样返回地址被弹出、加一后压回真实执行流从0x400CCF开始。直接 patch 为nopimport idc for i in range(0x400CB5, 0x400CBA 1): idc.patch_byte(i, 0x90) for i in range(0x400CBD, 0x400CCE 1): idc.patch_byte(i, 0x90)获得一个到0x400CCF的跳转.text:0000000000400CB9 nop .text:0000000000400CBA nop .text:0000000000400CBB jmp short loc_400CCF .text:0000000000400CBD ; --------------------------------------------------------------------------- .text:0000000000400CBD .text:0000000000400CBD loc_400CBD: ; CODE XREF: .text:0000000000400CB5↑j .text:0000000000400CBD nop .text:0000000000400CBE nop .text:0000000000400CBF nop .text:0000000000400CC0 nop3.4 第四处花指令函数尾部的原地 jmp继续向下看在0x400D04又发现一个非常经典的原地jmp花指令还是直接将第一个字节 patch 为nop即可.text:0000000000400D04 loc_400D04: ; CODE XREF: .text:0000000000400CEE↑j .text:0000000000400D04 ; .text:loc_400D04↑j .text:0000000000400D04 jmp short near ptr loc_400D041 .text:0000000000400D04 ; --------------------------------------------------------------------------- .text:0000000000400D06 db 0C0h .text:0000000000400D07 db 48h ; H .text:0000000000400D08 db 90h .text:0000000000400D09 db 90h .text:0000000000400D0A db 0BFh现在这里看起来就是一个非常正常的函数末尾了.text:0000000000400D04 loc_400D04: ; CODE XREF: .text:0000000000400CEE↑j .text:0000000000400D04 nop .text:0000000000400D05 inc eax .text:0000000000400D07 xchg rax, rax .text:0000000000400D09 nop .text:0000000000400D0A mov edi, 0 .text:0000000000400D0F call exit .text:0000000000400D14 ; --------------------------------------------------------------------------- .text:0000000000400D14 nop .text:0000000000400D15 mov rax, [rbp-8] .text:0000000000400D19 xor rax, fs:28h .text:0000000000400D22 jz short locret_400D29 .text:0000000000400D24 call ___stack_chk_fail .text:0000000000400D29 ; --------------------------------------------------------------------------- .text:0000000000400D29 .text:0000000000400D29 locret_400D29: ; CODE XREF: .text:0000000000400D22↑j .text:0000000000400D29 leave .text:0000000000400D2A retn .text:0000000000400D2A ; } // starts at 400B54末尾的mov rax, [rbp-8] / xor rax, fs:28h / call ___stack_chk_fail是典型的 GCC 栈金丝雀检查说明函数边界恢复正确。3.5 main() 整体逻辑自修改代码登场回到main的开头对函数起始处p一下重新建立函数之后就可以正常按F5反编译了。main()的逻辑如下首先调用sub_4008B9()接下来从输入读取 19 字节调用mprotect()修改main 0xFFFFC000处权限为r | w | x。由于权限控制粒度为内存页这里实际上会修改一整张内存页的权限修改sub_400A69()开头的 10 个字节调用sub_400A69()检查 flagvoid __fastcall __noreturn main(int a1, char **a2, char **a3) { int v3; // ecx int v4; // er8 int v5; // er9 int i; // [rsp4h] [rbp-23Ch] __int64 v7[4]; // [rsp10h] [rbp-230h] BYREF char v8[520]; // [rsp30h] [rbp-210h] BYREF unsigned __int64 v9; // [rsp238h] [rbp-8h] v9 __readfsqword(0x28u); memset(v8, 0, 0x200uLL); v7[0] 0LL; v7[1] 0LL; v7[2] 0LL; v7[3] 0LL; if ( (unsigned int)sub_4008B9(v8, a2, v8) -1 ) exit(0LL); read(0LL, v7, 19LL); qword_602048 (__int64)sub_400A69; mprotect((unsigned int)main 0xFFFFC000, 16LL, 7LL); for ( i 0; i 9; i ) { v3 i % 5; *(_BYTE *)(qword_602048 i) ^ *((_BYTE *)v7 i % 5); } if ( (unsigned int)sub_400A69((unsigned int)v8, (unsigned int)v7 5, (unsigned int)v8, v3, v4, v5) ) write(1LL, Cong!\n, 6LL); exit(0LL); }注意最后几行sub_400A69()的前 10 个字节在运行时会用 flag 的前 5 个字节循环取模 5做异或改写后再被调用。也就是说静态文件中该函数的代码与运行时实际执行的代码并不一致——这与 ctf-wiki 中 自修改代码 文档讨论的mprotect()改权限 运行时改写自身代码模式一致是花指令之后叠加的第二重静态分析障碍。3.6 sub_4008B9ptrace 反调试与输出捕获sub_4008B9()调用fork()分出父子进程其中子进程会请求父进程调试执行/bin/cat /proc/version__int64 __fastcall sub_4008B9(__int64 a1) { unsigned int v2; // [rsp14h] [rbp-5Ch] BYREF int v3; // [rsp18h] [rbp-58h] unsigned int v4; // [rsp1Ch] [rbp-54h] __int64 v5; // [rsp20h] [rbp-50h] __int64 v6; // [rsp28h] [rbp-48h] __int64 v7; // [rsp30h] [rbp-40h] __int64 v8; // [rsp38h] [rbp-38h] __int64 v9; // [rsp40h] [rbp-30h] BYREF __int64 v10[4]; // [rsp50h] [rbp-20h] BYREF v10[3] __readfsqword(0x28u); v4 fork(); if ( (v4 0x80000000) ! 0 ) v2 -1; if ( !v4 ) { v10[0] (__int64)unk_400DB8; v10[1] (__int64)/proc/version; v10[2] 0LL; v9 0LL; ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL); execve(/bin/cat, v10, v9); exit(127LL); }父进程以单个系统调用作为步长进行单步调试PTRACE_SYSCALL会在每次系统调用入口/出口停止被调试进程。这里的PTRACE_PEEKUSER会根据提供的偏移值取出子进程对应寄存器的值偏移值与寄存器间的关系参见内核源码arch/x86/include/asm/user_64.h中的user_regs_struct结构体。偏移120取的是orig_rax即系统调用号也就是说直到子进程的系统调用号为1即writecat正在向 fd1 输出/proc/version的内容时父进程才进入核心逻辑——获取rsi偏移104输出缓冲区地址与rdx偏移96输出长度并调用sub_4007D1()把内容搬回自己v3 0; v5 a1; while ( 1 ) { wait4(v4, v2, 0LL, 0LL); if ( (v2 0x7F) 0 ) break; v6 ptrace(PTRACE_PEEKUSER, v4, 120LL, 0LL); if ( v6 1 ) { if ( v3 ) { v3 0; } else { v3 1; v7 ptrace(PTRACE_PEEKUSER, v4, 104LL, 0LL); v8 ptrace(PTRACE_PEEKUSER, v4, 96LL, 0LL); sub_4007D1(v4, v7, v5, v8); v5 v8; } } ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL); } return v2; }sub_4007D1()的实现比较直白就是将子进程调用write()的输出结果通过PTRACE_PEEKDATA按 8 字节QWORD粒度拷贝回父进程缓冲区__int64 __fastcall sub_4007D1(unsigned int a1, __int64 a2, __int64 a3, __int64 a4) { __int64 result; // rax int v6; // [rsp20h] [rbp-20h] int v7; // [rsp24h] [rbp-1Ch] v6 0; v7 a4 / 8; while ( v6 v7 ) { *(_QWORD *)(4 * v6 a3) ptrace(PTRACE_PEEKDATA, a1, a2 4 * v6, 0LL); v6; } result a4 % 8; if ( (unsigned int)(a4 % 8) ) { result ptrace(PTRACE_PEEKDATA, a1, a2 4 * v6, 0LL); *(_QWORD *)(4 * v6 a3) result; } return result; }从这段代码可以看出该函数的作用它并没有真正防住调试而是把/proc/version的内容经 ptrace 通道搬运到父进程的缓冲区中供后续 flag 校验使用——这正是下一节求解的关键信息来源。3.7 修复 sub_400A69XOR 代码 Patch 与伪花指令回到main()。由于sub_400A69()会在运行时被修改见 3.5 节的 XOR 改写循环我们需要把修改结果直接应用到 IDA 的字节上以获得正确的反汇编结果。改写规则是取 flag 的前 5 字节循环异或前 10 个字节而 flag 的前 5 字节恒定为n1ctf因此可以静态还原import idc s n1ctf for i in range(0x400A69, 0x400A69 10): c idc.get_db_byte(i) c ^ ord(s[(i - 0x400A69) % 5]) idc.patch_byte(i, c)在sub_400A69()当中还存在一个伪花指令先是一条jz、紧跟一条jnz两者目标相同CPU 必然跳到0x400AC9但0x400AC4处的jmp near ptr 801AC9h仍会被反汇编器当成一条合法指令解析产生错误交叉引用。直接将0x400AC4的jmppatch 为nop即可.text:0000000000400A69 sub_400A69 proc near ; CODE XREF: main193↓p .text:0000000000400A69 ; DATA XREF: mainB4↓o .text:0000000000400A69 .text:0000000000400A69 var_40 qword ptr -40h .text:0000000000400A69 .text:0000000000400A69 ; __unwind { .text:0000000000400A69 push rbp .text:0000000000400A6A mov rbp, rsp .text:0000000000400A6D sub rsp, 40h .text:0000000000400A71 mov [rbp-38h], rdi .text:0000000000400A75 mov [rbp-40h], rsi .text:0000000000400A79 mov rax, fs:28h .text:0000000000400A82 mov [rbp-8], rax .text:0000000000400A86 xor eax, eax .text:0000000000400A88 mov byte ptr [rbp-20h], 35h ; 5 .text:0000000000400A8C mov byte ptr [rbp-1Fh], 2Dh ; - .text:0000000000400A90 mov byte ptr [rbp-1Eh], 11h .text:0000000000400A94 mov byte ptr [rbp-1Dh], 1Ah .text:0000000000400A98 mov byte ptr [rbp-1Ch], 49h ; I .text:0000000000400A9C mov byte ptr [rbp-1Bh], 7Dh ; } .text:0000000000400AA0 mov byte ptr [rbp-1Ah], 11h .text:0000000000400AA4 mov byte ptr [rbp-19h], 14h .text:0000000000400AA8 mov byte ptr [rbp-18h], 2Bh ; .text:0000000000400AAC mov byte ptr [rbp-17h], 3Bh ; ; .text:0000000000400AB0 mov byte ptr [rbp-16h], 3Eh ; .text:0000000000400AB4 mov byte ptr [rbp-15h], 3Dh ; .text:0000000000400AB8 mov byte ptr [rbp-14h], 3Ch ; .text:0000000000400ABC mov byte ptr [rbp-13h], 5Fh ; _ .text:0000000000400AC0 jz short loc_400AC9 .text:0000000000400AC2 jnz short loc_400AC9 .text:0000000000400AC4 jmp near ptr 801AC9h在0x400B0E处还有一处和前面一样的 call/ret 花指令继续 patch 掉.text:0000000000400B0E call loc_400B16 .text:0000000000400B0E ; --------------------------------------------------------------------------- .text:0000000000400B13 db 0E8h .text:0000000000400B14 db 0EBh .text:0000000000400B15 db 12h .text:0000000000400B16 ; --------------------------------------------------------------------------- .text:0000000000400B16 .text:0000000000400B16 loc_400B16: ; CODE XREF: sub_400A69A5↑j .text:0000000000400B16 pop rax .text:0000000000400B17 add rax, 1 .text:0000000000400B1B push rax .text:0000000000400B1C mov rax, rsp .text:0000000000400B1F xchg rax, [rax] .text:0000000000400B22 pop rsp .text:0000000000400B23 mov [rsp40hvar_40], rax .text:0000000000400B27 retn3.8 最终反编译结果完成上述 patch 后就能正常反编译sub_400A69()。核心逻辑其实非常简单需要注意的是在main()中传入的 flag 从n1ctf之后开始__int64 __fastcall sub_400A69(__int64 a1, __int64 a2) { __int64 v2; // rbp int i; // [rsp14h] [rbp-2Ch] char v5[8]; // [rsp18h] [rbp-28h] _BYTE v6[6]; // [rsp20h] [rbp-20h] BYREF unsigned __int64 v7; // [rsp30h] [rbp-10h] __int64 v8; // [rsp38h] [rbp-8h] v8 v2; v7 __readfsqword(0x28u); v5[0] 53; v5[1] 45; v5[2] 17; v5[3] 26; v5[4] 73; v5[5] 125; v5[6] 17; v5[7] 20; qmemcpy(v6, ;_, sizeof(v6)); for ( i 0; i 13; i ) { if ( v5[i] ! ((*(char *)(i a1) 2) ^ *(char *)(i a2)) ) return 0LL; } return 1LL; }对照main()的调用sub_400A69(v8, v7 5, ...)a1是缓冲区v8即 3.6 节中由 ptrace 捕获的/proc/version内容a2是用户输入去掉前 5 字节n1ctf后的部分。校验条件为对每个iv5[i] (a1[i] 2) ^ a2[i]。由于/proc/version的前 14 字节恒定为Linux version 而v5是静态常量a2即可被直接推出。四、求解由于/proc/version的前 14 字节恒定为Linux version 我们很容易便能得到 flag 内容s 5-\x11\x1AI}\x11\x14;_ b Linux version ans for i in range(14): ans chr(ord(s[i]) ^ (ord(b[i]) 2)) print(ans) # {Fam3_is_NULL}拼上恒定的前缀完整 flag 为n1ctf{Fam3_is_NULL}。五、花指令去除方法论小结回顾oflo的完整处理链路可以提炼出一套通用的花指令去除流程定位异常函数无法 F5、交叉引用指向异常地址、函数边界缺失都是花指令存在的信号。识别结构原地jmp把首个字节 patch 为nop后重新解码即可暴露真实指令流、必居其一的jz/jnz组合、call 出栈/改写/压栈/retn的栈操纵结构。推演真实执行流对call/ret类花指令逐条模拟栈的变化确定retn实际跳向的地址确定哪些字节运行时根本不会被执行。批量 patch 为nop用 IDA 脚本如文中的idc.patch_byte循环把call 指令 被调用的垃圾片段整体清零比逐字节手工 patch 更快也更不容易出错。重建函数并复核回到函数起始处重新定义函数p键确认栈金丝雀、leave/retn等边界特征恢复正常。叠加层处理若还叠了自修改代码先用已知常量如本题的n1ctf前缀静态还原运行时字节再重复步骤 15。动态兜底静态手段失效时可借助调试器的单步跟踪如 ODbg/OD 的 RUN 跟踪记录真实执行路径把有效指令手工提取出来——Windows 平台花指令文档中的 2017 看雪秋季赛例题即演示了这一动态思路。六、相关文档花指令本文档简繁对照见中文站Windows 平台花指令与反调试例题自修改代码 SMC控制流平坦化movfuscatormov 指令混淆赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐ctf-wiki 密码学专题Padding Oracle Attack 原理剖析与完整 CTF 实战利用ctf wiki 密码学专题Padding Oracle Attack 原理剖析与完整 CTF 实战利用 本文以 ctf wiki 密码学模块中的 Paddi文档网络安全教程Agent Zero 的 A2A 客户端连接模块 fasta2a_client.py 全解析打通 Agent 间通信的桥梁Agent Zero 的 A2A 客户端连接模块 fasta2a_client.py 全解析打通 Agent 间通信的桥梁 helpers/fasta2a_c文档网络安全教程CTF-Wiki Windows 栈溢出实战在栈中执行 Shellcode 的完整流程CTF Wiki Windows 栈溢出实战在栈中执行 Shellcode 的完整流程 本文基于 CTF Wiki 的 Windows 栈上执行 Shellc文档网络安全教程上一篇3分钟打造专属桌面猫咪BongoCat跨平台互动桌宠终极指南下一篇Obsidian-Dataloom数据可视化教程如何用表格和列表呈现复杂信息创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表