admin 管理员组

文章数量: 887021

实验五

  • 第1题 CraMe1.exe
    • 第(1)问:修改exe使得出现成功提示
    • 第(2)问:不修改exe输入正确的密码达到成功的目的
  • 第2题 Exe: super_mega_protection.exe
    • 第(1)问:(简单)在任何调试器的帮助下,强制程序接受更改后的密钥文件
    • 第(2)问:(中等)您的目标是将用户名修改为另一个用户名,而不需要修补程序。
    • 题外话:
  • 第3题(选做)Reverseme1.exe
    • 第(1)问:窗体中有一个隐藏按钮,将它显示出来并起作用
    • 第(2)问:按F7功能键,显示出这个消息框。
      • 步骤1、输入表添加RegisterHotKey
      • 步骤2、RegisterHotKey(ss:[ebp+0x8],0,0,76)注册F7热键
      • 步骤3、按下F7显示消息框
        • 方法1、扩大这4个字节的空间
        • 方法2、把判断放在没有代码的空白区域00401256

第1题 CraMe1.exe

要求:运行CraMe1.exe,提示 "u r right!"代表成功。首先修改exe使得出现成功提示,其次不修改exe输入正确的密码达到成功的目的。

第(1)问:修改exe使得出现成功提示

先打开运行CraMe1.exe:

  应该是输入一串字符,经过判断之后输出“u r wrong”或者“u r right!”。
  用IDA打开CraMe1.exe,在String Windows搜索字符串“u r right!”:

双击进入IDA View-A,这里是所有字符串在内存中的虚拟地址:

选中字符数组aURRight,按下“X”键,单击将指向使用该字符串所在的地址:

  在这里我们可以清楚的看到上一行的jnz(比较不相等则跳转),如果比较之后相等则不跳转输出“u r right!”,如果不相等则跳转到虚拟地址411613输出“u r wrong”:

打开视图可以看的更清楚代码逻辑:

  所以我们的目的就是修改跳转,将jnz(不相等时跳转)改为jz(相等时跳转),这样只要我们没有输出正确的flag,都会输出“u r right!”。
  用16进制编辑器或者OD之类的修改都行,这里用OD修改直接搜索二进制75 19找到相应的位置:

二进制编辑将75改为74:

复制到文件,保存:

然后保存到可执行文件,保存成名为CraMe11.exe的可执行程序,之后运行修改后的程序随便输入一个数据,结果如下图所示:

第(2)问:不修改exe输入正确的密码达到成功的目的

  首先还是找到关键跳转的地方,然后发现上面对ebp-0xc0处的数据进行了比较,根据比较是否相等来决定是否跳转,所以向上继续查找有没有对ebp-0xc0的数据进行修改的地方

然后找到如图所示的地方:

这两处的跳转都应该实现,然后在上边较远处下一个断点然后一步一步走,最后会发现关键代码是:

  对其进行分析,一共可以分为两部分,第一部分是对前17位进行比对,要求相等,程序中给出了下列字符串:
Str=swfxc{gdv}fwfctslydRddoepsckaNDMSRITPNsmr1_=2cdsef66246087138”
  然后内存中又给出了一串数据:
Arr={0x01,0x04,0x0E,0x0A,0x05,0x24,0x17,0x2A,0x0D,0x13,0x1C,0x0D,0x1B,0x27,0x30,0x29,0x2A,0x1A,0x14,0x3B,0x04,0x00}
首先是从内存中一个一个找Str[Arr[i]]与输入的字符串进行比较,只比较前17位,然后如果不相等就会给ebp-0xC0处的数据赋值为1,然后跳转到失败。
  所以前17位就是Str[Arr[i]],后面还有比较的地方可以得出后面还有5位分别为0x31,0x30,0x32,0x34,0x7D

将它们一个一个转换成Ascii码对应的字符串,最后得出flag为:
wctf{Pe_cRackme1_1024}

第2题 Exe: super_mega_protection.exe

Key file: sample.key
This is a software copy protection imitation, which uses a key file. The key file contain a user (or customer) name and a serial number.

There are two tasks:

(Easy) with the help of any debugger, force the program to accept a changed key file.
(Medium) your goal is to modify the user name to another, without patching the program.

第(1)问:(简单)在任何调试器的帮助下,强制程序接受更改后的密钥文件

