среда, 30 августа 2017 г.

Работа с памятью в SourcePawn

Вступление

Доброго времени суток всем. Не так давно я решил заняться собственным сервером Team Fortress 2, и для этого мне понадобилось написание плагинов с помощью SourcePawn. Я начал изучать этот язык относительно недавно и в целом он мне нравится, за исключением некоторых моментов, о которых я бы хотел поговорить сегодня, а также обсудить решения для них.
Если вы не хотите читать философские рассуждения на тему ограниченности языка, то я рекомендую пропустить данный раздел и сразу перейти к следующему.

Один из этих моментов - ограничение в использовании адресного пространства процесса srcds. Увы, но мы можем работать только с библиотеками server и engine. Ну, этого, по идее, должно хватить, так как основная работа движка происходит именно в этих модулях. Однако, не поймите меня неправильно, но я не люблю чувствовать себя ограниченным при работе с памятью, хех. Тем не менее, с этим всё же можно смириться, думал я первое время.

Однако то, что мне действительно чертовски не понравилось, так это отсутствие возможности "смешивания" памяти плагина и сервера. То есть если я захочу внедрить указатель на какой-либо участок памяти своего плагина в любую библиотеку процесса или сам процесс srcds - я буду вынужден воздержаться, поскольку во-первых, получать нативные адреса собственных переменных или функций плагина невозможно, а во-вторых, работать я могу только с, опять же, server или engine. Даже если бы у меня была возможность получения адресов с помощью "#emit", то получал бы я отнюдь не адреса относительно начала памяти процесса (нуля), а относительно специальной памяти плагина. Более того, в самом плагине нельзя выделить память процесса.

Опять же, не поймите меня неправильно! Я не критикую язык SourcePawn с намеком на то, что язык нуждается в доработке. SourcePawn - не тот язык, который предназначен для сложных манипуляций с памятью. Он предназначен для написания обычных плагинов, которые будут менять существующий функционал на сервере или добавлять новый. В общем, неплохой язык для написания сервер-сайд модификаций. Однако те моменты, которые я перечислил выше, лично мне очень не нравятся, и поэтому здесь я бы хотел предложить варианты решения нескольких проблем для серверов на платформе Windows, с которыми я столкнулся при разработке собственных плагинов.

Я практически уверен в том, что кто-то скажет, что для реализации подобного функционала можно воспользоваться native функциями, и я буду вынужден согласиться. С помощью native можно реализовать всё, чем я был недоволен до этого, однако, я предпочитаю не таскать за своими проектами огромное количество модульных зависимостей, а native функции заставят вас это делать. Поэтому все решения проблем я буду воспроизводить с использованием чистого SourcePawn кода, который можно будет аккуратно завернуть в include файл.

Начнем же.

Что мы имеем?

Чтобы найти какой-то паттерн кода с целью произвести его патчинг, нам дают возможность использовать специальную вещь - gamedata. Это файл, в котором вы указываете паттерн для поиска, модуль, операционную систему и оффсет. Достаточно полезная вещь.

Для работы с чистой памятью процесса (не плагина) мы имеем две основные функций - StoreToAddress() и LoadFromAddress(). Данные функции дают возможность обращаться к памяти по указанному адресу, но так как полноценной информации о адресах у нас нет (мы можем получать адреса только поиском с помощью gamedata), то мы остаемся слепы в этом вопросе.

Еще одна самая главная особенность чистого языка - функция SDKCall(), которая была разработана для вызовов функций классов, как ни странно.

Вот и всё. Эти вещи позволят нам сделать SourcePawn намного мощнее в плане работы с памятью.

Получение версии Windows

Для начала начнем с самого простого - попробуем получить версию нашей операционной системы. Как это сделать, не прибегая к native? Очень просто! Следите за руками.

В ОС Windows есть особое место в памяти, адрес которого статичен еще со времен Windows 2000. Это место представляет собой структуру KUSER_SHARED_DATA, которая как раз-таки и содержит необходимую для нас информацию. Адрес данной структуры - 0x7FFE0000. Давайте на основе имеющейся информации напишем функцию для получения версии ОС.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
stock void GetWindowsVersion(int& iMajorVer, int& iMinorVer)
 
{
 
    Address pUserSharedData = view_as<Address>(0x7FFE0000);
 
    
 
    /* ((KUSER_SHARED_DATA*)0x7FFE0000)->NtMajorVersion */
 
    iMajorVer = LoadFromAddress(view_as<Address>(view_as<int>(pUserSharedData) + 0x26C), NumberType_Int32);
 
    /* ((KUSER_SHARED_DATA*)0x7FFE0000)->NtMinorVersion */
 
    iMinorVer = LoadFromAddress(view_as<Address>(view_as<int>(pUserSharedData) + 0x270), NumberType_Int32);
 
}
 
 
 
