虚函数表的指针在哪里?

逆向工程 C++ 手臂 虚函数 海湾合作委员会
2021-06-29 02:44:24

我曾经认为指向虚拟函数表(VFT,也称为虚拟方法表,VMT)的指针是对象二进制表示的第一个 32 位字。

但是现在我看到一个VFT,它的索引是13(!!!!),也就是offset=0x34。(我写“索引”是因为调用 Qt 函数的代码o.metaObject()((func***)o)[13][0](o))。OMG,这是怎么回事?为什么VFT地址位于...在哪里?

编辑(在抱怨问题不清楚之后):

每个具有虚函数的对象都有一个指向虚函数表的指针。通常,这是对象二进制表示中的第一个 32 位值(并且可以作为 访问((void**)objAddr)[0])。但是在下面的例子中,VMT 指针的偏移量不是 0!(函数名可能会被c++filt; 为了可读性起见,类名已缩短为AbcXyz):

.text:02EF171C _ZN3XyzC2EP7QObject ; constructor Xyz::Xyz(QObject*), r0 = objAddr, r1 = QObject addr
.text:02EF171C                 PUSH.W          {R4-R8,LR}
.text:02EF1720                 MOV             R4, R0
.text:02EF1722                 LDR             R5, =(_GLOBAL_OFFSET_TABLE_ - 0x02EF1730)
.text:02EF1724                 MOV             R7, R1
.text:02EF1726                 BL.W            _ZN4AbcdC2EP7QObject ; superclass_constructor(objAddr)
.text:02EF172A ; ---------------------------------------------------------------------------
.text:02EF172A                 LDR             R3, =(_ZTVN3XyzE_ptr - 0x27E4BE0) ; vtable for Xyz
.text:02EF172C                 ADD             R5, PC ; _GLOBAL_OFFSET_TABLE_
.text:02EF172E                 MOV             R6, R4
.text:02EF1730                 MOV             R1, R7
.text:02EF1732                 LDR             R3, [R5,R3] ; _ZTVN3XyzE_ptr ; pointer to vtable for Xyz
.text:02EF1734                 ADDS            R3, #8 ; *_ptr points to the (-2)nd element of VMT
.text:02EF1736                 STR.W           R3, [R6],#0x34 ; OOPS! the offset is 0x34 !!!

我希望能够为任何对象找到指向 VMT 的指针,但如上例所示,指向 VMT 的指针不一定是((void**)objAddr)[0]

所以问题是:

1)为什么 VMT 指针在对象二进制表示的中间?这个地方一定有什么特别之处。

2)如何找出 VMT 指针的实际位置?(理想情况下,在运行时给定对象地址。我有代码来区分有效地址和无效地址。我对 Android/ARM 的 GCC 感兴趣,尽管适用于不同平台的技术可能适用。)

PS在Android上检测有效地址的代码是:

#include <unistd.h>
#include <fcntl.h>
int isValidPtr(const void*p, int len) {
    if (!p) { return 0; }
    int ret = 1;
    int nullfd = open("/dev/random", O_WRONLY); // does not work with /dev/null !!!
    if (write(nullfd, p, len) < 0) {
        ret = 0; /* Not OK */
    }
    close(nullfd);
    return ret;
}

更新

在以下示例中,VMT 偏移量为 0:

class Base {
public:
  int x,y;
};
class Derived: public Base {
public:
  int z;
  Derived();
  virtual int func();
  virtual int func2();
};

来自Base*to 的强制Derived*编译为:SUBS R0, #4

int test3(Base*b) {
    Derived*d = (Derived*)b;
    int r = addDerived(*d);
    return r;
}

 ; test3(Base *)
 _Z5test3P4Base
 CBZ             R0, loc_1C7A
 SUBS            R0, #4
 B.W             _Z10addDerivedR7Derived ;

更新2

我试过

struct Cls2 {
    unsigned x[13];
    Derived d;
    Cls2();
};

这是拆卸:

.text:00001CE2 _ZN4Cls2C2Ev ; Cls2::Cls2(void)
.text:00001CE2                 PUSH            {R4,LR}
.text:00001CE4                 MOV             R4, R0
.text:00001CE6                 ADD.W           R0, R0, #0x34
.text:00001CEA                 BL              _ZN7DerivedC2Ev ; Derived::Derived(void)
.text:00001CEE                 MOV             R0, R4
.text:00001CF0                 POP             {R4,PC}

