Windows
January 8

Чистая Windows и разработка "без всего"

Есть компьютер с чистой копией Windows, без доступа в интернет и без установленных средств разработки. Только одна чистая голая венда. Думаете все, конец мечтам о разработке? Да хера с два! Читайте и просвящайтесь — сможете писать софт даже из «мест не столь отдаленных».

Ради этого скриншота я развернул какую-то "детскую" версию Windows 11 в виртуалке

Ужасы познания

На самом деле в ОС Windows с самого их начала было напихано столько всякого интересного, что никакой статьи не хватит чтобы все описать.

Но почему-то мало кто об этом знает даже из разработчиков, особенно современных.

Спросите ради интереса знакомых разработчиков, как программировать на "чистой" Windows — удивитесь ответам.

Плюс насаждаемый «пользовательский» подход от самой Microsoft, которая ковыряние в кишках своих продуктов никогда не поощряла — создал эдакий ареол простоты и надежности, без необходимости разбираться как оно внутри устроено.

Включил и работает — тот самый «Plug&Play».

Поэтому описанное ниже наверное вызовет легкое ох#ение как у обычных пользователей, так и «мамкиных сисодминов» и даже некоторых разрабочиков — особенно если они обучались по видеокурсам.

Начну с цитаты из одной интересной статьи:

Over the past few months, I've received several variations on this question for other operating systems and all of the released versions of the .NET Framework. When the .NET Framework is installed as a part of the OS, it does not appear in the Programs and Features (or Add/Remove Programs) control panel. The following is a complete list of which version of the .NET Framework is included in which version of the OS

И ниже длинный такой список с версиями. Вот вам еще один если вдруг первого было мало.

Ну казалось бы и.. что? Чего тут такого?

Про .NET SDK все и так знают, временами надо поставить «для запуска игор», временами он сам ставится в виде зависимой библиотеки и никому не мешает.

Все так да, но вот только внутрь вы ведь не заглядывали, правда? И на что эта штука на самом деле способна вы не представляете.

А я представляю и сейчас расскажу.

Заходите в папку Windows на вашем компьютере, вот сюда:

Этот скриншот из Windows 10, в нем используется системная .NET SDK 3.5

Вот этот csc.exe — самый настоящий компилятор, фактически портал в ад на вашем домашнем компьютере.

Почему все так страшно?

Потому что появляется возможность создания нативных программ сразу на вашем компьютере, минуя стадию проверки электронной подписи, проверки антивирусом, проверки электронного письма и так далее.

В отличие от VB или PowerShell-скриптов, которые анализируются перед запуском любым приличным антивирусом, антивирусы не анализируют исходный код программ на C# и куда лояльнее относятся к программам собранным локально на этой же машине.

Так что веселье начинается.

Простой пример

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

Весь процесс я записал на видео:

Разработка в Notepad, старая школа - лучшая школа!

Код казалось бы максимально простой, но с нюансом про который чуть ниже:

using System;
using System.Runtime.InteropServices;

namespace yoba
{
  class Program
  {
    // импортирование нативной WinAPI функции MessageBox.
    [DllImport("user32.dll")]
    public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

    static void Main(string[] args)
    {
      //вызываем и показываем диалог
      MessageBox(IntPtr.Zero, "Йоу!", "Добро пожаловать в разработку!", 0);
    }
  }
}

Сохраняете этот текст обычным «блокнотом» в файл yoba.cs и запускаете сборку:

c:\Windows\Microsoft.NET\Framework\v3.5\csc.exe yoba.cs

Так я запускал сборку на Windows 10, но имейте ввиду что версия системного .NET SDK может отличаться и например в Windows 11 уже будет:

c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe yoba.cs

После сборки рядом с исходным файлом yoba.cs появится и запускабельный бинарник yoba.exe, который вы сможете запустить.

А теперь про нюанс.

Нюанс

Существует определенное предубеждение по отношению к managed-языкам вроде Java и С#, что они вроде как все такие «защищенные» и не подходят для серьезных дел вроде написания эксплоитов, использования 0day-уязвимостей и пенетрации ядра.

