Hola a tod@s!

Me preguntan muchas veces por qué en una shellcode de Cobalt o Metasploit no se ve qué APIs se utilizan. ¿Por qué no observamos cadenas como LoadLibraryA, InternetOpenA o InternetConnectA? Os refresco la memoria con una captura del artículo anterior.

Shellcode Cobalt Strike API

Sin embargo, sí que vemos la ip o dominio, el user-agent, referencias a wininet, la url, etc. ¿Y con sólo esas funciones que se ven en la captura son suficientes para descargar y ejecutar la shellcode?

La primera vez que vi en ejecución scdbg con una de estas, pensé eso, que no están todas las funciones que deberían, faltan unas cuantas, después lo fuí comprobando paso a paso, como hago en estos casos.

Si cargamos nuestra shellcode con radare2, vemos lo siguiente:

Wininet

“Wininet” lo identificamos rápidamente, pero LoadLibrary, no tanto, ¿verdad?

Se realiza una serie de push, primero se introduce el valor de “wininet” en la pila y después el hash correspondiente a LoadLibraryA para finalmente llamarlo con la call, como algunos ya habréis adivinado.

Nos vamos a esta url para ver el siguiente código:

Radare2 Código

Es el mismo código que se contemplaba en radare2. Si seguimos bajando, observamos como las llamadas se hacen siempre de la misma forma, con hashes.

Llamadas con hashes Cobalt

