FPS game anti-cheat system design: signature code scanning and heuristic scanning

Category: Tags: ,

Previous article:

FPS game anti-cheat system design: API call backtracking

FPS game anti-cheat system design: drive layer to achieve countermeasures

1. Introduction
This article will introduce signature scanning and heuristic scanning. Signatures and heuristic scanning have actually been heard in anti-virus software. This article will try to introduce it in a simple and easy-to-understand form. I hope it can help future generations to have a reference when designing wheels.

2, the realization of signature scanning and inspired scanning technology
Before understanding the feature code scanning, you need to understand that all programs are ultimately assembled instructions composed of a bunch of machine codes. Drag the program into od or ida or x64dbg and you will see this

In any case, the program will be like this. For feature code scanning, we only need to extract one or more segments of the machine code, and then store it in the database. When scanning, extract the machine code from the database and traverse this program to match.

For heuristic scanning, we need to observe program behavior. If a program sets the behaviors of booting/creating services/network communication/writing files at the same time, it is likely to be a virus, but it may also be normal software (this is the case with false positives), then how to monitor program behavior? Some anti-virus software uses its own virtual machine and sandbox to let the program run, and at the same time strips off the sleep function of the program. It executes for 20 seconds and 10 seconds, and then judges the behavior. Or through the API call relationship For processing (you can go to vxjump to see the in-depth article if you are interested in this technology). Of course, my side is anti-plug, not so high-end (because most plug-ins have login windows, so virtual machines In fact, the enlightenment is very helpless for this kind of plug-in), I use the toy-level heuristic scan.

Three, realize feature code scanning
Prior to this, I used the memory signature code, because the memory signature code can ignore some encrypted shells. But the disadvantage of the memory signature code is that some memory addresses are random. The address will not be the next time you boot. For example:

mov eax,1212(Random address)

 call eax

 

If we take mov eax 12 12 call eax as the feature code, the next time it starts, the match may be mov eax 88 88 call eax, and 1212 will change! So we need the wildcard symbol ??:

mov eax ?? ?? call eax , no matter what 1212 becomes, we can match it correctly

Look at the process again:

1. Read files in memory

2. Find the code segment, we will not scan other segments.

3. Randomly take three positions and get its characteristic code

4. Add wildcards to the randomly changing parts of the feature code

5.Match the feature code.

Code directly

Get the base address, here is the plan to scan even the memory module, but after thinking about it, it is not necessary, just scan the main program

HMODULE Megrez_GetBase(HANDLE hProcess)
{
    HMODULE hModule[100] = { 0 };
    DWORD dwRet = 0;
    BOOL bRet = ::EnumProcessModulesEx(hProcess, (HMODULE*)(hModule), sizeof(hModule), &dwRet, LIST_MODULES_ALL);
    if (FALSE == bRet)
    {
#ifdef DEBUG
        WCHAR _buf[256] = { 0 };
        swprintf_s(_buf, 256, L"Signature scan enumeration module failed");
        OutputDebugStringW(_buf);
#endif
        ::CloseHandle(hProcess);
        return NULL;
    }
    // Get the first module load base address
    HMODULE pProcessImageBase = hModule[0];
    return pProcessImageBase;
}

Get the code snippet:

DWORD Megrez_GetCodeSegAttr(HANDLE hProcess, HMODULE hBase, OUT PDWORD pSizeofCode)
{
    PBYTE pSection = (PBYTE)hBase;
    SIZE_T dReadNum;
    DWORD dPE = NULL;
    ReadProcessMemory(hProcess, (PBYTE)hBase + offsetof(_IMAGE_DOS_HEADER, e_lfanew), &dPE, 4, &dReadNum);
    pSection += dPE;
    pSection += 4;
    pSection += sizeof(IMAGE_FILE_HEADER);
    DWORD dBaseOfCode, dSizeOfCode;
    ReadProcessMemory(hProcess, (PBYTE)pSection + 
offsetof(_IMAGE_OPTIONAL_HEADER, SizeOfCode), &dSizeOfCode, 4, &dReadNum);
    ReadProcessMemory(hProcess, (PBYTE)pSection + 
offsetof(_IMAGE_OPTIONAL_HEADER, BaseOfCode), &dBaseOfCode, 4, &dReadNum);
    *pSizeofCode = dSizeOfCode;
    return dBaseOfCode;
}

Get a random three-segment feature code:

