使用IDA/Hexrays反编译伪代码修改程序

逆向工程 艾达 拆卸 反编译 C++ dll注入
2021-07-07 11:09:34

有点啰嗦,先说声抱歉。问题的更新在最后。

我正在尝试在程序中使用挂钩函数并根据 IDA 的反编译伪代码修改它们。为此,我编写了一个简单的、无目的的 32 位程序(这并不重要,无需尝试分析):

// main.cpp
#include <iostream>


class C {
    public:
    int a = 0;
    int b = 0;
    int c[255];
    int *d = nullptr;
    int stuff = 0;
    C(){
        std::cout << d << std::endl;
        d = new int[300];
        std::cout << d << std::endl;
        ++stuff;
        for(int i = 1; i < 255; ++i){
            c[i] = i;
            if( i == 254){
                c[i] = 0;
            }
        }
        for(int i = 1; i < 300; ++i){
            d[i] = i;
            if( i == 299){
                d[i] = 0;
            }
        }
    }
    ~C(){
        std::cout << d << std::endl;
        delete[] d;
        std::cout << d << std::endl;
        ++stuff;
    }

    void play(){
        bool c_stop, d_stop;
        c_stop = false;
        d_stop = false;
        for(int i = 0;;++i){
            if(c[i] == 0 && !c_stop){
                std::cout << "C had " << i + 1<< std::endl;
                c_stop = true;
            }
            if(d[i] == 0 && !d_stop){
                std::cout << "D had " << i + 1 << std::endl;
                d_stop = true;
            }
            if(d_stop && c_stop){
                break;
            }
        }
    }
};

void func(){
    C clae;
    std::cout << "One missisipi " << clae.stuff << std::endl;
}

int main(){
    func();
    C clae;
    std::cout << clae.stuff << std::endl;
    clae.play();
}

虽然无需尝试了解它的作用,但它通常会产生如下输出:

00000000
0007D958
One missisipi 1
0007D958
0007D958
00000000
0007D958
1
C had 255
D had 300
0007D958
0007D958

我说通常是因为地址会随着执行而改变。

现在,使用 IDA,我找到了对应的子程序void func()

void *__thiscall sub_1161000(void *this)
{
  int v1; // eax@1
  int v2; // edx@1
  int v4; // [sp+0h] [bp-43Ch]@1
  int v5; // [sp+8h] [bp-434h]@1
  int v6; // [sp+Ch] [bp-430h]@1
  int v7; // [sp+10h] [bp-42Ch]@1
  char v8; // [sp+14h] [bp-428h]@1
  int v9; // [sp+41Ch] [bp-20h]@1
  int *v10; // [sp+420h] [bp-1Ch]@1
  void *v11; // [sp+424h] [bp-18h]@1
  int (__cdecl *v12)(int, int, int, int); // [sp+428h] [bp-14h]@1
  int v13; // [sp+42Ch] [bp-10h]@1

  v10 = &v4;
  v13 = -1;
  v12 = sub_1161200;
  v11 = this;
  v1 = sub_1161280(&v8);
  v13 = 0;
  v7 = v1;
  v6 = sub_11613F0(&unk_119B300, "One missisipi ");
  v5 = sub_1161810(v9);
  sub_1161AB0(sub_1161AD0);
  sub_1161B30((int)&v8, v2);
  return v11;
}

我确定这是手头的功能,因为跳过对该地址的调用会打印:

00000000
0007D958
One missisipi 1
0007D958
0007D958

这是预期的func()输出。此外,进入该子例程将打印上述内容,但分阶段对应于构造函数、打印函数和析构函数。

所以,现在我已经将伪代码修补到有效的 C 中,如下所示:

void *__thiscall func(void *_this) {
    int v1;                                 // eax@1
    int v2;                                 // edx@1
    int v4;                                 // [sp+0h] [bp-43Ch]@1
    int v5;                                 // [sp+8h] [bp-434h]@1
    int v6;                                 // [sp+Ch] [bp-430h]@1
    int v7;                                 // [sp+10h] [bp-42Ch]@1
    char v8;                                // [sp+14h] [bp-428h]@1
    int v9;                                 // [sp+41Ch] [bp-20h]@1
    int *v10;                               // [sp+420h] [bp-1Ch]@1
    void *v11;                              // [sp+424h] [bp-18h]@1
    int(__cdecl * v12)(int, int, int, int); // [sp+428h] [bp-14h]@1
    int v13;                                // [sp+42Ch] [bp-10h]@1

    int *base_addr = (int *)GetModuleHandle(NULL); // Windows function to get the process base address.

    v10 = &v4;
    v13 = -1;
    v12 = INT_FUNC(0x1161200);
    v11 = _this;
    v1 = INT_FUNC(0x1161280)(&v8);
    v13 = 0;
    v7 = v1;
    v6 = INT_FUNC(0x11613F0)(0x119B300, "One missisipi ");
    v5 = INT_FUNC(0x1161810)(v9);
    PTR_FUNC(0x1161AB0)(PTR_FUNC(0x1161AD0));
    PTR_FUNC(0x1161B30)((int)&v8, v2);
    return v11;
}