我们先运行exe文件和key:super_mega_protection.exe sample.key

再打开key文件:

  可以看到注册文件的用户名:Dennis Yurichev,所以我们可以推测super_mega_protection.exe从sample.key中得到用户名计算出注册码。
  题目要求我们更改密钥文件,所以我们将文件改成如图所示:

先用IDA打开super_mega_protection.exe文件,找到main函数,F5反编译:

  我们可以看到在输出正确的Serial number之前,有3个if判断来确定是否输入了正确的key文件,所以我们只要保证这3个if判断接受更改后的密钥文件即可。
  再用OD打开文件,由于要输入参数key,所以点击OD的文件->打开,选择打开super_mega_protection.exe文件,参数指定new.key,如图所示:

点击打开,就可以正常破解了。
查找字符串,下断点,找到代码判断的关键位置,有3个JNZ跳转对应之前的if跳转:

第一个JNZ是检查是否有key文件参数输入,所以不用修改,我们只需要把后面两个JNZ改成JZ,即可让它们不跳转,输出Serial number
然后找到关键跳转的地方,如下图所示:

在此处下断点,然后点击运行
发现接下来的跳转不成立,直接改掉,将jnz改成jz即可,其中:

汇编指令对应的机器码作用
JZ/JE74Z=1,为零/等于则跳转
JNZ/JNE75Z=0 ,不为零/不等于则跳转

把JNZ对应的机器码75改成74:

然后继续运行
结果如下图所示,输出文件内容:

或者把修改的代码保存到文件:

重新运行:super_mega_protection(OD修改后的).exe new.key

第(2)问:(中等)您的目标是将用户名修改为另一个用户名,而不需要修补程序。

所以我们要完整的逆向出判断的算法,才能修改为另一个用户名且通过校验。
我们继续用IDA反编译,查看每部分算法的作用:

源代码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const char *v3; // eax
  char *v4; // ebx
  __int16 v5; // ax
  char v7; // [esp+4h] [ebp-24h]
  int v8; // [esp+1Ch] [ebp-Ch]

  sub_4025A0();
  puts("Super-mega-protected software");
  if ( argc != 2 )
    sub_4016F0("Usage: %s <filename.key>\n", (unsigned int)*argv);
  v3 = (const char *)sub_401720((char *)argv[1], (int)&v8);// 负责打开key文件,之后v7被赋值为文件长度,v3赋值为key文件的内容
  v4 = (char *)v3;
  if ( v8 != 132 )                              // 文件长度是否等于132字节
    sub_4016F0("%s: incorrect size!\n", (unsigned int)argv[1]);
  v5 = strlen(v3);                              // 字符串的长度
  if ( (unsigned __int16)sub_4015F0((int)v4, v5) != -7131 )// 将key文件内容的指针数值和长度作为输入,要求输出-7131
  sub_4016F0("Keyfile is incorrect\n", v7);
  puts("Registration info:");
  printf("Username=%s\n", v4);
  printf("Serial number: %d\n", *((_DWORD *)v4 + 32));
  free(v4);
  return 0;
}

其中sub_401720函数是负责打开key文件,之后v7被赋值为文件长度,v3赋值为key文件的内容:

源代码:

void *__cdecl sub_401720(const char *a1, size_t *a2)
{
  FILE *v2; // eax
  FILE *v3; // ebx
  int v4; // eax
  void *v5; // ebp
  char v7; // [esp-28h] [ebp-28h]

  v2 = fopen(a1, "rb");                         // v2文件指针
  v3 = v2;                                      // v3 = v2
  if ( !v2 )
    sub_4016F0("Cannot open file %s\n", (char)a1);// fseek文件指针定位到0(文件开头)、1(文件当前位置)、2(文件末尾)文件末尾,偏移0个字节
                                                // ftell得到文件位置指针当前位置相对于文件首的偏移字节数
                                                // malloc分配了v4个字节,并返回了指向这块内存的指针
  if ( fseek(v2, 0, 2) || (v4 = ftell(v3), *a2 = v4, v5 = malloc(v4), fseek(v3, 0, 0)) )// 把文件指针v2定位到文件末尾
// v4是文件指针v3是文件末尾,ftell就是返回文件末尾到文件开始的
// 申请一块v4长度的地址,返回这块地址的指针给v5
// v3文件指针定位到文件开始位置
    sub_4016F0("fseek()\n", v7);
  if ( fread(v5, *a2, 1u, v3) != 1 )            // fread读取v3指向的数据赋值给v5,每次读取*a2字节数,读取1次
    sub_4016F0("Cannot read file %s\n", (char)a1);
  fclose(v3);
  return v5;                                    // 返回key文件的内容v5
}

