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.
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” 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:
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.
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!!