// ...
 
 
 
public void OnPluginStart()
 
{
 
    int iMajor, iMinor;
 
    GetWindowsVersion(iMajor, iMinor);
 
    PrintToServer("Windows Version : Major=%d, Minor=%d", iMajor, iMinor);
 
}


Как видите - ничего сложного, за исключением не самой лучшей читабельности кода, но это можно исправить с помощью "#define".

Получение PEB

Process Environment Block, пожалуй, одна из самых полезных и нужных для нас вещей. С его помощью можно получить доступ к списку загруженных модулей, а также адрес самого процесса srcds. Есть только одно "но" - его невозможно получить с помощью LoadFromAddress(), так как его адрес меняется от запуска к запуску сервера. Более того, получить его можно только прибегнув к x86 ассемблеру. Вот как это выглядит на языке C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
void* GetPEB()
 
{
 
    __asm
 
    {
 
        mov eax, dword ptr fs:[30];
 
    }
 
}

Выглядит просто, не так ли? Увы, но выполнить подобный трюк на SourcePawn невозможно. Ну а если сильно захотеть? Давайте попробуем.

Прежде всего необходимо создать файл в папке gamedata, назовем его "any.tutor.memory":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"Games"
 
{
 
    "#default"
 
    {
 
        "Addresses"
 
        {
 
            "server"
 
            {
 
                "windows"
 
                {
 
                    "signature" "Find_Server"
 
                }  
 
            }      
 
        }
 
        
 
        "Signatures"
 
        {
 
            "Find_Server"
 
            {
 
                "library"    "server"
 
                "windows"    "\x4D\x5A"
 
            }          
 
        }
 
    }
 
}

Увы, но решение без использования данного файла я не нашел. Зачем это необходимо я разъясню позднее.

Далее реализуем необходимый функционал. Для начала напишем вот такой код:

1
2
3
4
5
6
7
8
9
#define Pointer Address
 
#define nullptr Address_Null
 
 
 
#define int(%1) view_as<int>(%1)
 
#define ptr(%1) view_as<Pointer>(%1)

Данные дефайны можно опустить, но дабы увеличить читабельность кода я буду их использовать.

Затем напишем функции чтения данных по адресу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
stock int ReadByte(Pointer pAddr)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return -1;
 
    }
 
    
 
    return LoadFromAddress(pAddr, NumberType_Int8);
 
}
 
 
 
stock int ReadWord(Pointer pAddr)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return -1;
 
    }
 
    
 
    return LoadFromAddress(pAddr, NumberType_Int16);
 
}
 
 
 
stock int ReadInt(Pointer pAddr)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return -1;
 
    }
 
    
 
    return LoadFromAddress(pAddr, NumberType_Int32);
 
}

Мы написали обертки для LoadFromAddress(), которые позволят читать типы данных, которые нам необходимы. Без этих функций наш код стал бы достаточно большим и неудобным в чтении.

После этих функций создадим еще две:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
stock Pointer Transpose(Pointer pAddr, int iOffset)
 
{
 
    return ptr(int(pAddr) + iOffset);
 
}
 
 
 
stock int Dereference(Pointer pAddr, int iOffset = 0)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return -1;
 
    }
 
    
 
    return ReadInt(Transpose(pAddr, iOffset));
 
}


Функция Transpose() нужна для изменения значения указателя, не прибегая к приведению типов. Функция Dereference() аналогична Transpose(), но после изменения адреса она еще и произведет разыменования указателя. Опять же - всё для повышенной читабельности кода.

А теперь реализуем действительно необходимый функционал:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
stock Pointer WriteData(Pointer pAddr, int[] data, int iSize)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return nullptr;
 
    }
 
    
 
    for(int i = 0; i < iSize; i++)
 
    {
 
        StoreToAddress(pAddr, data[i], NumberType_Int8);
 
        
 
        pAddr++;
 
    }
 
    
 
    pAddr++;
 
    
 
    return pAddr;
 
}
 
 
 
stock Pointer FindPattern(Pointer pStart, int iSize, int[] pattern, int iPatternSize, int iOffset)
 
