Unicorn Of Hunt

When Unicorns Go Quiet: BITS Jobs and the Art of Stealthy Transfers

2025-05-22

During my exploration of offensive techniques, I’ve always been particularly interested in abuses of built-in Windows mechanisms. One such technique involves Background Intelligent Transfer Service (BITS) Jobs. For a solid overview of its relevance in cybersecurity, see the MITRE ATT&CK page for T1197.

However, I often feel that LOLBINs are a bit overhyped. Focusing detection solely on the use of a single binary—like bitsadmin.exe—via process creation events leaves gaps. If an adversary skips bitsadmin and instead uses the BITS COM interface, they can easily bypass many standard detections.

There is a lot of Sigma detection rules that you can bypass if you would avoid usage of bitsadmin:

https://github.com/SigmaHQ/sigma/tree/master/rules/windows/process_creation (ctrl+f bits). Example:

https://github.com/SigmaHQ/sigma/blob/master/rules/windows/process_creation/proc_creation_win_bitsadmin_download.yml

I highly recommend checkin out Pavel Yosifovich take on the topic and his code which you can use to implement this functionality:

We just established an idea that we can download files with the usage of BITS without invoking bitsadmin, the only question that is left is to check what telemetry is being generated when BITS Job is created.

I am testing the functionality with the below piece of code:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <windows.h>
#include <bits.h>
#include <iostream>
#include <comdef.h> // For _com_error

#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "bits.lib")

int main()
{
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (FAILED(hr))
{
std::cerr << "CoInitializeEx failed: 0x" << std::hex << hr << std::endl;
return 1;
}

IBackgroundCopyManager* pManager = nullptr;
hr = CoCreateInstance(__uuidof(BackgroundCopyManager), NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pManager));
if (FAILED(hr))
{
std::cerr << "CoCreateInstance for BackgroundCopyManager failed: 0x" << std::hex << hr << std::endl;
CoUninitialize();
return 1;
}

IBackgroundCopyJob* pJob = nullptr;
GUID jobId;

// Create a new BITS job
hr = pManager->CreateJob(
L"EVIL UNICORN JOB", // Job name
BG_JOB_TYPE_DOWNLOAD, // Job type (download)
&jobId,
&pJob);
if (FAILED(hr))
{
std::cerr << "CreateJob failed: 0x" << std::hex << hr << std::endl;
pManager->Release();
CoUninitialize();
return 1;
}

// Add file to download
// Source URL (must be wide string)
const wchar_t* fileUrl = L"https://raw.githubusercontent.com/olafhartong/sysmon-modular/refs/heads/master/7_image_load/include_bitsproxy.xml";
// Destination file path
const wchar_t* destPath = L"C:\\Windows\\Tasks\\evil.txt";

hr = pJob->AddFile(fileUrl, destPath);
if (FAILED(hr))
{
std::cerr << "AddFile failed: 0x" << std::hex << hr << std::endl;
pJob->Release();
pManager->Release();
CoUninitialize();
return 1;
}

// Resume the job to start downloading
hr = pJob->Resume();
if (FAILED(hr))
{
std::cerr << "Resume failed: 0x" << std::hex << hr << std::endl;
pJob->Release();
pManager->Release();
CoUninitialize();
return 1;
}

std::cout << "BITS download started...\n";

// Wait for job to finish
BG_JOB_STATE state;
do
{
Sleep(1000); // Wait 1 second

hr = pJob->GetState(&state);
if (FAILED(hr))
{
std::cerr << "GetState failed: 0x" << std::hex << hr << std::endl;
break;
}

switch (state)
{
case BG_JOB_STATE_QUEUED: std::cout << "Job queued...\n"; break;
case BG_JOB_STATE_CONNECTING: std::cout << "Connecting...\n"; break;
case BG_JOB_STATE_TRANSFERRING: std::cout << "Transferring...\n"; break;
case BG_JOB_STATE_SUSPENDED: std::cout << "Suspended.\n"; break;
case BG_JOB_STATE_ERROR: std::cout << "Error!\n"; break;
case BG_JOB_STATE_TRANSFERRED: std::cout << "Transferred.\n"; break;
case BG_JOB_STATE_ACKNOWLEDGED: std::cout << "Acknowledged.\n"; break;
case BG_JOB_STATE_CANCELLED: std::cout << "Cancelled.\n"; break;
}
} while (state != BG_JOB_STATE_TRANSFERRED && state != BG_JOB_STATE_ERROR && state != BG_JOB_STATE_CANCELLED);

