移位指针系C中常用的特征,IDA在反编译中借鉴使用了其原理来表示具有相关偏移特征的指针。
假设源码中存在下面结构:
struct mystruct
{
char buf[16];
int dummy;
int value; // <- myptr 指向这
double fval;
};
那么移位指针定义如下:
int *__shifted(mystruct,20) myptr;
myptr
是指向 int
的指针,如果我们将其递减 20 个字节,则最终得到 mystruct*
。事实上,偏移值不限于结构体中,甚至可以是负数。此外,“父”类型不必是结构体,而是可以是除 void
之外的任何类型。这在某些情况下很有用。
当移位指针用来调整数据结构时候,IDA会使用 ADJ
辅助显示,这是一个伪运算符,它将指针返回到父类型(在我们的例子中为 mystruct
)。例如,如下将指向fval参数:
ADJ(myptr)->fval
结构数组中的使用
编译处理结构体数组的代码时,编译器可以优化循环,以便“当前项”指针指向结构的中间而不是开头。当仅访问一小部分字段时,这种情况尤其常见。考虑这个例子:
struct mydata
{
int a, b, c;
void *pad[2];
int d, e, f;
char path[260];
};
int sum_c_d(struct mydata *arr, int count)
{
int sum=0;
for (int i=0; i< count; i++)
{
sum+=arr[i].d+arr[i].c*43;
}
return sum;
}
当使用 Microsoft Visual C++ x86 编译时,可以生成以下代码:
?sum_c_d@@YAHPAUmydata@@H@Z proc near
arg_0 = dword ptr 4
arg_4 = dword ptr 8
mov edx, [esp+arg_4] ;count
push esi
xor esi, esi
test edx, edx
jle short loc_25
mov eax, [esp+4+arg_0] ;*arr
add eax, 14h
loc_12: ; CODE XREF: sum_c_d(mydata *,int)+23↓j
imul ecx, [eax-0Ch], 2Bh ; '+'
add ecx, [eax]
lea eax, [eax+124h]
add esi, ecx
sub edx, 1
jnz short loc_12
loc_25: ; CODE XREF: sum_c_d(mydata *,int)+9↑j
mov eax, esi
pop esi
retn
即使添加并指定了正确的类型,初始反编译看起来也很奇怪:
int __cdecl sum_c_d(struct mydata *arr, int count)
{
int v2; // edx
int v3; // esi
int *p_d; // eax
int v5; // ecx
v2 = count;
v3 = 0;
if ( count <= 0 )
return v3;
p_d = &arr->d;
do
{
v5 = *p_d + 43 * *(p_d - 3);
p_d += 73;
v3 += v5;
--v2;
}
while ( v2 );
return v3;
}
显然,编译器决定使用指向 d 字段的指针并访问相对于它的 c 。我们怎样才能让这个看起来更好呢?
我们可以通过手动计算、检查反汇编或在伪代码中将鼠标悬停在其上来找出 d 在结构中的偏移量。
因此,我们可以将 p_d 的类型更改为 int * __shifted(mydata, 0x14) 以获得改进的伪代码:
int __cdecl sum_c_d(struct mydata *arr, int count)
{
int v2; // edx
int v3; // esi
int *__shifted(mydata,0x14) p_d; // eax
int v5; // ecx
v2 = count;
v3 = 0;
if ( count <= 0 )
return v3;
p_d = &arr->d;
do
{
v5 = ADJ(p_d)->d + 43 * ADJ(p_d)->c;
p_d += 73;
v3 += v5;
--v2;
}
while ( v2 );
return v3;
}
前置元数据
此技术用于原始内存块需要附加一些管理信息的情况,即堆分配器、托管字符串等。作为一个具体示例,让我们考虑经典的 MFC 4.x CString 类。它使用放置在实际字符数组之前的结构:
struct CStringData
{
long nRefs; // reference count
int nDataLength; // length of data (including terminator)
int nAllocLength; // length of allocation
// TCHAR data[nAllocLength]
TCHAR* data() // TCHAR* to managed data
{
return (TCHAR*)(this+1);
}
};
也就是逆向中总是会看到CString的字符串前面多了几个结构块。CString类本身只有一个数据成员:
class CString
{
public:
// Constructors
[...skipped]
private:
LPTSTR m_pchData; // pointer to ref counted string data *******
// implementation helpers
CStringData* GetData() const;
[...skipped]
};
inline
CStringData*
CString::GetData(
) const
{
ASSERT(m_pchData != NULL);
return ((CStringData*)m_pchData)-1;
}
内存中的结构如下:
┌───────────────┐
│ nRefs │
├───────────────┤
CStringData │ nDataLength │
├───────────────┤
│ nAllocLength │
├───────────────┴─────┐
┌──►│'H','e','l','l','o',0│
│ └─────────────────────┘
│
│
┌─┴────────┐
CString │m_pchData │
└──────────┘
下面是 CString 的析构函数在初始反编译中的样子:
void __thiscall CString::~CString(CString *this)
{
if ( *(_DWORD *)this - (_DWORD)off_4635E0 != 12 && InterlockedDecrement((volatile LONG *)(*(_DWORD *)this - 12)) <= 0 )
operator delete((void *)(*(_DWORD *)this - 12));
}
即使在使用单个成员 *m_pszData 创建 CString 结构之后,它仍然有些令人困惑:
void __thiscall CString::~CString(CString *this)
{
if ( this->m_pszData - (char *)off_4635E0 != 12 && InterlockedDecrement((volatile LONG *)this->m_pszData - 3) <= 0 )
operator delete(this->m_pszData - 12);
}
最后,如果我们如上所述创建 CStringDatastruct 并将 CString 成员的类型更改为:char *__shifted(CStringData,0xC) m_pszData,结构将会比较清晰:
void __thiscall CString::~CString(CString *this)
{
if ( ADJ(this->m_pszData)->data - (char *)off_4635E0 != 12 && InterlockedDecrement(&ADJ(this->m_pszData)->nRefs) <= 0 )
operator delete(ADJ(this->m_pszData));
}
现在代码更容易理解了:如果递减的引用计数变为零,则删除 CStringData 实例。
原文始发于微信公众号(TIPFactory情报工厂):TIPs | 理解IDA中移位指针的使用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论