Curiosity is insubordination in its purest form. -Vladimir Nabokov

miércoles, 3 de abril de 2019

Haciendo un cargador (loader) para crackear un programa MS-DOS en ejecucion (II)

NOTA: Esta entrada es un addendum a la primera parte. Si lo que buscas es un tutorial mas o menos detallado te recomiendo que le eches un vistazo a la primera parte.

Comenzamos. En el cargador de la primera parte, para crackear La Colmena, parcheabamos en memoria asi:
mov Word Ptr [0097], 9090h  ; CRACK ~ escribimos los bytes 90 90 en DS:[0097]
En este caso es correcto, ya que cuando la instruccion MOV se ejecuta, los bytes que queremos parchear estan en DS:[0097], pero normalmente no tenemos el valor del segmento en ningun registro, por lo que en la practica la situacion es que las instrucciones a sobreescribir residen en un segmento distinto en cada ejecucion, y tendremos que buscar en la pila en que posicion se ha pusheado el segmento, o a malas siempre podriamos averiguarlo por fuerza bruta.

Vamos a poner un ejemplo, otro juego para el que que yo sepa nunca se ha publicado crack: GoGo Our Star [1], un juego coreano de naves de 1993 donde la pantalla de claves aparece al finalizar el primer nivel. Esta vez me voy a ahorrar el proceso y voy a poner un par de cargadores directamente. Primero por fuerza bruta, y despues intentaremos hacerlo mas limpio.

Fuerza bruta pues. Vemos que el desplazamiento siempre es el mismo, asi que haremos fuerza bruta sobre el segmento, recorriendo todos los segmentos posibles desde [0001] hasta [FFFD] y buscando en ellos los bytes que queremos parchear. Si los encontramos, entonces parcheamos. La interrupcion que capturamos es la 10h (Video).
SIZE EQU 1024       ; Este programa y su pila caben en 1 KB
OVL segment para 'code'
    assume cs:OVL, ds:OVL
    org 100h        ; Esto sera un programa .COM

START: jmp INITCODE ; Codigo de inicializacion

OLDINT10 dw 0,0           ; Espacio para el puntero a la INT_10h del sistema

NEWINT proc far
    cmp ax, 0013h   ; Se ha llamado a INT 10h con AX=0013 ?
    jnz EXIT        ; NO: saltamos a la INT 10h del sistema

    push es         ; Guardamos ES
    push ax         ; Guardamos AX
    xor ax, ax      ; AX=0

; fuerza bruta
; buscamos en todos los segmentos [0001]..[FFFD] los bytes que queremos parchear
INICIO:
    inc ax
    cmp ax, 0fffe   ; Hemos llegado al ultimo segmento?
    je NOTRIGHT     ; SI: nos vamos sin parchear
    mov es, ax      ; ES=segmento a comprobar
    cmp Word Ptr es:[032b], 0374h   ; ¿ES:[032b] = 74 03?
    jne INICIO                      ; NO: vamos a comprobar el segmento siguiente
    cmp Word Ptr es:[034a], 3675h   ; ¿ES:[034a] = 75 36?
    jne INICIO                      ; NO: vamos a comprobar el segmento siguiente
    cmp Word Ptr es:[0366], 1a75h   ; ¿ES:[0366] = 75 1a?
    jne INICIO                      ; NO: vamos a comprobar el segmento siguiente

; Hemos encontrado el segmento! :) Vamos a parchear
; ES:[032b] = JMP SHORT
    mov Word Ptr es:[032b], 03ebh   ; Crack ~ escribimos los bytes EB 03 en es:[032b]

; ES:[034a] = NOP NOP
    mov Word Ptr es:[034a], 9090h   ; Crack ~ escribimos los bytes 90 90 en es:[034a]

; ES:[0366] = NOP NOP
    mov Word Ptr es:[0366], 9090h   ; Crack ~ escribimos los bytes 90 90 en es:[0366]

NOTRIGHT:
    pop ax      ; Restauramos ax
    pop es      ; Restauramos es
EXIT:
    jmp DWord Ptr cs:[OLDINT]   ; Saltamos a la INT 10h del sistema
NEWINT endp

INITCODE:
    mov sp, SIZE            ; Redefinimos la pila
    mov bx, SIZE/16
    mov ah, 4ah             ; Redimensionamos bloque memoria
    int 21h

    lea dx, MSG
    mov ah, 9               ; Escribimos texto por pantalla
    int 21h

    xor ah, ah              ; Esperamos pulsacion de tecla
    int 16h

    mov ax, 3510h           ; Queremos el valor del puntero a la INT 10h del sistema
    int 21h
    mov OLDINT[0], bx       ; Guardamos puntero a la INT 10h del sistema
    mov OLDINT[2], es       ;  (necesitaremos llamarla despues)

    mov ax, 2510h           ; Sobreescribimos en el vector de interrupciones el puntero a INT 10h por
    lea dx, NEWINT          ; un puntero a nuestra rutina NEWINT proc far
    int 21h

    push cs
    pop es                  ; Necesitaremos ES:BX apuntando a EXEC_INFO

    lea bx, EXEC_INFO
    mov DWord Ptr [bx], 0
    mov DWord Ptr [bx+2], 80h   ; PSP
    mov Word Ptr [bx+4], cs
    mov Word Ptr [bx+6], 5ch    ; FCB 0
    mov Word Ptr [bx+8], cs
    mov Word Ptr [bx+0ah], 6ch  ; FCB 1
    mov Word Ptr [bx+0ch], cs

    lea dx, FILENAME
