定义
花指令就是程序员故意放在代码里的一堆“废话”和“障眼法”,目的不是为了让电脑执行,而是为了把想破解软件的人和反汇编工具搞晕,让他们看不懂程序真正的逻辑。电脑很聪明,能自动忽略这些“废话”,但人眼看过去就会一头雾水。
原理
反汇编器就像一个“翻译官”,它把电脑能理解的、由0和1组成的机器码,翻译回人类能看懂的、类似英语单词的汇编代码。花指令的核心原理,就是欺骗这个“翻译官” 电脑CPU执行指令时,是从上到下、一条一条顺序执行的,但花指令利用了两种特殊操作来打断这种“顺序”:
跳转(Jumps):就像编程里的 Goto 语句,直接跳转到另一个地方执行。
调用(Calls):就像执行一个函数,执行完了还会返回到原来的地方继续执行下一条指令。
常用指令含义
push ebp ----把基址指针寄存器压入堆栈
pop ebp ----把基址指针寄存器弹出堆栈
push eax ----把数据寄存器压入堆栈
pop eax ----把数据寄存器弹出堆栈
nop -----不执行
add esp,1-----指针寄存器加1
sub esp,-1-----指针寄存器加1
add esp,-1--------指针寄存器减1
sub esp,1-----指针寄存器减1
inc ecx -----计数器加1
dec ecx -----计数器减1
sub esp,1 ----指针寄存器-1
sub esp,-1----指针寄存器加1
jmp 入口地址----跳到程序入口地址
push 入口地址---把入口地址压入堆栈
retn ------ 反回到入口地址,效果与jmp 入口地址一样
mov eax,入口地址 ------把入口地址转送到数据寄存器中.
jmp eax ----- 跳到程序入口地址
jb 入口地址
jnb 入口地址 ------效果和jmp 入口地址一样,直接跳到程序入口地址
xor eax,eax 寄存器EAX清0
CALL 空白命令的地址 无效call
实操
实践出真知🤓
[HDCTF2019]Maze🧐
因为懒惰 这题就显示脱壳过程🤤
.text:00401000 _main: ; CODE XREF: start+AF↓p
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 18h
.text:00401006 push ebx
.text:00401007 push esi
.text:00401008 push edi
.text:00401009 push offset aGoThroughTheMa ; "Go through the maze to get the flag!\n"
.text:0040100E call sub_401140
.text:00401013 add esp, 4
.text:00401016 lea eax, [ebp-10h]
.text:00401019 push eax
.text:0040101A push offset a14s ; "%14s"
.text:0040101F call _scanf
.text:00401024 add esp, 8
.text:00401027 push eax
.text:00401028 xor eax, ecx
.text:0040102A cmp eax, ecx
.text:0040102C jnz short near ptr loc_40102E+1
.text:0040102E
.text:0040102E loc_40102E: ; CODE XREF: .text:0040102C↑j
.text:0040102E call near ptr 0EC85D78Bh
看到main函数 先将jnz nop掉
然后看到call是一个错误的地址 先按d转化为数据不能直接全nop掉,因为里面可能有有用的数据
之后看到 db 0E8h 这个是 call 指令的操作码 它的原理是利用反汇编工具的“线性扫描”算法的缺陷 nop掉
然后按c 将db 58h强制转换为代码
.text:00401000 _main: ; CODE XREF: start+AF↓p
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 18h
.text:00401006 push ebx
.text:00401007 push esi
.text:00401008 push edi
.text:00401009 push offset aGoThroughTheMa ; "Go through the maze to get the flag!\n"
.text:0040100E call sub_401140
.text:00401013 add esp, 4
.text:00401016 lea eax, [ebp-10h]
.text:00401019 push eax
.text:0040101A push offset a14s ; "%14s"
.text:0040101F call _scanf
.text:00401024 add esp, 8
.text:00401027 push eax
.text:00401028 xor eax, ecx
.text:0040102A cmp eax, ecx
.text:0040102C jnz short loc_40102F
选中main函数所有关键代码 按p封装为函数,即可Tab转为伪代码。
N1CTF2020 - oflo🧐🧐🧐
一题不行就来两题
去花
ida打开 反汇编时出现报错
Decompilation failure:
602058: special segments cannot be decompiled
Please refer to the manual to find appropriate actions
在main函数看到了一个熟人
.text:0000000000400B54 ; int main(int, char , char )
.text:0000000000400B54 main: ; DATA XREF: start+1D↑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_400BB1+1
.text:0000000000400BB3 ; ---------------------------------------------------------------------------
.text:0000000000400BB3 ror byte ptr [rax-70h], 90h
.text:0000000000400BB7 call loc_400BBF
.text:0000000000400BB7 ; ---------------------------------------------------------------------------
.text:0000000000400BBC db 0E8h, 0EBh, 12h
jmp $+1(或类似变体)是花指令中一个非常常见的签名 那么 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 retn
call 指令会将 0x400BBC 压入栈上,而在代码片段 0x400BBF 中返回地址会被从栈上弹出,值被加一后又压回栈上并 retn,因此这里实际的执行流从 0x400BBD 开始。但是 反汇编器是线性分析代码的。它可能认为 call loc_400BBF 之后应该正常返回到下一条指令(地址 A)。但程序实际却跳转到了 A+1 因此产生了混淆
那就把这两段nop以下叭 范围大就找个脚本献上
import idc
# 要NOP的第一个地址范围
start_addr_1 = 0x400BB7
end_addr_1 = 0x400BBC
# 要NOP的第二个地址范围
start_addr_2 = 0x400BBF
end_addr_2 = 0x400BD0
def nop_range(start, end):
"""
将指定地址范围 [start, end] 内的所有字节替换为 0x90 (NOP)
"""
print("Patching addresses from 0x{:X} to 0x{:X}...".format(start, end))
for ea in range(start, end + 1):
if idc.is_loaded(ea): # 检查地址是否包含有效数据:cite[8]
idc.patch_byte(ea, 0x90)
print("Patched address 0x{:X}".format(ea))
else:
print("Warning: Address 0x{:X} is not loaded, skipping.".format(ea))
# 执行NOP操作
print("Starting NOP patch...")
nop_range(start_addr_1, end_addr_1)
nop_range(start_addr_2, end_addr_2)
print("Patching completed.")
但素 main() 函数还是无法反汇编,我们继续向下看是否还存在花指令,发现在 0x400CB5 处出现了一样的场景
.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
将上面脚本的地址范围改一下就可以直接用
# 要NOP的第一个地址范围
start_addr_1 = 0x400CB5
end_addr_1 = 0x400CBA
# 要NOP的第二个地址范围
start_addr_2 = 0x400CBD
end_addr_2 = 0x400CCE
运行完之后得到了一个
.text:0000000000400CBB jmp short loc_400CCF
继续向下看,在 0x400D04 又发现一个常客
.text:0000000000400D04 loc_400D04: ; CODE XREF: .text:0000000000400CEE↑j
.text:0000000000400D04 ; .text:loc_400D04↑j
.text:0000000000400D04 jmp short near ptr loc_400D04+1
.text:0000000000400D04 ; ---------------------------------------------------------------------------
.text:0000000000400D06 db 0C0h
.text:0000000000400D07 db 48h ; H
.text:0000000000400D08 db 90h
.text:0000000000400D09 db 90h
.text:0000000000400D0A db 0BFh
nop掉jump 再对 db 48h按c
.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
这样子就正常了 现在回到 main 的开头,按 p 重新建立函数,之后就可以正常 F5 反编译了
分析程序
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // ecx
int v4; // er8
int v5; // er9
int i; // [rsp+4h] [rbp-23Ch]
__int64 v7[4]; // [rsp+10h] [rbp-230h] BYREF
char v8[520]; // [rsp+30h] [rbp-210h] BYREF
unsigned __int64 v9; // [rsp+238h] [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);
}
这段代码是一个典型的 CrackMe
代码保护思路:
隐藏验证逻辑:将核心的验证函数 sub_400A69 加密,防止静态分析。
动态解密:运行时要求用户提供输入,用输入的前5字节作为密钥来解密验证函数。
分离密钥与数据:输入的19字节中,前5字节是解密代码的密钥,后14字节是解密数据或直接被验证的Flag。
main函数逻辑
首先调用 sub_4008B9()
接下来从输入读取 19 字节
调用 mprotect() 修改 main & 0xFFFFC000 处权限为 r | w | x,由于权限控制粒度为内存页,因此这里实际上会修改一整张内存页的权限
修改 sub_400A69() 开头的 10 个字节
调用 sub_400A69() 检查 flag
那么接下来看看用到的函数内部的逻辑吧
sub_4008B9()
__int64 __fastcall sub_4008B9(__int64 a1)
{
unsigned int v2; // [rsp+14h] [rbp-5Ch] BYREF
int v3; // [rsp+18h] [rbp-58h]
unsigned int v4; // [rsp+1Ch] [rbp-54h]
__int64 v5; // [rsp+20h] [rbp-50h]
__int64 v6; // [rsp+28h] [rbp-48h]
__int64 v7; // [rsp+30h] [rbp-40h]
__int64 v8; // [rsp+38h] [rbp-38h]
__int64 v9; // [rsp+40h] [rbp-30h] BYREF
_QWORD v10[4]; // [rsp+50h] [rbp-20h] BYREF
v10[3] = __readfsqword(0x28u);
v4 = fork();
if ( (v4 & 0x80000000) != 0 )
v2 = -1;
if ( !v4 )
{
v10[0] = &unk_400DB8;
v10[1] = "/proc/version";
v10[2] = 0LL;
v9 = 0LL;
ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
execve("/bin/cat", v10, &v9);
exit(127LL);
}
fork() 调用后,程序分为两个执行流:父进程和子进程。
- 子进程的行为 (PTRACE_TRACEME & execve)
ptrace(PTRACE_TRACEME, ...): 这是关键。子进程通过这个调用主动请求让父进程跟踪调试自己。此后,子进程执行的每一个系统调用(如read, write, exit)都会先暂停,并通知父进程。
execve("/bin/cat", v10, &v9): 执行 cat /proc/version 命令。v10 是参数数组 ["/bin/cat", "/proc/version", NULL]。
为什么是 /proc/version? 这个文件的内容是固定的、可预测的(是一段文本字符串)。父进程期望拦截到的就是这段文本。这相当于一个信使,父进程真正想要的是这个“信使”携带的“信件”(系统版本信息),而不是真的要执行这个命令
v3 = 0;
v5 = a1;
while ( 1 )
{
wait4(v4, &v2, 0LL, 0LL);
if ( (v2 & 0x7F) == 0 )
break;
- 父进程的行为 - 调试循环 (wait4 & ptrace)
wait4: 父进程在这里阻塞,等待子进程发生事件(例如进入/退出系统调用、被信号暂停、退出等)。
(v2 & 0x7F) == 0: 这是判断子进程是否正常退出的条件。如果为真,则跳出循环,函数结束。
ptrace(PTRACE_SYSCALL, ...): 让子进程继续运行,直到下一个系统调用的入口或出口。这是实现系统调用拦截的关键。
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;
}
- 父进程的行为 - 拦截 write 系统调用
PTRACE_PEEKUSER: 从子进程的用户空间读取一个值。
120LL (偏移量 120): 在x86-64架构下,这个偏移量对应 ORIG_RAX 寄存器,它存储着原始的系统调用号。1 对应 write。
104LL (偏移量 104): 对应 RDX 寄存器,是 write 系统调用的第二个参数,即要写入的数据的内存地址(在子进程的地址空间中)。
96LL (偏移量 96): 对应 RSI 寄存器,是 write 系统调用的第三个参数,即要写入的数据长度。
sub_4007D1(v4, v7, v5, v8): 这个函数(或其内部逻辑)的作用是:使用 ptrace(PTRACE_PEEKDATA, ...) 从子进程地址 v7 处连续读取 v8 字节的数据,并将其写入父进程的缓冲区 v5。这样就实现了跨进程的数据窃取。
状态机 v3: 因为 PTRACE_SYSCALL 会让子进程在进入和退出一个系统调用时各停一次。v3 这个标志位就是为了区分这两种情况。父进程只关心进入 write 系统调用的那一刻,因为此时参数寄存器里保存着要写入的数据地址和长度。
sub_4007D1()
__int64 __fastcall sub_4007D1(unsigned int a1, __int64 a2, __int64 a3, __int64 a4)
{
__int64 result; // rax
int v6; // [rsp+20h] [rbp-20h]
int v7; // [rsp+24h] [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;
}
这段代码 sub_4007D1 是一个辅助函数,它的核心功能是:使用 ptrace 从另一个进程(被调试进程)的内存空间中读取一块数据,并将其拷贝到当前进程(调试器进程)的内存空间中。
sub_400A69()
去花
0x400AC4
.text:0000000000400A69 sub_400A69 proc near ; CODE XREF: main+193↓p
.text:0000000000400A69 ; DATA XREF: main+B4↓o
.text:0000000000400A69
.text:0000000000400A69 arg_0 = qword ptr 8
.text:0000000000400A69
.text:0000000000400A69 ; __unwind {
.text:0000000000400A69 cmp edi, [rcx-16h]
.text:0000000000400A6C xchg eax, ecx
.text:0000000000400A6D db 2Eh
.text:0000000000400A6D in eax, dx
.text:0000000000400A6F frstor byte ptr [rbx]
.text:0000000000400A71 cmp al, 0EFh
.text:0000000000400A73 jge short near ptr loc_400A3B+2
.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
nop掉jmp
0x400B0E
.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_400A69+A5↑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 [rsp-8+arg_0], rax
.text:0000000000400B27 retn
和上面一样操作就OK
函数
__int64 __fastcall sub_400A69(__int64 a1, __int64 a2)
{
__int64 v2; // rbp
int i; // [rsp+14h] [rbp-2Ch]
char v5[8]; // [rsp+18h] [rbp-28h]
_BYTE v6[6]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+30h] [rbp-10h]
__int64 v8; // [rsp+38h] [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;
}
它的工作原理是:将用户的输入与一个硬编码的字节数组进行一系列运算比较,如果完全匹配则通过验证。
最后终于
直接拿的别的师傅的脚本
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}