Что все подобные вещи творят в глубокой тайне на чистом Си, в крайнем случае на C++ а все эти ваши Java/C# это так, погремушки для детей.

Вот тут и начинается нюанс.

Потому как «все совсем не так как на самом деле» и вообще не надо слушать всяких блогеров на ютубе (кроме меня разумеется).

Посмотрите например на эту радость:

[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

Это мои дорогие телезрители, ни что иное как вызов нативного WinAPI, с помощью которого творили всякую нехорошую дичь еще в далекие 90е.

C# и .NET имеет оооочень глубокую интеграцию с Windows, несмотря на всю свою «безопасность» и управляемость, поэтому легко и просто может заменить собой и Си и С++ в качестве инструмента для нехороших дел.

И оно живет на вашем компьютере, дома и в офисе

Но разумеется столь простого примера мало для осознания, поэтому я подготовил кое-что более серьезное.

Сложный пример: выключаем Windows

Итак, это будет относительно небольшое приложение на C#, выключающее компьютер без предупреждения и подтверждения пользователя.

Просто так, внезапно.

Весь процесс на видео, разумеется это виртуальная машина:

А теперь код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.Diagnostics;
using System.Management;
using System.Security.Permissions;
using System.Runtime.InteropServices;
  
namespace yoba
{	
	// See http://www.developmentnow.com/g/33_2004_12_0_0_33290/Access-Denied-on-ManagementEventWatcher-Start.htm 
	// Calling this code on backup/restore seems to enable BCD
	public class TokenHelper
	{
		// PInvoke stuff required to set/enable security privileges
		[DllImport("advapi32", SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern int OpenProcessToken(
			System.IntPtr ProcessHandle, // handle to process
			int DesiredAccess, // desired access to process
			ref IntPtr TokenHandle // handle to open access token
			);

		[DllImport("kernel32", SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern bool CloseHandle(IntPtr handle);

		
		[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern int AdjustTokenPrivileges(
			IntPtr TokenHandle,
			int DisableAllPrivileges,
			IntPtr NewState,
			int BufferLength,
			IntPtr PreviousState,
			ref int ReturnLength);

		[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern bool LookupPrivilegeValue(
			string lpSystemName,
			string lpName,
			ref LUID lpLuid);

		[StructLayout(LayoutKind.Sequential)]
			internal struct LUID 
		{
			internal int LowPart;
			internal int HighPart;
		}

		[StructLayout(LayoutKind.Sequential)]
			struct LUID_AND_ATTRIBUTES 
		{
			LUID Luid;
			int Attributes;
		}

		[StructLayout(LayoutKind.Sequential)]
			struct _PRIVILEGE_SET 
		{
			int PrivilegeCount;
			int Control;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst=1)] // ANYSIZE_ARRAY = 1
			LUID_AND_ATTRIBUTES [] Privileges;
		}

		[StructLayout(LayoutKind.Sequential)]
			internal struct TOKEN_PRIVILEGES
		{
			internal int PrivilegeCount;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst=3)]
			internal int[] Privileges;
		}
		const int SE_PRIVILEGE_ENABLED = 0x00000002;
		const int TOKEN_ADJUST_PRIVILEGES = 0X00000020;
		const int TOKEN_QUERY = 0X00000008;
		const int TOKEN_ALL_ACCESS = 0X001f01ff;
		const int PROCESS_QUERY_INFORMATION = 0X00000400;

		public static bool SetPrivilege (string lpszPrivilege, bool
			bEnablePrivilege )
		{
			bool retval = false;
			int ltkpOld = 0;
			IntPtr hToken = IntPtr.Zero;
			TOKEN_PRIVILEGES tkp = new TOKEN_PRIVILEGES();
			tkp.Privileges = new int[3];
			TOKEN_PRIVILEGES tkpOld = new TOKEN_PRIVILEGES();
			tkpOld.Privileges = new int[3];
			LUID tLUID = new LUID();
			tkp.PrivilegeCount = 1;
			if (bEnablePrivilege)
				tkp.Privileges[2] = SE_PRIVILEGE_ENABLED;
			else
				tkp.Privileges[2] = 0;
			if(LookupPrivilegeValue(null , lpszPrivilege , ref tLUID))
			{
				Process proc = Process.GetCurrentProcess();
				if(proc.Handle != IntPtr.Zero) 
				{
					if (OpenProcessToken(proc.Handle, TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,
						ref hToken) != 0) 
					{
						tkp.PrivilegeCount = 1;
						tkp.Privileges[2] = SE_PRIVILEGE_ENABLED;
						tkp.Privileges[1] = tLUID.HighPart;
						tkp.Privileges[0] = tLUID.LowPart;
						const int bufLength = 256;
						IntPtr tu = Marshal.AllocHGlobal( bufLength );
						Marshal.StructureToPtr(tkp, tu, true);
						if(AdjustTokenPrivileges(hToken, 0, tu, bufLength, IntPtr.Zero, ref
							ltkpOld) != 0)
						{
							// successful AdjustTokenPrivileges doesn't mean privilege could be	changed
								if (Marshal.GetLastWin32Error() == 0)
								{
									retval = true; // Token changed
								}
						}
						TOKEN_PRIVILEGES tokp = (TOKEN_PRIVILEGES) Marshal.PtrToStructure(tu,
							typeof(TOKEN_PRIVILEGES) );
						Marshal.FreeHGlobal( tu );
					}
				}
			}
			if (hToken != IntPtr.Zero)
			{
				CloseHandle(hToken);
			}
			return retval;
		}
	}
	
    class ShutDown
    {
       
        [DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
        internal static extern bool ExitWindowsEx(int flg, int rea);  
        
		internal const int EWX_FORCE = 0x00000004;
        internal const int EWX_POWEROFF = 0x00000008;
    
		static void Main(string[] args)
		{
		    TokenHelper.SetPrivilege("SeShutdownPrivilege",true);	          
			ExitWindowsEx(EWX_FORCE | EWX_POWEROFF, 0);			
		}
	}
}

Собирается по аналогии с предыдущим примером:

c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe Shutdown.cs

После запуска компьютер практически немедленно выключится: проверено и в виртуальной машине и на железе, на 10й и 11й Windows.

Теперь расскажу как это работает.

Ключевая функция тут это ExitWindowsEx, которая и отвечает за завершение работы ОС. Функция старая и известная еще со времен Windows 95.

Но для ее вызова нужны «привилегии», которые и выставляет программно класс TokenHelper.

Константы:

internal const int EWX_FORCE = 0x00000004;
internal const int EWX_POWEROFF = 0x00000008;    

используются вместе с "побитовым или" для указания на требуемое действие.

Вот еще варианты:

internal const int EWX_LOGOFF = 0x00000000;
internal const int EWX_SHUTDOWN = 0x00000001;
internal const int EWX_REBOOT = 0x00000002;
internal const int EWX_FORCEIFHUNG = 0x00000010;  

Описание их всех находится все там же.

Теперь давайте разбираться как же работает поклание программного х#я на систему защиты, а именно как работает:

 TokenHelper.SetPrivilege("SeShutdownPrivilege",true);

И начнем мы с импортов.

Первое что импортируется это функция OpenProcessToken:

[DllImport("advapi32", SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern int OpenProcessToken(
			System.IntPtr ProcessHandle, // handle to process
			int DesiredAccess, // desired access to process
			ref IntPtr TokenHandle // handle to open access token
			);

Функция отвечает за получение данных о наборе «привилегий», связанных с конкретным процессом. Собственно набор таких привилегий и называется «токеном».

Вот как эта функция вызывается:

if (OpenProcessToken(proc.Handle, TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,
						ref hToken) != 0) 
					{
					..

Тут надо отметить передачу по ссылке в стиле Си (ref hToken), когда в функцию передается ссылка на объект C#, дальше функция этот объект заполняет данными. А возвращает она просто true или false — статус выполнения, отработала функция или нет.

Дальше импортируется простая и банальная функция освобождения ресурсов:

[DllImport("kernel32", SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern bool CloseHandle(IntPtr handle);

Вызывается она в самом конце после всей логики и нужна только для освобождения использованной памяти под токен привилегий:

if (hToken != IntPtr.Zero)
			{
				CloseHandle(hToken);
			}

Наконец главная функция, непосредственно отвечающая за переключение привилегий:

	[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern int AdjustTokenPrivileges(
			IntPtr TokenHandle,
			int DisableAllPrivileges,
			IntPtr NewState,
			int BufferLength,
			IntPtr PreviousState,
			ref int ReturnLength);

Вот весь ключевой блок логики смены привилегий:

if (OpenProcessToken(proc.Handle, TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,
						ref hToken) != 0) 
					{
						tkp.PrivilegeCount = 1;
						tkp.Privileges[2] = SE_PRIVILEGE_ENABLED;
						tkp.Privileges[1] = tLUID.HighPart;
						tkp.Privileges[0] = tLUID.LowPart;
						const int bufLength = 256;
						IntPtr tu = Marshal.AllocHGlobal( bufLength );
						Marshal.StructureToPtr(tkp, tu, true);
						if(AdjustTokenPrivileges(hToken, 0, tu, bufLength, IntPtr.Zero, ref
							ltkpOld) != 0)
						{
							// successful AdjustTokenPrivileges doesn't mean privilege could be	changed
								if (Marshal.GetLastWin32Error() == 0)
								{
									retval = true; // Token changed
								}
						}
						TOKEN_PRIVILEGES tokp = (TOKEN_PRIVILEGES) Marshal.PtrToStructure(tu,
							typeof(TOKEN_PRIVILEGES) );
						Marshal.FreeHGlobal( tu );
					}

Как видите вызов достаточно сложный, используется Сишный подход к заполнению полей объекта и передачи его по ссылке в вызываемую функцию.

После вызова проверяется наличие ошибки, также в стиле Си:

if (Marshal.GetLastWin32Error() == 0)
								{
									retval = true; // Token changed
								}

0 это код возрата для успешного вызова, если он есть то считается что операция смены привилегий была выполнена успешно.

Наконец последняя функция, про которую стоит рассказать:

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true),
		SuppressUnmanagedCodeSecurityAttribute]
		static extern bool LookupPrivilegeValue(
			string lpSystemName,
			string lpName,
			ref LUID lpLuid);

Она отвечает за поиск привилегии по имени, вы ведь заметили что мы передаем некое кодовое наименование при вызове TokenHelper:

TokenHelper.SetPrivilege("SeShutdownPrivilege",true);	 

Именно эта функция отвечает за поиск конкретной привилегии по названию «SeShutdownPrivilege», вот так выглядит ее вызов:

if (bEnablePrivilege)
	tkp.Privileges[2] = SE_PRIVILEGE_ENABLED;
else
	tkp.Privileges[2] = 0;

if(LookupPrivilegeValue(null , lpszPrivilege , ref tLUID))
			{
			..

Переменная bEnablePrivilege булевая, это и есть то самое true передаваемое в качестве второго аргумента, а блок:

if (bEnablePrivilege)
	tkp.Privileges[2] = SE_PRIVILEGE_ENABLED;
else
	tkp.Privileges[2] = 0;

отвечает за формирование правильного вызова с использованием системных констант (SE_PRIVILEGE_ENABLED).

При вызове также передается ссылка (ref tLUID) на объект LUID, который будет содержать после вызова указание на найденную привилегию.

Вот такие вот дела.

Итого

Все описанное не призыв к немедленным действиям, а лишь повод к размышлению о смысле бытия. Ну там насчет надежности, безопасности и всего такого — что вам продает большая иностранная корпорация.

Задумайтесь, если увидите венду на атомной станции или военном объекте — без всяких ЦРУ и хакеров в ОС Windows множество нарисованных дверей и фейковых ограничений.

Я много еще чего могу рассказать про мир Windows и его внутренее устройство, будут еще статьи на эту тему. И надеюсь хоть кто-то задумается что «массовому» продукту не место в серьезных местах.