在 SPARC 上,当一个分支放置在另一个分支的分支延迟槽中时会发生什么?

逆向工程 混淆 二元分析 斯帕克
2021-07-04 01:37:00

我们的团队最近不得不查看SPARC 组装规范,问题是我没有 SPARC 处理器来尝试它(我应该设置一个模拟器或在我的桌子上放一个这些旧的 Sparc 站......我知道)。

不知何故,ba(总是分支)指令让我感到困惑,因为它是一条延迟分支执行指令。这是不是意味着刚好位于该指令ba跳发生之前得到执行。

我的一位同事提出了一个非常有趣的问题,在这种情况下会发生什么:

0x804b38 ba 0x805a10
0x804b3c ba 0x806844
...
0x805a10 add %r3, %r2, %r5
...
0x806844 sub %r3, %r5, %r2
...

根据规范,我们的猜测是运行应该像这样:

0x805a10 add %r3, %r2, %r5
0x806844 sub %r3, %r5, %r2
0x806848 ...

这意味着您可能可以在其他指令块中跳转并拾取一条指令,然后运行到下一条指令ba......我想知道 CFG 会是什么样子。

无论如何,这是“简单”的情况,如果我们有动态跳转(jmp指令类似于 aba但基于给定寄存器中存储的地址):

0x804b38 jmp %r3
0x804b3c jmp %r0
...
(%r3)    change %r0
...

这是误导静态分析仪的好方法吗?或者,有没有办法通过简单的计算来猜测它在做什么?

1个回答

我稍微编辑了整个示例,以便更好地匹配问题。

我的 SPARC 程序集 fu 很弱,但我所做的是在 C 中编写了一个小“Hello world”(或者可以说是跳转/ gotos),并用于gcc -S将其转换为程序集。我有一个运行它的 SPARC,详细信息:

$ isainfo -v
64-bit sparcv9 applications
        vis2 vis
32-bit sparc applications
        vis2 vis v8plus div32 mul32

注意: b与 相同jmp,它只是同一事物的不同助记符,真的。一个取一个立即数 ( b),另一个取一个寄存器 ( jmp)。

事实证明,您提供的链接对于 GCC 是正确的:

请注意,最后一条指令在跳转发生之前执行,而不是在子程序返回之后执行。跳转后的第一条指令称为延迟槽。通常的做法是用不执行任何任务的特殊操作填充延迟槽,称为无操作,或nop

实际生活测试

我认为我们需要在使用和不使用调试器的情况下执行此操作,因为尚不清楚它在调试器下的行为是否会有所不同。所以代码应该输出一些可读的东西,这样我们就可以看到我们的修补有什么样的效果;)

C代码

#include <stdio.h>

int foo(int argc)
{
        switch(argc)
        {
        case 0:
        case 1:
                goto a1;
        case 2:
                return 3;
        case 4:
                goto a2;
        case 5:
                return -1;
        default:
                goto a4;
        }
a1:     return 1;
a2:     return 2;
a4:     return 4;
}

int main(int argc, char** argv)
{
        printf("Hello world: %i\n", foo(argc));
        return foo(argc);
}

这给了我很多分支指令来处理问题中提出的想法。

程序集创建者 gcc -S

这是我修改之前的程序集:

        .file   "test.c"
        .section        ".text"
        .align 4
        .global foo
        .type   foo, #function
        .proc   04
foo:
        !#PROLOGUE# 0
        save    %sp, -120, %sp
        !#PROLOGUE# 1
        st      %i0, [%fp+68]
        ld      [%fp+68], %g1
        cmp     %g1, 5
        bgu     .LL11
        nop
        ld      [%fp+68], %g1
        sll     %g1, 2, %i5
        sethi   %hi(.LL12), %g1
        or      %g1, %lo(.LL12), %g1
        ld      [%i5+%g1], %g1
        jmp     %g1
         nop
.LL6:
        mov     3, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL9:
        mov     -1, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL5:
        mov     1, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL8:
        mov     2, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL11:
        mov     4, %g1
        st      %g1, [%fp-20]
