0x20000420
0x2000042c
0x20000434
0x200003e8
0x200003f8
可以看到程序在0x20000418这个地址出错了,可是这时候的指令是addi r3,r3,29,不应该出错。查看内容,真实shellcode都已经正确解码了。
接下来看看一个灵异的事情:
(gdb) b *0x20000418 Breakpoint 1 at 0x20000418 (gdb) r
The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/san/test
Breakpoint 1, 0x20000418 in shellcode () (gdb) x/8i $pc
0x20000418
0x2000042c
0x20000434
0x200003e8
0x200003f8
(gdb)
在地址0x20000418先设一个断点,你会发现这个指令没有被解码!实在让人匪夷所思,和watercloud讨论,他认为可能是分支预测的问题,但是我们在真实shellcode前加了很多nop指令也是出现这个问题。和alert7讨论过这个事情,他觉得有可能是指令缓存的问题,并且找到几年前一些人的讨论:
http://seclists.org/lists/vuln-dev/2001/Nov/0325.html
看来老外早就讨论过这个事情了。Andersen, Thomas Bjoern (TBAndersen_at_kpmg.com)认为用一系列缓存同步指令(dcbst,sync,icbi,isync?)可以正确执行。
PowerPC 体系结构开发者指南指出自修改代码可以按照下面的序列执行代码修改用到的指令:
1. 存储修改的指令。
2. 执行 dcbst 指令,强制包含有修改过的指令的高速缓存行进行存储。 3. 执行 sync 指令,确保 dcbst 完成。
4. 执行 icbi 指令,使将要存放修改后指令的指令高速缓存行无效。
5. 执行 isync 指令,清除所有指令的指令管道,那些指令在高速缓存行被设为无效之前可能早已被取走了。
6. 现在可以运行修改后的指令了。当取这个指令时会发生指令高速缓存失败,结果就会从存储器中取得修改后的指令。
但是还有另外的问题。我的AIX机器的操作系统和CPU信息是这样的:
-bash-2.05b$ uname -a AIX aix5 1 5 001381144C00 -bash-2.05b$ lsattr -El proc0
state enable Processor state False type PowerPC_604 Processor type False frequency 232649620 Processor Speed False
测试的结果很意外,我的AIX测试机并不支持一些缓存指令:
bash-2.05b$ cat testasm.s .globl .main .csect .text[PR] .main:
icbi %r6, %r13 dcbf %r6, %r13
bash-2.05b$ gcc testasm.s testasm.s: Assembler messages:
testasm.s:4: Error: Unrecognized opcode: `icbi' testasm.s:5: Error: Unrecognized opcode: `dcbf'
bash-2.05b$ /usr/ccs/bin/as testasm.s Assembler:
testasm.s: line 4: 1252-149 Instruction icbi is not implemented in the current assembly mode COM. testasm.s: line 4: 1252-142 Syntax error.
testasm.s: line 5: 1252-149 Instruction dcbf is not implemented in the current assembly mode COM. testasm.s: line 5: 1252-142 Syntax error.
不管是GNU的as还是操作系统自己带的as都不能识别这几个关于缓存的指令。于是想着用sync和isync是不是能够解决这个问题:
-bash-2.05b$ cat test.c char shellcode[] = // decoder
\\\
\
\\
\\\
\\
\
\\
// real shellcode
\\\\\
\\
\\;
int main() {
int jump[2]={(int)shellcode,0}; ((*(void (*)())jump)());
}
用gdb调试: (gdb) r
Starting program: /home/san/test
Program received signal SIGSEGV, Segmentation fault. 0x20000420 in shellcode () (gdb) x/8i $pc-8
0x20000418
0x20000420
0x20000434
0x200003e8
0x200003f8
程序在0x20000420这个地址崩溃了,这个地方的指令是\,正好是真正的shellcode,解码正确,但执行错误。
在0x20000420这个地址下个断点再试:
(gdb) b *0x20000420 Breakpoint 1 at 0x20000420 (gdb) r
The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/san/test
Breakpoint 1, 0x20000420 in shellcode () (gdb) x/8i $pc-8
0x20000418
0x20000420
0x20000428
0x20000434
0x200003e8
0x200003f8
这时可以发现0x20000420这个地址的指令并没有被解码,sync和isync的操作没有效果。
在isync指令的地址下个断点:
(gdb) b *0x2000041c Breakpoint 1 at 0x2000041c (gdb) r
The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/san/test
Breakpoint 1, 0x2000041c in shellcode () (gdb) c Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap. 0x10000100 in ?? () (gdb) c Continuing. $ exit
Program exited normally.
这样执行就一切正常。
我还尝试了很多办法,比如把解码后的shellcode写到离当前地址很远的地方,然后把那个地址保存到lr寄存器,再用blr跳过去执行,种种努力都失败了。
0dd上一个叫phil的朋友提到他在ARM芯片上的一些经验,他说可以通过运行系统调用来刷新I-cache的指令。watercloud也提到这种实现方法。经过测试,这种方法果然可行: