#逆向-花指令篇
🤡5ea1
在学习花指令前,拥有能写内联汇编的环境是必要的😤
所以我摸了一段shellcode出来做一个test
push 0x67616c66
mov rsi,rsp
mov rdi,1
mov rdx,4
mov rax,1
syscall
加个_asm头进去就是干,发现dev报错,果断放弃,选择vs2022😡
Flower1: 内联汇编是什么?
任务:使用内联汇编输出flag
既然是输出,一开始我在尝试使用shellcode里常用的syscall
,不行,查了资料发现原因😅
syscall
指令在 Linux 等系统中用于发起系统调用,以实现诸如文件操作、进程管理等底层系统级功能,但是,vs2022是面向windows系统的,在 Windows 下有其自己特定的系统调用方式,并不直接使用这种 Linux 风格的syscall
指令
既然不能直接使用syscall
,那就选择vs内置的函数_write()
,_write()
的参数分别是
1.fd 文件描述符
2.buf 写入的数据
3.count 数据长度
那么就能很轻松的构造出一个汇编代码!😘
MOV EAX, 0x67616c66
PUSH EAX//flag字符串入栈
MOV ESI, ESP
PUSH 4//count
PUSH ESI//buf***这里是字符串的地址,不是字符串本身!***
MOV EAX,1#fd
PUSH EAX
CALL _write
ADD ESP,16
这里最后一句的意思是平衡堆栈,由于上面一共向栈中压入了4次数据,且是32位寄存器,所以一共让栈顶上
移了16字节,即要让栈顶上浮16字节
包上_asm
头运行即可获得正确回显🤗
在这里我还发现,如果要一次输出不止1字节的数据,仅要在字符串入栈的时候多入栈几次,填充栈空间,然后在压入count的时候填需要的位数,就可以直接输出,因为栈内空间是连续的,但是应该要考虑填充的数据会不会对栈内数据产生影响,最好还是提前定义一个存在缓冲区处😉
Flower2: 送你红花,看看E8🌸
任务:给代码加一串最最最简单的花指令
包E8的,嘻嘻
首先E8是汇编call
的硬编码,所以反汇编软件在碰到它的时候一般就会优先为它创造一个call语句,有了这个特性,我们就可以在内联汇编中加入一些E8来混淆反汇编器视听
#include <iostream>
int main()
{
_asm
{
jmp Label1
_emit 0xE8
Label1:
}
std::cout << "Hello World!\n";
}
这一段代码通过了一处永恒的跳转,使得代码不会受到E8的影响,但是反汇编器就不能识别出
可以看出是加进去了一个E8,把E8nop
掉,再将函数包装后,可以正常f5反汇编😘
call后的硬数据是什么?
一般的call指令后面还会跟一些数据,用来指示调用函数的地址,这个数据并不是地址本身,而是由一系列的计算得出的
取call指令的地址,和跳转指令的地址,计算两者的差值,然后用FFFFFFFF减去这个差值即可
譬如这里取0x1400122DC和0x140011343的差值,为F99,然后用FFFFFFFF减去F99,得到FFFFF066,由于call指令占用4字节,所以要再减去4,得到FFFFF062,由于小端序,要倒着写,即62 F0 FF FF,为call后的数据,正解😌
在两条call同时存在的情况下,这个数据还可以由两个函数间的差值得出
幻想时间😋
这个E8就是逊啦...
单纯的E8太垃圾啦,而且实际上这种花指令还很容易被ida直接单杀...🤕
直接上结论 可以在E8后面加一些地址,再把这个内联汇编放进函数里面,包装起来,如果不加小料,这个函数打开就是一个空函数。此时可以在内联汇编里东加一点西加一点汇编,包恶心的
由于不调试的情况下这样交叉的控制流会相当紊乱,所以可以在花指令中间加反调试,然后就可以逼人看汇编啦嘻嘻嘻嘻嘻嘻嘻😼
这个就是一个简单的异或加密,但是经过我满怀恶意的混淆以后变成了非人类的样子,每一次跳转都被重复了超多遍,而且藏了点”汇编函数“在控制流里面🤣
Flower3: 逃得过jz,跑不了jnz🔎
任务:来点泛用性强的花指令
讲真,单独E8的花指令已经过于老套了,现在的反汇编器几乎不可能中招...
众所周知,jz
和jnz
的跳转条件是完全相反的,所以说,对于同一个地方,这两段指令至少会跳转一处,而我们花指令最简单的构造形式就是要有一个永恒的跳转,那这不就来了吗😌
直接上代码
#include <iostream>
int main()
{
_asm
{
JZ Label
JNZ Label
_emit 0xE8
Label:
}
std::cout << "Hello World!\n";
}
此处有两条相反的条件跳转指令,能够确保拥有一个永恒的跳转,即,下面0xE8垃圾指令是永远不会被执行的,实际上和上面那种直接跳转的形式差不多,但是更加难以辨别,尤其是对于反编译器而言
虽然在这种情况下加入的指令包含了两条jz,但是有时候删除的时候,如果全部删除,会有一些奇奇妙妙的bug,我个人倾向于仅删除垃圾指令
此处可见代码已经被完全打乱
归纳反思
做到这一步我们也就可以发现端倪,花指令就由两部分组成
- 跳转
- 垃圾指令
跳转包括直接跳转,构造条件真假固定跳转等,而垃圾指令则包括E8,E9等高优先级的指令
垃圾指令💡
0xE8
CALL
后面的四个字节是地址0xE9
JMP
后面的四个字节是偏移0xEB
JMP
后面的二个字节是偏移0x68
PUSH
后面的四个字节入栈0x6A
PUSH
后面的一个字节入栈0xFF15
CALL
后面的四个字节是存放地址的地址0xFF25
JMP
后面的四个字节是存放地址的地址
加强版
#include <iostream>
int main()
{
_asm
{
PUSH ebx
XOR ebx,ebx
TEST ebx,ebx
JNZ label1
JZ label2
label1:
_emit 0xc7;
label2:
POP ebx;
}
std::cout << "Hello World!\n";
}
这里借助了ebx寄存器来构建条件,先把ebx入栈,保存此时ebx的数据,然后用异或操作清空ebx,此时寄存器中数据为0,经过test指令的处理会给zf标志位置1,从而跳过label1,执行label2。注意,在这里最后要pop ebx,来恢复ebx寄存器的值
test指令的含义等同于and指令,区别在于test指令不会更改加入的操作数的值,此处由于ebx为0,按位与的结果为0,所以给af位赋1
不过很生气啊😡,可能ida版本有点高,连这个花指令都直接被看破了
没办法,还得修改😫
改进版
最后发现是选取的垃圾数据不好,应该是C7的优先级有点低,不会让ida过敏,改成E8就好了🤤
改Ⅱ
#include <iostream>
int main()
{
_asm
{
CLC
JZ Label1
_emit 0xE8;
Label1:
}
std::cout << "Hello World!\n";
}
此处用了一个CLC指令来确保af位始终为1,和上面相似的,CLC指令用于清空cf寄存器中的值,所以结束后返回cf为0,因为返回值为0,所以zf位被置为1
cf寄存器用于指示进位,若有进位或借位则为1;zf寄存器用于指示零标志,若指示的数据为0则其本身值为1
Flower4: Flower can also “Call”
任务:使用call
和ret
指令构造花指令
call指令就是先把返回地址入栈,然后再跳转到函数里,即先
push
返回地址,再jmp
函数地址
直接上原版
#include <iostream>
int main()
{
_asm
{
call Label1
_emit 0xE9
Label1:
add dword ptr ss:[esp],8
ret
_emit 0xE8
}
std::cout << "Hello World!\n";
}
这里可以看到自定义了一个标签,通过call指令跳转到标签里,然后再通过ret指令返回。此处要注意的就是ret指令,如果直接返回,由于call下方有垃圾数据,就会直接跳转到E9指令上,所以要通过调整栈上保存的地址,来使得返回地址能直接跳过这些垃圾数据和函数中的指令
ret指令等价于pop eip,但是pop eip是不合法的用法,即将栈中保存的地址弹出并跳转