也就是说, 的 VFT 指针Cls2::d确实会在偏移量 0x34 处,但是没有STR.W R3,[R6],#0x34,因此它不是 Willem Hengeveld 建议的 #2。

但是如果我们注释掉构造函数,

struct Cls2 {
    unsigned x[13];
    Derived d;
//    Cls2();
};

int testCls2() {
    Cls2 c;
    return c.d.func2();
}

我们得到

.text:00001C9E _Z8testCls2v
.text:00001C9E var_18          = -0x18
.text:00001C9E                 PUSH            {LR}
.text:00001CA0                 SUB             SP, SP, #0x4C
.text:00001CA2                 ADD             R0, SP, #0x50+var_18
.text:00001CA4                 BL              _ZN7DerivedC2Ev ; Derived::Derived(void)
.text:00001CA8                 ADD             R0, SP, #0x50+var_18
.text:00001CAA                 BL              _ZN7Derived5func2Ev ; Derived::func2(void)
.text:00001CAE                 ADD             SP, SP, #0x4C
.text:00001CB0                 POP             {PC}

这与原始代码非常相似,但在我的情况下,VMTvtable for XyzXyz::Xyz()从封闭函数而不是从封闭函数写入的

2个回答

我可以想到 2 种情况,其中 VMT 不在对象的第一个单词中:

  • 使用多重继承
  • 当对象具有具有虚方法的成员变量时

多重继承

struct base1 {
    uint32_t x[12];
    virtual void m1() { }
};


struct base2 {
    virtual void m2() { }
};

struct cls : base1, base2 {
};

现在 base2 的 VMT 位于偏移量 0x34

虚拟会员

struct cls2 {
    uint32_t x[13];
    base2   b;
};

现在 base2 的 VMT 也位于偏移量 0x34

检测和打印虚函数表指针的代码是:

int isIdentifier(const char* s) { // true if points to [0-9a-zA-Z_]*\x00
    if(!isValidPtr(s,0x10)) { return 0; }
    if(!s[0]) { return 0; }
    int i;
    for (i=0; s[i] && i<512; i++) {
        if( i/0x10 && i%0x10 == 0 && !isValidPtr(s,0x10)) { return 0; }
        unsigned char c = s[i];
        if ('0'<=c && c<='9' || 'a'<=c && c <= 'z' || 'A'<=c && c <= 'Z' || '_' == c) {
        } else {
            return 0;
        }
    }
    return !s[i];
}

char* isVftPtr(void*addr) { // returns addr of mangled class name (prefix it with _Z to demangle with c++filt)
    unsigned int* vmtaddr = isValidPtr(addr,4)
                     && 0 == (3 & *(int*)addr)
                     && isValidPtr(*(int**)addr,4)
                     ? *(unsigned int**)addr
                     : (void*)0;
    if (vmtaddr
      &&isValidPtr(vmtaddr-2,0x20)
     ) {
        char**ptypeinfo = ((char***)vmtaddr)[-1];
        if (isValidPtr(ptypeinfo,4)
          &&isValidPtr((char***)ptypeinfo[0]-1,8)
          &&isValidPtr(((char***)ptypeinfo[0])[-1],8)
          &&isValidPtr(((char***)ptypeinfo[0])[-1][1],0x20)
          &&isIdentifier(ptypeinfo[1])
        ) {
            return !strncmp(((char***)ptypeinfo[0])[-1][1], "N10__cxxabiv",12) ? ptypeinfo[1] : 0;
        }
    }
    return 0;
}
// Usage example: printVfts("pThis", pThis, -8, 0x400)
void printVfts(const char*tag, void* addr, int from, int upto) {
    void** start = addr+from;
    void** end = addr+upto;
    DLOG("{ %s ====== printVfts %p (%p..%p)", tag, addr,start,end);
    void**p;
    char*n = 0;
    for(p=addr;p<end;p++) {
        if (n = isVftPtr(p)) {
            DLOG("vft at %p [off=0x%x] _Z%s",p,(unsigned)p - (unsigned)addr, n);
        }
    }
    DLOG("} %s ====== printVfts %p", tag, addr);
}

该代码适用于 Android/ARM。

isValidPtr()问题中给出了函数,日志宏如下:

#include <android/log.h>
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , "~~~~~~", __VA_ARGS__)
#define DLOG(...) __android_log_print(ANDROID_LOG_DEBUG  , "~~~~~~", __VA_ARGS__)

最后: printVfts()显示在偏移量 0 处还有另一个 VFT 指针。