Classic VB Corner

Inside Alternative Data Streams

NTFS offers an almost unknown way to obscure streams of data behind the most innocent looking files. Find out how to do this with VB6.

NTFS, the file system of choice on most machines these days, offers something called Alternate Data Streams (ADS) to tuck data away, out of sight from most users. If you search for ADS, you'll see that these neat little payloads are the current rage of the security crowd. As they were five years ago. And before that, too. They certainly are eye-opening, the first time you run into them.

The NTFS file system (do you hate redundancies like that as much as I do?) supports multiple streams of data within each file and folder, and has been around since the introduction of Windows NT. It was originally meant to provide some level of compatibility with HFS, the old Macintosh Hierarchical File System. It's somewhat ironic that the FAT32 thumb drives many folks carry around don't support ADS, but do offer cross-platform capabilities with today's Apple computers.

Under FAT32, the file system stores the filename and extension, its associated attributes, and the file's data. In NTFS, the file system of course also stores the filename and extension, security attributes, the main stream of file data, and optionally many other alternate streams of data as well. The main data stream is unnamed, but each of the alternate streams goes by its own name.

Working with ADS
To specify an ADS, you simply append a colon to the file name and follow that with the stream name. Each stream, including the main one, also carries a totally superfluous extension, ":$DATA", which may be freely omitted when referencing a stream. The gritty details are all available through Google, but the important thing to be aware of is that most VB file I/O functionality works perfectly fine with streams.

There are a few areas where native VB needs a little augmentation from Windows, however. Foremost among them would be the enumeration of ADS within any given file or folder. I've created a drop-in ready CStreams class, suitable for VB5/VB6/VBA, which you may download from the Streams sample on my website. CStreams provides an enumeration of all the streams, and their sizes, within any file or folder. It must be said, though, that ADS within folders require an extra level of permissions to get into. More on that in a bit.

So let's get right into it. Here's the CStreams.Refresh method. This is called as needed, after the class has been handed a file name to work with, and has used GetVolumeInformation to determine the file does indeed reside on an NTFS volume.

Public Sub Refresh()
   Dim hFile As Long
   Dim ioStatus As IO_STATUS_BLOCK
   Dim InfoBlock() As Byte
   Dim BlockSize As Long
   Dim Flags As Long
   Dim nRet As Long
   ' Reset cached values.
   m_Count = 0
   ' Streams not available for folders unless
   ' process has backup privilege.
   If IsFolder(m_FileName) Then
      If BackupPrivs(True) = False Then Exit Sub
   End If
   ' Attempt to read stream names by building progressively
   ' larger buffer until sufficient to contain all data.
   hFile = CreateFile(m_FileName, 0&, FILE_SHARE_READ, _
      ByVal 0&, OPEN_EXISTING, Flags, 0&)
      BlockSize = 1024
         ReDim InfoBlock(0 To BlockSize - 1) As Byte
         nRet = NtQueryInformationFile(hFile, ioStatus, _
            InfoBlock(0), BlockSize, FileStreamInformation)
         Select Case nRet
            Case STATUS_SUCCESS
               If ioStatus.Information Then
                  Call GetStreams(InfoBlock())
               End If
               BlockSize = BlockSize * 2
            Case Else
               Debug.Print "NtQueryInformationFile failed: &h" _
                           & Hex$(nRet), BlockSize
         End Select
      Loop While nRet = STATUS_BUFFER_OVERFLOW
      ' Release open file handle.
      Call CloseHandle(hFile)
      ' Restore prior process privileges.
         Call BackupPrivs(False)
      End If
   End If
End Sub

Let's skip over the backup semantics necessary for folders, for a moment. Enumerating ADS requires a call to the only-recently documented (and rather sparsely at that) NtQueryInformationFile API function, requesting a memory block that contains all the stream information. In order to make this call, we first must obtain an API-based file handle using CreateFile to open the existing file.

Once we have the file handle, we create what we hope to be an adequate-sized buffer for NtQueryInformationFile to stuff and give that a shot. If there are a lot of ADS within the file in question, the buffer may need to be expanded until it is sufficiently sized to hold all the requested information. We then begin the happy task of picking apart the buffer to obtain the information we sought. Each ADS within the file is identified within the buffer with a structure that looks roughly like this:

   NextEntryOffset As Long
   StreamNameLength As Long
   StreamSize As LARGE_INTEGER
   StreamAllocationSize As LARGE_INTEGER
   StreamName As String