其中关键的一句代码是:

if ( fseek(v2, 0, 2) || //把文件指针v2定位到文件末尾
(v4 = ftell(v3),  //v4是文件指针v3是文件末尾,ftell就是返回文件末尾到文件开始的偏移字节数也就是整个文件的长度给v4
 *(_DWORD *)a2 = v4, //
 v5 = malloc(v4),//申请一块v4长度的地址,返回这块地址的指针给v5
fseek(v3, 0, 0)) )//v3文件指针定位到文件开始位置

得到了key文件的长度
再回到主函数,其中判断的关键就是sub_4015F0((int)v4, v5)函数:

进入sub_4015F0函数:

  sub_4015F0函数的两个参数是key文件的首地址和用户名的长度,作用是将key文件中的用户名逐个字节进行计算,第一个字节v5和结果v4(初始值位0xFFFF),计算的结果v4再和下一个字节v5一起计算,计算用户名的长度这么多次,最后得到的结果与-7131进行比较。
  不相等则输出“Keyfile is incorrect”,相等则继续,输出Username和Serial number,其中Serial number是*((_DWORD *)v4 + 32),实际上就是key文件最后的四个字节(字符):0x00BC614E,按照%d格式输出就是12345678:

  因为关键的算法不能逆向推导,所以现在的破解思路就是暴力破解:key文件的长度为132字节,最后四个字节为Serial number,可以自己指定,也可以随机。前面128字节可以遍历0X00到0XFF,直到计算得到-7131,可以认为当前的字节是一个正确的key,中间部分用00填充,最后四个字节Serial number可以自己指定。
  所以我们尝试遍历了一个字节,但是没有找到正确的key;在遍历二个字节的时候找到了一个正确的key:1E 64,后续还有很多其他的解,原理都是一样的,就不一一求解了。
源代码:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

/**
*
* char current_character:当前的字符,unsigned int v4上一次计算的结果
*/
int sub_4015F0(char current_character,unsigned int v4){
    int v3; // ecx
    ///unsigned int v4; // edx
    unsigned int v5; // eax
    ///代表8位无符号数,表示一个字节255以内
    unsigned __int8 v6; // di
    unsigned int v7; // edx
    unsigned int v8; // esi
    char v9; // di
    unsigned int v10; // esi
    unsigned int v11; // edx
    unsigned int v12; // edi
    char v13; // si
    unsigned int v14; // edx
    unsigned int v15; // edi
    char v16; // si
    unsigned int v17; // edx
    unsigned int v18; // edi
    char v19; // si
    unsigned int v20; // edx
    unsigned int v21; // edi
    char v22; // si
    unsigned int v23; // edx
    unsigned int v24; // edi
    char v25; // si
    unsigned int v26; // edx
    unsigned int v27; // esi
    int v28; // eax
    int v29; // edx

    ///直接用当前字符来代替源代码的数字和字符之间的转换,68
    v5 = (unsigned char)current_character;      
      ///255
      v6 = v4;
      ///32767
      v7 = v4 >> 1;
      ///64503
      v8 = v7 ^ 0x8408;
      ///(68^255)&1 =1
      if ( !(((unsigned __int8)v5 ^ v6) & 1) )
        ///32767
        v8 = v7;
      ///-43(325);
      v9 = v8 ^ (v5 >> 1);
      ///32251
      v10 = v8 >> 1;
      ///63987
      v11 = v10 ^ 0x8408;
      ///!(-43&1) = 0
      if ( !(v9 & 1) )
        v11 = v10;
      ///31993
      v12 = v11 >> 1;
      v13 = v11 ^ (v5 >> 2);
      v14 = (v11 >> 1) ^ 0x8408;
      if ( !(v13 & 1) )
        v14 = v12;
      v15 = v14 >> 1;
      v16 = v14 ^ (v5 >> 3);
      v17 = (v14 >> 1) ^ 0x8408;
      if ( !(v16 & 1) )
        v17 = v15;
      v18 = v17 >> 1;
      v19 = v17 ^ (v5 >> 4);
      v20 = (v17 >> 1) ^ 0x8408;
      if ( !(v19 & 1) )
        v20 = v18;
      v21 = v20 >> 1;
      v22 = v20 ^ (v5 >> 5);
      v23 = (v20 >> 1) ^ 0x8408;
      if ( !(v22 & 1) )
        v23 = v21;
      v24 = v23 >> 1;
      v25 = v23 ^ (v5 >> 6);
      v26 = (v23 >> 1) ^ 0x8408;
      if ( !(v25 & 1) )
        v26 = v24;
      v27 = v26 >> 1;
      v28 = v26 ^ (v5 >> 7);
      v4 = (v26 >> 1) ^ 0x8408;
// v28的最低位是0则进入,最低位是1则跳过
      if ( !(v28 & 1) )
        v4 = v27;
    ///55835
    return v4;
}

