Skip to content

Commit 815ce29

Browse files
authored
Prevent browser from caching samples (malware) (#721)
1 parent 2568912 commit 815ce29

File tree

5 files changed

+75
-9
lines changed

5 files changed

+75
-9
lines changed

mwdb/model/file.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import shutil
55
import tempfile
66

7+
import numpy as np
78
import pyzipper
89
from sqlalchemy import or_
910
from sqlalchemy.dialects.postgresql.array import ARRAY
@@ -279,6 +280,37 @@ def iterate(self, chunk_size=1024 * 256):
279280
finally:
280281
File.close(fh)
281282

283+
def negate_bits(self, chunk):
284+
"""
285+
xor data with key equal 255 of length of chunk; using numpy
286+
https://stackoverflow.com/questions/23312571/fast-xoring-bytes-in-python-3
287+
"""
288+
key = np.frombuffer(b"\xff" * len(chunk), dtype="uint8")
289+
chunk = np.frombuffer(chunk, dtype="uint8")
290+
return (key ^ chunk).tobytes()
291+
292+
def iterate_obfuscated(self, chunk_size=1024 * 256):
293+
r"""
294+
Iterates over bytes in the file contents with xor applied
295+
The idea behind xoring before send is to prevent browsers
296+
from caching original samples (malware). Unxoring is provided
297+
in mwdb\web\src\components\ShowSample.js in SamplePreview
298+
"""
299+
fh = self.open()
300+
try:
301+
if hasattr(fh, "stream"):
302+
yield from map(self.negate_bits, fh.stream(chunk_size))
303+
else:
304+
while True:
305+
chunk = fh.read(chunk_size)
306+
chunk = self.negate_bits(chunk)
307+
if chunk:
308+
yield chunk
309+
else:
310+
return
311+
finally:
312+
File.close(fh)
313+
282314
@staticmethod
283315
def close(fh):
284316
"""

mwdb/resources/file.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,13 @@ def get(self, identifier):
381381
description: |
382382
File download token for direct link purpose
383383
required: false
384+
- in: query
385+
name: obfuscate
386+
schema:
387+
type: string
388+
description: |
389+
Obfuscated response flag to avoid AV detection on preview
390+
required: false
384391
responses:
385392
200:
386393
description: File contents
@@ -402,6 +409,7 @@ def get(self, identifier):
402409
Request canceled due to database statement timeout.
403410
"""
404411
access_token = request.args.get("token")
412+
obfuscate = request.args.get("obfuscate")
405413

406414
if access_token:
407415
file_obj = File.get_by_download_token(access_token)
@@ -424,11 +432,22 @@ def get(self, identifier):
424432
if file_obj is None:
425433
raise NotFound("Object not found")
426434

427-
return Response(
428-
file_obj.iterate(),
429-
content_type="application/octet-stream",
430-
headers={"Content-disposition": f"attachment; filename={file_obj.sha256}"},
431-
)
435+
if obfuscate == "1":
436+
return Response(
437+
file_obj.iterate_obfuscated(),
438+
content_type="application/octet-stream",
439+
headers={
440+
"Content-disposition": f"attachment; filename={file_obj.sha256}"
441+
},
442+
)
443+
else:
444+
return Response(
445+
file_obj.iterate(),
446+
content_type="application/octet-stream",
447+
headers={
448+
"Content-disposition": f"attachment; filename={file_obj.sha256}"
449+
},
450+
)
432451

433452
@requires_authorization
434453
def post(self, identifier):

mwdb/web/src/commons/api/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ function removeAttributePermission(key, group_name) {
411411
});
412412
}
413413

414-
function downloadFile(id) {
415-
return axios.get(`/file/${id}/download`, {
414+
function downloadFile(id, obfuscate = 0) {
415+
return axios.get(`/file/${id}/download?obfuscate=${obfuscate}`, {
416416
responseType: "arraybuffer",
417417
responseEncoding: "binary",
418418
});

mwdb/web/src/components/ShowSample.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@ function SampleDetails() {
234234
);
235235
}
236236

237+
// negate the buffer contents (xor with key equal 0xff)
238+
function negateBuffer(buffer) {
239+
const uint8View = new Uint8Array(buffer);
240+
const xored = uint8View.map((item) => item ^ 0xff);
241+
return xored.buffer;
242+
}
243+
237244
function SamplePreview() {
238245
const [content, setContent] = useState("");
239246
const api = useContext(APIContext);
@@ -243,8 +250,15 @@ function SamplePreview() {
243250
async function updateSample() {
244251
try {
245252
const fileId = objectContext.object.id;
246-
const fileContentResponse = await api.downloadFile(fileId);
247-
setContent(fileContentResponse.data);
253+
const obfuscate = 1;
254+
const fileContentResponse = await api.downloadFile(
255+
fileId,
256+
obfuscate
257+
);
258+
const fileContentResponseData = negateBuffer(
259+
fileContentResponse.data
260+
);
261+
setContent(fileContentResponseData);
248262
} catch (e) {
249263
objectContext.setObjectError(e);
250264
}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ pyJWT==2.4.0
2828
Flask-Limiter==2.1.3
2929
python-dateutil==2.8.2
3030
pyzipper==0.3.5
31+
numpy==1.23.5

0 commit comments

Comments
 (0)