void Megrez_GetSigCode(HANDLE hProcess, PBYTE pTEXTInMemory, 
DWORD dSizeOfText, OUT std::vector<PBYTE>& vecSigCode)
{
    std::random_device rd;
    std::default_random_engine e(rd());
    SIZE_T Temp;
    DWORD dSigRVA[3] = { 0 };
    dSigRVA[0] = e() % (dSizeOfText - 25);
    dSigRVA[1] = e() % (dSizeOfText - 25);
    dSigRVA[2] = e() % (dSizeOfText - 25);
    /*for (int i = 0; i < 3; i++)
    {
        printf("%p ", dSigRVA[i]);
    }
    printf("\n");*/
    for (int i = 0; i < 3; i++)
    {
        PBYTE pTemp = (PBYTE)malloc(100);
        PBYTE pTempTemp = pTemp;
        memset(pTemp, 0, 100);
        ReadProcessMemory(hProcess, pTEXTInMemory + dSigRVA[i], pTemp, 25, &Temp);
        std::string Temp;
        for (int j = 0; j < 20; j++)
        {
            char subStr[3] = { 0 };
            sprintf(subStr, "%02x", *pTemp);
            Temp += subStr;
            Temp += ' ';
            pTemp++;
        }
        PBYTE SigCode = (PBYTE)malloc(100);
        memset(SigCode, 0, 100);
        memcpy(SigCode, Temp.c_str(), Temp.length() + 1);
        vecSigCode.push_back(SigCode);
        free(pTempTemp);
    }
}

Matching signature:

DWORD Megrez_StringMatching(HANDLE hProcess, std::vector<PBYTE> vec, 
PBYTE pTEXTInMemory, DWORD dSizeOfText)
{
    std::string A((char*)vec[0]);
    const char* pat1 = A.c_str();
    DWORD firstMatch1 = 0;
    DWORD dCodeEnd1 = 0;
    PBYTE pMemory = (PBYTE)malloc(dSizeOfText);
    memset(pMemory, 0, dSizeOfText);
    SIZE_T dReadSize;
    ReadProcessMemory(hProcess, pTEXTInMemory, pMemory, dSizeOfText, &dReadSize);
    for (PBYTE pCur = pMemory; pCur < pMemory + dSizeOfText; pCur++)
    {
        if (dCodeEnd1 == 0)
        {
            if (!*pat1)//My string ends
            {
                dCodeEnd1 = 1;
            }
            if (dCodeEnd1 == 0)
            {
                if (*(PBYTE)pat1 == '?' || *pCur == getByte(pat1))//  Matched
                {
                    if (!firstMatch1)
                        firstMatch1 = 1;
                    if (!pat1[2])
                    {
                        dCodeEnd1 = 1;
                    }
                    if (dCodeEnd1 == 0)
                    {
                        if (*(PWORD)pat1 == '\?\?' || *(PBYTE)pat1 != '\?')
                            pat1 += 3;
                        else
                            pat1 += 2; //one ?
                    }
                }
                else
                {
                    pat1 = A.c_str();                              //
                    firstMatch1 = 0;
                }
            }
        }
      //Omit some special xxoo tricks
      //.....
    }
    free(pMemory);
    return firstMatch1 & firstMatch2 & firstMatch3;
}

