我稍微编辑了整个示例,以便更好地匹配问题。
我的 SPARC 程序集 fu 很弱,但我所做的是在 C 中编写了一个小“Hello world”(或者可以说是跳转/ goto
s),并用于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
对汇编代码的修改
首先,我感到不得不以“优化”中的说明LL6
,LL9
,LL5
,LL8
,LL11
和LL1
这样的:
.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。
让我们看看结果(我添加modified
到for
循环中的项目列表中):
$ ./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 似乎对新代码有些困惑,请观看此图表视图:
单击此处获取全尺寸图像(以前的版本)
小 GDB 会话
0x106CC
是mov 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
在分支之前执行。这对我来说似乎暗示,即使您链接多个分支指令,首先要执行的是链中最后一个之后的内容。