; ES:BX apuntando a EXEC_INFO
; DS:DX apuntando al nombre ASCIIZ del archivo a ejecutar
    mov ax, 4b00h
    int 21h             ; cargar y ejecutar programa

    mov dx, OLDINT[0]
    mov ds, OLDINT[2]
    mov ax, 2516h       ; Restauramos la INT_10h del sistema
    int 21h

    push cs
    pop ds              ; DS = CS

    mov ax, 4c00h       ; terminar
    int 21h

FILENAME    db "OURSTAR.EXE",0   ; programa a ejecutar
EXEC_INFO   db 22 DUP (0)
MSG         db 0dh,0ah
            db "Go! Go! OurStar floppy version crack.",0dh,0ah
            db "2019, vlan7",0dh,0ah,"$"

OVL ends

end START
Funcional, pero un poco salvaje ¿no? Podria eliminar algunas direcciones reservadas donde seguro que no van a estar los bytes que queremos modificar, o quizas tener en cuenta el modelo de memoria segmentada de DOS, donde los segmentos se "solapan" parcialmente y hay multiples segmento:desplazamiento diferentes para apuntar al mismo byte. 7c0:0 , 0:7c , 204:5b0 ... todos esos punteros apuntan al mismo byte en memoria. Pero bueno no queria complicarlo, de todas formas seguiria siendo salvaje.

Si queremos hacerlo limpio podemos averiguar el segmento donde queremos parchear sabiendo como funcionan las instrucciones CALL/RET y que ocurre en el prologo/epilogo de una funcion. Sin entrar en detalle, cuando se ejecuta una instruccion (near) CALL se hace un push de IP (en un far CALL -> push de CS e IP), de tal forma que cuando se ejecute RET el flujo de ejecucion pueda continuar en la instruccion siguiente a dicha instruccion CALL. Es un poco enrevesado ponerlo por escrito, cosas del lenguaje, pero la idea es mas simple. En un video se veria mejor, seguro que hay alguno en internet que lo explica de forma simple.

Bien, los bytes que queremos parchear estan en una direccion de memoria [segmento]:[desplazamiento]. El desplazamiento ya lo hemos averiguado con el debugger. Para averiguar el segmento vamos a bucear en la pila.

Hacemos que BP apunte a la cima de la pila (SP) y al sumar cierto valor a BP estaremos apuntando algunos CALLs hacia atras. Dicho de otro modo, para algun valor de x, SS:[BP+x] apunta al segmento que buscamos, el segmento donde estan los bytes a parchear. Para averiguar x, bien, ver comentarios en el codigo.
SIZE EQU 1024       ; Este programa y su pila caben en 1 KB
OVL segment para 'code'
    assume cs:OVL, ds:OVL
    org 100h        ; Esto sera un programa .COM

START: jmp INITCODE ; Codigo de inicializacion

OLDINT10 dw 0,0           ; Espacio para el puntero a la INT_10h del sistema

NEWINT10 proc far
    cmp ax, 0013h   ; Nos llaman para INT_10h con AX=0013 ?
    jnz EXIT        ; NO: saltamos a la INT_10h del sistema

;; Necesitamos el segmento donde estan los bytes que queremos parchear
    push bp         ; Guardamos BP
    mov bp, sp      ; BP apunta a la cima de la pila

    push es         ; Guardamos ES
    push ax         ; Guardamos AX

; En este punto, podemos cambiar la vista de datos a SS:BP
; (DOSBox debugger -> "d ss:bp")
; De todos esos valores, de dos en dos bytes, uno es el segmento que buscamos.
; Y no deberia estar muy lejos desde SS:BP

; Pero quizas sea mas rapido hacer una lista de instrucciones tal que asi:
; mov ax,[bp]      mov ax,[bp+2h]      mov ax,[bp+4h]      mov ax,[bp+6h] ...
; Con el debugger ejecutaremos instruccion por instruccion viendo que valor va
; tomando AX.

