识别可变参数函数

逆向工程 拆卸 调用约定 C
2021-07-07 06:18:24

printf(char* format, ...)反汇编后的 C 变量参数函数会是什么样子?

是否总是通过调用约定来识别,还是有更多的方法来识别它?

2个回答

它在某些架构中非常简单,而在其他架构中则不是很明显。我将描述一些我熟悉的。

SystemV x86_64(Linux、OS X、BSD)

可能是最容易识别的。由于在 中指定使用的 XMM 寄存器数量的愚蠢决定al,大多数 vararg 函数开始如下:

    push    rbp
    mov     rbp, rsp
    sub     rsp, 0E0h
    mov     [rbp+var_A8], rsi
    mov     [rbp+var_A0], rdx
    mov     [rbp+var_98], rcx
    mov     [rbp+var_90], r8
    mov     [rbp+var_88], r9
    movzx   eax, al
    lea     rdx, ds:0[rax*4]
    lea     rax, loc_402DA1
    sub     rax, rdx
    lea     rdx, [rbp+var_1]
    jmp     rax
    movaps  xmmword ptr [rdx-0Fh], xmm7
    movaps  xmmword ptr [rdx-1Fh], xmm6
    movaps  xmmword ptr [rdx-2Fh], xmm5
    movaps  xmmword ptr [rdx-3Fh], xmm4
    movaps  xmmword ptr [rdx-4Fh], xmm3
    movaps  xmmword ptr [rdx-5Fh], xmm2
    movaps  xmmword ptr [rdx-6Fh], xmm1
    movaps  xmmword ptr [rdx-7Fh], xmm0
loc_402DA1:

请注意它如何al用于确定溢出到堆栈上的 xmm 寄存器的数量。

Windows x64 又名 AMD64

在 Win64 中,它不太明显,但有一个迹象:与椭圆形参数相对应的寄存器总是溢出到堆栈上,并位于与堆栈上传递的其余参数对齐的位置。例如,这是printf的序言:

  mov     rax, rsp
  mov     [rax+8], rcx
  mov     [rax+10h], rdx
  mov     [rax+18h], r8
  mov     [rax+20h], r9

在这里,rcx包含了固定的format说法,和椭圆的参数传递中rdxr8并且r9然后在堆栈中。我们可以观察到rdx,r8r9被一个接一个地存储,并且刚好在以 开始的其余参数的下方rsp+0x28区域 [rsp+8..rsp+0x28]正是为此目的保留的,但非可变参数函数通常不会在那里存储所有寄存器参数,或者将该区域重用于局部变量。例如,这是一个-vararg 函数序言:

  mov     [rsp+10h], rbx
  mov     [rsp+18h], rbp
  mov     [rsp+20h], rsi

您可以看到它使用保留区域来保存非易失性寄存器,而不是溢出寄存器参数。

手臂

ARM 调用约定使用R0-R3作为第一个参数,因此 vararg 函数需要将它们溢出到堆栈上以与堆栈上传递的其余参数对齐。因此,您将看到R0- R3(或R1- R3,或R2-R3或只是R3)被推入堆栈,这通常不会发生在非可变参数函数中。这不是一个 100% 万无一失的指标——例如微软的编译器有时会推送R0——R1到堆栈上并使用SP而不是移动到其他寄存器并使用它来访问它们但我认为这对 GCC 来说是一个非常可靠的迹象。以下是 GCC 编译函数的示例:

STMFD   SP!, {R0-R3}
LDR     R3, =dword_86090
STR     LR, [SP,#0x10+var_14]!
LDR     R1, [SP,#0x14+varg_r0] ; format
LDR     R0, [R3]        ; s
ADD     R2, SP, #0x14+varg_r1 ; arg
BL      vsprintf
LDR     R3, =dword_86094
MOV     R2, #1
STR     R2, [R3]
LDR     LR, [SP+0x14+var_14],#4
ADD     SP, SP, #0x10
RET

它显然是一个 vararg 函数,因为它正在调用vsprintf,我们可以看到R0-R3在开始时被推送(在此之前你不能推送任何其他东西,因为潜在的堆栈参数存在,SP所以R0-R3必须在它们之前)。

(我的答案是特定于 x86 的)。

在函数内部,它看起来就像任何其他函数。唯一的区别是,在函数执行过程中的某个时刻,它将获取最后一个非变量参数的(堆栈)地址,并将其增加平台上的字长;然后将 this 用作指向变量参数基址的指针。在函数的外部,您将观察到不同数量的参数作为参数传递给函数(通常其中一个非可变参数将作为可变参数函数的一些明显指示符,例如硬编码格式字符串或相似的东西)。不能是可变参数函数__stdcall,因为__stdcall依赖于预编译ret XXh指令,而可变参数函数的重点是可以传递未知数量的参数。因此,这些函数必须是__cdecl,即调用者必须更正堆栈以移除所有推送的参数。