.LL1:
        ld      [%fp-20], %i0
        ret
        restore
        .align 4
        .align 4
.LL12:
        .word   .LL5
        .word   .LL5
        .word   .LL6
        .word   .LL11
        .word   .LL8
        .word   .LL9
        .size   foo, .-foo
        .section        ".rodata"
        .align 8
.LLC0:
        .asciz  "Hello world: %i\n"
        .section        ".text"
        .align 4
        .global main
        .type   main, #function
        .proc   04
main:
        !#PROLOGUE# 0
        save    %sp, -112, %sp
        !#PROLOGUE# 1
        st      %i0, [%fp+68]
        st      %i1, [%fp+72]
        ld      [%fp+68], %o0
        call    foo, 0
         nop
        mov     %o0, %o5
        sethi   %hi(.LLC0), %g1
        or      %g1, %lo(.LLC0), %o0
        mov     %o5, %o1
        call    printf, 0
         nop
        ld      [%fp+68], %o0
        call    foo, 0
         nop
        mov     %o0, %g1
        mov     %g1, %i0
        ret
        restore
        .size   main, .-main
        .ident  "GCC: (GNU) 3.4.3 (csl-sol210-3_4-branch+sol_rpath)"

我将专注于修改 的结果foo(),所以我不会再次重复所有的汇编代码,而是只重复一些零碎的部分。

顺便说一句:GCC 为nop指令创建了额外的缩进,但当然可以很容易地发现它们。

涉及修补的从 C 到可执行文件的步骤

以下是进入修改后的程序的步骤。

  • 用于gcc -S test.c获取test.s文件
  • 修改 test.s
  • 组装它 gas -o test.o test.s
  • 使用 GCC 链接 gcc -o test test.o

对汇编代码的修改

首先,我感到不得不以“优化”中的说明LL6LL9LL5LL8LL11LL1这样的:

.LL6:
        mov     3, %i0
        b       .LL1
         nop
.LL9:
        mov     -1, %i0
        b       .LL1
         nop
.LL5:
        mov     1, %i0
        b       .LL1
         nop
.LL8:
        mov     2, %i0
        b       .LL1
         nop
.LL11:
        mov     4, %i0
.LL1:
        ret
        restore

应该清楚的是,如果您的同事是对的,我们应该能够将nop说明替换为 amov ..., %i0以查看预期值以外的内容。

我调用了我修改后的程序集文件modified.s以免混淆自己;)

验证我的“优化”

第一个测试是我的“仅优化”。我写了一个小测试脚本:

#!/usr/bin/env bash
for i in optimized test; do
        echo -n "$i: "; ./$i
        echo -n "$i: "; ./$i a1
        echo -n "$i: "; ./$i a1 a2
        echo -n "$i: "; ./$i a1 a2 a3
done

二进制文件被称为optimized(我上面的“优化”)和test(由 GCC 从 C 代码创建的普通程序集)。

结果:

$ ./runtest
optimized: Hello world: 1
optimized: Hello world: 3
optimized: Hello world: 4
optimized: Hello world: 2
test: Hello world: 1
test: Hello world: 3
test: Hello world: 4
test: Hello world: 2

所以我的“优化”似乎很好。现在让我们稍微修改一下。

修改修改程序计数器的指令

声称任何超过 a jmp(ie b) 的东西都在跳转之前执行我们有几个带有跳转的标签,所以让我们nop用改变内部值的东西替换每个标签,从而改变%i0的返回值foo()

变化:

.LL6:
        mov     3, %i0
        b       .LL1
        mov     30, %i0
.LL9:
        mov     -1, %i0
        b       .LL1
        mov     42, %i0
.LL5:
        mov     1, %i0
        b       .LL1
        mov     10, %i0
.LL8:
        mov     2, %i0
        b       .LL1
        mov     20, %i0
.LL11:
        mov     4, %i0
.LL1:
        ret
        restore

因此,除了返回代码-1(变为42)和4(保持不变)之外,现在一切都应该返回原始值乘以 10。

让我们看看结果(我添加modifiedfor循环中的项目列表中):