; Pongamos que estamos buscando el segmento donde estan los bytes 74 03.
; Pongamos que ejecutamos en el debugger mov ax,[bp+12h]. Ya conocemos el
; desplazamiento, digamos que es 032b, entonces iriamos a AX:32b (DOSBox
; debugger -> d ax:32b) y si vemos 7403 enhorabuena, hemos encontrado el
; segmento en la pila, podemos añadir al codigo la instruccion mov ax,[bp+12h]

    mov ax, [bp+12h]    ; SS:[bp+12h] son unos cuantos CALLs hacia atras
                        ; y ahi tenemos el segmento donde estan los bytes a parchear
    mov es, ax      ; lo movemos a ES
;;

;; Ahora comprobamos que valor tienen los bytes del juego en memoria que
; queremos parchear, porque si ya los hemos parcheado en alguna llamada previa a
; INT_10/13, no queremos volver a hacerlo.
; Ojo!! x86 es little endian asi que los bytes van "al reves"
    cmp Word Ptr es:[032b], 0374h   ; ¿ES:[032b] = 74 03? (JE ($+3))
    jne YAPARCHEADO                 ; NO: nos vamos sin parchear
    cmp Word Ptr es:[034a], 3675h   ; ¿ES:[034a] = 75 36? (JNE ($+36))
    jne YAPARCHEADO                 ; NO: nos vamos sin parchear
    cmp Word Ptr es:[0366], 1a75h   ; ¿ES:[0366] = 75 1a? (JNE ($+1a))
    jne YAPARCHEADO                 ; NO: nos vamos sin parchear
;;

;; Si estamos aqui es que aun no hemos parcheado. Vamos a parchear en memoria :)
; ES:[032b] = JMP SHORT
    mov Byte Ptr es:[032b], 0ebh    ; Crack ~ escribimos el byte EB en ES:[032b]

; ES:[034a] = NOP NOP
    mov Word Ptr es:[034a], 9090h   ; Crack ~ escribimos 90 90 en ES:[034a]

; ES:[0366] = NOP NOP
    mov Word Ptr es:[0366], 9090h   ; Crack ~ escribimos 90 90 en ES:[0366]
;;

YAPARCHEADO:
    pop ax      ; Restauramos AX
    pop es      ; Restauramos ES
    pop bp      ; Restauramos BP
EXIT:
    jmp DWord Ptr cs:[OLDINT10]   ; Saltamos a la INT_10h del sistema
NEWINT10 endp

INITCODE:
    mov sp, SIZE            ; Redefinimos la pila
    mov bx, SIZE/16
    mov ah, 4ah             ; Redimensionamos bloque memoria
    int 21h

    lea dx, MSG
    mov ah, 9               ; Escribimos texto por pantalla
    int 21h

    xor ah, ah              ; Esperamos pulsacion de tecla
    int 16h

    mov ax, 3510h           ; Queremos el puntero a la INT_10h del sistema
    int 21h
    mov OLDINT10[0], bx     ; Guardamos puntero a la INT_10h del sistema
    mov OLDINT10[2], es     ;  (necesitaremos llamarla despues)

    push cs
    pop es                  ; Necesitaremos ES:BX apuntando a EXEC_INFO

    mov ax, 2510h           ; Sobreescribimos en el vector de interrupciones el
                            ; puntero a INT_10h por
    lea dx, NEWINT10        ; un puntero a nuestra rutina NEWINT10 proc far
    int 21h

;; Parameter block
    lea bx, EXEC_INFO
    mov DWord Ptr [bx], 0
    mov DWord Ptr [bx+2], 80h   ; PSP
    mov Word Ptr [bx+4], cs
    mov Word Ptr [bx+6], 5ch    ; FCB 0
    mov Word Ptr [bx+8], cs
    mov Word Ptr [bx+0ah], 6ch  ; FCB 1
    mov Word Ptr [bx+0ch], cs
;;

    lea dx, FILENAME
; ES:BX apuntando a EXEC_INFO
; DS:DX apuntando al nombre ASCIIZ del archivo a ejecutar
    mov ax, 4b00h
    int 21h             ; cargar y ejecutar programa

    mov dx, OLDINT10[0]
    mov ds, OLDINT10[2]
    mov ax, 2516h       ; Restauramos la INT_10h del sistema
    int 21h

    push cs
    pop ds              ; DS = CS

    mov ax, 4c00h       ; terminar
    int 21h

FILENAME    db "OURSTAR.EXE",0   ; programa a ejecutar
EXEC_INFO   db 22 DUP (0)
MSG         db 0dh,0ah
            db "Go! Go! OurStar floppy version crack.",0dh,0ah
            db "2019, vlan7",0dh,0ah,"$"

OVL ends

end START
Para ensamblar [2]:
a86 CRACK.ASM
Feliz reversing.

[1] GoGo Our Star https://www.mediafire.com/file/bbnak8waayg49d6/gogo.7z/file
[2] A86 http://www.eji.com/a86/
[+] GOGO.ASM http://zen7.vlan7.org/file-cabinet/GOGO.ASM

Related Posts by Categories



0 comentarios :