Done.Congratulations on your realization of an antivirus software worth $500,00 (doge

4. Implement heuristic scanning
In fact, the kind of virtual machine-inspired scanning that engages in anti-virus software is not practical. Because the plug-in has login and registration, the virtual machine cannot judge the result after login. So I use a simple heuristic scan.

Let’s review the import table: some apis to be called by the program will be written in the program import table in advance. Scanning the import table means that you can judge what api is called by the program, and if the program calls the scan api, you know the behavior of the program.

So the process is as follows:

1. Read into memory file

2.Analyze the api in the memory file import table to classify these apis, high-risk, medium-risk, and low-risk.

3. Scan the import table, add 30 points for high-risk APIs (such as reading and writing processes, dll injection), 20 for medium-risk, and 10 for low-risk

4. When the score is greater than a certain value, it can be said that this thing is suspected to be a plug-in and uploaded to the cloud server.

Put the code directly:

Read into memory file:

HMODULE CHeuristicScan::Megrez_GetBase(HANDLE hProcess)
{
    HMODULE hModule[100] = { 0 };
    DWORD dwRet = 0;
    BOOL bRet = ::EnumProcessModulesEx(hProcess, (HMODULE*)(hModule), 
sizeof(hModule), &dwRet, LIST_MODULES_ALL);
    if (FALSE == bRet)
    {
        ::CloseHandle(hProcess);
        return NULL;
    }
    // Get the first module load base address
    HMODULE pProcessImageBase = hModule[0];
    return pProcessImageBase;
}

Parse the import table and grade the api:

PBYTE CHeuristicScan::Megrez_GetScetionBaseAndSize(DWORD RVA, PDWORD pSize)
{
    SIZE_T sReadNum;
    for (int i = 0; i < this->image_file_header.NumberOfSections; i++)
    {
        DWORD dVirtualSize, dVirtualAddress;
        ReadProcessMemory(this->hProcess, this->pFirstSectionTable +
 offsetof(IMAGE_SECTION_HEADER, Misc.VirtualSize ) + 
sizeof(IMAGE_SECTION_HEADER) * i, &dVirtualSize, 4, &sReadNum);
        ReadProcessMemory(this->hProcess, this->pFirstSectionTable +
 offsetof(IMAGE_SECTION_HEADER, VirtualAddress) +
 sizeof(IMAGE_SECTION_HEADER) * i, &dVirtualAddress, 4, &sReadNum);
        if (RVA >= dVirtualAddress && RVA < dVirtualAddress + dVirtualSize)
        {
            *pSize = dVirtualSize;
            return this->pImageBase + dVirtualAddress;
        }
        
    }
    return NULL;
}

DWORD CHeuristicScan::Scan()
{   
    DWORD dScore = 0;
    SIZE_T dReadNum;
    DWORD dCodeSecSize;
    PBYTE pSectionAddr = this->Megrez_GetScetionBaseAndSize(this->
image_data_directory.VirtualAddress, &dCodeSecSize);
    
    this->pSectionBase = (PBYTE)malloc(dCodeSecSize);
    ReadProcessMemory(this->hProcess, pSectionAddr, this->pSectionBase, 
dCodeSecSize, &dReadNum);
    PBYTE pSectionBaseTemp = this->pSectionBase;
    DWORD dOffset = Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, 
this->image_data_directory.VirtualAddress);
    unordered_set<string> hashsetProcNameHTemp(hashsetProcNameH);
    unordered_set<string> hashsetProcNameMTemp(hashsetProcNameM);
    unordered_set<string> hashsetProcNameLTemp(hashsetProcNameL);
    //遍历
    for (int i = 0; *(PDWORD)(this->pSectionBase + dOffset) != 0 ; i++ , 
dOffset += sizeof(IMAGE_IMPORT_DESCRIPTOR))
    {
        DWORD dINTOff = Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, 
*(PDWORD)(this->pSectionBase + dOffset));
       // printf("%s\n", this->pSectionBase + Megrez_GetOffsetOfSectoin(this->
pImageBase, pSectionAddr, *(PDWORD)(this->pSectionBase + dOffset + 12)));
        for (int j = 0; *(PDWORD)(this->pSectionBase + dINTOff) != 0; j++ , 
this->isX64 ? dINTOff += 8: dINTOff += 4)
        {
            if (this->isX64)
            {
                if ((*(unsigned long long*)(this->pSectionBase + dINTOff) & 
0x8000000000000000) == 0x8000000000000000)
                {
                    continue;
                }
                else
                {
                    DWORD dStringOff = Megrez_GetOffsetOfSectoin(this->
pImageBase, pSectionAddr, *(PDWORD)(this->pSectionBase + dINTOff)) + 2;


                    auto iter = hashsetProcNameHTemp.find((char*)(this->
pSectionBase + dStringOff));
                    if (iter != hashsetProcNameHTemp.end())
                    {
                        //High-risk api
                        printf("30:%s\n", (char*)(this->pSectionBase + dStringOff));
                        dScore += 30;
                        Remove((char*)(this->pSectionBase + dStringOff), 
hashsetProcNameHTemp);
                    }
                    iter = hashsetProcNameMTemp.find((char*)(this->pSectionBase + 
dStringOff));
                    if (iter != hashsetProcNameMTemp.end())
                    {
                        //Intermediate api
                        printf("10:%s\n", (char*)(this->pSectionBase + dStringOff));
                        dScore += 10;
                        Remove((char*)(this->pSectionBase + dStringOff), 
hashsetProcNameMTemp);
                    }
                    iter = hashsetProcNameLTemp.find((char*)(this->pSectionBase +
 dStringOff));
                    if (iter != hashsetProcNameLTemp.end())
                    {
                        //低危
                        printf("5:%s\n", (char*)(this->pSectionBase + dStringOff));
                        dScore += 5;
                        Remove((char*)(this->pSectionBase + dStringOff), 
hashsetProcNameLTemp);
                    }
                }


            }
            else
            {
               DWORD dStringOff = Megrez_GetOffsetOfSectoin(this->pImageBase, 
pSectionAddr, *(PDWORD)(this->pSectionBase + dINTOff)) + 2;
               //.. omitted, same as above
            }
            
        }
    }
    if (dScore >= Greater than a certain value)
    {
        this->bIsGameTool = TRUE;
        //Report
        //....
    }
    return dScore;
}

The result of running:

 

As you can see, the game plug-in and dll injector can be successfully identified.

Congratulations, you have completed a heuristic scanning engine worth $100w (doge

5. Expansion and reflection

In fact, this is nothing new. There are many countermeasures. The most common feature scan is to add a virtualization shell to dynamically virtualize. In this way, the feature scan will fail, but for anti-plugging. There is no normal program. The virtualization shell is added, so if you encounter abnormal programs, such as those that are not c++ c# vc+6.0 deiph, they will be reported and judged.

The simplest way to fight against heuristic scanning is dynamic call, but dynamic call can also directly scan the call function address through memory to determine behavior.

Offensive and defensive is endless. There is more to learn

 

Reviews

There are no reviews yet.

Be the first to review “FPS game anti-cheat system design: signature code scanning and heuristic scanning”

Your email address will not be published. Required fields are marked *