编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

一次不用脚本的pwn解题(无脚本测试)

wxchong 2024-08-22 23:57:45 开源技术 9 ℃ 0 评论

原创 紫色仰望 合天智汇

0x01:前言

我们在 ctf 中 ,更多的是在 web方向中会 使用到 linux 中的/proc 目录,这里将对它进行一次介绍与学习,并且最后再举一次pwn中 运用到它的一次实例记录.


0x02:关于proc

我们这可看下维基百科对它的介绍:

在许多类 Unix 计算机系统中, procfs 是 进程 文件系统 (file system) 的缩写,包含一个伪文件系统(启动时动态生成的文件系统),用于通过内核访问进程信息。这个文件系统通常被挂载到 /proc 目录。<br><br>由于 /proc 不是一个真正的文件系统,它也就不占用存储空间,只是占用有限的内存。

以下操作系统支持 procfs :

Linux中的 /proc实现也克隆了 九号项目 中对应的部分。<br>每个正在运行的进程对应于/proc下的一个目录,目录名就是进程的PID,每个目录包含:<br>/proc/PID/cmdline, 启动该进程的命令行.<br>/proc/PID/cwd, 当前工作目录的符号链接.<br>/proc/PID/environ 影响进程的环境变量的名字和值.<br>/proc/PID/exe, 最初的可执行文件的符号链接, 如果它还存在的话。<br>/proc/PID/fd, 一个目录,包含每个打开的文件描述符的符号链接.<br>/proc/PID/fdinfo, 一个目录,包含每个打开的文件描述符的位置和标记<br>/proc/PID/maps, 一个文本文件包含内存映射文件与块的信息。<br>/proc/PID/mem, 一个二进制图像(image)表示进程的虚拟内存,<br> 只能通过ptrace化进程访问.<br>/proc/PID/root, 该进程所能看到的根路径的符号链接。如果没有chroot监狱,那么进程的根路径是/.<br>/proc/PID/status包含了进程的基本信息,包括运行状态、内存使用。<br>/proc/PID/task, 一个目录包含了硬链接到该进程启动的任何任务

即 /proc目录下各个文件其实就是运行程序的 PID号。我们可以使用cat 命令获取到每个进程中的相关信息。

前言中说到在web 方向中会使用到/proc/这个目录,

/proc/self      获取当前 进程
/proc/self/cwd     进程中包含的文件
/proc/self/environ  系统环境变量

而在 二进制方向的 pwn中,我也第一次遇到了 proc 的 题目,这里记录下来以及分享下为了大家更好得理解和阅读通畅,在开始之前我们 地该程序中相关一些函数和知识点 进行 学习。


0x03:程序涉及的相关知识

  • /dev/urandom

我们在linux中 获取随机数的时候,我的话目前经常见到是使用 rand函数,但其实 它是有很大的安全隐患的,即它是伪随机的,在(用srand产生)随机数种子一样的情况下,使用rand每次获得的序列都会是相同的。

其实 linux上的/dev/urandom文件产生较好的随机数(还有其相关文件/dev/random,有兴趣可以在网上搜搜索学习下)

"/dev/random和/dev/urandom是Linux系统中提供的随机伪设备,这两个设备的任务,是提供永不为空的随机字节数据流。很多解密程序与安全应用程序(如SSH Keys,SSL Keys等)需要它们提供的随机数据流。"

我们用open打开该文件,然后便可以从文件描述符中获取随机数据。

这里写个demo:

#include<stdio.h>
#include<fcntl.h>
int main()
{
  
int suiji_num;
int fd=open("/dev/urandom",O_RDONLY);
  
read(fd,&suiji_num,sizeof(int));
  
close(fd);

printf("suiji_num is 0x%x\n",suiji_num);
  
return 0;
}

输出结果:<br>

  • chdir()

头文件:include<unisd.h>函数原型:int chdir(const char * path);函数功能:将调用进程的当前工作目录改变为 参数path 所指的 目录

返回值:执行成功 返回 0 失败 返回 -1

写个demo:

#include<stdio.h>
#include <unistd.h>
main()
{ 
        chdir("/tmp");
        printf("当前目录为: %s\n", getcwd(NULL, NULL)); 
        return 0;
}

输出结果:<br>


0x04:实例学习

我们 以 find your self 这个pwn 题 学习下 ,一个不用写脚本的 pwn。

例行检查:64位 ELF 文件,开启了canary 和 NX保护,但其实对这题 并没有什么影响。

$ file fys

fys: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=76f8c2cb2804973c40b8c6999ecc4dca5f732786, not stripped

$ checksec fys

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

拖入ida,看main 函数

int __cdecl main(int argc, const char **argv, const char **envp{  char s1; // [rsp+0h] [rbp-90h]  char buf; // [rsp+40h] [rbp-50h]  unsigned __int64 v6; // [rsp+88h] [rbp-8h]  v6 = __readfsqword(0x28u);  init();  memset(&buf, 0, 0x40uLL);  getcwd(&buf, 0x40uLL);                        // 当前的工作目录绝对路径复制到参数buf 所指的内存空间  puts("where are you?");  read_n((__int64)&s1, 0x40u);                  // 向 s 处最多 输入 0x40 即64 的字节的字符串  if ( strcmp(&s1, &buf) )                      // 需要 绕过 这个 if 判断 需要 使s1 字符串与buf字符串相同  {    puts("nonono,not there");    exit(0);  }  read_n((__int64)&s1, 0x14u);                  // 覆盖 s1 ,最多输入 0x14 字节长度的 数据  if ( (unsigned int)check2(&s1) == -1 )        // 不能  s1 不能含有  *,sh,cat,..,&,|,>,<   命令符号  {    puts("oh,it's not good idea");    exit(0);                                   }  close(1);                                     // 关闭 stdout  close(2);                                     // 关闭 stderr  system(&s1);  return 0;                                     // 然后 执行系统函数}

这里有个 两个 if 判断,先看第一个 我们要绕过它的话,就得使我们 输入字符串 s 等于 当前的工作目录绝对路径(在后面的分析中我们可以知道这个当前的工作目录绝对路径是通过 chdir()函数 设置的 目录字符串)

第二个 if 判断 ,我们输入的字符串 s 要经过 check 2 函数处理,我们看下 分析下check2函数 即 我们 输入 s1 不能含有 *,sh,cat,..,&,|,>,< 这些符号即过滤了

sh
cat
 * 
 &
 |
 >
 <
signed __int64 __fastcall check2(const char *a1)
{
  signed __int64 result; // rax
  
  if ( strchr(a1, '*') 
      || strstr(a1, "sh")
      || strstr(a1, "cat")
      || strstr(a1, "..")
      || strchr(a1, '&')
      || strchr(a1, '|') 
      || strchr(a1, '>')
      || strchr(a1, '<') ) 
  { 
    result = 0xFFFFFFFFLL;                                                    // -1
  }
  else
  {
    result = 0LL; 
  } 
  return result;
}

另外 这里我们呢看到了在执行最后system(&s1) 之前 关闭了标准输出和错误输出。

close(1);                 // 关闭 stdoutclose(2);                 // 关闭 stderr

所以 无论 system(&s1) 执行 什么 系统命令,屏幕上都不会显示 任何输出信息。所以,我们需要 解决的的第二件事情就是,将屏幕回显 。这样

exec 1>&0       //把stdout 重定向 stdin  即可

在main 函数中我们看到程序首先执行了 init 函数用于初始化 工作

这里的具体分析请详见代码中的注释!

unsigned __int64 init()
{
  int buf; // [rsp+4h] [rbp-51Ch]
  int i; // [rsp+8h] [rbp-518h]
  int fd; // [rsp+Ch] [rbp-514h]
  int v4[52]; // [rsp+10h] [rbp-510h]
  char v5[1008]; // [rsp+E0h] [rbp-440h]
  char s; // [rsp+4D0h] [rbp-50h]
  char command; // [rsp+4F0h] [rbp-30h]
  unsigned __int64 v8; // [rsp+518h] [rbp-8h]
  
  v8 = __readfsqword(0x28u);
  setbuf(stdin, 0LL);
  setbuf(_bss_start, 0LL);
  fd = open("/dev/urandom", 0);
  buf = 0;
  read(fd, &buf, 1uLL);                         // 向 buf写入一个 随机数
  buf %= 50;                                       // 可认为是 50以内大小的 随机数
  if ( fd < 0 )
   exit(-1);
  chdir("./tmp");                               // 把当前目录改为 ./tmp
  for ( i = 0; i <= 49; ++i )  {    read(fd, &v4[i], 4uLL);                     // 向v4[52]中 循环 输入了 50 次的随机数    snprintf(&v5[20 * i], 20uLL, "0x%x", (unsigned int)v4[i]);// 将上面  50个的随机数 以16进制的形式再循环写入 v5[20*i]中    mkdir(&v5[20 * i], 0x1EDu);                 // 然后已 这些 16进制随机数 命名,新建50个 文件夹  }  snprintf(&s, 0x16uLL, "./%s", &v5[20 * buf]); // 将 v5 [20*buf] 转化为 字符串形式 然后前面连接上./ 存在了s中  chdir(&s);                                    // 然后再将 当前 工作空间 更换成s 处字符串的名字  puts("find yourself");  read_n((__int64)&command, 0x19u);             // 这里 我们可以 执行 系统命令,最长 0x19  即25 长度  if ( (unsigned int)check1(&command) != -1 )    system(&command);  return __readfsqword(0x28u) ^ v8;}

即首先在 /tmp 目录下创建 50个空文件夹,文件名是随机的16进制位数,

接着有通过 chdir(&s) 随机 更改了当前进程的工作目录,然后我们可以执行 system(&s1)系统函数,最后返回到main函数 。但s1 要经过 check1函数检测。

我们进入check1 函数分析下代码

signed __int64 __fastcall check1(const char *a1){  signed __int64 result; // rax  int i; // [rsp+1Ch] [rbp-14h]  for ( i = 0; i < strlen(a1); ++i )  {                                             // 即能 输入的字符有 55个:                                                // 97-122 对应 a-z    26                                                // 65-90  对应 A-Z    26                                                // '/' ' ' '-'        3    if ( (a1[i] <= 96 || a1[i] > 122) && (a1[i] <= 64 || a1[i] > 90) && a1[i] != '/' && a1[i] != ' ' && a1[i] != '-' )      return 0xFFFFFFFFLL;  }  if ( strstr(a1, "sh") || strstr(a1, "cat") || strstr(a1, "flag") || strstr(a1, "pwd") || strstr(a1, "export") )    result = 0xFFFFFFFFLL;                      // 不能 含有的                                                // sh                                                // cat                                                // flag                                                // pwd                                                // export  else    result = 0LL;  return result;}

分析check1函数得,我们输入的字符串 s1 只能是 a-z,A-Z 再加上 '/' ,' ','-' 共55 的字符组合

a b c d e f g h i j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
/   -

且不能含有

sh
cat
flag
pw
dexport

整个程序,可以执行两次 system(s) 命令为了可以执行第二次的,我们第一次输入需要 得到 当前程序 的 绝对目录 这里即可使用,上面说过的

ls -l /proc/self/cwd 当前工作目录的符号链接,即文件夹名

为了阅读方通常我把check1的过滤放在下面:


这是我们已经绕过了 main函数中的 if 函数了,接着我们要输入的system(&s) 的参数 s 时我们要绕过 check2


因为我们只有这一次输入机会了,我们 这步要拿到shell,这个很好绕过 我们可以通过

$0  //拿到shell

或者字符串拼接

a=h;s$a

为了很多新手看不懂,这里简单说下,字符串拼接的原理

字符串拼接绕过 过滤 是 做ctf中(特别是web 方向)会 经常遇到的,这个是Linux shell的语法 初始化 a为变量,并赋值为 h, ';'是linux shell中的命令分隔符,然后 s$a 就相当于输入了 sh即可拿到 shell。

然后我们 便可执行任意命令去 得到 flag,我们接着可以再 将 屏幕的 回显打开

exec 1>&0       //把stdout 重定向 stdin

然后再 cat /flag 便成功了<br>

这题是我在二进制方向第一次用到 /proc 这个知识点。觉得这次很详细了,可供给大家学习下。

如果想更多系统的学习CTF,可点击“http://www.hetianlab.com/pages/CTFLaboratory.jsp”,进入CTF实验室学习。

声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表