其中INT_FUNC定义为:

#define PE_BASE 0x1160000

#define OFFSET(x) (int*)(base_addr + ((int*)x - (int*)PE_BASE))

#define PTR_FUNC(x) (((void *(*)())OFFSET(x)))
#define INT_FUNC(x) (((int (*)())OFFSET(x)))

我所做的是一些指针运算,将 IDA 中的地址转换为运行时对应正确地址的地址,并将它们转换为函数。PE_BASE是 IDA 中进程的基地址(我知道您可以将它们重新设置为零)。请注意,在此阶段,我根本没有修改该函数,而是仅从伪代码中重新创建了它。

最后,为了挂钩我的函数,我使用调试器启动程序,加载包含我的自定义 DLL 的 DLL func,然后在目标子例程的开头,我将那里的指令替换为:

push dword my_dll.func
ret

每当执行目标子例程时,它就会愉快地跳转到我的函数。问题是,它只是不起作用。如果我现在运行我的程序,它只会打印:

00000000
01393FE8

我在函数被hook的时候调试了程序,罪魁祸首是这行,导致段错误:

在此处输入图片说明

这是以这种方式调用的第一个函数地址(未调用之前的函数,但仅将其地址传递给v12)。我的第一个想法是我的指针算法中的一个错误导致它跳转到一个错误的地址,但检查反汇编确认情况并非如此:

在此处输入图片说明

哪个是正确的函数(基于上图中的值edx):

在此处输入图片说明 在此处输入图片说明

似乎一切都在它应该在的地方。

所以,我的问题是:发生了什么?它为什么这样做?我尝试使用 IDA 的伪代码是失败的,因为它不可靠吗?或者,我有什么错误吗?

更新:

感谢bart1e的回答,我现在修复了代码以反映 IDA 评论的变量的实际大小(代码比原始版本更简化,但问题仍然相同):

int func() {
    char v1[0x408]; // [sp+18h] [bp-414h]@1
    int v2;  // [sp+420h] [bp-Ch]@1

    int* base_addr = (int*)GetModuleHandle(NULL);

    INT_FUNC(0x331F04)(v1);
    INT_FUNC(0x3B7E90)(OFFSET(0x3C57C0), "One missisipi ");
    INT_FUNC(0x37E7C0)(v2); // <<<<<<<< SEGMENTATION FAULT
    INT_FUNC(0x37E5C0)(INT_FUNC(0x3B5FD0));
    return INT_FUNC(0x332044)(v1);
}

/* int func()
{
  char v1; // [sp+18h] [bp-414h]@1
  int v2; // [sp+420h] [bp-Ch]@1

  sub_331F04((int)&v1);
  sub_3B7E90((int)&dword_3C57C0, "One missisipi ");
  sub_37E7C0(v2);
  sub_37E5C0(sub_3B5FD0);
  return sub_332044(&v1);
} */

但是现在,当我尝试调试 DLL 时,它仍然会导致分段错误,并打印:

00000000
015CFFE8
One missisipi
1个回答

如果您将反编译器返回的伪代码与您自己的C代码进行比较,您会发现一个非常重要的区别——在反编译器的输出中,它只是一个注释,但它非常重要。我说的是以下几行:

int v7;                                 // [sp+10h] [bp-42Ch]@1
char v8;                                // [sp+14h] [bp-428h]@1
int v9;                                 // [sp+41Ch] [bp-20h]@1

查看变量v7,v8v9位于堆栈中的位置:

  • v7位于sp + 0x10并需要4字节,这很好 - 正是int需要的
  • v8char但需要0x41C - 0x14 = 0x408字节。这里似乎有些不对劲;如果你声明charin C/C++,编译器将只分配1字节,绝对不是0x408

这里发生的事情是,v8代表在堆栈上分配的空间来保存您的clae(类C)对象,并且每个这样的对象占用0x408字节(可能更多;mov dword ptr [eax+408h], 0构造函数的反汇编行表明它至少也占用了v9的空间)。

但是反编译器不知道这一点,它看到的是有一些函数sub_1161280(在这种情况下构造函数)并且它需要一些地址,它是指向堆栈内存的指针(实际上,指向新创建的对象的指针)。

反编译器不知道这个指针实际上指向一个0x408字节块——它看到只v8引用了字节 at并且不确定其他0x407字节用于什么这就是为什么它只创建1-byte 变量v8而不命名剩余0x407字节。它可能是char v8[0x408], 但也可能是char v80x407字节组成的任何数据int可能是某些s,然后可能是其他chars 等)。

所以,如果你改变char v8char v8[0x408]应该被罚款,但我不这么彻底地分析你的代码,以确保没有任何更多的错误。

而且,顺便说一下,反编译器经常会产生不准确、不可读甚至不正确的结果,所以你永远不应该完全依赖它们;它们在某些情况下可能会有所帮助,但也会令人困惑。他们产生的只是一个伪代码,它只是一个关于原始函数可能看起来如何的提示,但绝对不是完全工作的代码。