{
 
    for(int i = 0; i < iSize; i++)
 
    {
 
        if(ReadByte(pStart) == pattern[0])
 
        {
 
            bool bFound = true;
 
            
 
            for(int j = 1; j < iPatternSize; j++)
 
            {
 
                int iDestByte = ReadByte(ptr(int(pStart) + j));
 
                int iSrcByte = pattern[j];
 
    
 
                if(iSrcByte != 0xFF)
 
                {
 
                    if(iDestByte != iSrcByte)
 
                    {
 
                        bFound = false;
 
                        break;
 
                    }
 
                }
 
            }
 
            
 
            if(bFound)
 
            {
 
                return ptr(int(pStart) + iOffset);
 
            }
 
        }
 
        
 
        pStart++;
 
    }
 
    
 
    return nullptr;
 
}
 
 
 
stock int GetModuleSize(Pointer pAddr)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return 0;
 
    }
 
    
 
    if(ReadWord(pAddr) == 0x5A4D) // MZ
 
    {
 
        int iOffset = Dereference(pAddr, 0x3C); // NT headers offset
 
        int iRes = Dereference(pAddr, iOffset + 0x50); // nt->OptionalHeader.SizeOfImage
 
        return iRes;
 
    }
 
    else
 
    {
 
        return -1;
 
    }
 
}
 
 
 
stock int GetModuleSize(Pointer pAddr)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return 0;
 
    }
 
    
 
    if(ReadWord(pAddr) == 0x5A4D) // MZ
 
    {
 
        int iOffset = Dereference(pAddr, 0x3C); // NT headers offset
 
        int iRes = Dereference(pAddr, iOffset + 0x50); // nt->OptionalHeader.SizeOfImage
 
        return iRes;
 
    }
 
    else
 
    {
 
        return -1;
 
    }
 
}


WriteData() нужна для записи массива байт по указанному адресу. Не обращайте внимание на то, что передается "int[]", функция работает с этим аргументом, как с "byte[]". GetModuleSize() функция получает виртуальный размер модуля, загруженного в адресное пространство. В аргумент pAddr нужно передавать результат GetModuleHandle()/LoadLibrary(), но об этом позднее. FindPattern() нужна для поиска паттерна в памяти без использования gamedata.

Теперь напишем достаточно специфичную функцию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
bool CreateMemoryForSDKCall(Pointer pAddr)
 
{
 
    if(pAddr == nullptr)
 
    {
 
        return false;
 
    }
 
    
 
    static Pointer pZeroMem = nullptr;
 
    
 
    if(pZeroMem != nullptr)
 
    {
 
        return true;
 
    }
 
    
 
    pAddr = ptr(int(pAddr) + GetModuleSize(pAddr) - 1);
 
    
 
    /* I could use "true", but compiler blames this line with "redundant test" */
 
    while(pAddr)
 
    {
 
        int b = ReadByte(pAddr);
 
        
 
        if(b != 0x00)
 
        {
 
            break;
 
        }
 
        
 
        pAddr--;
 
    }
 
    
 
    /* Align for safe code injection */
 
    pZeroMem = ptr(int(pAddr) + 0x10 & 0xFFFFFFF0);
 
    
 
    /* Add unique signature for PrepSDKCall_SetSignature() call */
 
    Addr = pZeroMem;
 
    for(int i = 0; i < 4; i++)
 
    {
 
        StoreToAddress(pAddr, 0xDEADBEEF, NumberType_Int32);
 
        pAddr = Transpose(pAddr, 4);
 
    }
 
    
 
    return true;
 
}


Пока что я не буду заострять на этой функции внимание, просто скажу, что без ее помощи у нас ничего не получится.

А теперь напишем кое-что действительно впечатляющее:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
Pointer g_pPEB = nullptr;
 
 
 
int g_GetPEBAsmCode[] =
 
{
 
    0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, // mov eax, dword ptr fs:[30]
 
    0xC3                                // ret
 
};
 
 
 
int g_DeadBeef[] =
 
{
 
    0xEF, 0xBE, 0xAD, 0xDE,
 
    0xEF, 0xBE, 0xAD, 0xDE,
 
    0xEF, 0xBE, 0xAD, 0xDE,
 
    0xEF, 0xBE, 0xAD, 0xDE
 
};
 
 
 
/* Get Process Environment Block pointer */
 
stock Pointer NtCurrentPeb()
 