/**
*两轮for循环找到2个字节的key
*/
int main(){
    ///v4的初始值63082
    int v4 = 0xFFFF;
    ///两轮循环找到2个字节的key
    for(unsigned __int8 i = 0; i<0xFF ; i++){
        ///第一次的结果v4,赋值给临时变量temp1
        unsigned int temp1 = sub_4015F0((char)i,v4);
        for(unsigned __int8 j = 0; j<0xFF ; j++){
            ///第二次的结果v4,赋值给临时变量temp
            unsigned int temp = sub_4015F0((char)j,temp1);

            ///对结果作变换,非:按位取反,v29=-55836=0xFFFF25E4
            int v29 = ~temp;
            ///printf("v29的值:%d\n",v29);
            ///-7131,计算得到result;(unsigned __int8 *)转化为__int8字节指针,让后面的+1是加4个字节的地址
            int result = (__int16)((v29 << 8) | *((unsigned __int8 *)&v29 + 1));
            ///其中int BYTE1 = (__int16)(v29) >>8;  =0x25=37
            /// 上面这一行的代码等价于int result = (__int16)((v29 << 8) | (__int16)v29 >>8);
            if(result == -7131){
                ///30,100
                printf("0x%x 0x%x计算得到正确的result值%d\n",i,j,result);
                return 0;            }
        }
    }
    return 0;
}

运行结果:

中间的字节我们用00填充,最后的四个字节我们指定为0x004F5DA2=5201314,所以我们最终得到的这一个key就是:

带入原程序运行:super_mega_protection.exe 111.key

成功破解,将用户名修改为另一个用户名,而不需要修补程序。

题外话:

  有几个需要注意的点:
(1)复现的伪代码中有两处代码_BYTE和BYTE1,其实都是IDA头文件defs.h中的宏定义,详细的可以参考我的另一篇博文:IDA的def.h文件;
(2)还有__int8、__int16、__int32、__int64之间的转换;
(3)EDX、DX、DH寄存器之间的赋值。
1、_BYTE == unsigned char

#define unsigned char    BYTE;

2、BYTE1(v29) = *((unsigned __int8 *)&v29 + 1)=37

#define BYTE1(x)   BYTEn(x,  1) 

  这里的BYTE1(v29)就是v29地址上开始的第一个字节,即v29[1],(unsigned __int8 *)转化为__int8字节指针,让后面的+1是加4个字节的地址;
  另外,BYTE1(v29)在OD中的代码是:movzx eax,dh,dh寄存器的值为0x25=37:

所以我们也可以BYTE1(v29) = (__int16)(v29) >>8,这也是代码中的等价语句int result = (__int16)((v29 << 8) | (__int16)v29 >>8);的由来。

第3题(选做)Reverseme1.exe

hint:Import Address Table

我们先打开Reverseme1.exe:

要求分为两部分:

1.窗体中有一个隐藏按钮,将它显示出来并起作用;
2.按F7功能键,显示出这个消息框。

第(1)问:窗体中有一个隐藏按钮,将它显示出来并起作用

Reverseme1.exe是基于win 32的GUI编程软件,
IDA打开只能看到调用的API函数和大致过程:

所以我们用OD打开程序,查找字符串到主模块(Reversem):

查看当前主模块(Reversem)中调用的函数:

当前模块(Reversem)中调用的函数:

函数名称函数作用
user32.DialogBoxIndirectParamA该函数根据内存中对话框模板创建一个模态的对话框。
user32. Endialog该函数清除一个模态对话框,并使系统中止对对话框的任何处理
kernel32.ExitProcess中止一个进程
user32.GetDlgItem该函数检索指定的对话框中的控制句柄
kerne132.GetModuleHandleA获取一个应用程序或动态链接库的模块句柄
kerne132.GlobalAlloc分配一块内存,该函数会返回分配的内存句柄
kernel32.GlobalFree释放指定内存块
user32.LoadIconA从指定的模块或应用程序实例中载入一个图标
user32.MessageBoxA该函数创建、显示、和操作一个消息框
<ModuleEntryPoint>模块入口点,就是一般你用od载入时停在那个地址
kernel32.MultiByteToWideChar映射一个字符串到一个宽字符(unicode)的字符串
user32.SendMessageA调用一个窗口的窗口函数,将一条消息发给那个窗口
user32.ShowWindow控制窗口的可见性

  其中最重要的就是ShowWindow函数,这个函数的作用就是控制窗口的可见性,用来隐藏或者显示一个按钮。
所以我们找到ShowWindow函数,再看一下前面的代码:

  我们发现有一个je short 004011BD 和jnz short 004011BD连起来的代码:

汇编指令对应的机器码作用
JZ/JE74ZF=1,为零/等于则跳转
JNZ/JNE75ZF=0 ,不为零/不等于则跳转

JE指令功能是在ZF标志位等于1时进行跳转,也就是(test等判断指令)结果为0(或者相等)的时候跳转。
JNZ指令功能是在ZF标志为0转移,就是结果不为零(或不相等)则转移。
  所以不论相等还是不相等都会进行跳转,所以这两句话连起来的意思就是jmp 004011BD:

那么中间的push edi到db 21这一段代码,就不会被执行,所以就相当于执行:

push 0x0,
push dword ptr ds:[0x403178]
jmp 004011BD
mov eax,Reversem.00401258                ; |
sub eax,0x8                              ; |
call eax                                 ; \ShowWindow

  而ShowWindow的函数原型是:BOOL ShowWindow(HWND hWnd,int nCmdShow),这里的参数是(00060EE2,0),也就是隐藏了一个窗口,应该就是隐藏了“退出”按钮。
我们把参数0改成1:

这时候“退出”按钮已经显示出来了:

  但是现在点击它还没有作用,让它起作用应该就是调用Endialog函数清除这个模态对话框,并使系统中止对对话框的任何处理,关闭对话框。
所以我们继续看代码:

  根据堆栈 ss:[ebp+0x10]的值来判断是否点击了第1个“About”和第2个“退出”按钮:
当点击了第1个“About”按钮,这时候堆栈 ss:[0019F8F8]的值为1,就进入MessageBoxA函数创建一个新的模态消息框:

  再退出了这个消息框之后,再跳转回原来的位置等待下一次点击;
当点击了第2个“退出”按钮,这时候堆栈 ss:[0019F8F8]的值为2,就运行到了:

004011F2   . /75 14         jnz short Reversem.00401208          ;  跳转到等待下一次点击
004011F4   . |EB 00         jmp short Reversem.004011F6
004011F6   > |EB 10         jmp short Reversem.00401208

  但是第二个跳转jmp short Reversem.00401208会让我们跳过Endialog函数,所以我们把它改为jmp short Reversem.004011FE(Endialog函数参数入栈的位置,且跳过ss:[ebp+0xC]的判断跳转):

点击退出按钮,继续运行程序:

程序终止,退出对话框。

第(2)问:按F7功能键,显示出这个消息框。

  要实现这个功能,首先要注册所需要的键值F7,要向系统注册一个全局热键,需要用到RegisterHotKey,函数用法如下(MSDN):

函数功能:该函数定义一个系统范围的热键。   函数原型:BOOL RegisterHotKey(HWND hWnd,int
id,UINT fsModifiers,UINT vk);   参数:
hWnd:接收热键产生WM_HOTKEY消息的窗口句柄。这里的窗口句柄是堆栈 ss:[ebp+0x8]。
id:定义热键的唯一标识符。目的是防止和其他热键冲突,我们就把它设为0.
fsModifoers:组合辅助按键,Ctrl(2)、Alt(1)、Shift(4)、等按键的组合;这里不用,值就是0。
vk:定义热键的虚拟键码。这里F7的虚拟键码是76。

