Analyzing .NET Core Single File Samples (DUCKTAIL Case Study)
This post is dedicated to my colleague Matt Graeber (@mattifestation) who showed me how to do the manual calculations and carving of PEs using CFF Explorer and a hex editor, making me think “there has to be a tool for this”. There are loads of ways to deploy .NET Framework applications, and I’ve mostly been familiar with just the traditional compile-and-run method. As .NET malware has evolved, adversaries have looked to use different deployment reasons for the same reason as legitimate developers. In some cases it’s easier, allows bundling files in different ways, or other reasons. One malware family named DUCKTAIL has recently embraced deployment in as a “.NET Single File” deployment feature. This feature is interesting in the sense that it allows a developer to deploy a .NET application to systems without requiring the systems to have a pre-deployed .NET Framework runtime installed. This dependency-free deployment is achieved by appending multiple binaries together into a single file, resulting in a large executable with multiple executables inside. In this post we’ll take a look at a DUCKTAIL sample and how the “single file” deployment choice affects malware analysis. The sample we’re working with is here in VT: https://www.virustotal.com/gui/file/740fd780b2b45c08d1abb45cddc6d1017c9fcc6bcce54fd8415d87a80d328ff6.
Triaging the file
As always, we can start out triaging the file using a combination of diec
and pehash
. First, let’s figure out the file type:
1
2
3
4
remnux@remnux:~/cases/ducktail$ diec ducktail.exe
PE32
Compiler: Microsoft Visual C/C++(-)[-]
Linker: Microsoft Linker(14.29**)[GUI32,signed]
Right off the bat, we can see our output is a little different than what we’d expect for a standard .NET Framework app. In this case we see diec
says the file was compiled using Visual C/C++ instead of a .NET language like C# or VB. Usually for .NET apps, the output would look similar to this:
1
2
3
4
PE32
Library: .NET(v4.0.30319)[-]
Compiler: VB.NET(-)[-]
Linker: Microsoft Linker(8.0)[GUI32]
We know for sure we’re dealing with a Windows Portable Executable (PE) file, so let’s take a look at the import and rich header hashes. If you’re looking to get the imphash for a sample, you can easily do so with pehash
. For the rich header hash, I like using the tool I made here that leverages the Python pefile library.
1
2
3
4
5
6
7
8
9
10
11
remnux@remnux:~/cases/ducktail$ pehash ducktail.exe
file
filepath: ducktail.exe
md5: 72840061e0f1b2f4bc373b5561970303
sha1: c773a6285a54183e792f23e499646f61d9b2f88f
sha256: 740fd780b2b45c08d1abb45cddc6d1017c9fcc6bcce54fd8415d87a80d328ff6
ssdeep: 1572864:ha0XsmjPyZmtmuwKl7E2LmZBhbCV6ZE5GSQiOjEBkqYnIgJM0cAZv7SGdAcA689p:jjPyZxuwz+6y
imphash: 34dc34e244a6f4378a06076ff16fc082
remnux@remnux:~/cases/ducktail$ ./rhh-md5.py ducktail.exe
e0f1735adef0e9f084efeaee57b351d2
The imphash and rich header hash values are different that what I expect of traditional .NET malware. Usually, .NET executables have an import hash of f34d5f2d4577ed6d9ceec516c1f5a744
and .NET DLLs have an import hash of dae02f32a21e03ce65412f6e56942daa
. In addition, .NET executables and DLLs usually don’t have rich header hashes. So this sample triages more like a native, unmanaged code binary than a .NET one.
Digging deeper with exiftool and pedump
We can get our first hints of the app being compiled as a .NET Core single file executable using exiftool
and pedump
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
remnux@remnux:~/cases/ducktail$ exiftool ducktail.exe
ExifTool Version Number : 12.42
File Name : ducktail.exe
Directory : .
File Size : 56 MB
File Modification Date/Time : 2022:08:07 21:14:35-04:00
File Access Date/Time : 2022:08:07 21:15:11-04:00
File Inode Change Date/Time : 2022:08:07 21:15:04-04:00
File Permissions : -rw-rw-r--
File Type : Win32 EXE
File Type Extension : exe
MIME Type : application/octet-stream
Machine Type : Intel 386 or later, and compatibles
Time Stamp : 2022:04:13 21:36:43-04:00
Image File Characteristics : Executable, 32-bit
PE Type : PE32
Linker Version : 14.29
Code Size : 233984
Initialized Data Size : 331776
Uninitialized Data Size : 0
Entry Point : 0x2f8f0
OS Version : 6.0
Image Version : 0.0
Subsystem Version : 6.0
Subsystem : Windows GUI
File Version Number : 1.0.0.0
Product Version Number : 1.0.0.0
File Flags Mask : 0x003f
File Flags : (none)
File OS : Win32
Object File Type : Executable application
File Subtype : 0
Language Code : Neutral
Character Set : Unicode
Company Name : DataExtractor
File Description : DataExtractor
File Version : 1.0.0.0
Internal Name : DataExtractor.dll
Legal Copyright :
Original File Name : DataExtractor.dll
Product Name : DataExtractor
Product Version : 1.0.0
Assembly Version : 1.0.0.0
The file size is 56 MB. It’s not the beefiest binary ever, but it’s still pretty heavy and that can indicate multiple binaries in a single file. From here we can look at binary properties with pedump
to get some more data. In the interest of brevity I’ve cut down the pedump
output to just the exports since it contains the relevant bits.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
remnux@remnux:~/cases/ducktail$ pedump --exports ducktail.exe
=== EXPORTS ===
# module "singlefilehost.exe"
# flags=0x0 ts="2106-02-07 06:28:15" version=0.0 ord_base=1
# nFuncs=23 nNames=23
ORD ENTRY_VA NAME
1 21580 corehost_initialize
2 20ad0 corehost_load
3 20f20 corehost_main
4 21090 corehost_main_with_output_buffer
5 21960 corehost_resolve_component_dependencies
6 eba0 corehost_set_error_writer
7 218e0 corehost_unload
8 f260 hostfxr_close
9 e8d0 hostfxr_get_available_sdks
a eaa0 hostfxr_get_native_search_directories
b ef80 hostfxr_get_runtime_delegate
c f190 hostfxr_get_runtime_properties
d efd0 hostfxr_get_runtime_property_value
e ed40 hostfxr_initialize_for_dotnet_command_line
f ee70 hostfxr_initialize_for_runtime_config
10 e520 hostfxr_main
11 e3e0 hostfxr_main_bundle_startupinfo
12 e490 hostfxr_main_startupinfo
13 e5c0 hostfxr_resolve_sdk
14 e720 hostfxr_resolve_sdk2
15 ef10 hostfxr_run_app
16 eba0 hostfxr_set_error_writer
17 f130 hostfxr_set_runtime_property_value
.NET core “single file” apps are multiple binaries appended to one another, right? Well, the first binary in the append chain has the responsibility of being a “.NET loader” that loads subsequent .NET resources (appended after the loader) into memory at runtime. Once the resources get loaded, the actual .NET app gets run. The export details seen here in pedump
are from the .NET loader overhead itself, which results in some good predictability. The .NET core “single file” apps should usually have exports like corehost_initialize
, corehost_load
, and others.
Getting the actual app/malware
We’ve got our bearings a bit and we know from documentation that a “single file” app is just a bunch of binaries appended to each other. So, logically, we should be able to walk the file and extract all the PEs from it. We can do this using pecheck.py
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
remnux@remnux:~/cases/ducktail$ pecheck -l P ducktail.exe
1: 0x00000000 EXE 32-bit 0x0350a17f 72840061e0f1b2f4bc373b5561970303 0x0350a17f (EOF) b'' b'singlefilehost.exe'
2: 0x0008a71c DLL 64-bit 0x0013e6cb d35f8c57d217a41dfc5e68bf25e5ecb1 0x0350a17f (EOF) b'empty' b'clrcompression.dll'
3: 0x0013e6cc DLL 64-bit 0x0024687b e127d23181160e02391e628192b1d08a 0x0350a17f (EOF) b'clrjit.dll' b'clrjit.dll'
4: 0x0024687c DLL 64-bit 0x00652623 99004b84b758edc90f90671221152667 0x0350a17f (EOF) b'CoreCLR.dll' b'coreclr.dll'
5: 0x00652624 DLL 64-bit 0x007443c3 ea613da6eeb3f2968faa2d65dabadab1 0x0350a17f (EOF) b'mscordaccore.dll' b'mscordac.dll'
6: 0x007443c4 DLL 32-bit 0x008655c3 e02613d1a6211eb1bfc8d15431acbd68 0x0350a17f (EOF) b'' b'e_sqlite3.dll'
...
24: 0x0087fbd0 DLL 32-bit 0x010aed77 d3cfe3422fb4d5a93c1cf9807debd230 0x0350a17f (EOF) b'' b'System.Private.CoreLib.dll'
25: 0x010aed80 DLL 32-bit 0x0111d57f 4ef7d9040e94a8c3a9ede74a8f66a73f 0x0350a17f (EOF) b'' b'Dapper.dll'
26: 0x0111d580 DLL 32-bit 0x0117b77f a660b3d199853c0b014812f39e46eaa8 0x0350a17f (EOF) b'' b'HtmlAgilityPack.dll'
27: 0x0117b780 DLL 32-bit 0x011ce97f 2904b6192503177cf287f6ae23ed65d5 0x0350a17f (EOF) b'' b'Microsoft.Data.Sqlite.dll'
28: 0x011ce980 DLL 32-bit 0x0135e57f 47d413a62176af3f801b9f6a1146e1a7 0x0350a17f (EOF) b'' b'Newtonsoft.Json.dll'
29: 0x0135e580 DLL 32-bit 0x019a2f7f 6697ec4f0f13bed443f3b070cc4192df 0x0350a17f (EOF) b'' b'BouncyCastle.Crypto.dll'
30: 0x019a2f80 DLL 32-bit 0x019a4b7f 9b59e64ef76c1a543983b8dcb1ce8d75 0x0350a17f (EOF) b'' b'SQLitePCLRaw.batteries_v2.dll'
31: 0x019a4b80 DLL 32-bit 0x019a637f 8b477db107c8ac8c219d90d94d93aaa4 0x0350a17f (EOF) b'' b'SQLitePCLRaw.nativelibrary.dll'
32: 0x019a6380 DLL 32-bit 0x019ba57f 5bacb4c47e3ba56dd53cf88781bb4e05 0x0350a17f (EOF) b'' b'SQLitePCLRaw.core.dll'
33: 0x019ba580 DLL 32-bit 0x019d077f 7a9ca8439b58afd87f4faec21968c087 0x0350a17f (EOF) b'' b'SQLitePCLRaw.provider.dynamic_cdecl.dll'
34: 0x019d0780 DLL 32-bit 0x019d837f 5b015246ff6883063438c8ecf4af101e 0x0350a17f (EOF) b'' b'System.Security.Cryptography.ProtectedData.dll'
35: 0x019d8380 DLL 32-bit 0x01a3417f ed5bdc648cba3d82edd0b14bed18b931 0x0350a17f (EOF) b'' b'Telegram.Bot.dll'
36: 0x01a34180 DLL 32-bit 0x01aa217f 6a62b196160d1a477effa8e07ae48533 0x0350a17f (EOF) b'' b'DataExtractor.dll'
37: 0x01aa2180 DLL 32-bit 0x01b7bb7f 0b360b2e48ad740b2045c96c228d8dfa 0x0350a17f (EOF) b'' b'Microsoft.CSharp.dll'
...
92: 0x03459580 DLL 32-bit 0x034c917f 6d306c25b62c2422a8411315307f5bf5 0x0350a17f (EOF) b'' b'System.Text.RegularExpressions.dll'
93: 0x034c9180 DLL 32-bit 0x034e0b7f 21fef48538579c3d2533532c4b143e75 0x0350a17f (EOF) b'' b'System.Threading.Channels.dll'
94: 0x034e0b80 DLL 32-bit 0x034f037f e58c38c4e4bfc5151c0f1ff350bfe6b7 0x0350a17f (EOF) b'' b'System.Threading.dll'
Using pecheck
with arguments to list the available PEs in the file reveals a whopping 94 different PE files within the single original ducktail.exe
sample. Thankfully, there are only a couple of PEs here that are interesting to us: 35/Telegram.Bot.dll and 36/DataExtractor.dll. We can extract those with pecheck
as well!
1
2
3
4
5
6
7
8
9
remnux@remnux:~/cases/ducktail$ pecheck -l 35 -g s -D ducktail.exe > Telegram.Bot.dll
remnux@remnux:~/cases/ducktail$ diec Telegram.Bot.dll
PE32
Library: .NET(v4.0.30319)[-]
remnux@remnux:~/cases/ducktail$ pecheck -l 36 -g s -D ducktail.exe > DataExtractor.dll
remnux@remnux:~/cases/ducktail$ diec DataExtractor.dll
PE32
Library: .NET(v4.0.30319)[-]
Excellent, we’ve successfully extracted the actual DUCKTAIL .NET code from its “single file” container!
Decompilation and further steps
Decompiling is a breeze with this sample thanks to ilspycmd
. Lately I’ve been using it with command line arguments to export code as a .NET project so I can get extra details in there like the icon used by the malware.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
remnux@remnux:~/cases/ducktail$ mkdir ducktail-src
remnux@remnux:~/cases/ducktail$ ilspycmd -p -o ./ducktail-src/ DataExtractor.dll
remnux@remnux:~/cases/ducktail$ mkdir telegrambot-src
remnux@remnux:~/cases/ducktail$ ilspycmd -p -o ./telegrambot-src/ Telegram.Bot.dll
remnux@remnux:~/cases/ducktail$ tree -a ducktail-src/
ducktail-src/
├── app.ico
├── cnData\Core\Models\Json
│ └── DataJsonModel.cs
├── CokiWin\Core\Models\Json\BusinessJsonModel
│ ├── Adaccount_Permissions.cs
│ ├── BusinessJsonModel.cs
│ ├── Clients.cs
│ ├── Cursors1.cs
│ ├── Cursors.cs
│ ├── Datum1.cs
│ ├── Datum.cs
│ ├── Paging1.cs
│ └── Paging.cs
├── DataExtractor
│ └── Program.cs
...
From here, if you want to get into deeper analysis you can start with the Program.cs
file and simply follow the flow of code to other files as relevant!