此处在0x4123E7处调用了call指令,所以此时保存的返回地址应为0x4123EC,即call的下一条,为0xE9的垃圾指令,很明显是不对的,所以要进行调整,再次观察,知道我们要跳转到0x4123F4处,所以作差得到偏移值为8,给esp加上8即可
此处add后的dword表示了操作数类型是双字节数据类型,而ptr则表示了操作数是一个指针,ss:则是段表示符,表示esp在栈段上,esp本身是一个偏移量,在此处写ss:[esp]表示了esp寄存器所指向的地址
PLUS
上面的引例给了我一种新的思路,既然可以自己调整返回地址,而且直接对寄存器的操作ida反应的不是很好,那我岂不是可以随便修改控制流?OK,于是我摸出来一个Flower4Plus。
总体思路就是通过修改控制流,来使得main函数反复被调用,而这一部分的调用ida本身看不出来,所以就可以循环操作密钥,使得密钥经过了一个加密,这里我采取了异或的形式,做了一个计数器反复加一,然后拿计数器和key去做一个异或运算,当计数器到达某一特殊值的时候,停止循环。
更改控制流&主程序内联汇编&花指令
void (*j_check_ptr)() = j__check;
int (*main_ptr)() = main;
_asm
{
CALL Label3
_emit 0xE9
Label3:
mov eax,[n]
call Label9
_emit 0xE9
Label9 :
add dword ptr ss : [esp] , 8
ret
_emit 0xE8
cmp eax,10
JZ Label2
JNZ Label1
Label2:
JMP Label4
_emit 0xE8
mov eax, main_ptr
push eax
ret
Label1:
call j_check_ptr
mov eax, main_ptr
push eax
ret
Label4 :
call j_check_ptr
}
这一部分是由call&ret花指令魔改而来的,首先call指令能构造一个稳定的跳转,在原先的方法中我们需要考虑修改返回地址,但是在这里不用,我们来分析一下程序的控制流。
CALL Label3
,跳转到标签3处CALL Label9
,这里是一个典型的call&ret花指令,即下面这一段可以无视
call Label9
_emit 0xE9
Label9 :
add dword ptr ss : [esp] , 8
ret
_emit 0xE8
cmp eax,10
,结合上面的mov eax,[n]
,可知这句指令等同于if(n==10),JZ Label2
和JNZ Label1
则是按照此时的标志位进行跳转,即
if ( n == 10 )
{
JMP Label2
}
else
{
JMP Label1
}
- Label1中先调用了j_check_ptr指针对应的函数,再将main_ptr 压入栈中,进行ret,整段指令的含义就是先调用j_check函数,再把控制流回到main的头部
Label1:
call j_check_ptr
mov eax, main_ptr
push eax
ret
- 那么很明显Label2就是一个误导标签,主体就是一个JMP花指令,中间的跳转部分并不会被执行,Label2将跳转到Label4中
Label2:
JMP Label4
_emit 0xE8
mov eax, main_ptr
push eax
ret
Label4 :
call j_check_ptr
- Label4再次调用了j_check函数,然后函数返回,退出内联汇编
整段内联汇编的逻辑也就出来了,即
if(n!=10)
{
j_check()
return->main
}
else
{
j_check()
break
}
j_check()函数
void j__check()
{
j_check_f = 0;
if (IsDebuggerPresent())
{
std::cout << "sealæ‰£æŽ‰äº†ä½ çš„è°ƒè¯•æŒ‰é’®" << std::endl;
system("pause");
exit(1);
}
j_configure_narrow_argv();
}
一个修正过的反调试函数,其中,j_check_f变量是一个标志位,后面用的到;而j_configure_narrow_argv()函数则是一个密钥换盒函数。
整体加密流程
由于只是Plus,所以只选择了单纯异或加密,给出密文,要求输入明文,明文进去加密滚一遍,跟密文比较,很经典的模式。
#include <stddef.h>
#include <iostream>
#include <windows.h>
int key = 0x114514;
int n = 5;
char enc[] = { 120,114,127,121,101,76,123,127,114,114,103,33,33,33,76,123,127,114,114,103,33,33,33,33,99 };
char key1[25] = "Seal_Seal_Seal_Seal_Seal";
char flag[100];
int j_check_f = 1;
void j_configure_narrow_argv()
{
key1[0] = 0;
key1[1] = 1;
key1[2] = 2;
key1[3] = 3;
key1[4] = 4;
key1[5] = 0;
key1[6] = 12;
key1[7] = 17;
key1[8] = 69;
key1[9] = 68;
key1[10] = 82;
key1[11] = 109;
key1[12] = 3;
key1[13] = 71;
key1[14] = 3;
key1[15] = 45;
key1[16] = 65;
key1[17] = 9;
key1[18] = 33;
key1[19] = 91;
key1[20] = 95;
key1[21] = 11;
key1[22] = 8;
key1[23] = 9;
key1[24] = 24;
return;
}
void j__check()
{
j_check_f = 0;
if (IsDebuggerPresent())
{
std::cout << "sealæ‰£æŽ‰äº†ä½ çš„è°ƒè¯•æŒ‰é’®" << std::endl;
system("pause");
exit(1);
}
j_configure_narrow_argv();
}
void j__check1()
{
if (IsDebuggerPresent())
{
std::cout << "sealæ‰£æŽ‰äº†ä½ çš„è°ƒè¯•æŒ‰é’®" << std::endl;
system("pause");
exit(1);
}
j_configure_narrow_argv();
}
void xorencrypt()
{
for (int i = 0; i <= 24; i++)
{
enc[i] ^= i;
enc[i] ^= key1[i];
}
}
void BACKDOOR()
{
xorencrypt();
for (int i = 0; i <= 24; i++)
{
if (flag[i] != enc[i])
{
std::cout << "笨嘻嘻嘻";
exit(0);
}
}
std::cout << "不,怎么可能!!!";
exit(0);
}
void Sentry_base()
{
}
void _JustMyCode_Default()
{
Sentry_base();
BACKDOOR();
}
void check()
{
for (int i = 0; i <= 24; i++)
{
if (flag[i] != enc[i])
{
return;
}
}
std::cout << "哈哈哈很好";
exit(0);
}
int main()
{
j__check1();
int (*main_ptr)() = main;
void (*j_check_ptr)() = j__check;
void (*check_ptr)() = _JustMyCode_Default;
n++;
key ^= n;
if (j_check_f)
{
scanf_s("%100s", flag, (unsigned)_countof(flag));
}
j__check();
if (strlen(flag) != 25)
{
std::cout << "wrong length";
exit(0);
}
_asm
{
CALL Label3
_emit 0xE9
Label3:
mov eax,[n]
call Label9
_emit 0xE9
Label9 :
add dword ptr ss : [esp] , 8
ret
_emit 0xE8
cmp eax,10
JZ Label2
JNZ Label1
Label2:
JMP Label4
_emit 0xE8
mov eax, main_ptr
push eax
ret
Label1:
call j_check_ptr
mov eax, main_ptr
push eax
ret
Label4 :
call j_check_ptr
}
for (int i = 0; i <= 24; i++)
{
flag[i] ^= key;
}
check();
_asm
{
CALL Label7
_emit 0xE9
Label7 :
call check_ptr
add dword ptr ss:[esp],8
ret
_emit 0xE8
}
exit(0);
}
上面提到过在j_check函数中对j_check_f进行了操作,即将其置0了,通过观察该变量的调用,可以发现这个变量用于指示是否要进行读取flag字符串,如果反调试函数被整个nop或其他情况整段绕过时,能在一定程度上产生干扰,但是直接下断点绕过就不能干扰到。
反调试部分里还有一个换盒函数,用于给key1换盒,如果不换盒,就会解出一个错误的彩蛋解。
我设定的n的初始值为5,每经过一次main头就能让n++,然后key会和n进行异或操作,即key会依次与678910异或。
最后的check函数有两个,一个用于判断错误解,没有对key1进行换盒;另一个用了内联汇编调用,有花,防止一眼看穿,然后这个函数有换盒操作。
check();
_asm
{
CALL Label7
_emit 0xE9
Label7 :
call check_ptr
add dword ptr ss:[esp],8
ret
_emit 0xE8
}
这里就是换盒操作
void xorencrypt()
{
for (int i = 0; i <= 24; i++)
{
enc[i] ^= i;
enc[i] ^= key1[i];
}
}
可以看到代码已经是炸完了的😊
Solution
想办法让代码动起来就好解决了,把反调试绕了...😡
ProMax
本来的想法是加入一些加密的算法,先将这些算法放进ida滚一遍拿到汇编代码,再把这些代码统一异或加密,变成一些无效数据,放在字符串中,当代码中需要用到的时候,就再对这些代码进行解密,放进内联汇编中运行。但是经过学长提点,我才知道这种思路被称为SMC代码自解密,然后我设想的那种方式效率有点低了,所以我打算之后再作SMC的学习。😘
Flower5:晋升!自定义花指令
1.CALL嵌套多重跳转
#include <iostream>
int main()
{
_asm
{
CALL Label1
_emit 0xE8
Label2:
JMP Label3
_emit 0
_emit 0xE8
_emit 0xF6
_emit 0xFF
_emit 0xFF
_emit 0xFF
Label1:
CALL Label2
Label3:
ADD esp,8
}
std::cout << "Hello World!\n";
}
此处可以调整Label2中E8跳转的地址,来更好的“欺诈”
2.CALL嵌套Ⅱ
#include <iostream>
int main()
{
_asm
{
//push eax
call LABEL1
_emit 0x29
_emit 0x5A
LABEL1 :
//POP eax
//imul eax, 3
call LABEL2
_emit 0x29
_emit 0x5A
LABEL2 :
add esp, 4
pop eax
}
std::cout << "Hello World!\n";
}
原例包含被我注释掉的部分指令,但是我自行调试后感觉这些指令意义不大,可能能起到一些误导作用吧,不懂。
3.替换ret指令
_asm
{
call LABEL9;
_emit 0xE8;
_emit 0x01;
_emit 0x00;
_emit 0x00;
_emit 0x00;
LABEL9:
push eax;
push ebx;
lea eax, dword ptr ds : [ebp - 0x0];
#将ebp的地址存放于eax
add dword ptr ss : [eax-0x50] , 26;
#该地址存放的值正好是函数返回值,
#不过该地址并不固定,根据调试所得。
#加26正好可以跳到下面的mov指令,该值也是调试计算所得
pop eax;
pop ebx;
pop eax;
jmp eax;
_emit 0xE8;
_emit 0x03;
_emit 0x00;
_emit 0x00;
_emit 0x00;
mov eax,dword ptr ss:[esp-8];
#将原本的eax值返回eax寄存器
}
这种方式是前面call&ret花指令的魔改版,通过偏移值直接对栈上的地址进行了修改操作,但是需要先调试来确定偏移,较为烦琐。😭
寄存器压栈操作
push eax;
push ebx;
push eax 和 push ebx 这两条指令的主要目的是将 eax 和 ebx 寄存器中的值压入栈中。这样做通常是为了保存这两个寄存器当前的状态,因为后续的代码执行可能会修改它们的值,而通过压栈可以方便在之后需要的时候恢复其原始内容,以保证程序执行流程的完整性以及寄存器状态的正确维护。例如,在函数调用前后,经常会采用这种方式来保护调用者环境中的寄存器值。
地址获取操作
lea eax, dword ptr ds : [ebp - 0x0];
lea指令在这里的作用是将一个内存地址加载到 eax 寄存器中。具体来说,它计算的是相对于 ebp(基址指针寄存器,常用于指向当前函数的栈帧底部)偏移量为 0 的地址,并将这个地址存放在 eax 中。也就是说,此时 eax 中存放的就是当前栈帧底部(也就是 ebp 所指向的位置)的地址。这一步操作往往是后续基于栈帧内特定位置进行数据访问的基础,比如要读写栈帧内存储的局部变量、函数参数或者其他相关数据结构等情况时,先获取到正确的起始地址很关键。
内存数据修改操作
add dword ptr ss : [eax - 0x50], 26;
这条指令是对内存中的数据进行修改。它的操作对象是通过 eax 寄存器中的地址偏移一定量(这里是减去 0x50)所指向的内存位置处存储的双字(dword,32 位)数据。具体执行的操作是将该内存位置处原有的双字数据加上 26。
这个被操作的内存位置所存放的值正好是函数返回值(尽管地址不固定,是通过调试发现的情况),而加 26 这个操作正好可以跳到下面的 mov 指令(同样该值也是调试计算所得)。这种利用对特定内存位置(恰好关联函数返回值所在处)的数据修改来改变程序执行流向的方式,很可能是一种比较巧妙(也可能是有意混淆的,比如在分析反汇编代码时遇到的花指令场景等)的程序控制手段。它绕过了常规的函数调用、返回等标准流程,通过直接操作内存中关键数据的方式来引导程序跳转到期望的后续指令处执行。
Flower6:拆!
任务:手动破解相对简单的花指令
例题:[SWPUCTF 2023 秋季新生赛]Junk Code
手动破除花指令
打开题目就发现ida报红,观察报红的地方
jmp near ptr 0C14867AEh
很熟悉的样式,jmp后跟一些别的数据,可以猜到有加_emit 0xE9
按空格退出流程图,发现上面又有一串特征代码
.text:0000000140001450 jz short near ptr loc_140001454+1
.text:0000000140001452 jnz short near ptr loc_140001454+1
jz&jnz,跟Flower3的特征符合,所以准备以此为模板开始破解
为了能够看到硬指令,要在Options栏中将这个参数调大,一般调成16就够了
可以看到硬指令了,然后框住所有数据按u解包,再在main的头部按c分析,此时断在此处
.text:0000000140001454 E9 55 53 48 81 jmp near ptr 0C14867AEh
由于已经推测是加了0xE9垃圾数据,尝试将其nop掉,对着这一行右键,选择patch,修改数据
修改完以后发现汇编代码正常
而下方还有几段类似的花指令
可以用同样方法一并解决,最后在main函数头部按p重新包装函数即可,f5得到代码
没有任何阻碍了,轻松写出exp
s="NRQ@PC}Vdn4tHV4Yi9cd#\\}jsXz3LMuaaY0}nj]`4a5&WoB4glB7~u"
flag=[]
for i in range(len(s)):
flag.append(str(chr(ord(s[i])^(i%9))))
flag="".join(flag)
print(flag)
IDC脚本破除花指令
这题中相同的花指令反复出现多次,手动破除费心还费力,为了省事,还有一种IDC脚本破除的方法可以选择。
ida python常用函数
MakeCode(ea) #分析代码区,相当于ida快捷键C
ItemSize(ea) #获取指令或数据长度
GetMnem(ea) #得到addr地址的操作码
GetOperandValue(ea,n) #返回指令的操作数的被解析过的值
PatchByte(ea, value) #修改程序字节
Byte(ea) #将地址解释为Byte
MakeUnkn(ea,0) #MakeCode的反过程,相当于ida快捷键U
MakeFunction(ea,end) #将有begin到end的指令转换成一个函数。如果end被指定为BADADDR(-1),IDA会尝试通过定位函数的返回指令,来自动确定该函数的结束地址
观察模式
由上面的分析可知,本题中的花指令可以被模式化为74 03 75 01 E9 . . . . . . ,即我们只需要比对这几位字节,就可以判断此处是不是花指令,然后再用函数将E9改为90即可
书写脚本
直接上脚本吧
#include<idc.idc>
static main()
{
auto i,begin,end;
begin=0x140001450;
end=0x1400015C8;
for(i=begin;i<=end;i++)
{
/*此处填充代码段*/
}
return 0;
}
#include<stdio.h>
int main()
{
int flower[5]={0x74,0x03,0x75,0x01,0xE9};
int n=5,i,j;
for(i=0;i<n;i++)
{
printf("if(Byte(i+%d)==%d)\n",i,flower[i]);
for(j=0;j<=i;j++)
{
printf(" ");
}
}
printf("{\n");
for(j=0;j<=n;j++)
{
printf(" ");
}
printf("PatchByte(i+%d, 0x90);\n",n-1);
for(j=0;j<=n;j++)
{
printf(" ");
}
printf("MakeCode(i+%d);\n",n-1);
for(j=0;j<=n;j++)
{
printf(" ");
}
printf("Message(\"Find FakeJmp Opcode!!\\n\");\n");
for(j=0;j<n;j++)
{
printf(" ");
}
printf("}\n");
return 0;
}
因为在idc里面不能使用数组,然后反复嵌套的if语句写起来实在是有点难受,所以写了上面两个脚本,其中第一个就是我们的idc脚本模板了,第二个脚本则是c++程序,在数组中存放要查询的模板字节,就能自己生成一段if嵌套代码段,懒人必备~~~
最后把这一段代码插入到idc代码中即可运行
运行后代码被还原
idc脚本
IDC:头文件
在IDC语言中,头文件一律为#include<idc.idc>
IDC:变量
在IDC脚本语言中,存在两种变量:
局部变量:这类变量在函数开头的时候声明创建,在函数退出的时候销毁(auto)
全局变量:在脚本加载进来的时候就被创建,然后当你关闭当前IDA分析的文件的时候,IDA也会清理你加载过的IDC脚本,其实可以理解为IDC脚本清理的时候销毁(extern)
IDC:函数
IDC脚本中的函数必须要有返回值。在IDC脚本中,支持两类函数:
-
内建函数
-
用户自定义函数
用户自定义函数一般是像下面这样的方式写的:
**static** func(arg1,arg2,arg3)
{
statements ...
}
需要注意的一点是,声明函数参数的时候,没必要指定函数参数的类型了,因为IDC会根据你传入的参数自动进行参数类型转换。
默认情况下,函数调用的时候参数传递是按值传递的,但是以下三种情况例外(引用传递):
-
对象类型的参数总是使用引用方式传参
-
函数类型的参数总是使用应用方式传参
-
还可以强制使用 & 符号来让参数使用引用方式传参
如果IDC脚本中调用的函数不存在,IDA就会尝试解析并使用当前被调试的进程中的符号或者标签,如果解析成功,那么就会执行一次 AppCall
IDC:AppCall
自行百度,又臭又长
IDC:语句
感觉除了switch语句,其他语句都能正常书写,表达式也同c++。
IDC模板:
#include<idc.idc>
static main()
{
auto i,begin,end;
begin=0x140001450;
end=0x1400015C8;
for(i=begin;i<=end;i++)
{
/*此处填充代码*/
}
return 0;
}
#include<stdio.h>
int main()
{
int flower[5]={0x74,0x03,0x75,0x01,0xE9};
int n=5,i,j;
for(i=0;i<n;i++)
{
printf("if(Byte(i+%d)==%d)\n",i,flower[i]);
for(j=0;j<=i;j++)
{
printf(" ");
}
}
printf("{\n");
for(j=0;j<=n;j++)
{
printf(" ");
}
printf("PatchByte(i+%d, 0x90);\n",n-1);
for(j=0;j<=n;j++)
{
printf(" ");
}
printf("MakeCode(i+%d);\n",n-1);
for(j=0;j<=n;j++)
{
printf(" ");
}
printf("Message(\"Find FakeJmp Opcode!!\\n\");\n");
for(j=0;j<n;j++)
{
printf(" ");
}
printf("}\n");
return 0;
}