Tout part de ce thread : Unchecky, Ninite, comment ca marche ?
Fut une époque pas si lointaine que ça, j’étais féru de comprendre comment certains logiciels fermés (comprendre : dont le code source n’est a priori pas disponible publiquement) fonctionnaient. Certains de mes articles en témoignent.
Dans ce thread, il est question de savoir comment un logiciel nommé "Unchecky" peut faire pour accéder à des fenêtres (comprendre : fenêtres Windows) appartenant à d’autres processus pour décocher des checkbox.
Pour faire simple, l’intérêt de ce genre d’utilitaire est de décocher automatiquement les cases cochées par défaut qui elles-mêmes activent des options pour installer des merdes supplémentaires dont on n’a pas besoin. Ou des options bidons genre "Faire de MSN mon moteur de recherche par défaut"1.
C’est à se demander comment un tel logiciel peut fonctionner. On peut supposer que l’équipe de développement en charge de Unchecky s’amuse à reverser les installeurs pour réussir à décocher les checkbox qui vont bien. Mais ça représenterait trop de boulot.
En fait, les API Win32 sont à la fois monstrueusement chiantes à utiliser mais surtout monstrueusement complètes. C’est ce dont je me suis aperçu à l’époque où je m’amusais à faire du reverse sur Windows2.
Je vais montrer qu’il est possible, pour un programme A qui possède une "checkbox", de la décocher à partir d’un logiciel B sans avoir recours à de la rétro-ingénierie, mais seulement à des API Win32 suffisamment bien documentées pour nous permettre de parvenir à nos faims3.
Se remettre dans le bain
Avant que les comètes ne tombent sur les dinosaures et scandent "bon maintenant on arrête les conneries et on fait des outils pour nous faciliter la vie et gagner du temps ceci est une citation un peu trop longue vous trouvez pas", les programmeurs d’application bureau faisaient leur fenêtre à la dure, façon hommes de cro-magnon. Avec un langage que j’affectionne tout particulièrement qu’est le langage C.
Mais on peut aller se gratter pour avoir quelque chose de standard. Ici il est question de macros, de defines, d’UpperCamelCase mêlé à de la notation Hongroise1.
Il m’a fallu donc ressortir un peu de documentation pour me rappeler comment on faisait une fenêtre. Mais pour apprécier un peu la gueule d’un programme minimaliste sous Windows, je vous laisse admirer le snippet suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <windows.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { MessageBox( NULL, TEXT("Hello World!"), TEXT("Message from the legacy world."), MB_OK | MB_ICONINFORMATION); return 0; } |
Ici on fait ni plus ni moins qu’afficher une boîte de message débile via l’API MessageBox. Un petit hello world des familles à la Windows, quoi.
La documentation de l’API, si elle est complète, demeure tout de même en Anglais. Mais cela ne devrait pas vous arrêter, même si comme moi vous aimez parfois jouer avec votre caca.
On peut voir qu’ici il n’est pas question de int main(int argc, char* argv)
mais de int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
. Si vous êtes curieux je vous renvoie à la doc : https://msdn.microsoft.com/en-us/library/windows/desktop/ff381406(v=vs.85).aspx. Pour l’heure je vais me passer d’explications qui ne sont pas dans le cadre de cette tribune.
Pour créer et gérer des fenêtres avec Windows, c’est un peu déroutant si l’on vient d’un milieu où on a l’habitude de faire du procédural et synchrone via l’interface de ligne de commandes.
Pour créer une fenêtre, on a besoin :
- d’une classe de fenêtre : vous avez déjà programmé en objet ? Eh bien le concept y ressemble : on définit une classe qui va nous permettre de définir certaines propriétés de base pour nos fenêtres à venir (car on peut en créer plusieurs) ;
- d’une routine de traitement de messages : Il s’agit techniquement d’une fonction, d’un callback que le système va appeler pour traiter… des messages.
Des messages ? Comme sur un répondeur ?
Très drôle.
En fait, les fenêtres communiquent entre elles par le biais de messages. Concrètement, un message, ça peut être :
- La souris bouge sur la droite
- La souris clique
- Une touche est pressée
- La fenêtre se créé
- Des données ont été lues sur le réseau (oui oui)
En résumé, on ne fait pas de code bloquant, excepté la boucle qui va récupérer les messages pour les transmettre à notre fenêtre.
Grosso-merdo, ça donne tout ça :
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 | #include <windows.h> LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { const LPCTSTR szClassName = TEXT("MainWindow"); HWND hMainWindow = INVALID_HANDLE_VALUE; WNDCLASS wndClsMainWindowClass = { .style = 0, .lpfnWndProc = MainWindowProc, .cbClsExtra = 0, .cbWndExtra = 0, .hInstance = hInstance, .hIcon = NULL, .hbrBackground = COLOR_WINDOW, .lpszMenuName = NULL, .lpszClassName = szClassName }; if (RegisterClass(&wndClsMainWindowClass) == 0) { MessageBox( NULL, TEXT("Could not register class."), TEXT("Error"), MB_OK | MB_ICONERROR); ExitProcess(-1); } hMainWindow = CreateWindow( szClassName, TEXT("Toggle my Checkbox (please)"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, // style CW_USEDEFAULT, // x CW_USEDEFAULT, // y CW_USEDEFAULT, // width CW_USEDEFAULT, // height NULL, // parent NULL, // menu hInstance, NULL, ); if (hMainWindow == INVALID_HANDLE_VALUE) { MessageBox( NULL, TEXT("Error"), TEXT("Could not create window."), MB_OK | MB_ICONERROR ); ExitProcess(-1); } ShowWindow(hMainWindow, nCmdShow); MSG msg; while (GetMessage(&msg, NULL, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CLOSE: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, message, wParam, lParam); } return 0; } |
Oui oui, 84 lignes pour une fenêtre, alors qu’on a besoin de 10 fois moins pour en faire avec Qt par exemple…
Il ne nous reste plus qu’à créer notre checkbox. Il faut donc créer une fenêtre enfant que nous rattacherons à notre fenêtre principale. Eh oui, une checkbox est aussi une "fenêtre" dont la classe est… "Button". Ça déroute, hein ?
Pour la créer, il faut traiter le message WM_CREATE
envoyé au callback responsable de traiter les messages de notre fenêtre principale. Lorsque nous recevons ce message, il faut préciser que nous créons des fenêtres enfant, à savoir notre checkbox. Comme ceci :
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 | #define ID_CHECKBOX_TOGGLEME 1 // Set an ID to our checkbox LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { BOOL checked = FALSE; switch (message) { case WM_CREATE: CreateWindow( TEXT("BUTTON"), TEXT("Install some uselessware on this computer, with a lot of " "annoying advertisements."), WS_VISIBLE | WS_CHILD | BS_CHECKBOX, 20, 20, 600, 35, hwnd, (HMENU)ID_CHECKBOX_TOGGLEME, ((LPCREATESTRUCT)lParam)->hInstance, NULL); CheckDlgButton(hwnd, ID_CHECKBOX_TOGGLEME, BST_CHECKED); break; /* ... */ return DefWindowProc(hwnd, message, wParam, lParam); } |
L’instruction CheckDlgButton(hwnd, 1, BST_CHECKED);
va indiquer à notre programme de cocher la checkbox par défaut. Notre objectif sera de la décocher en utilisant un programme externe.
Le code final :
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 | #include <windows.h> #define ID_CHECKBOX_TOGGLEME 1 LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { const LPCTSTR szClassName = TEXT("MainWindow"); HWND hMainWindow = INVALID_HANDLE_VALUE; WNDCLASS wndClsMainWindowClass = { .style = 0, .lpfnWndProc = MainWindowProc, .cbClsExtra = 0, .cbWndExtra = 0, .hInstance = hInstance, .hIcon = NULL, .hbrBackground = COLOR_WINDOW, .lpszMenuName = NULL, .lpszClassName = szClassName }; if (RegisterClass(&wndClsMainWindowClass) == 0) { MessageBox( NULL, TEXT("Could not register class."), TEXT("Error"), MB_OK | MB_ICONERROR); ExitProcess(-1); } hMainWindow = CreateWindow( szClassName, TEXT("Toggle my Checkbox (please)"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, // style CW_USEDEFAULT, // x CW_USEDEFAULT, // y CW_USEDEFAULT, // width CW_USEDEFAULT, // height NULL, // parent NULL, // menu hInstance, NULL, ); if (hMainWindow == INVALID_HANDLE_VALUE) { MessageBox( NULL, TEXT("Error"), TEXT("Could not create window."), MB_OK | MB_ICONERROR ); ExitProcess(-1); } ShowWindow(hMainWindow, nCmdShow); MSG msg; while (GetMessage(&msg, NULL, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { BOOL checked = FALSE; switch (message) { case WM_CREATE: CreateWindow( TEXT("BUTTON"), TEXT("Install some uselessware on this computer, with a lot of " "annoying advertisements."), WS_VISIBLE | WS_CHILD | BS_CHECKBOX, 20, 20, 600, 35, hwnd, (HMENU)ID_CHECKBOX_TOGGLEME, ((LPCREATESTRUCT)lParam)->hInstance, NULL); CheckDlgButton(hwnd, ID_CHECKBOX_TOGGLEME, BST_CHECKED); break; case WM_COMMAND: checked = IsDlgButtonChecked(hwnd, ID_CHECKBOX_TOGGLEME); if (checked) { CheckDlgButton(hwnd, ID_CHECKBOX_TOGGLEME, BST_UNCHECKED); } else { CheckDlgButton(hwnd, ID_CHECKBOX_TOGGLEME, BST_CHECKED); } break; case WM_CLOSE: PostQuitMessage(0); break; } return DefWindowProc(hwnd, message, wParam, lParam); } |
Et le rendu (attention les yeux !) :
-
Monsieur et Madame GroisDansMaBaignore ont une fille. Comment s’appelle-t-elle ? ↩
Décocher "à distance"
On a fait notre programme cible. Maintenant on va faire un autre programme qui va décocher de lui-même cette case à cocher.
Je vous le répète : l’API Win32 est monstrueusement bien fournie. On va se servir de ces APIs :
- FindWindow pour récupérer un descripteur sur la fenêtre cible (qui possède la case à cocher)
- EnumChildWindows pour lister les fenêtres enfantes d’une fenêtre parente (note : dans ces fenêtres enfantes, il y aura la checkbox en question ! )
- SendMessage : pour envoyer un message à la checkbox et lui dire de se décocher. Niark niark!
Pas besoin de faire de reversing pour le coup.
Le code du décocheur :
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 | #include <windows.h> #define MAX_BUF 255 BOOL CALLBACK TargetWindowEnumChildProc(HWND hwnd, LPARAM lParam); int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { HWND hTargetWindow = FindWindow( NULL, TEXT("Toggle my Checkbox (please)")); if (hTargetWindow == NULL) { MessageBox( NULL, TEXT("Could not find the target window. " "Maybe you should launch it?"), TEXT("OooOops!"), MB_OK | MB_ICONWARNING ); ExitProcess(-1); } EnumChildWindows( hTargetWindow, TargetWindowEnumChildProc, 0); return 0; } BOOL CALLBACK TargetWindowEnumChildProc(HWND hwnd, LPARAM lParam) { CHAR cClassName[MAX_BUF] = {'\0'}; // Hope this is enough... LONG lWindowStyle = 0; GetClassName(hwnd, cClassName, MAX_BUF - 1); if (strcmp(cClassName, "Button") == 0) { // It's a match! Check that it is a checkbox lWindowStyle = GetWindowLong(hwnd, GWL_STYLE); if (lWindowStyle & BS_CHECKBOX) { // It is a checkbox: uncheck it. :D SendMessage( hwnd, BM_SETCHECK, BST_UNCHECKED, 0); MessageBox( NULL, TEXT("I have unchecked this unwanted damn checkbox!"), TEXT("Rock'n'roll!"), MB_OK | MB_ICONINFORMATION ); } } return TRUE; } |
Pour la démo, c’est par là ! Et pour ceux qui veulent tester eux-mêmes et tenter quelque chose de plus poussé, retrouvez les sources ici : https://github.com/Ge0/ToggleMyCheckbox
Les API Win32, c’est démodé, mais qu’est-ce que c’est rigolo quand on veut commencer à faire des trucs marrants sous Windows. Vous n’êtes pas d’accord ?