------------
-- Based on logic from the original IntegrityCheckBypass executable.
----

--- Constants
local PROCESS_NAME = 'thedivision.exe'
-- Following offset will need to be updated when the game gets patched.
local VMPROTECT_THREAD_OFFSET = 0x3C60
-- Build address string: "thedivision.exe+3C60"
local VMPROTECT_THREAD_ENTRYPOINT = string.format(
        '%s+%X', PROCESS_NAME, VMPROTECT_THREAD_OFFSET
)

-- Maintain a local namespace for some of the shared or
-- singleton references.
local _L = {
        addr = {},
        flag = {}
}

--- CE Utilities
-- @section ceutil

--- Tests if CE is attached to a process.
local function isReady()
        local bReady = false
        local hPID = getOpenedProcessID()
        if type(hPID) == 'number' and hPID > 0 then
                local isStr = type(_G.process) == 'string'
                bReady = isStr and string.len(_G.process) > 0
        end
        return bReady
end

--- Utility wrapper to make timer creation a bit cleaner
local function timer(interval, callback)
        local self = createTimer(getMainForm(), false)
        self:setOnTimer(callback)
        self:setInterval(500)
        self:setEnabled(true)
        return self
end

local function destroyStrings(self)
        self:clear()
        object_destroy(self)
end

local function destroyTimer(self)
        self:setEnabled(false)
        object_destroy(self)
end

--- Native Crap
-- @section native

--- Allocates and assembles the native code required by our bypasser.
-- If there's already an existing procedure allocated, this local function
-- will simply return successfully.
local function ensureNatives()
        -- Maintain a 'lock' flag so we don't have multiple procedures
        -- allocated at the same time
        if _L.flag.natives then
                return true
        else
                _L.flag.natives = true
        end
        
        -- Resolve our VMProtect thread entrypoint to an actual address.
        local status, entrypoint = pcall(function()
                return getAddress(VMPROTECT_THREAD_ENTRYPOINT)
        end)
        if not status then
                _L.flag.natives = false
                error('Failed to resolve target entrypoint: ' .. entrypoint)
                return false -- not hit
        end
        
        -- AutoAssemble our procedure into the current CE process.
        status = autoAssemble([[
        alloc(ICB_NewMem,$1000)

        label(ICB_CheckThreadId)
        label(_ValidThread)
        label(_Cleanup)
        label(_Prologue)
        label(ICB_VmProtectThreadId)
        label(ICB_VmProtectEntrypoint)

        loadlibrary(ntdll.dll)
        loadlibrary(kernel32.dll)

        registersymbol(ICB_CheckThreadId)
        registersymbol(ICB_VmProtectThreadId)
        registersymbol(ICB_VmProtectEntrypoint)

        ICB_NewMem:
        ICB_CheckThreadId:
                mov qword [rsp+10], rbx
                mov qword [rsp+18], rsi
                push rdi
                sub rsp, 30
                mov r8d, [ICB_VmProtectThreadId]
                mov edi, 2
                xor esi, esi
                lea ecx, [rdi+3F]
                xor edx, edx
                mov qword [rsp+40], rsi
                call OpenThread
                mov rbx, rax
                test rax, rax
                jz _Prologue
        _ValidThread:
                mov r9d, 8
                lea r8, [rsp+40]
                mov rcx, rax
                lea edx, [r9+1]
                mov qword [rsp+20], rsi
                call NtQueryInformationThread
                test eax, eax
                jnz _Cleanup
                mov rax, [ICB_VmProtectEntrypoint]
                cmp qword [rsp+40], rax
                jnz _Cleanup
                mov edi, 1
                mov rcx, rbx
                mov edx, edi
                call TerminateThread
                test eax, eax
                cmovne edi, esi
        _Cleanup:
                mov rcx, rbx
                call CloseHandle
        _Prologue:
                mov rbx, [rsp+48]
                mov rsi, [rsp+50]
                mov eax, edi
                add rsp, 30
                pop rdi
                ret
        ICB_VmProtectThreadId:
                dd 0
        ICB_VmProtectEntrypoint:
                dq 0
        ]], true)
        
        if not status then
                _L.flag.natives = false
                error('Failed to assemble our Integrity Bypasser procedure.')
                return false -- not hit
        end
        
        -- Lookup addresses to our procedure and data and store for later use.
        _L.addr.CheckTID = getAddress('ICB_CheckThreadId', true)
        _L.addr.VMPTID = getAddress('ICB_VmProtectThreadId', true)
        _L.addr.VMPEP = getAddress('ICB_VmProtectEntrypoint', true)
        -- getAddress(string.format('%s+%X', PROCESS_NAME, VMPROTECT_THREAD_OFFSET))
        
        -- Copy over the resolved VMProtect thread entrypoint address
        writeQwordLocal(_L.addr.VMPEP, entrypoint)
        return true
end

--- Wrapper around our native procedure.
-- Handles calling the initialization local function if need be, and returns the
-- result as a boolean.
local function CheckThreadId(thid)
        -- Initialize our native procedure if necessary
        if not ensureNatives() then
                error('Attempting call to CheckThreadId, but procedure creation failed!')
        end
        
        -- Execute and return the result as a boolean
        -- Note: There was a bug in executeCodeLocal which was only recently
        --       fixed in the official CE sources. Since most people probably
        --       aren't building CE themselves, the code would likely fail if
        --       I called executeCodeLocal with a parameter. As a result, I'm
        --       using the less efficient approach of storing the thread ID
        --       each time we call our auto-assembled code.
        writeIntegerLocal(_L.addr.VMPTID, thid)
        local retval = executeCodeLocal(_L.addr.CheckTID)
        -- local retval = executeCodeLocal(_L.addr.CheckTID, thid)
        if retval == 0 then
                return true
        elseif retval == 2 then
                return false
        else
                error('Located target thread, but failed to terminate it!')
        end
end

--- Integrity Bypasser
-- @section bypasser

local function bypass()
        local threads = createStringlist()
        getThreadlist(threads)
        if threads.Count > 1 then
                local stop, idx = threads.Count - 1
                for idx = 0,stop do
                        -- Thread IDs supplied by getThreadlist are given as
                        -- hexadecimal strings
                        local thid = tonumber(threads[idx], 16)
                        
                        -- Run the thread through our procedure. A true
                        -- result means we've bypassed the integrity check and
                        -- are done at this point. False means we need to keep going.
                        if CheckThreadId(thid) then
                                destroyStrings(threads)
                                return true
                        end
                end
        end
        -- Reaching this point means we weren't able to locate our
        -- target thread.
        destroyStrings(threads)
        return false
end

--- Integrity bypasser job creator
-- Create a timer-based job that will re-attempt to bypass the VMProtect
-- integrity check until it's successful or until it exceeds the maximum
-- number of allowed attempts. Timer is used to avoid overriding any 
-- handlers the user might have for onOpenProcess.
local function integrityBypasser()
        local interval = 500
        timer(interval, function(sender)
                if isReady() then
                        interval = interval + 250
                        if bypass() then
                                print('Successfully applied bypass.')
                                destroyTimer(sender)
                        else
                                if interval > 3000 then
                                        destroyTimer(sender)
                                        error('Exceeded maximum number of attempts for integrity bypassing')
                                else
                                        sender:setInterval(interval)
                                end
                        end
                end
        end)
end

integrityBypasser()

-- Uncomment the line below to auto-attach to the division process.
-- strings_add(getAutoAttachList(), PROCESS_NAME)