Po co aplikacjom znać własne imię
Odkąd w czasach szkolnych zacząłem interesować się programowaniem, wiele kursów języków programowania pokazywało pierwszy program, który niemal zawsze wyglądał bardzo podobnie.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char* argv[]) {
printf("Hello world!");
return EXIT_SUCCESS;
}CTen bardzo prosty przykład pokazuje podstawowy szkielet aplikacji w C – przez długi czas nie wiedziałem do czego służą argumenty argc i argv… Dłuższą chwilę później, na studiach, klasyczna książka Kerninghama i Ritchiego Język ANSI C pokazała mi, że nie są one konieczne ale mogą być bardzo użyteczne.
void main(void)CJednak jednej tajemnicy nie mogłem zrozumieć… Tajemnicy argv[0]… Po co programowi znać własną nazwę? Zazwyczaj w filmach o egzorcystach, kluczową dla nich informacją jest imię demona żeby go wypędzić. Jednak programy (a tym bardziej systemowe demony1) raczej same siebie egzorcyzmować nie będą. Myślałem, że jest to jedna z konwencji potrzebnych dla jądra systemu. Byłem w błędzie. W tym artykule dowiecie się skąd się ono bierze i pokażę Wam kilka przykładów w jaki sposób możemy wykorzystać ten pierwszy argument.
Uruchamianie procesu
W systemach uniksowych i uniksopodobnych takich jak Linux, Mac OS, FreeBSD czy QNX istnieje kilka bardzo użytecznych funkcji związanych z uruchamianiem procesu:
- Funkcje fork/vfork czy clone, które tworzą nowy proces potomny (tak, mówimy tu o relacji procesu rodzica i dziecka), będący kopią oryginalnego
- Rodzina funkcji execv*, która podmienia wykonywaną aplikację.
Ich definicje znajdują się w pliku nagłówkowym unistd.h. W przykładach będziemy wykorzystywać obie funkcje jednak wywołaniem systemowym fork zajmiemy się przy innej okazji.
Oto fragment z linuksowego manuala2:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);CRóżnią się one przede wszystkim sposobem przekazania argumentów, ale zasadniczo robią to samo – podmieniają proces, który wywołuje to polecenie systemowe na nowy proces wykonujący inny plik wykonywalny. Jedne przekazują również zmienne środowiskowe, inne oczekują argumentów wiersza poleceń zebranych w tablicy albo jako kolejnych argumentów funkcji. Co istotne, zachowuje PID uruchamiającego go procesu. PID jest to numer identyfikacyjny procesu a PPID jest identyfikatorem jego rodzica.
Funkcje te przydadzą się, aby zademonstrować zastosowania argv[0], które zostaną opisane w kolejnych akapitach.
Samodrukowanie kodu
Pierwszy przykład, jest raczej łamigłówką programistyczną niż realnym zastosowaniem. Przypomniała mi się stosunkowo niedawno, a wspomniał ją kiedyś dawno, dawno temu (na pierwszym roku studiów) wykładowca od Wstępu do programowania. Zacznijmy więc od postawienia problemu:
Napisz program, który wydrukuje własny kod (np. w postaci kodu assemblera)
W czasie studiów rozwiązanie nie było dla mnie oczywiste, jednak w czasie pracy zdarzyło mi się zetknąć z narzędziem objdump będącym częścią pakietu binutils. Ma on ma niejedną interesującą funkcję, ale jedna będzie nas w tym momencie szczególnie interesować – możliwość dezasemblacji. Aby to zrobić należy przekazać mu jako argumenty adres do pliku wykonywalnego oraz flagi –x86-asm-syntax=intel oraz -D. Pierwsza mówi, aby użyć składni Intela (w narzędziach linuksowych domyślną jest składnia AT&T), natomiast druga odpowiada za dezasemblację. Poniżej znajdziecie rozwiązanie – kod programu, który podmienia sam siebie na objdump’a z przekazaną ścieżką do uruchomionego programu.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
printf("Dumped assembler:\n");
char *args[] = {"objdump", argv[0], "--x86-asm-syntax=intel", "-D", NULL};
execvp("objdump", args);
return EXIT_SUCCESS;
}CNie jest to co prawda rozwiązanie idealne, można to zrobić bez użycia argv[0] jednak przestanie ono być przenośne. Dla systemów linuksowych możemy użyć pliku /proc/self/exe, natomiast na Macu musimy użyć wywołania systemowego proc_pidpath z biblioteki libproc:
char path[PROC_PIDPATHINFO_MAXSIZE];
proc_pidpath(getpid(), path, sizeof(path));CTak więc pierwszy raz wykorzystaliśmy argv[0] a dopiero się rozkręcamy.
Busybox/Toybox
Kolejny przykład przyszedł mi do głowy w trakcie pracy. Podczas pracy z terminalem w systemach uniksowych, przywykliśmy do pracy z programami takimi jak ls, ln, cp, mv i wieloma innymi wbrew pozorom nie musimy zawsze korzystać z tych samych narzędzi. Na Linuxie są one dystrybuowane poprzez pakiet GNU Coreutils, podczas gdy na Mac OSie korzystamy z wersji BSD (być może wspólnych z systemem FreeBSD). Lekkie systemy linuksowe na urządzeniach wbudowanych często nie mają rozbudowanego pakietu coreutils, a wykorzystują aplikację busybox albo (w przypadku Androida) toybox.
Czym się one różnią. Nigdy nie wczytywałem się w kod Toybox’a ale podobno jest przejrzystszy, bardziej POSIX-compliant, niż GNU-compliant oraz opublikowany jest przede wszystkim na bardziej liberalnej licencji. W końcu z jakiegoś powodu od Androida 6.0 przestano utrzymywać w nim Busybox’a.
Dlaczego oba przykłady są ciekawe? Otóż są to pojedyncze aplikacje, które zastępuje przynajmniej część z podstawowych narzędzi coreutils (GNU lub BSD). Jednak z poziomu ich użytkownika, poza ograniczoną funkcjonalnością nie widać większej różnicy w sposobie użytkowania. Jest to zasługa tego, że w systemach są stworzone linki symboliczne do wywołań np. ls do busybox ls, mv do busybox mv itp. Możecie to przetestować z wykorzystaniem ADB na Waszych telefonach z Androidem. Aplikacje te (podobnie jak przykład poniżej) na podstawie nazwy przekazanej w argv[0] decydują w jakiego odpowiednika aplikacji z coreutils użyć.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char* get_command_name(const char* path) {
for (int i = strlen(path) - 1; i > 0; --i) {
if (path[i] == '/') {
return &path[i + 1];
}
}
return path;
}
int main(int argc, char* argv[]) {
printf("argv[0]: %s\n", argv[0]);
if (strcmp(get_command_name(argv[0]), "mybusybox") == 0) {
printf("Generic run: ");
for (int i = 1; i < argc; ++i) {
printf("%s ", argv[i]);
}
printf("\n");
}
else if (strcmp(get_command_name(argv[0]), "ls") == 0) {
printf("Run ls\n");
}
else if (strcmp(get_command_name(argv[0]), "ps") == 0) {
printf("Run ps\n");
}
return EXIT_SUCCESS;
}CWykonajcie komendy poniżej i sprawdźcie jaki będzie wynik uruchomienia aplikacji busybox oraz linków ls i ps.
gcc busybox.c -o busybox
ln -s busybox ls
ln -s busybox ps
Wnioski z tego są następujące – argv[0] możemy zastosować aby np:
- Jedna binarka mogła udawać inne,
- Określać tryby kompatybilności z wersjami aplikacji,
- Zgodność CLI z innymi narzędziami,
- Zarządzanie konfiguracjami, np. sufix -debug z włączonymi dodatkowymi logami czy lokalizacja językowa.
Jeśli chcecie sprawdzić toybox’a na telefonach musicie włączyć tryb deweloperski i debugowanie po kablu USB oraz zainstalować w systemie adb. Następnie wykonajcie te polecenia:
adb devices -l
List of devices attached
fea49a23 device product:alioth_eea model:M2012K11AG device:alioth transport_id:1
adb -s fea49a23 shell
which ls
/system/bin/ls
ls -l /system/bin/ls | grep ls
lrwxr-xr-x 1 root shell 6 2009-01-01 01:00 /system/bin/ls -> toybox
toybox # Lista wszystkich wspieranych poleceń
Jak widzicie faktycznie polecenie ls jest linkiem symbolicznym do aplikacji toybox.
Zaszyfrowana binarka
Od razu muszę Wam napomknąć, że nie jest to rozwiązanie nadające się do zastosowania komercyjnego, jednak mające potencjał do dalszych ulepszeń i pokazujące przede wszystkim ideę w jaki sposób zrealizować taką funkcjonalność. No więc do rzeczy!
Kojarzycie może samorozpakowujące się archiwa? Są to aplikacje, które same wypakowują archiwum, bez korzystania z żadnego dodatkowego oprogramowania. Działają one najprawdopodobniej w ten sposób, że archiwum jest wbudowane w pewne środowisko uruchomieniowe – w tym przypadku rozpakowujące. Podobnie zrealizujemy naszą zaszyfrowaną aplikację, jednak skoro szyfrujemy aplikację, aby zabezpieczyć ją przed dezasemblacją (odzyskaniem kodu aplikacji na podstawie pliku binarnego) to będziemy musieli ją zdeszyfrować. Jednak na samym początku zanim omówię cały proces przygotujmy sobie prostą aplikację, która zostanie zaszyfrowana. Możecie napisać własną, albo wziąć tą poniżej… Koniec końców potrafimy pisać programy w C więc nie ma co z nią zbyt dużo kombinować 🙂
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
printf("Encrypted app\n");
return 0;
}CPrzygotowanie zaszyfrowanej aplikacji będzie przebiegać w trzech etapach zaprezentowanych na infografice.