End Type

I say "roughly" because Windows doesn't do strings quite the way VB does. What we really have are just the raw Unicode bytes laid out in order, and we can extract the StreamName string directly using a bit of math to calculate its offset and the length provided in the StreamNameLength element. Walking through this InfoBlock is just this simple:

Private Sub GetStreams(InfoBlock() As Byte)
   Dim nIndex As Long
   Dim lpBlock As Long
   ' Reset count to zero, and walk through information block.
   m_Count = 0
   nIndex = LBound(InfoBlock)
      ' Expand persisted storage for stream information
      ReDim Preserve m_Streams(0 To m_Count) _
      With m_Streams(m_Count)
         ' Calculate pointer to beginning of this block.
         lpBlock = VarPtr(InfoBlock(nIndex))
         ' Find offset to next record.
         .NextEntryOffset = PointerToDWord(lpBlock)
         ' Read each of the remaining attributes for this stream.
         .StreamNameLength = PointerToDWord(lpBlock + 4)
         .StreamSize = PointerToLargeInt(lpBlock + 8)
         .StreamAllocationSize = PointerToLargeInt(lpBlock + 16)
         If .StreamNameLength Then
            .StreamName = PointerToStringW(lpBlock + 24, _
         End If
         ' Increment count of streams.
         m_Count = m_Count + 1
         ' Bump up buffer pointer to next record.
         If .NextEntryOffset Then
            nIndex = nIndex + .NextEntryOffset
            Exit Do
         End If
      End With
End Sub

The only way to know when you've iterated through the entire returned structure will be that the NextEntryOffset element of the current item is 0. Unlike files, which always contain a default unnamed ADS, it's possible that there are no ADS at all in folders, so you need to be prepared for this scenario. I chose to bail in the Refresh method, if the returned ioStatus structure indicated no data had been returned, as this allowed a single GetStreams routine to work with both files and folders.
Now, as I said, your application will require the SE_BACKUP_NAME privilege in order to enumerate ADS in folders. This may be an issue in least-privileged user situations. Luckily, file ADS provides nearly all the opportunity you'll likely need, so it will be the rather rare case indeed where this matters. In order to elevate your application to this privilege level, you need to invoke a few more APIs:

Private Function BackupPrivs(ByVal Enable As Boolean) As Boolean
   Dim hProcess As Long
   Dim DesiredAccess As Long
   Dim hToken As Long
   Dim nRet As Long
   ' Cache a copy of priviliges as we found them.
   ' Get psuedohandle to current process.
   hProcess = GetCurrentProcess()
   ' Ask for handle to query and adjust process tokens.
   If OpenProcessToken(hProcess, DesiredAccess, hToken) Then
      ' Get LUID for backup privilege name.
      If LookupPrivilegeValue(vbNullString, SE_BACKUP_NAME, _
            tkp.LUID) Then
         If Enable Then
            ' Enable the backup priviledge.
            tkp.PrivilegeCount = 1
            tkp.Attributes = SE_PRIVILEGE_ENABLED
            If AdjustTokenPrivileges(hToken, False, tkp, _
                  Len(bup), bup, nRet) Then
               BackupPrivs = True
            End If
            ' Restore prior backup privilege setting.
            If AdjustTokenPrivileges(hToken, False, bup, 0&, _
                  ByVal 0&, nRet) Then
               BackupPrivs = True
            End If
         End If
      End If
      ' Clean up token handle.
      Call CloseHandle(hToken)
   End If
End Function

I built this routine so it could work as a toggle, requesting and restoring privileges as needed. It does this by storing the previous privilege state in a static TOKEN_PRIVILEGES structure between calls. Please realize that this design is somewhat fragile, as there are no checks to insure you are going T-F-T-F-T-F rather than T-T-F-T-T-F, or following some other non-regular pattern. Worst case, though, that would simply mean your application would never back down its elevated privilege level, which probably won't be too dire a situation.

I said earlier that most normal VB file I/O methods will work just fine with ADS. One that doesn't is Kill, which returns an error 53 (File not found) if you pass it an ADS name. To overcome that limitation, I've added a KillStream method to the CStream class which uses the DeleteFile API directly:

Public Function KillStream(ByVal Index As Long) As Boolean
   If Index >= 0 And Index < m_Count Then
      ' Whack-a-Mole!  VB's Kill triggers an error 53.
      KillStream = CBool(DeleteFile(m_FileName & _
   End If
End Function

Just to show you how easy it is to use and abuse ADS, consider this simple little scenario:

Public Sub Main()
   Call WriteFile("C:\test.txt", "This is a normal data stream.")
   If WriteFile("C:\test.txt:MyADS", "This is an ADS.") Then
      Debug.Print ReadFile("C:\test.txt:MyADS")
   End If
End Sub

Public Function ReadFile(ByVal FileName As String) As String
   Dim hFile As Long
   On Error GoTo Hell
   hFile = FreeFile
   Open FileName For Binary As #hFile
      ReadFile = Space$(LOF(hFile))
      Get #hFile, , ReadFile
   Close #hFile
End Function

Public Function WriteFile(ByVal FileName As String, _
      ByVal Text As String) As Boolean
   Dim hFile As Long
   On Error GoTo Hell
   hFile = FreeFile
   Open FileName For Output As #hFile
   Print #hFile, Text;
   Close #hFile
   WriteFile = Not CBool(Err.Number)
End Function

What do you suppose shows up in the Immediate window? Yep, it's that easy! And note that this is without any sort of supporting CStreams class, or anything else I've written about, whatsoever. And it will be the rare user indeed that ever notices that extra ":MyADS:$DATA" stream tacked onto their formerly innocent little text file.

Be Careful Out There
If you're still having trouble imagining what you might do with ADS, take a look at your own hard drive. The Streams sample on my website provides a little tool that will recursively drill through a directory hierarchy, and provide you with a listing of all the files that contain ADS content.

Ever wonder how Windows knows to pester you about running that EXE you downloaded? Each one of them is tagged with an ADS named "Zone.Identifier", which becomes a tag-along INI file the system may query at will. How about those pesky thumbnails Windows wants to persist for all your image files? You may be shocked if you enumerate your "My Pictures" folder with my Streams tool. If you right-click on a text file, select Properties, and edit a few of the fields on the Summary tab, any guesses where that info is stored?

It's no wonder the security freaks are freaked out by these critters! These aren't your ordinary boogeyman-type threats. ADS are literally everywhere. They travel almost invisibly, only going away when their host file is transferred to a non-NTFS file system. And, very darn few users have any concept of ADS whatsoever. Handle with care.

About the Author

Karl E. Peterson wrote Q&A, Programming Techniques, and various other columns for VBPJ and VSM from 1995 onward, until Classic VB columns were dropped entirely in favor of other languages. Similarly, Karl was a Microsoft BASIC MVP from 1994 through 2005, until such community contributions were no longer deemed valuable. He is the author of's new Classic VB Corner column. You can contact him through his Web site if you'd like to suggest future topics for this column.

comments powered by Disqus


  • GitHub Copilot for Azure Gets Preview Glitches

    This reporter, recently accepted to preview GitHub Copilot for Azure, has thus far found the tool to be, well, glitchy.

  • New .NET 9 Templates for Blazor Hybrid, .NET MAUI

    Microsoft's fifth preview of .NET 9 nods at AI development while also introducing new templates for some of the more popular project types, including Blazor Hybrid and .NET MAUI.

  • What's Next for ASP.NET Core and Blazor

    Since its inception as an intriguing experiment in leveraging WebAssembly to enable dynamic web development with C#, Blazor has evolved into a mature, fully featured framework. Integral to the ASP.NET Core ecosystem, Blazor offers developers a unique combination of server-side rendering and rich client-side interactivity.

  • Nearest Centroid Classification for Numeric Data Using C#

    Here's a complete end-to-end demo of what Dr. James McCaffrey of Microsoft Research says is arguably the simplest possible classification technique.

  • .NET MAUI in VS Code Goes GA

    Visual Studio Code's .NET MAUI workload, which evolves the former Xamarin.Forms mobile-centric framework by adding support for creating desktop applications, has reached general availability.

Subscribe on YouTube