if (state == BG_JOB_STATE_TRANSFERRED)
{
// Complete the job to move file from temp cache to final location
hr = pJob->Complete();
if (SUCCEEDED(hr))
{
std::cout << "Download completed successfully.\n";
}
else
{
std::cerr << "Complete failed: 0x" << std::hex << hr << std::endl;
}
}
else if (state == BG_JOB_STATE_ERROR)
{
// Retrieve error info
IBackgroundCopyError* pError = nullptr;
hr = pJob->GetError(&pError);
if (SUCCEEDED(hr))
{
BG_ERROR_CONTEXT context;
HRESULT errorHr;
pError->GetError(&context, &errorHr);

std::wcerr << L"BITS job error. Context: " << context << L", HRESULT: 0x" << std::hex << errorHr << std::endl;

pError->Release();
}
}
else if (state == BG_JOB_STATE_CANCELLED)
{
std::cout << "BITS job was cancelled.\n";
}

// Cleanup
pJob->Release();
pManager->Release();
CoUninitialize();

return 0;
}

While performing debbuging we can see all the loaded modules (COM Instance not yet created):

Loaded Modules Before

After 19th line of code is run we can see that bunch of new DLLs were loaded:

Loaded Modules After

Especially, interesting for us is: BitsProxy.dll.

We should observe the Sysmon Image load events being generated from this activity (given the proper configuration):

https://github.com/olafhartong/sysmon-modular/blob/master/7_image_load/include_bitsproxy.xml

1
2
3
4
5
6
7
8
9
<Sysmon schemaversion="4.40">
<EventFiltering>
<RuleGroup name="" groupRelation="or">
<ImageLoad onmatch="include">
<ImageLoaded name="technique_id=T1197,technique_name=BITS" condition="end with">bitsproxy.dll</ImageLoaded>
</ImageLoad>
</RuleGroup>
</EventFiltering>
</Sysmon>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Image loaded:
RuleName: technique_id=T1197,technique_name=BITS
UtcTime: 2025-05-24 12:32:29.181
ProcessGuid: {f4a83506-bc48-6831-cc09-000000001100}
ProcessId: 11652
Image: C:\Users\unicorn\source\repos\BITSD\x64\Debug\BITSD.exe
ImageLoaded: C:\Windows\System32\BitsProxy.dll
FileVersion: 7.8.26100.1882 (WinBuild.160101.0800)
Description: Background Intelligent Transfer Service Proxy
Product: Microsoft® Windows® Operating System
Company: Microsoft Corporation
OriginalFileName: qmgrprxy.dll
Hashes: SHA1=DDE59105E322DD0742FD582DE685B98C731B21C0,MD5=FDC8DFDBCFDC7637CEA74CECF9D580AB,SHA256=39B245CD0BF0F27241AAAFBB317AEC0D7D01DBF7750851EEF37BB319255C214D,IMPHASH=E68B2C7E33E04DC8081D5A96FEB7F59A
Signed: true
Signature: Microsoft Windows
SignatureStatus: Valid
User: unicorn\unicorn

So our first idea would be to detect unknown executables that are loading BitsProxy.dll. In Sentinel we could do this with (semi-parsed ImageLoad)

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
Event
| where EventID == 7
| parse-kv EventData as (
ProcessGuid:string,
ProcessId:string,
Image:string,
ImageLoaded:string,
FileVersion:string,
Description:string,
Product:string,
Company:string,
OriginalFileName:string,
CommandLine:string,
CurrentDirectory:string,
User:string,
LogonGuid:string,
LogonId:string,
IntegrityLevel:string,
Hashes:string,
Signed:string,
Signature:string,
SignatureStatus:string,
)
with (regex=@'<Data Name="(\w+)">{?([^<]*?)}?</Data>')
|where ImageLoaded has "BitsProxy"
|project TimeGenerated, ImageLoaded, Image, Description, User, Computer, Signature, SignatureStatus, Signed
ImageLoad Bits Proxy

