medibash: C ile Basit ve Kontrollü Bir Restricted Shell Tasarımı
Konular:
-
Genel Mimari Yaklaşım
-
Sabitler ve Makro Tanımları
-
Sinyal Yönetimi
-
Ortam Değişkenlerinin Temizlenmesi
-
Prompt (Komut Satırı) Üretimi
-
Kullanılan Sistem Fonksiyonları
-
Komut Ayrıştırma
-
Komut Çalıştırma Mantığı
-
Whitelist Yaklaşımı
-
Local Fallback Mekanizması
-
Güvenlik Perspektifi
-
medibash Zafiyetleri ve Sömürülmesi (Bypass/Exploitation)
-
Sonuç
-
medibash Kaynak Kodu
Merhabalar. Modern Unix benzeri sistemlerde kabuklar, kullanıcı ile işletim sistemi arasındaki en güçlü arayüzlerden biridir. Konumuzda, C dili kullanılarak geliştirilen ve bilinçli olarak sınırlandırılmış bir kabuk olan medibash üzerinden, kısıtlı bir shell tasarımının temel prensiplerini ele alacağız. Kısıtlanmış bir yapıda olmasına rağmen bazı komut ve uygulamaların nasıl zafiyetlere yol açabileceğini inceleyeceğiz.
medibash::
medibash adlı kısıtlı kabuğumuzun (minimal restricted shell) C dili ile nasıl tasarlandığını, hangi bileşenlerden oluştuğunu ve kullanılan temel sistem çağrılarının neden tercih edildiğini açıklayacağız. Amacımız; öğretici, anlaşılır ve pratik bir teknik referans sunmak, araştırma konusu sağlamak, sistem çağrılarını basitçe ele almak ve Capture The Flag yarışmalarında kullanılmak üzere kabuk oluşturmaktır.
Bu çalışma bir güvenlik sınırı (security boundary) iddiası taşımamaktadır. Tasarım; eğitim, laboratuvar ve CTF senaryoları için bilinçli olarak sade tutulmuştur. Güvenlik zafiyeti örnekleriyle de eğlenceli bir hale getirilmiştir.
Genel Mimari Yaklaşım
medibash’in mimarisi bilinçli olarak basit ve şeffaf olacak şekilde kurgulanmıştır. Karmaşıklıktan kaçınılmış, POSIX süreç modeline doğrudan temas eden bir yapı tercih edilmiştir.
Shell’in temel çalışma akışı şu adımlardan oluşmaktadır:
- Kullanıcıdan satır bazlı komut okunur
- Komut argümanlarına ayrıştırılır
- Önceden tanımlı whitelist ile karşılaştırılır
- Listede yer alan komutlar seçildiğinde işletim sistemi üzerinden mevcut kullanıcı yetkisiyle çağrı yapılır.
Bu yaklaşım, klasik PATH çözümlemesini tamamen devre dışı bırakır ve beklenmeyen komut yürütmelerini önler.
Sabitler ve Makro Tanımları
Shell içerisinde kullanılan sabitler kodun davranışını kontrol altına almak amacıyla tanımlanmıştır.
/* Shell ayarlari */
#define SHELL_INPUT_SIZE 256
#define MAX_ARGS 32
/* ANSI renkleri */
#define RED "\033[1;31m"
#define GREEN "\033[1;32m"
#define BLUE "\033[1;34m"
#define RESET "\033[0m"
/* Ekran temizleme */
#define CLEAR_SCREEN "\033[2J\033[H"}
SHELL_INPUT_SIZE, kullanıcıdan okunacak maksimum komut uzunluğunu sınırlar.
MAX_ARGS ise bir komutta kabul edilecek maksimum argüman sayısını belirler.
Bu tasarım sayesinde:
- Dinamik bellek kullanımına gerek kalmaz
- Buffer overflow riski azaltılır
- Kod okunabilirliği ve sürdürülebilirliği artar
Sinyal Yönetimi
medibash, kullanıcı tarafından gönderilen bazı sinyalleri bilinçli olarak yok sayar.
signal(SIGINT, ignore_signal);
signal(SIGQUIT, ignore_signal);
SIGINT (Ctrl+C) ve SIGQUIT (Ctrl+\\) sinyallerinin engellenmesinin temel nedenleri şunlardır:
- Shell’in istemsiz şekilde sonlandırılmasını önlemek
- CTF senaryolarında kaçış yüzeyini azaltmak
- Kullanıcı deneyimini daha deterministik hale getirmek
Bu davranış, boş bir sinyal handler fonksiyonu aracılığıyla sağlanır. Sinyal alındığında süreç herhangi bir işlem yapmaz.
Ortam Değişkenlerinin Temizlenmesi
Shell başlatılırken clearenv() çağrısı ile tüm environment değişkenleri silinir.
clearenv();
Bu tercih şu güvenlik ve kontrol hedeflerine hizmet eder:
- PATH manipülasyonlarının önüne geçmek
- LD_PRELOAD gibi dinamik bağlama temelli saldırıları engellemek
- Çalışma ortamını tamamen deterministik hale getirmek
Prompt (Komut Satırı) Üretimi
medibash, bash benzeri bir kullanıcı deneyimi sunmak amacıyla renkli bir prompt üretir:
void print_prompt(void) {
char cwd[PATH_MAX];
char host[64];
char display_path[PATH_MAX];
const char *user;
gethostname(host, sizeof(host));
getcwd(cwd, sizeof(cwd));
struct passwd *pw = getpwuid(getuid());
user = pw ? pw->pw_name : "unknown";
const char *home = getenv("HOME");
if (home && strncmp(cwd, home, strlen(home)) == 0) {
snprintf(display_path, sizeof(display_path),
"~%s", cwd + strlen(home));
} else {
snprintf(display_path, sizeof(display_path), "%s", cwd);
}
printf(
RED "medibash::" RESET
GREEN "%s@%s" RESET
":" BLUE "%s" RESET "$ ",
user, host, display_path
);
fflush(stdout);
}
medibash::mediuser@akkus:/home/mediuser
Kullanılan Sistem Fonksiyonları
getpwuid(getuid()): Aktif kullanıcının kullanıcı adını elde etmek için kullanılır.
gethostname(): Sistem adını almak için kullanılır.
getcwd(): Mevcut çalışma dizinini öğrenmek için kullanılır.
getenv("HOME"): Ev dizinini tespit edip yolu ~ ile kısaltmak için kullanılır.
Bu fonksiyonlar, kullanıcıya bağlam hissi veren ve alışıldık bir kabuk deneyimi sağlayan bir arayüz oluşturur.
Komut Ayrıştırma
Kullanıcının girdiği komutlar basit bir ayrıştırma mantığı ile argümanlarına bölünür.
Bu işlem sırasında strtok() fonksiyonu kullanılır. Boşluk karakteri ayırıcı olarak kabul edilir.
Bu yaklaşım bilinçli olarak sınırlıdır:
- Tırnak işleme yoktur
- Escape karakterleri desteklenmez
- Karmaşık shell genişletmeleri bulunmaz
Bu sadelik, shell davranışının öngörülebilir olmasını sağlar ve parsing kaynaklı kaçış ihtimallerini azaltır.
Komut Çalıştırma Mantığı
Komutların yürütülmesi klasik POSIX süreç modeli üzerinden gerçekleştirilir.
void run_command(const char *path, char *argv[]) {
pid_t pid = fork();
if (pid == 0) {
execve(path, argv, NULL);
_exit(1);
} else if (pid > 0) {
waitpid(pid, NULL, 0);
}
}
fork() ile yeni bir süreç oluşturulur, ardından execve() çağrısı ile hedef binary çalıştırılır. Ebeveyn süreç ise waitpid() ile çocuğun tamamlanmasını bekler.
execve() tercih edilmesinin nedeni:
- system() gibi shell arka planı kullanan fonksiyonlardan kaçınmak
- execvp() ile yapılan PATH çözümlemesini engellemek
- Ortam değişkenlerini NULL geçirerek çevresel etkiyi sıfırlamak
Whitelist Yaklaşımı
medibash, yalnızca önceden izin verilmiş komutların çalıştırılmasına olanak tanır.
while (1) {
print_prompt();
if (!fgets(input, sizeof(input), stdin)) {
clearerr(stdin);
continue;
}
input[strcspn(input, "\n")] = 0;
if (strcmp(input, "help") == 0 || strcmp(input, "?") == 0) {
print_help();
continue;
}
char input_copy[SHELL_INPUT_SIZE];
strncpy(input_copy, input, sizeof(input_copy));
input_copy[sizeof(input_copy) - 1] = '\0';
int argc = parse_args(input_copy, argv);
if (argc == 0)
continue;
/* === WHITELIST === */
if (strcmp(argv[0], "clear") == 0)
handle_clear();
else if (strcmp(argv[0], "whoami") == 0)
run_command("/usr/bin/whoami", argv);
else if (strcmp(argv[0], "id") == 0)
run_command("/usr/bin/id", argv);
else if (strcmp(argv[0], "pwd") == 0)
run_command("/bin/pwd", argv);
else if (strcmp(argv[0], "ls") == 0)
run_command("/bin/ls", argv);
else if (strcmp(argv[0], "stat") == 0)
run_command("/bin/stat", argv);
else if (strcmp(argv[0], "chmod") == 0)
run_command("/bin/chmod", argv);
else if (strcmp(argv[0], "ln") == 0)
run_command("/bin/ln", argv);
else if (strcmp(argv[0], "ps") == 0)
run_command("/bin/ps", argv);
else if (strcmp(argv[0], "df") == 0)
run_command("/bin/df", argv);
else if (strcmp(argv[0], "free") == 0)
run_command("/usr/bin/free", argv);
else if (strcmp(argv[0], "uptime") == 0)
run_command("/usr/bin/uptime", argv);
else if (strcmp(argv[0], "date") == 0)
run_command("/bin/date", argv);
else if (strcmp(argv[0], "uname") == 0)
run_command("/bin/uname", argv);
else if (strcmp(argv[0], "ifconfig") == 0)
run_command("/sbin/ifconfig", argv);
else if (strcmp(argv[0], "find") == 0)
run_command("/usr/bin/find", argv);
else if (strcmp(argv[0], "nc") == 0)
run_command("/bin/nc", argv);
else if (strcmp(argv[0], "curl") == 0)
run_command("/usr/bin/curl", argv);
/* === LOCAL FALLBACK === */
else {
}
Her komut, string karşılaştırması ile eşleştirilir ve ilgili binary mutlak yol üzerinden çağrılır.
Bu yaklaşımın avantajları:
- Hangi programın çalışacağı nettir
- Alias veya PATH hijack mümkün değildir
- Kod okunabilirliği yüksektir
Local Fallback Mekanizması
Whitelist dışındaki komutlar için yalnızca mevcut dizinde bulunan ve çalıştırılabilir olan dosyalara izin verilir.
Bu mekanizma zafiyet oluşturması için yerleştirilmiştir.
/* === LOCAL FALLBACK === */
else {
char local_path[PATH_MAX];
snprintf(local_path, sizeof(local_path), "./%s", argv[0]);
if (access(local_path, X_OK) == 0)
run_command(local_path, argv);
else
printf("Yetkisiz komut. 'help' yaziniz.\n");
}
Bu mekanizma özellikle eğitim ve CTF senaryolarında özel olarak bırakılmıştır.
Güvenlik Perspektifi
medibash, gerçek dünyada tam güvenlik sağlayan bir çözüm değildir.
Gerçek üretim ortamları için aşağıdaki tekniklerin değerlendirilmesi gerekir:
- seccomp filtreleri
- chroot veya container izolasyonu
- Sistem çağrısı seviyesinde denetimler
medibash Zafiyetleri ve Sömürülmesi (Bypass/Exploitation)
medibash için "mediuser" adlı bir kullanıcı oluşturulmuştur ve passwd dosyası üzerinden bu kullanıcıya geçiş yapıldığında /bin/sh veya /bin/bash yerine /home/mediuser/bins/medibash için yani medibash in kendisine yönlendirme yapılmıştır.Netcat ile medibash Bypass
medibash içerisinde tanımlı olan "nc" komutu doğrudan NetCat çalıştırmaktadır. Netcat (nc), TCP ve UDP üzerinden veri okumak ve yazmak için kullanılan, son derece basit ama çok güçlü bir ağ aracıdır.Find ile medibash Bypass
medibash içerisinde tanımlı diğer komutlardan birisi ise "find" komutudur. Find, Unix/Linux sistemlerinde dosya ve dizinleri belirli kriterlere göre aramak ve işlem yapmak için kullanılan güçlü bir komuttur. Saldırganlar find komutunun "-exec" parametresinin nelere yol açabildiğini fark edebilir.Curl ile medibash Lokal Dosya Okuma/Yazma
Yine kullanılmasına izin verilen uygulamalardan birinin "Curl" olduğu görülmektedir. curl, çeşitli ağ protokolleri üzerinden veri transferi yapmak için kullanılan bir komut satırı aracıdır. medibash içerisinde "cat", "echo", "mv" veya dosya editörleri olan "vi", "nano" gibi komutlara izin verilmez. Dolayısıyla dosya okuma ve yazmak için saldırganlar çözüm yolu arayabilir.Local Fallback aracılığıyla Restriction Atlatma
medibash içerisinde yine sürpriz olarak eklenen bir "Local Fallback" mekanizması mevcuttur. Aslında burada bir zafiyet olduğu kaynak kod incelendiğinde tespit edilebilir. Fakat medibash kaynak kodunu göremeyen bir saldırgan buradaki zafiyeti nasıl tespit edebilir buna göz atalım. Local Fallback mekanizması medibash dizininde yer alan bir dosyayı ismiyle çalıştırmak için bulunmaktadır. Örneğin medibash ile aynı dizinde "test" adında bir binary dosyası olsaydı, medibash içerisinde test komutunu gönderdiğimizde buna kızmayacak ve "test" binary çalıştırılacaktır. medibash "bins" klasörü içerisinde çalışmaktadır. Saldırgan medibash içerisinden tekrar medibash çağırmayı denediğinde "help" ile gelen komut listesinde "medibash" olmadığını fakat medibash yazdığında tekrar çalıştığını fark edebilir. "ps" komutu da zaten serbestti. Kontrolünü de sağlayabilir.Sonuç
medibash, kısıtlı ama anlaşılır bir shell tasarımını örnekleyen öğretici bir projedir.
C ile sistem programlama öğrenenler, CTF hazırlayanlar ve POSIX süreç modelini anlamak isteyenler için güçlü bir başlangıç noktası sunar.
Aşağıda yer alan komutların sömürülmesi mümkündür.
medibash::mediuser@akkus:/home/mediuser$ help
Sömürülebilir komutlar:
ln
find
nc
curl
medibash::mediuser@akkus:/home/mediuser$
medibash gibi bir kısıtlanmış kabuk oluşturulmak istendiğinde saldırgan gözünden incelemek ve test etmek oldukça önemlidir.
medibash Kaynak Kodu
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys wait.h="">
#include <signal.h>
#include <stdlib.h>
#include <pwd.h>
#include <limits.h>
/* Shell ayarlari */
#define SHELL_INPUT_SIZE 256
#define MAX_ARGS 32
/* ANSI renkleri */
#define RED "\033[1;31m"
#define GREEN "\033[1;32m"
#define BLUE "\033[1;34m"
#define RESET "\033[0m"
/* Ekran temizleme */
#define CLEAR_SCREEN "\033[2J\033[H"
void print_banner(void) {
printf(
"\033[1;34m"
" ___ ___ _ _ _ _ \n"
" _/ __\\_/__ \\_ | (_) | | | \n"
"/ | | \\ _ __ ___ ___ __| |_| |__ __ _ ___| |__ \n"
"\\ | >_ | / | '_ ` _ \\ / _ \\/ _` | | '_ \\ / _` / __| '_ \\ \n"
"/ |_______| \\ | | | | | | __/ (_| | | |_) | (_| \\__ \\ | | |\n"
"\\_ _ _/ |_| |_| |_|\\___|\\__,_|_|_.__/ \\__,_|___/_| |_|\n"
" \\___/ \\___/ \n"
"\n"
" medibash :: restricted shell environment\n"
" by AkkuS \n"
"\033[0m"
);
}
void handle_clear(void) {
printf(CLEAR_SCREEN);
print_banner();
fflush(stdout);
}
void ignore_signal(int sig) {
(void)sig;
}
/* Yardim mesaji */
void print_help(void) {
printf("Kullanilabilir komutlar:\n");
printf(" whoami\n");
printf(" id\n");
printf(" pwd\n");
printf(" ls\n");
printf(" stat\n");
printf(" chmod\n");
printf(" ln\n");
printf(" ps\n");
printf(" df\n");
printf(" free\n");
printf(" uptime\n");
printf(" date\n");
printf(" uname\n");
printf(" ifconfig\n");
printf(" find\n");
printf(" nc\n");
printf(" curl\n");
printf(" clear\n");
printf(" help, ?\n");
}
/* Basit arguman ayirici */
int parse_args(char *input, char *argv[]) {
int argc = 0;
char *token = strtok(input, " ");
while (token && argc < MAX_ARGS - 1) {
argv[argc++] = token;
token = strtok(NULL, " ");
}
argv[argc] = NULL;
return argc;
}
/* Komut calistir */
void run_command(const char *path, char *argv[]) {
pid_t pid = fork();
if (pid == 0) {
execve(path, argv, NULL);
_exit(1);
} else if (pid > 0) {
waitpid(pid, NULL, 0);
}
}
/* Renkli bash-benzeri prompt */
void print_prompt(void) {
char cwd[PATH_MAX];
char host[64];
char display_path[PATH_MAX];
const char *user;
gethostname(host, sizeof(host));
getcwd(cwd, sizeof(cwd));
struct passwd *pw = getpwuid(getuid());
user = pw ? pw->pw_name : "unknown";
const char *home = getenv("HOME");
if (home && strncmp(cwd, home, strlen(home)) == 0) {
snprintf(display_path, sizeof(display_path),
"~%s", cwd + strlen(home));
} else {
snprintf(display_path, sizeof(display_path), "%s", cwd);
}
printf(
RED "medibash::" RESET
GREEN "%s@%s" RESET
":" BLUE "%s" RESET "$ ",
user, host, display_path
);
fflush(stdout);
}
int main(void) {
char input[SHELL_INPUT_SIZE];
char *argv[MAX_ARGS];
clearenv();
signal(SIGINT, ignore_signal);
signal(SIGQUIT, ignore_signal);
printf("\033[2J\033[H"); // ekran temizleme (opsiyonel)
print_banner();
while (1) {
print_prompt();
if (!fgets(input, sizeof(input), stdin)) {
clearerr(stdin);
continue;
}
input[strcspn(input, "\n")] = 0;
if (strcmp(input, "help") == 0 || strcmp(input, "?") == 0) {
print_help();
continue;
}
char input_copy[SHELL_INPUT_SIZE];
strncpy(input_copy, input, sizeof(input_copy));
input_copy[sizeof(input_copy) - 1] = '\0';
int argc = parse_args(input_copy, argv);
if (argc == 0)
continue;
/* === WHITELIST === */
if (strcmp(argv[0], "clear") == 0)
handle_clear();
else if (strcmp(argv[0], "whoami") == 0)
run_command("/usr/bin/whoami", argv);
else if (strcmp(argv[0], "id") == 0)
run_command("/usr/bin/id", argv);
else if (strcmp(argv[0], "pwd") == 0)
run_command("/bin/pwd", argv);
else if (strcmp(argv[0], "ls") == 0)
run_command("/bin/ls", argv);
else if (strcmp(argv[0], "stat") == 0)
run_command("/bin/stat", argv);
else if (strcmp(argv[0], "chmod") == 0)
run_command("/bin/chmod", argv);
else if (strcmp(argv[0], "ln") == 0)
run_command("/bin/ln", argv);
else if (strcmp(argv[0], "ps") == 0)
run_command("/bin/ps", argv);
else if (strcmp(argv[0], "df") == 0)
run_command("/bin/df", argv);
else if (strcmp(argv[0], "free") == 0)
run_command("/usr/bin/free", argv);
else if (strcmp(argv[0], "uptime") == 0)
run_command("/usr/bin/uptime", argv);
else if (strcmp(argv[0], "date") == 0)
run_command("/bin/date", argv);
else if (strcmp(argv[0], "uname") == 0)
run_command("/bin/uname", argv);
else if (strcmp(argv[0], "ifconfig") == 0)
run_command("/sbin/ifconfig", argv);
else if (strcmp(argv[0], "find") == 0)
run_command("/usr/bin/find", argv);
else if (strcmp(argv[0], "nc") == 0)
run_command("/bin/nc", argv);
else if (strcmp(argv[0], "curl") == 0)
run_command("/usr/bin/curl", argv);
/* === LOCAL FALLBACK === */
else {
char local_path[PATH_MAX];
snprintf(local_path, sizeof(local_path), "./%s", argv[0]);
if (access(local_path, X_OK) == 0)
run_command(local_path, argv);
else
printf("Yetkisiz komut. 'help' yaziniz.\n");
}
}
return 0;
}