Os dejo este enlace por si queréis investigar más sobre este técnica. En este punto, podríamos descargarnos todo el código de Metasploit, ejecutar una serie de greps buscando “; hash(” y almacenar los valores en un fichero. Nos interesan los de este directorio.

grep -R "; hash(" . | awk -F"0x" '{print $2}' | grep "\.dll\"," | awk '{print $1" "$5}' | sed 's/\"//g' | sort -u

006B8029 WSAStartup
0726774C LoadLibraryA
13DD2ED7 DeleteFileA
160D6838 CreateThread
2977A2F1 setsockopt
33BEAC94 wsaaccept
35269F1D SetEvent
3B2E55EB HttpOpenRequestA
40A438C8 RtlCreateUserThread
4FDAF6DA CreateFileA
528796C6 CloseHandle
5BAE572D WriteFile
5BB31098 WinHttpOpenRequest
5FC8D902 recv
601D8708 WaitForSingleObject
614D6E75 closesocket
6174A599 connect
6737DBC2 bind
709D8805 WinHttpReceiveResponse
7B18062D HttpSendRequestA
7E24296C WinHttpReadData
803428A9 gethostbyname
863FCC79 CreateProcessA
869E4675 InternetSetOptionA
876F8B31 WinExec
91BB5895 WinHttpSendRequest
9DBD95A6 GetVersion
A779563A InternetOpenA
BB9D1F04 WinHttpOpen
C21E9B46 WinHttpConnect
C69F8957 InternetConnectA
E0DF0FEA WSASocketA
E13BEC74 accept
E2899612 InternetReadFile
E449F330 GetTempPathA
E553A458 VirtualAlloc
FF38E9B7 listen

Sin embargo, encontré esto ya hecho con muchísimas funciones más, nunca se sabe lo que te puedes encontrar en una shellcode.

Hay 5 enlaces diferentes con la mayoría de hashes de las API, seleccionamos todo el contenido de cada una de ellas, de los 5 enlaces relacionados que vienen en ese post:

ModuleName= C:\Windows\SysWOW64\ntdll.dll (0x4414F3EA)
 Export: A_SHAFinal(0xB52E8E6D)
 Export: A_SHAInit(0xD79837F8)
 Export: A_SHAUpdate(0xDFAE19E0)
...

Y lo guardamos en un fichero de texto. Ahora creamos un script sencillo en Python y r2pipe. Queremos abrir el fichero con la shellcode, buscar “push 0x……..” (x86) y “mov r10d, 0x…….” (x64).

Una vez tenemos los valores, lo buscamos en el fichero anterior. Este sería el script:

# -*- coding: utf-8 -*-
import sys
import os
import argparse
import r2pipe

def use():
    print ("API hash shellcode finder with radare2 - by Rafa")
    print ("Use: \"" + sys.argv[0] + "\" -f \"<shellcode.bin>\"")
    sys.exit(1)

def findapi(apihash):
    if len(apihash) == 6:
        apihash = '00'+apihash
    elif len(apihash) == 7:
        apihash = '0'+apihash
    textfile = open(filename, 'r')
    with open(filename, 'r') as f:
        for line in f:
            if '(0x'+apihash.upper()+")" in line:
                return (line.strip())
    return None

if __name__ == "__main__":
    try:
        parser = argparse.ArgumentParser()
        parser.add_argument('-f','--shellcodefile', help="shellcode file",required=True)
        args = parser.parse_args()
        sc = args.shellcodefile
        filename = 'api_hashes.txt'

        if not os.path.isfile(sc):
            print (sc + ' not exists')
            use()

        if not os.path.isfile(filename):
            print (filename + ' not exists')
            use()
        
        r2=r2pipe.open(sc)        
        r2.cmd("aaa")
        r2.cmd("e scr.rows        = 100")
        r2.cmd("e scr.columns     = 80")
        r2.cmd("e scr.utf8        = true")

        pdj_x86 = r2.cmd("pd 1024~push 0x")
        pdj = pdj_x86
        for i in pdj.encode('utf-8').split('\n'):
            if 'push 0x' in i:
                if 'ffffffff' in i:
                    i = i.replace('ffffffff','')
                opcodehash_x86 = i.split('push 0x')[1].split(' ')[0].strip()
                opcodehash = opcodehash_x86
                if ((len(opcodehash) >= 6) and (len(opcodehash) <= 8)):
                    findhash = findapi(opcodehash)
                    if findhash != None:
                        print (findhash)

        pdj_x64 = r2.cmd("pd 1024~mov r10d, 0x")
        pdj = pdj_x64
        for i in pdj.encode('utf-8').split('\n'):
            if 'mov r10d, 0x' in i :
                if 'ffffffff' in i:
                    i = i.replace('ffffffff','')
                opcodehash_x64 = i.split('mov r10d, 0x')[1].split(' ')[0].strip()
                opcodehash = opcodehash_x64
                if ((len(opcodehash) >= 6) and (len(opcodehash) <= 8)):
                    findhash = findapi(opcodehash)
                    if findhash != None:
                        print (findhash)
                    
    except Exception, msg:
        print ("[!] error: %s" % msg)

Como véis, no me he complicado nada en tratar de saber si es una shellcode de 32 o 64 bits. Lo ejecuto independientemente. Solamente 1 de ellas me dará el resultado buscado.

Vemos la salida de una shellcode de CobalStrike:

$ python find_api_hashes_r2.py -f shellcode_cobalt_x32_from_dll.bin
Export: LoadLibraryA(0x0726774C)
Export: InternetOpenA(0xA779563A)
Export: InternetConnectA(0xC69F8957)
Export: HttpOpenRequestA(0x3B2E55EB)
Export: HttpSendRequestA(0x7B18062D)
Export: GetLastError(0x5DE2C5AA)
Export: GetDesktopWindow(0x315E2145)
Export: InternetErrorDlg(0x0BE057B7)
Export: ExitProcess(0x56A2B5F0)
Export: VirtualAlloc(0xE553A458)
Export: InternetReadFile(0xE2899612)

Y ahora una de Metasploit:

$ python find_api_hashes_r2.py -f metasploit_reverse_tcp_x32
Export: LoadLibraryA(0x0726774C)
Export: WSAStartup(0x006B8029)
Export: WSASocketA(0xE0DF0FEA)
Export: connect(0x6174A599)
Export: ExitProcess(0x56A2B5F0)
Export: CreateProcessA(0x863FCC79)
Export: WaitForSingleObject(0x601D8708)
Export: GetVersion(0x9DBD95A6)

$ python find_api_hashes_r2.py -f metasploit_reverse_tcp_x64
Export: LoadLibraryA(0x0726774C)
Export: WSAStartup(0x006B8029)
Export: WSASocketA(0xE0DF0FEA)
Export: connect(0x6174A599)
Export: recv(0x5FC8D902)
Export: VirtualAlloc(0xE553A458)
Export: recv(0x5FC8D902)
Export: VirtualFree(0x300F2F0B)
Export: closesocket(0x614D6E75)

De esta forma podemos sacar las funciones utilizadas con api hashing para poder poner breakpoints en los lugares adecuados y obtener los parámetros, aunque como hemos visto en otros artículos, en estas shellcode no hacen falta, se pueden sacar de forma estática.

¿Y si utilizamos el script que sacaba los datos y le añadimos esta parte y sustituimos los valores para las api que contienen la información que nos interesa? Quedaría así:

# -*- coding: utf-8 -*-
import r2pipe
import re
import sys
import os

wininetx32 = []
wininetx64 = ['0x000000d7']

def use():
    print "Cobalt Strike Shellcode configuration http/s x(32|64) with Radare2 and r2pipe - by Rafa"
    print "Use: \"" + sys.argv[0] + "\" -f \"<shellcode_x64.bin>\""
    sys.exit(1)

def findapi(apihash,apifilename):
    if len(apihash) == 6:
        apihash = '00'+apihash
    elif len(apihash) == 7:
        apihash = '0'+apihash
    with open(apifilename, 'r') as f:
        for line in f:
            if '(0x'+apihash.upper()+")" in line:
                return (line.strip())
    return None

def printerror(scfile):
    print  "The file \"" + scfile + "\" is not a http/s Cobalt Strike Shellcode x(32/64)"

def isipaddress(my_ip):
    is_valid = re.match("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", my_ip)
    if is_valid:
         return True
    else:
        return False

def isdomain(my_hostname):
    is_valid = re.match("^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$", my_hostname)
    if is_valid:
         return True
    else:
        return False

def isurl(my_url):
    is_valid = re.match("^(/(\w+))", my_url)
    if is_valid:
         return True
    else:
        return False

def getwininet(arch):
    if arch == 'x64':
        wininet = r2.cmd("izz~wininet[2]").split('\n')[0]
        good = '0x000000d7'
        if wininet == good:
            return True
        else:
            return False
    else:
        wininet1 = r2.cmd("izz~hnet[6]").split('\n')[0]
        wininet2 = r2.cmd("izz~hwini[6]").split('\n')[0]
        good1 = 'hnet'
        good2 = 'hwini'
        if ((good1 in wininet1) and (good2 in wininet2)):
            return True
        else:
            return False

def getmagic():
    magic = r2.cmd("p8 8").split('\n')[0]
    goodx64 = '4883e4f0e8c800'
    goodx32 = 'e8890000006089'
    if goodx64 in magic:
        return 'x64'
    elif goodx32 in magic:
        return 'x32'
    else:
        return ''

def getuseragent(arch):
    if arch == 'x64':
        useragent = r2.cmd("ps @ 0x000001f9")
        good = 'User-Agent:'
        if good in useragent:
            return useragent.replace('\\x0d','')
        else:
            return ''
    else:
        finduseragent = r2.cmd("izz~User-Agent:[2]").split('\n')[0]
        if finduseragent != '':
            useragent = r2.cmd("ps @ "+finduseragent)
            good = 'User-Agent:'
            if good in useragent:
                return useragent.replace('\\x0d','')
            else:
                return ''
        else:
                return ''
  
def getipordomain(arch):
    if arch == 'x64':
        ipordomain = r2.cmd("ps @ 0x0000038d").split('\n')[0]
        if ((isipaddress(ipordomain) == True) or (isdomain(ipordomain) == True)):
            return ipordomain
        else:
            return ''
    else:
        findipordomain = r2.cmd("/x fdffff")
        ipordomaindir = ''
        for i in findipordomain.split('\n'):
             if 'fdffff' in i:
                ipordomaindir = i.split(' ')[0]
                break
        if ipordomaindir != '':
            ipordomain = r2.cmd("ps @ ("+ipordomaindir+"+0x3)").split('\n')[0]
            if ((isipaddress(ipordomain) == True) or (isdomain(ipordomain) == True)):
                return ipordomain
            else:
                return ''
        else:
                return ''

def geturl(arch):
    if arch == 'x64':
        url = r2.cmd("ps @ 0x000001a9").split('\n')[0]
        if (isurl(url) == True):
            return url
        else:
            return ''
    else:
        findurl = r2.cmd("izz~ascii /[2]").split('\n')[0]
        if findurl != '':
            url = r2.cmd("ps @ "+findurl).split('\n')[0]
            if (isurl(url) == True):
                return url
            else:
                return ''
        else:
            return ''

def getport(arch):
    if arch == 'x64':
        porta = r2.cmd("p8 1 @0x113").split('\n')[0]
        portb = r2.cmd("p8 1 @0x112").split('\n')[0]
        port = r2.cmd("? 0x"+porta+portb+"~^int32[1]").split('\n')[0]
        try:
            if 1 <= int(port) <= 65535:
                return port
            else:
                return ''
        except:
            return ''
    else:
        findport=r2.cmd("izz~WWWWWh[2]").split('\n')[0]
        if findport != '':
            porta = r2.cmd("p8 1 @ ("+findport+"+0x1c)").split('\n')[0]
            portb = r2.cmd("p8 1 @ ("+findport+"+0x1b)").split('\n')[0]
            port = r2.cmd("? 0x"+porta+portb+"~^int32[1]").split('\n')[0]
            try:
                if 1 <= int(port) <= 65535:
                    return port
                else:
                    return ''
            except:
                return ''
        else:
            return ''

if len(sys.argv) != 2:
    print ("You need an argument to filename")
    sys.exit(1)

path = sys.argv[1]
apifilename = 'api_hashes.txt'

if not os.path.isfile(apifilename):
    print (apifilename + 'not exists')
    use()

if os.path.exists(path):  
    path = sys.argv[1]
    r2=r2pipe.open(path)
    peformat = r2.cmd("i~format[1]").split('\n')[0]
    if peformat not in ('any'):
        printerror(path)
        use()
    myarch = getmagic()
    if myarch == '':
        printerror(path)
        use()
    if getwininet(myarch) == False:
        printerror(path)
        use()
    myuseragent = getuseragent(myarch)
    if myuseragent == '':
        printerror(path)
        use()
    ipordomain = getipordomain(myarch)
    if ipordomain == '':
        printerror(path)
        use()
    myurl = geturl(myarch)
    if myurl == '':
        printerror(path)
        use()
    myport = getport(myarch)
    if myport == '':
        printerror(path)
        use()

    print ('[*] Shellcode Architecture: %s' % (myarch))
    print ('[*] Result: http[s]://%s:%s%s' % (ipordomain,myport,myurl))
    print ('[*] IP or domain: %s' % ipordomain)
    print ('[*] Port: %s' % myport)
    print ('[*] URL: %s' % myurl)
    print ('[*] Header: %s' % myuseragent.strip())
    print ('[*] Apihash:')

    pdj_x86 = r2.cmd("s 0x0;pd 1024~push 0x")
    pdj = pdj_x86
    for i in pdj.encode('utf-8').split('\n'):
        if 'push 0x' in i:
            if 'ffffffff' in i:
                i = i.replace('ffffffff','')
            opcodehash_x86 = i.split('push 0x')[1].split(' ')[0].strip()
            opcodehash = opcodehash_x86
            if ((len(opcodehash) >= 6) and (len(opcodehash) <= 8)):
                findhash = findapi(opcodehash,apifilename)
                if findhash != None:
                    if 'InternetConnectA' in findhash:
                        print ("Export: InternetConnectA(Server = " + ipordomain + ", Port = " + myport + ")")
                    elif 'HttpSendRequestA' in findhash:
                        print ("Export: HttpSendRequestA(Header = " + myuseragent.strip())
                    elif 'HttpOpenRequestA' in findhash:
                        print ("Export: HttpOpenRequestA(Path = " + myurl + ")")
                    else:
                        print (findhash)

    pdj_x64 = r2.cmd("s 0x0;pd 1024~mov r10d, 0x")
    pdj = pdj_x64
    for i in pdj.encode('utf-8').split('\n'):
        if 'mov r10d, 0x' in i :
            if 'ffffffff' in i:
                i = i.replace('ffffffff','')
            opcodehash_x64 = i.split('mov r10d, 0x')[1].split(' ')[0].strip()
            opcodehash = opcodehash_x64
            if ((len(opcodehash) >= 6) and (len(opcodehash) <= 8)):
                findhash = findapi(opcodehash,apifilename)
                if findhash != None:
                    if 'InternetConnectA' in findhash:
                        print ("Export: InternetConnectA(Server = " + ipordomain + ", Port = " + myport + ")")
                    elif 'HttpSendRequestA' in findhash:
                        print ("Export: HttpSendRequestA(Header = " + myuseragent.strip())
                    elif 'HttpOpenRequestA' in findhash:
                        print ("Export: HttpOpenRequestA(Path = " + myurl + ")")
                    else:
                        print (findhash)

else:
    print "The file \"" + path + "\" not exists!"
    use()

Veamos que nos muestra ahora:

$ python getinfohttpshellcodecobalt.py shellcode_cobalt_x32_from_dll.bin
[*] Shellcode Architecture: x32
[*] Result: http[s]://185.153.199.164:80/BHii
[*] IP or domain: 185.153.199.164
[*] Port: 80
[*] URL: /BHii
[*] Header: User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; Touch; MALCJS)
[*] Apihash:
Export: LoadLibraryA(0x0726774C)
Export: InternetOpenA(0xA779563A)

Export: InternetConnectA(Server = 185.153.199.164, Port = 80)
Export: HttpOpenRequestA(Path = /BHii)
Export: HttpSendRequestA(Header = User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; Touch; MALCJS)

Export: GetLastError(0x5DE2C5AA)
Export: GetDesktopWindow(0x315E2145)
Export: InternetErrorDlg(0x0BE057B7)
Export: VirtualAlloc(0xE553A458)
Export: InternetReadFile(0xE2899612)

Esta información que vemos, con este último script es útil si la shellcode que analizamos es http/s. En caso contrario el contenido nos indicará que no se trata de una shellcode http/s.

Espero que os haya gustado. Hasta otra!!