MacOS.Forensics.AppleDoubleZip

Search for zip files containing leaked download URLs included by MacOS users.

MacOS filesystem can represent extended attributes. Similarly to Windows’s ZoneIdentifier, when a file is downloaded on MacOS it also receives an extended attribute recording where the file was downloaded from. (See the Windows.Analysis.EvidenceOfDownload artifact)

What makes MacOS different however, is that when a user adds a file to a Zip file (in Finder, right click the file and select “compress”), MacOS will also record the extended attributes in the zip file under the __MACOSX folder.

This is a huge privacy leak because people often do not realize that the source of downloads for a file is being included inside the zip file, which they end up sending to other people!

Therefore this artifact can also work on other platforms because Zip files created by MacOS users can end up on other systems, and contain sensitive URLs embedded within them.


name: MacOS.Forensics.AppleDoubleZip
description: |
  Search for zip files containing leaked download URLs included by
  MacOS users.

  MacOS filesystem can represent extended attributes. Similarly to
  Windows's ZoneIdentifier, when a file is downloaded on MacOS it also
  receives an extended attribute recording where the file was
  downloaded from. (See the `Windows.Analysis.EvidenceOfDownload`
  artifact)

  What makes MacOS different however, is that when a user adds a file
  to a Zip file (in Finder, right click the file and select
  "compress"), MacOS will also record the extended attributes in the
  zip file under the __MACOSX folder.

  This is a huge privacy leak because people often do not realize that
  the source of downloads for a file is being included inside the zip
  file, which they end up sending to other people!

  Therefore this artifact can also work on other platforms because Zip
  files created by MacOS users can end up on other systems, and
  contain sensitive URLs embedded within them.

reference:
  - https://opensource.apple.com/source/Libc/Libc-391/darwin/copyfile.c
  - https://datatracker.ietf.org/doc/html/rfc1740

parameters:
  - name: ZipGlob
    description: Where to search for zip files.
    default: /Users/*/Downloads/*.zip

export: |
    -- Offsets are aligned to 4 bytes
    LET Align(value) = value + value - int(int=value / 4) * 4

    LET Profile = '''[
    ["Header", 0, [
      ["Magic", 0, "uint32b"],
      ["Version", 4, "uint32b"],
      ["Filler", 8, "String", {
          length: 16,
      }],
      ["Count", 24, "uint16b"],
      ["Items", 26, "Array", {
          count: "x=>x.Count",
          type: "Entry",
      }],
      ["attr_header", 84, "attr_header"]
    ]],
    ["Entry", 12, [
      ["ID", 0, "uint32b"],
      ["Offset", 4, "uint32b"],
      ["Length", 8, "uint32b"],
      ["Value", 0, "Profile", {
           type: "ASFinderInfo",
           offset: "x=>x.Offset",
      }]
    ]],
    ["attr_header", 0, [

      # Should be ATTR
      ["Magic", 0, "String", {
          length: 4,
      }],

      ["total_size", 8, "uint32b"],
      ["data_start", 12, "uint32b"],
      ["data_length",16, "uint32b"],
      ["flags", 32, "uint16b"],
      ["num_attr", 34, "uint16b"],
      ["attrs", 36, "Array", {
          count: "x=>x.num_attr",
          type: "attr_t",
      }]
    ]],
    ["attr_t", "x=>Align(value=x.name_length + 11)", [
     ["offset", 0, "uint32b"],
     ["length", 4, "uint32b"],
     ["flags", 8, "uint16b"],
     ["name_length", 10, "uint8"],
     ["name", 11, "String", {
         length: "x=>x.name_length",
     }],
     ["data", 0, "Profile", {
        type: "String",
        type_options: {
            term: "",
            length: "x=>x.length",
        },
        offset: "x=>x.offset",
     }]
    ]]
    ]
    '''

    LET ParseData(data) = if(condition=data =~ "^bplist",
         then=plist(accessor="data", file=data), else=data)

    LET ParseAppleDouble(double_data) = SELECT name AS Key, ParseData(data=data) AS Value
       FROM foreach(row=parse_binary(
            filename=double_data, accessor="data",
            profile=Profile, struct="Header").attr_header.attrs)

sources:
 - query: |
     LET DoubleFiles = SELECT * FROM foreach(row={
        SELECT OSPath AS ZipPath
        FROM glob(globs=ZipGlob)
     }, query={
        SELECT OSPath, pathspec(parse=OSPath) AS PathSpec
        FROM glob(
             globs="__MACOSX/**",
             accessor="zip",
             root=pathspec(DelegatePath=ZipPath))
     })

     SELECT * FROM foreach(row=DoubleFiles,
     query={
       SELECT PathSpec.DelegatePath AS ZipFile,
              PathSpec.Path AS Member,
              Key, Value
       FROM ParseAppleDouble(double_data=read_file(filename=OSPath, accessor="zip"))
     })