所以注册热键的代码就是RegisterHotKey(ss:[ebp+0x8],0,0,76)。
之后我们要按F7功能键,显示出这个消息框,步骤可以分为3步:
(1)在输入表中添加RegisterHotKey函数;
(2)在exe的合适位置添加代码:RegisterHotKey(ss:[ebp+0x8],0,0,76)来注册F7功能键;
(3)当收到按下F7功能键的Windwos消息是,跳转到显示出这个消息框的代码。

步骤1、输入表添加RegisterHotKey

  手动修改输入表更为精准但比较复杂,我们这里为了方便就直接用loadPE、StudyPE这些工具来向输入表中添加RegisterHotKey函数,但是每个工具修改输入表的原理和把添加的函数放在的具体位置位置都会有些许差异,会影响到后面调用添加的RegisterHotKey函数。
先查看Reverseme1.exe的输入表:

输入表里已经有之前我们在exe中看到的那些函数了,接下来我们添加导入user32.dll的RegisterHotKey函数:

再查看文件的输入表,已经成功导入了user32.dll的RegisterHotKey函数:

步骤2、RegisterHotKey(ss:[ebp+0x8],0,0,76)注册F7热键

  我们再用OD打开修改输入表后的exe,首先要找一块能够把注册的代码RegisterHotKey(ss:[ebp+0x8],0,0,76)写进去的地方,只要能够插入代码,且不会影响程序正常运行的位置都行:
1、可以写在这个函数里第一问中不会被执行的代码处;
2、也可以像第(3)步中写在函数外没有代码00401256之后的空白区域。
这里我们选择在第一问中不会被执行的代码处,作为我们写入注册热键的位置:

为了不影响showwindwos函数的运行,我们把

004011BD   .  B8 58124000   mov eax,Reversem.00401258   ; |
004011C2   .  83E8 08       sub eax,0x8                 ; |
004011C5   .  FFD0          call eax                    ; \ShowWindow

这三行移到这两行代码下面,让它们连成一个完整的函数

004011A1   .  6A 00         push 0x0
004011A3   .  FF35 78314000 push dword ptr ds:[0x403178]

修改后的汇编代码如下图:

接下来写入注册热键RegisterHotKey(ss:[ebp+0x8],0,0,76)的汇编代码:

push 76           ;//F7的虚拟键码
push 0            ;唯一标识符
push 0            ;不用辅助按键
push dword ptr ss:[ebp+0x8]        ;窗口句柄
call dword ptr ss:[0x404464]      ;//ss:[0x404464]是user32.RegisterHotKey在文件的地址

修改之后的汇编代码:

步骤3、按下F7显示消息框

按下F7快捷键之后,就会产生一个Windows消息WM_HOTKEY=0x312:当用户按下由RegisterHotKey()注册的热键时产生此消息。
在文件中已经有了3次对Windows消息的判断:判断0x110,然后判断0x111,最后判断0x10
1、

00401174   .  817D 0C 10010000      cmp dword ptr ss:[ebp+0xC],0x110      ;  判断是否是在被显示前发送的消息0x110
0040117B   .  75 4C                 jnz short Reversem.004011C9           ;  不是就跳转

2、

004011C9   > \817D 0C 11010000      cmp dword ptr ss:[ebp+0xC],0x111      ;  判断是不是选择控件时发送的消息0x111,给它的父窗口或按下快捷键时产生此消息
004011D0   .  75 26                 jnz short Reversem.004011F8           ;  不是就跳转

3、

004011F8   > \837D 0C 10            cmp dword ptr ss:[ebp+0xC],0x10       ;  判断是不是窗口或应用程序要关闭时发送的信号0x10
004011FC   .  75 0A                 jnz short Reversem.00401208           ;  不是就跳转

  所以我们只要加上一次判断0x312,把它放在任意两次判断之间都可以,这里我们考虑到先后顺序就把放在0x110之后、0x111之前:

cmp dword ptr ss:[ebp+0xC],0x312           ;  判断是不是按下RegisterHotKey()注册的热键产生的消息0x312
jnz short Reversem.004011C9           ;  不是就跳转,继续去判断0x111
jmp short Reversem.004011D8           ;  是就跳转到MessageBoxA函数

  但是因为之前的没用的那段代码的空间已经只剩下4个nop了,但是加入的判断代码大于4个字节,所以我们接下来有两种办法:
1、继续放在这个函数里,但是扩大这4个字节的空间,让空间足够放下判断的代码;
2、把判断代码放在函数外一段没有代码的空白区域,利用JNZ来跳转。

方法1、扩大这4个字节的空间

为了腾出空间,我们先把上面的这个调用的汇编代码缩短:

0040119C   .  A3 78314000           mov dword ptr ds:[0x403178],eax       ;  Reversem.00401171
004011A1      6A 01                 push 0x1
004011A3   .  FF35 78314000         push dword ptr ds:[0x403178]

改成

0040119C  |.  6A 01         push 0x1                                 ; |/ShowState = SW_SHOWNORMAL
0040119E  |.  50            push eax                                 ; ||hWnd = 0019FFCC

修改之后的汇编代码:

这样我们就腾出了10个字节的空间,再把下面的汇编代码往上移,让10个字节的空间和之前的4个字节连在一起组成14个字节,修改之后:

  上面的每一步,我们修改之后,Reverseme1.exe都是依旧可以运行的,且没有影响到第一问的“About”、“退出”按钮的功能。接下来我们在把判断加上去:

004011BB      817D 0C 12030        cmp dword ptr ss:[ebp+0xC],0x312           ;  判断是不是按下RegisterHotKey()注册的热键产生的消息0x312
004011C2      75 05         jnz short Reversem.004011C9           ;  不是就跳转,继续去判断0x111
004011C4      EB 12         jmp short Reversem.004011D8           ;  是就跳转到MessageBoxA函数

别忘了把判断0x111后的JNZ跳转的地址004011C9改为0x312的地址004011BB:

  这样判断windwos消息的顺序就变成了:判断0x110,之后判断0x312,然后判断0x111,最后判断0x10。
到此,第二问也做完了:

按下F7,成功显示第二个消息框。

方法2、把判断放在没有代码的空白区域00401256

  因为jnz short的跳转范围是-128~127,超过这个范围的就要使用jnz(jnz long),而且它们对应的汇编代码也完全不同:jnz short 某个地址只用两个字节,jnz(jnz long)某个地址却要6个字节
(i)假如我们把判断windows消息的顺序改为:判断0x110,之后判断0x312,然后判断0x111,最后判断0x10
那么从0x111的代码到空白区域00401256肯定超过127字节,就要使用JNZ和JMP指令,
先把0x111的cmp dword ptr ss:[ebp+0xC],0x111改为jmp 00401256:

再把判断0x312的代码放在空白区域00401256:

00401256   > \817D 0C 12030>cmp dword ptr ss:[ebp+0xC],0x312              ;  判断是不是按下RegisterHotKey()注册的热键产生的消息0x312

0040125D   .^ 0F84 75FFFFFF je Reversem.004011D8                    ;  是就跳转到MessageBoxA函数
00401263      817D 0C 11010>cmp dword ptr ss:[ebp+0xC],0x111; 不是就先判断0x111,再跳转回去
0040126A    ^ 75 8F         jnz short Reversem.004011F8              ;  不是0x111就跳转,继续判断0x10
0040126C    ^ E9 61FFFFFF   jmp Reversem.004011D2                    ;  是0x111就跳转,到判断按钮

保存到可执行文件,运行:

按下F7,成功显示第二个消息框。

(ii)假如我们把判断windows消息的顺序改为:判断0x110,之后判断0x111,然后判断0x10,最后判断0x312
那么从0x10的代码到空白区域00401256就在127字节之内,就可以使用jnz short和jmp short指令
先把0x10判断后的跳转jnz short 00401208改为jnz short 00401256

再把判断0x312的代码放在空白区域00401256

00401256      817D 0C 12030>cmp dword ptr ss:[ebp+0xC],0x312              ;  判断是不是按下RegisterHotKey()注册的热键产生的消息0x312
0040125D    ^ 75 A9         jnz short Reversem.00401208              ;  不是就跳转,到结束retn
0040125F    ^ E9 74FFFFFF   jmp Reversem.004011D8                    ;  是就跳转到MessageBoxA函数

保存到可执行文件,运行:

按下F7,显示出第二个消息框。

本文标签: 语言 工程 Windows