{  
 
    /* Don't do these crazy stuffs again */
 
    if(g_pPEB != nullptr)
 
    {
 
        return g_pPEB;
 
    }
 
    
 
    Handle h = LoadGameConfigFile("any.tutor.memory");
 
    if(h == null)
 
    {
 
        return nullptr;
 
    }
 
    
 
    Pointer pServerBase = GameConfGetAddress(h, "server");
 
    
 
    delete h;  
 
    
 
    if(pServerBase == nullptr)
 
    {
 
        return nullptr;
 
    }
 
    
 
    if(CreateMemoryForSDKCall(pServerBase) == false)
 
    {
 
        return nullptr;
 
    }
 
    
 
    /* Init handle variable for call */
 
    Handle hGetPEB = INVALID_HANDLE;
 
    
 
    /* SDKCall_Static means don't use "this" argument */
 
    StartPrepSDKCall(SDKCall_Static);
 
    
 
    /* Create SDK call by finding our memory pointer from CreateMemoryForSDKCall() */
 
    PrepSDKCall_SetSignature(SDKLibrary_Server, "\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE", 16);
 
    PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
 
    hGetPEB = EndPrepSDKCall();
 
    
 
    /* Failed to create SDKCall handle. Probably special signature wasn't found */
 
    if(hGetPEB == INVALID_HANDLE)
 
    {
 
        return nullptr;
 
    }
 
    
 
    int iServerSize = GetModuleSize(pServerBase);
 
    Pointer p = FindPattern(pServerBase, iServerSize, g_DeadBeef, 16, 0);
 
    
 
    if(p == nullptr)
 
    {
 
        return nullptr;
 
    }
 
    
 
    /* Create little function that will help us to get process' PEB */
 
    WriteData(p, g_GetPEBAsmCode, 7);
 
    
 
    g_pPEB = SDKCall(hGetPEB);
 
    
 
    /* We don't need this SDK call anymore */
 
    delete hGetPEB;
 
    
 
    return g_pPEB;
 
}

Впечатляет, не правда ли? Возможно, кто-то даже назовет всё это извращением и безумием, но тем не менее давайте разберем всё по порядку.

Сначала функция проверяет, был ли уже найдена PEB, и возвращает его адрес, если была. Затем происходит получение адреса модуля server. Мы используем здесь небольшой трюк, основываясь на архитектуре PE файлов, а именно правилу, что первые два байта exe или dll равны константе "MZ". При старте поиска ядро SourceMod сразу же натыкается на эти два байта и возвращает адрес начала модуля. После всего этого вызывается функция CreateMemoryForSDKCall(). Ее цель - создание уникального паттерна в неиспользуемой области адресного пространства модуля server, который будет найден функцией PrepSDKCall_SetSignature() и FindPattern(). Затем мы строим SDK вызов, который на самом деле является не SDK, а нашей будущей функцией, написанной на x86 ассемблере. PrepSDKCall_SetSignature() просто находит наш уникальный паттерн, в который мы в будущем сможем записать любой код. Затем мы с помощью FindPattern() находим это же самое место и изменяем его, внедряя туда наш ассемблерный код. После всего этого мы производим вызов SDKCall() функции и получаем наш PEB адрес. Хитро, не правда ли?

Теперь, имея на руках PEB, можно делать очень много интересных вещей. Давайте попробуем получить адрес srcds, например.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
stock Pointer GetSRCDSPtr()
 
{
 
    /* Impossible to get module handle without PEB */
 
    if(NtCurrentPeb() == nullptr)
 
    {
 
        return nullptr;
 
    }
 
    
 
    return ptr(Dereference(NtCurrentPeb(), 8)); // g_pPEB->ImageBaseAddress
 
}
 
 
 
// ...
 
 
 
public void OnPluginStart()
 
{
 
    PrintToServer("srcds.exe : Addr=0x%X", GetSRCDSPtr());
 
}


Переменная ImageBaseAddress, которая входит в состав структуры PEB и располагается по смещению 8, содержит адрес процесса, который загрузил библиотеку. Функция просто получает это число, ничего сложного. Точно так же работает функция GetModuleHandle(), если в ее аргументе указать 0.

Заключение

Туториал получился достаточно большой, как по мне, поэтому я считаю, что пока что достаточно. Мы рассмотрели реализацию получения версии Windows и возможность выполнения собственного x86 ассемблерного кода. Как видите, это возможно. Если пользователям понравится подобная тематика, то я продолжу написание туториалов, в которых рассмотрю еще более интересные вещи. Благодарю за чтение.

3 комментария:

  1. Серьёзно. Даже более чем.
    У меня есть кое-какие вопросы, если еще не отошел от этого языка я бы хотел связаться с тобой.

    ОтветитьУдалить
  2. Seminole Hard Rock Hotel Casino - MapyRO
    Find Seminole Hard Rock Hotel Casino, Fort 상주 출장샵 Lauderdale in picturesque Florida. 청주 출장안마 Seminole Hard Rock Hotel & 포천 출장마사지 Casino, Fort Lauderdale, is 남원 출장샵 a hotel on the 충청북도 출장마사지 lake

    ОтветитьУдалить