Dla uproszczenia fazy szyfrowania program poniżej na każdym bajcie aplikacji niezaszyfrowanej wykonuje xor z liczbą 0xff.
#include <stdio.h>
#include <stdlib.h>
#define ENCRYPTION_BYTE 0xFF
#define BUFFER_LENGTH 32
int main(int argc, char* argv[]) {
FILE* input_desc = fopen(argv[1], "rb");
FILE* output_desc = fopen("encrypted_binary", "wb");
char buffer[BUFFER_LENGTH];
size_t size;
while ((size = fread(buffer, sizeof(char),
BUFFER_LENGTH, input_desc)) > 0) {
for (int i = 0; i < size; ++i) {
buffer[i] ^= ENCRYPTION_BYTE;
}
fwrite(buffer, sizeof(char), size, output_desc);
}
return EXIT_SUCCESS;
}
CKolejnym krokiem po zaszyfrowaniu pliku binarnego jest przygotowanie pliku nagłówkowego z tablicą zawierającą aplikację jako tablicę bajtów. Przygotowujemy go wykorzystując aplikację xxd z flagą -i. Nazwa tablicy oraz zmiennej są odpowiednio encrypted_binary i encrypted_binary_len – są to nazwy, wynikającej z nazwy pliku binarnego, który „osadzamy” w tablicy. Jak dokładnie się to wykonuje możecie podejrzeć w skrypcie bash’a:
echo "Encrypting app: $1"
./bin/encrypting_app $1
xxd -i encrypted_binary > encrypted_binary.h
gcc -std=c99 encrypted_app.c -o encrypted_app
rm encrypted_binary # sprzątamy tymczasowy, zaszyfrowany plik binarny
rm encrypted_binary.h # usuwamy niepotrzebny już nagłówekBashBrakuje nam już tylko środowiska uruchomieniowego. Stworzy on pomocniczy plik tymczasowy do którego zapisze zdeszyfrowaną aplikację i nada mu później prawa do wykonywania. Teraz (tj. od linii 19) zaczyna się kluczowy element – rozgałęziamy naszą aplikację tworząc proces poboczny, wykonujący naszą zdeszyfrowaną aplikację. Co jest ważne? Widoczne jest tutaj jak na talerzu, że argv[0] nie jest przekazywane przez jądro systemu a przez użytkownika wywołania systemowego exec. Przekazaliśmy jej nazwę naszej oryginalnej aplikacji więc na liście aktywnych procesów nie zobaczymy naszej tymczasowej, zdeszyfrowanej aplikacji.
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include "encrypted_binary.h"
#define ENCRYPTION_BYTE 0xFF
int main(int argc, char* argv[]) {
FILE *fp = fopen("/tmp/decrypted", "wb");
for (size_t i = 0; i < encrypted_binary_len; ++i) {
encrypted_binary[i] ^= ENCRYPTION_BYTE;
}
fwrite(encrypted_binary, sizeof(char), encrypted_binary_len, fp);
chmod("/tmp/decrypted", 0755);
fclose(fp);
pid_t pid = fork();
if (pid == 0) {
execl("/tmp/decrypted", argv[0], NULL);
}
wait(NULL);
remove("/tmp/decrypted");
return 0;
}CCo tutaj da się ulepszyć? Z całą pewnością metodę szyfrowania, która na pewno nie jest bezpieczna (w końcu to dość antyczna metoda) oraz nie ma potrzeby tworzyć pliku fizycznie w systemie – pokażę Wam to już przy następnej okazji!
Koniec
Chociaż to jest dość lekki temat, może się wydawać mało istotny ale wydaje mi się, że pokazuje on całkiem fajne trick’i i to, że czasem na pozór bezużyteczne informacje mogą być bardzo użyteczne.
- daemon – proces uruchomiony w tle przez system operacyjny, który nie komunikuje się bezpośrednio z użytkownikiem ↩︎
- https://linux.die.net/man/3/execv ↩︎
Dodaj komentarz