$ ./runtest
optimized: Hello world: 1
optimized: Hello world: 3
optimized: Hello world: 4
optimized: Hello world: 2
test: Hello world: 1
test: Hello world: 3
test: Hello world: 4
test: Hello world: 2
modified: Hello world: 10
modified: Hello world: 30
modified: Hello world: 4
modified: Hello world: 20

与您的示例尽可能接近的更改

        mov     39, %i0
        jmp     %g1
        b       .LL11
        b       .LL1
.LL6:
        mov     37, %i0
        b       .LL1
        mov     30, %i0
[...]
.LL11:
        mov     4, %i0
.LL1:
        ret
        restore

修改测试脚本,输出如下:

$ ./runtest
optimized: Hello world: 1
optimized: Hello world: 3
optimized: Hello world: 4
optimized: Hello world: 2
test: Hello world: 1
test: Hello world: 3
test: Hello world: 4
test: Hello world: 2
modified: Hello world: 10
modified: Hello world: 30
modified: Hello world: 4
modified: Hello world: 20
question: Hello world: 4
question: Hello world: 4
question: Hello world: 4
question: Hello world: 4

莫名其妙!

结果

毫无疑问,您可以在逆向工程师的头脑中耍花招。我学到了一些新东西,仅此一项就值得了。

这是情况

jmp     %g1
b       .LL11 ; <-- this is the branch taken
b       .LL1
mov     37, %i0 ; <-- but this gets executed first (at least in GDB)

现在我不知道这是否适用于所有 SPARC 机器,但肯定适用于我用于测试的机器(规格在顶部)

结论

是的,这当然可以用来欺骗不知情的逆向工程师和反汇编程序(静态分析工具)。它基本上是一个不透明的谓词。即结果在编译时是明确的,但它看起来是动态的。

鉴于我只有 IDA Pro 并且objdump在此处可用,因此很难看出不同反汇编程序的性能如何我有根据的猜测是,它们与其他不透明谓词的处理方式相同,即有时它们会被愚弄,有时它们会出奇地聪明。因此,这是否是一种合适的混淆方法仍未解决。

奖金信息

与编辑之前相反,IDA 似乎对新代码有些困惑,请观看此图表视图:

IDA 有点困惑 单击此处获取全尺寸图像以前的版本

小 GDB 会话

0x106CCmov 39, %i0通过 IDA 找到指令。

$ gdb -q ./question
(no debugging symbols found)
(gdb) b *0x106CC
Breakpoint 1 at 0x106cc
(gdb) run a1
Starting program: /export/home/builder/test/question a1
[New LWP 1]
[New LWP 2]
[LWP 2 exited]
[New LWP 2]
(no debugging symbols found)
(no debugging symbols found)

Breakpoint 1, 0x000106cc in foo ()
(gdb) disp/i $pc
1: x/i $pc
0x106cc <foo+44>:       mov  0x27, %i0
(gdb) si
0x000106d0 in foo ()
1: x/i $pc
0x106d0 <foo+48>:       jmp  %g1
0x106d4 <foo+52>:       b  0x1070c <foo+108>
0x106d8 <foo+56>:       b  0x10710 <foo+112>
0x106dc <foo+60>:       mov  0x25, %i0
(gdb)
0x000106d4 in foo ()
1: x/i $pc
0x106d4 <foo+52>:       b  0x1070c <foo+108>
0x106d8 <foo+56>:       b  0x10710 <foo+112>
0x106dc <foo+60>:       mov  0x25, %i0
(gdb)
0x000106dc in foo ()
1: x/i $pc
0x106dc <foo+60>:       mov  0x25, %i0
(gdb)
0x0001070c in foo ()
1: x/i $pc
0x1070c <foo+108>:      mov  4, %i0
(gdb)
0x00010710 in foo ()
1: x/i $pc
0x10710 <foo+112>:      ret
0x10714 <foo+116>:      restore
(gdb)

所以根据GDB,我们mov 37, %i0在分支之前执行这对我来说似乎暗示,即使您链接多个分支指令,首先要执行的是链中最后一个之后的内容。