What’s also interesting is the fact that svchost.exe will be spawned and it will also load BitsProxy.dll. The same svchost.exe is then responsible for downloading the content.

We will observe also this svchost.exe creating our file, however, it will only be visible by creating this temporary BITS file:

1
File created: RuleName: - UtcTime: 2025-05-24 12:59:08.244 ProcessGuid: {F4A83506-C297-6831-F20A-000000001100} ProcessId: 13524 Image: C:\WINDOWS\System32\svchost.exe TargetFilename: C:\Windows\Tasks\BITE39A.tmp CreationUtcTime: 2025-05-24 12:59:07.873 User: NT AUTHORITY\SYSTEM 
1
Dns query: RuleName: - UtcTime: 2025-05-24 12:59:07.944 ProcessGuid: {F4A83506-C297-6831-F20A-000000001100} ProcessId: 13524 QueryName: raw.githubusercontent.com QueryStatus: 0 QueryResults: ::ffff:185.199.108.133;::ffff:185.199.111.133;::ffff:185.199.109.133;::ffff:185.199.110.133; Image: C:\Windows\System32\svchost.exe User: NT AUTHORITY\SYSTEM 

NOTE: Sysmon Event ID 3 would also generate, it was lacking in my Sysmon Modular configuration, I would guess due to significant number of potential noise events. Still, if Event ID 22 (DNS Query) is there then Event ID 3 should also generate.

Looking for svchost.exe would seem to lead to many false positives, only interesting if you want to know if there was ANY BITS Job created, or perhaps you would want to track creation of BITS Temp files in some weird directories - like C:\Windows\Tasks\ etc.

This is very cool from the offensive point of view, as you are successfully “detaching” the download activity from your potentially malicious process.

That’s all cool and rainbows when you create your own binary, can we do it simpler? Surprisingly, the answer is yes, lol.

Yuu an simply utilize PowerShell one liner:

1
Start-BitsTransfer -DisplayName "BITSJOBFROMPOWERSHELL" -Source "https://raw.githubusercontent.com/olafhartong/sysmon-modular/master/7_image_load/exclude_cscript_scrobj.xml" -Destination "C:\Windows\Tasks\exclude_cscript_scrobj.xml"
1
<DataItem type="System.XmlData" time="2025-05-24T06:26:04.7586679-07:00" sourceHealthServiceId="A18FEB8F-23F9-A8C4-0E93-245BABEE7B44"><EventData xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><Data Name="RuleName">technique_id=T1197,technique_name=BITS</Data><Data Name="UtcTime">2025-05-24 13:26:04.749</Data><Data Name="ProcessGuid">{f4a83506-c8dd-6831-6a0b-000000001100}</Data><Data Name="ProcessId">14356</Data><Data Name="Image">C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Data><Data Name="ImageLoaded">C:\Windows\System32\BitsProxy.dll</Data><Data Name="FileVersion">7.8.26100.1882 (WinBuild.160101.0800)</Data><Data Name="Description">Background Intelligent Transfer Service Proxy</Data><Data Name="Product">Microsoft® Windows® Operating System</Data><Data Name="Company">Microsoft Corporation</Data><Data Name="OriginalFileName">qmgrprxy.dll</Data><Data Name="Hashes">SHA1=DDE59105E322DD0742FD582DE685B98C731B21C0,MD5=FDC8DFDBCFDC7637CEA74CECF9D580AB,SHA256=39B245CD0BF0F27241AAAFBB317AEC0D7D01DBF7750851EEF37BB319255C214D,IMPHASH=E68B2C7E33E04DC8081D5A96FEB7F59A</Data><Data Name="Signed">true</Data><Data Name="Signature">Microsoft Windows</Data><Data Name="SignatureStatus">Valid</Data><Data Name="User">unicorn\unicorn</Data></EventData></DataItem>

Again svchost is also spawned:

1
<DataItem type="System.XmlData" time="2025-05-24T06:26:04.7583783-07:00" sourceHealthServiceId="A18FEB8F-23F9-A8C4-0E93-245BABEE7B44"><EventData xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><Data Name="RuleName">technique_id=T1197,technique_name=BITS</Data><Data Name="UtcTime">2025-05-24 13:26:04.747</Data><Data Name="ProcessGuid">{f4a83506-c8e8-6831-700b-000000001100}</Data><Data Name="ProcessId">14680</Data><Data Name="Image">C:\Windows\System32\svchost.exe</Data><Data Name="ImageLoaded">C:\Windows\System32\BitsProxy.dll</Data><Data Name="FileVersion">7.8.26100.1882 (WinBuild.160101.0800)</Data><Data Name="Description">Background Intelligent Transfer Service Proxy</Data><Data Name="Product">Microsoft® Windows® Operating System</Data><Data Name="Company">Microsoft Corporation</Data><Data Name="OriginalFileName">qmgrprxy.dll</Data><Data Name="Hashes">SHA1=DDE59105E322DD0742FD582DE685B98C731B21C0,MD5=FDC8DFDBCFDC7637CEA74CECF9D580AB,SHA256=39B245CD0BF0F27241AAAFBB317AEC0D7D01DBF7750851EEF37BB319255C214D,IMPHASH=E68B2C7E33E04DC8081D5A96FEB7F59A</Data><Data Name="Signed">true</Data><Data Name="Signature">Microsoft Windows</Data><Data Name="SignatureStatus">Valid</Data><Data Name="User">NT AUTHORITY\SYSTEM</Data></EventData></DataItem>

The same svchost.exe creates temporary BITS file:

1
<DataItem type="System.XmlData" time="2025-05-24T06:26:05.1491514-07:00" sourceHealthServiceId="A18FEB8F-23F9-A8C4-0E93-245BABEE7B44"><EventData xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><Data Name="RuleName">-</Data><Data Name="UtcTime">2025-05-24 13:26:05.148</Data><Data Name="ProcessGuid">{f4a83506-c8e8-6831-700b-000000001100}</Data><Data Name="ProcessId">14680</Data><Data Name="Image">C:\WINDOWS\System32\svchost.exe</Data><Data Name="TargetFilename">C:\Windows\Tasks\BIT8F95.tmp</Data><Data Name="CreationUtcTime">2025-05-24 13:26:04.772</Data><Data Name="User">NT AUTHORITY\SYSTEM</Data></EventData></DataItem>

Of course, in the situation of BITS Jobs there is Event Log Microsoft-Windows-Bits-Client/Operational, which is superior in providing us with actual details on what has happened:

Event ID 3

1
<DataItem type="System.XmlData" time="2025-05-24T05:59:07.8700635-07:00" sourceHealthServiceId="A18FEB8F-23F9-A8C4-0E93-245BABEE7B44"><EventData xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><Data Name="jobTitle">EVIL UNICORN JOB</Data><Data Name="jobId">{7f68cced-f925-4864-9525-37b40b3496d7}</Data><Data Name="jobOwner">unicorn\unicorn</Data><Data Name="processPath">C:\Users\unicorn\source\repos\BITSD\x64\Debug\BITSD.exe</Data><Data Name="processId">12796</Data><Data Name="ClientProcessStartKey">4785074604083945</Data></EventData></DataItem>

Event ID 16403

1
<DataItem type="System.XmlData" time="2025-05-24T05:59:07.8893135-07:00" sourceHealthServiceId="A18FEB8F-23F9-A8C4-0E93-245BABEE7B44"><EventData xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><Data Name="User">unicorn\unicorn</Data><Data Name="jobTitle">EVIL UNICORN JOB</Data><Data Name="jobId">{7f68cced-f925-4864-9525-37b40b3496d7}</Data><Data Name="jobOwner">unicorn\unicorn</Data><Data Name="fileCount">1</Data><Data Name="RemoteName">https://raw.githubusercontent.com/olafhartong/sysmon-modular/refs/heads/master/7_image_load/include_bitsproxy.xml</Data><Data Name="LocalName">C:\Windows\Tasks\evil.txt</Data><Data Name="processId">12796</Data><Data Name="ClientProcessStartKey">4785074604083945</Data></EventData></DataItem>

To sum up:

← Back to Home