Τετάρτη 25 Μαΐου 2011

DIY One Touch Backup στο Linux Part 1: Reverse engineering

Σε διάφορα forums, σε threads που έχουν να κάνουν με backup, χρήστες των windows έχουν κάνει σχόλια του στυλ: "Δεν μπορεί να είναι τόσο δύσκολο, εγώ για να πάρω backup απλά τοποθετώ τον εξωτερικό δίσκο και πατάω το κουμπάκι που λέει One Touch Backup".

Καθώς δεν υπάρχει κανένας σοβαρός τεχνικός λόγος γιατί να μην μπορεί αυτό να δουλέψει και στο Linux, αποφάσισα να ρίξω μια ματιά στους εξωτερικούς δίσκους μου για να δω τι θα χρειαζόταν ώστε να υποστηριχθεί το "κουμπί" που συμπεριλαμβάνουν.


Καταρχήν πρέπει να πούμε ότι οι συσκευές Maxtor OneTouch ήδη υποστηρίζονται στο Linux (συμπεριλαμβανομένου του button) μέσω ενός επιπλέον driver (onetouch). Αυτό συμβαίνει γιατί οι συσκευές αυτές έχουν υλοποιήσει το button με το σωστό τεχνικά τρόπο, δηλαδή μέσω ενός ξεχωριστού USB HID interface. To interface αυτό ο onetouch driver το "εξάγει" απλά ως ένα input device το οποίο μπορεί να χρησιμοποιηθεί από οποιαδήποτε εφαρμογή θέλουμε (π.χ., gnome shortcuts ).

Οι δικές μου συσκευές ("no-name") που περιλαμβάνουν one touch backup button, περιέχουν ελεγκτές της εταιρείας JMicron, συγκεκριμμένα JMicron 20336 (SATA port replicator to USB 2.0 adaptor) και JMicron 20338 (SATA/IDE to USB 2.0 Combo adaptor).

Σε αυτό το post θα ασχοληθούμε με τον JM20338 αλλά και ο JM20336 είναι πολύ παρόμοιος.

Η πρώτη ιδέα ήταν να δω μήπως η συσκευή υλοποιεί το interface για το κουμπί όπως ο onetouch, πράγμα που θα μου επέτρεπε να την υποστηρίξω απλά προσθέτοντας το USB ID της στον onetouch driver (152d:2338 είναι το VendorID:ProductID της συσκευής):

lsusb -vvv -d 152d:2338

Bus 002 Device 008: ID 152d:2338 JMicron Technology Corp. / JMicron USA Technology Corp. JM20337 Hi-Speed USB to SATA & PATA Combo Bridge
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0        64
  idVendor           0x152d JMicron Technology Corp. / JMicron USA Technology Corp.
  idProduct          0x2338 JM20337 Hi-Speed USB to SATA & PATA Combo Bridge
  bcdDevice            1.00
  iManufacturer           1 JMicron
  iProduct                2 USB to ATA/ATAPI Bridge
  iSerial                 5 376871830782
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           32
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          4 USB Mass Storage
    bmAttributes         0xc0
      Self Powered
    MaxPower                2mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         8 Mass Storage
      bInterfaceSubClass      6 SCSI
      bInterfaceProtocol     80 Bulk (Zip)
      iInterface              6 MSC Bulk-Only Transfer
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               0
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x02  EP 2 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               0
Device Qualifier (for other device speed):
  bLength                10
  bDescriptorType         6
  bcdUSB               2.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0        64
  bNumConfigurations      1
Device Status:     0x5301
  Self Powered

Όπως βλέπουμε υπάρχει μόνο ένα interface, τύπου USB Mass Storage. Άρα θα αναγκαστούμε να ρίξουμε μια πιο προσεκτική ματιά στο πώς οδηγεί τη συσκευή το συνοδευτικό της software στα windows για να καταλάβουμε τι γίνεται ...

Συνήθως οι συσκευές τύπου mass storage είναι οι πιο βολικές USB συσκευές από πλευράς συμβατότητας με εικονικές μηχανές, οπότε το πρώτο που σκεφτόμαστε είναι να χρησιμοποιήσουμε virtualization για το reverse engineering της συσκευής μας. Έτσι δε χρειαζόμαστε πανάκριβους hardware usb analyzers, ούτε reboots, ούτε (μερικές φορές αναξιόπιστους) filter drivers για τα windows.

Μια και το USB support (ειδικά ο ehci controller) του Qemu αυτή τη στιγμή είναι υπό πυρετώδη ανάπτυξη και ως εκ τούτου παρουσιάζει προβληματάκια, μπορούμε να καταφύγουμε στο virtualbox για να μας βοηθήσει. Για το tracing θα χρησιμοποιήσουμε το υποσύστημα usbmon του πυρήνα του Linux.
Η διαδικασία περιγράφεται αναλυτικά στο http://polytechnitis.blogspot.com/2011/05/usb-reverse-engineering-virtualbox.html οπότε δε θα την επαναλάβω.

Παρατηρώντας ότι μόλις ξεκινούσε το πρόγραμμα PC Clone Ex, ξεκινούσε μεγάλη "δραστηριότητα" στο USB bus μέχρι να πατηθεί το κουμπί του backup, είναι λογικό να υποθέσουμε ότι η εφαρμογή κάνει polling για την κατάσταση του κουμπιού (πατημένο ή όχι). Άρα περιμένουμε να δούμε ένα επαναλαμβανόμενο "Data In" transaction στο log του usbmon, του οποίου το output να αλλάζει ανάλογα με το αν το κουμπί είναι πατημένο ή όχι.

Πράγματι, μετά από μια περίοδο αρχικοποίησης της συσκευής, βασικών transaction του στυλ SCSI INQUIRY / Read Capacity κλπ, παρατηρούμε ότι επαναλαμβάνεται συνεχώς το παρακάτω:

S Bo:2:008:2 -115 31 = 55534243 309ac081 02000000 80000cdf 10000002 00000000 0000ff00 000000
C Bo:2:008:2 0 31 >
S Bi:2:008:1 -115 2 <
C Bi:2:008:1 0 2 = 9700
S Bi:2:008:1 -115 512 <
C Bi:2:008:1 0 13 = 55534253 309ac081 00000000 00

Επιπλέον παρατηρήθηκε ότι η τιμή που διαβάζει το transaction αυτό από τη συσκευή είναι 9600 αν το κουμπί είναι πατημένο και 9700 αν όχι. Εδώ είμαστε λοιπόν :)

Ας αναλύσουμε λίγο περισσότερο το παραπάνω transaction για όσους δεν έχουν ξαναδεί usbmon / usb mass storage logs. Οι 2 πρώτες στήλες του log δε μας ενδιαφέρουν στη συγκεκριμένη περίπτωση κι έτσι έχουν αφαιρεθεί.

Η πρώτη στήλη που βλέπουμε έχει το σύμβολο 'S' που σημαίνει submit to device ή το 'C' που σημαίνει callback. Επίσης το Bo σημαίνει "Bulk OUT" (bulk είναι ένας από τους τύπους μεταφοράς usb δεδομένων) και Bi σημαίνει "Bulk IN" ενώ ακολουθεί το Bus number (2) και Port number (8).
Το 0 ή -115 είναι το status code και ακολουθεί ο αριθμός των bytes που στέλνεται στη συσκευή ή ζητείται από τη συσκευή.

Αυτό που γίνεται λοιπόν είναι ότι ο driver στέλνει αυτά τα 31 bytes στη συσκευή, το callback του λέει ότι όντως στάλθηκαν εντάξει, μετά ζητά 2 bytes και του επιστρέφεται το 00 97 (πρέπει να θυμόμαστε ότι ο USB είναι little endian) ή το 00 96 αν το κουμπί είναι πατημένο, κάτι που δε φαίνεται παραπάνω. Τέλος, ο driver ζητά ένα πακέτο ως 512 bytes και του επιστρέφονται τα 13 bytes που βλέπουμε.

Η πρώτη ιδέα με βάση τα παραπάνω για να υποστηρίξουμε αυτή τη λειτουργικότητα στο Linux θα ήταν να γράψουμε ένα userspace USB driver (μπορούμε ακόμα και σε python) ο οποίος θα αναλάβει να ανάπαράγει ακριβώς το παραπάνω transaction. Δυστυχώς όμως αυτό είναι μη πρακτικό. Ο λόγος είναι ότι στο Linux κάθε driver έχει την "αποκλειστική κυριαρχία" ενός usb interface. Το οποίο σημαίνει ότι αν γράφαμε το δικό μας usb driver θα έπρεπε να "εκτοπίσουμε" τον mass-storage driver του Linux από το interface οπότε θα μπορούσαμε να χρησιμοποιήσουμε τη συσκευή μας ως "κουμπί" αλλά όχι πια ως δίσκο (!!) (αν δε θέλουμε να ξαναγράψουμε ουσιαστικά τον mass storage driver σε userspace). Total fail :P

Για να λύσουμε το πρόβλημα, θα πρέπει να κατανοήσουμε το transaction μας λίγο καλύτερα. Όπως είδαμε από το lsusb output, το interface μας είναι τύπου "bulk-only transfer". Τι σημαίνει όμως αυτό; Η απάντηση βρίσκεται εδώ, στο specification του Bulk Only Transfer, το οποίο μάλιστα είναι αρκετά ευανάγνωστο. Ουσιαστικά μας λέει πώς μεταφέρουμε SCSI commands πάνω από USB Bulk transfers.
Το ενδιαφέρον κομμάτι είναι το παρακάτω σχήμα, που μας δείχνει τη "ροή" ενός command πάνω από B.O.T:



Πρέπει να συμπληρώσουμε ότι το CBW (Command Block Wrapper) είναι ακριβώς 31 bytes ενώ το CSW (Command Status Wrapper) είναι ακριβώς 13! Συμπεραίνουμε λοιπόν ότι αυτό που βλέπουμε να επαναλαμβάνεται συνέχεια στα logs είναι απλά ένα SCSI Data-In command!

(Αυτός ο τρόπος λειτουργίας παρεμπιπτόντως θυμίζει λίγο τους SCSI scanners που επίσης χρησιμοποιούσαν αυτού του στυλ το polling πάνω από SCSI commands για τα buttons τους.)

Το ωραίο λοιπόν στην υπόθεση λοιπόν, είναι ότι αρκεί να αλληλεπιδράσουμε με τη συσκευή σε επίπεδο SCSI και μπορούμε να αφήσουμε τον usb mass-storage driver του Linux απείραχτο :)

Με βάση αυτά που ξέρουμε τώρα, ας αναλύσουμε λίγο το log μας ώστε να το καταλάβουμε περισσότερο. Στο σύνδεσμο https://wiki.kubuntu.org/Kernel/Debugging/USB αναλύεται πολύ ωραία η δομή ενός SCSI/Mass storage πακέτου. Με βάση αυτά που βλέπουμε εκεί, αφενός μπορούμε να καταλάβουμε ότι το ενδιαφέρον στο status μας είναι τα μηδενικά (που σημαίνει success), ενώ το CBW μας αναλύεται ως εξής:

02000000                     => BufferLen = 2 (είπαμε, little endian)

80                           => Flags = SG_DXFER_FROM_DEV;

00

0c                           => CommandLength = 12

df 10000002 00000000 0000ff  => Αυτό είναι το SCSI Command Descriptor Block (CDB)

00000000                     => 4 byte padding ως τα 16 (standard CDB size).


Βρήκαμε λοιπόν ότι αρκεί να στέλνουμε το συγκεκριμμένο non-standard SCSI command στη συσκευή περιοδικά για να διαβάσουμε την κατάσταση του button (μας νοιάζει μόνο το τελευταίο bit από τα 16 που θα πάρουμε).

Πώς τώρα στέλνουμε το command μας; Εδώ έρχεται να μας βοηθήσει ο πυρήνας 2.6, ο οποίος υποστηρίζει το SG_IO ioctl το οποίο κάνει ακριβώς ότι θέλουμε :)

Ο παρακάτω κώδικας σε ψευδο-C δείχνει χονδρικά τι χρειάζεται να κάνουμε:

int main(int argc, char **argv)
{
 int fd, rc;
 struct sg_io_hdr sg_io;

 uint8_t sense[32];
 uint8_t data[2];
 uint8_t cdb[16] = { 0xdf, 0x10, 0x00, 0x00, 0x02, 0x00,
                     0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
                            0x00, 0x00, 0x00, 0x00};

 fd = open (argv[1], O_RDWR);

 // set sense, data, sg_io to zero

 sg_io.interface_id    = 'S';
 sg_io.cmdp            = cdb;
 sg_io.cmd_len         = 12;
 sg_io.dxferp          = data;
 sg_io.dxfer_len       = 2;
 sg_io.dxfer_direction = SG_DXFER_FROM_DEV;
 sg_io.sbp             = sense;
 sg_io.mx_sb_len       = sizeof(sense);
 sg_io.timeout         = 5000;   // 5 seconds

 rc = ioctl (fd, SG_IO, &sg_io);

 if ((data[1] & 0x01) == 0)
  //print "button is pressed";
        else
                //print "button is NOT pressed";

 return rc;
}


Το device που θα περάσουμε ως όρισμα στη main() μας μπορεί να είναι το
sdX που θα φτιαχτεί για το δίσκο που αντιστοιχεί στο mass-storage device
(e.g., sdb) ή το αντίστοιχο sgX (scsi generic device), το ioctl θα δουλέψει
εξίσου καλά και στις 2 συσκευές.

Για να δοκιμάσουμε τη θεωρία μας, μπορούμε να μεταγλωττίσουμε ένα εκτελέσιμο
που να στέλνει ένα ioctl στη συσκευή και να παρατηρήσουμε το "διάλογο" μεταξύ
του εκτελέσιμου αυτού και της συσκευής μέσω του usbmon (π.χ., για να βεβαιωθούμε
ότι δεν έχουμε μπερδευτεί πουθενά σχετικά με το τι είναι big endian και τι little
endian).

Για ένα "πρωτόγονο" τρόπο να μετατρέψουμε μετά το εκτελέσιμο αυτό σε κάτι που κάνει
polling (αν υποθέσουμε ότι ονομάσαμε το εκτελέσιμό μας jbutton) μπορούμε να τρέξουμε
απλά

watch jbutton

Αυτό θα έχει ως συνέπεια να πρέπει να κρατήσουμε για 2s πατημένο κάθε φορά το κουμπί προκειμένου να δούμε αλλαγή, αλλά για δοκιμή αυτό δε μας πειράζει. Όταν βεβαιωθούμε ότι το πρόγραμμα δουλεύει σωστά και βλέπουμε το σωστό μήνυμα κάθε φορά ανάλογα με το αν το κουμπί είναι πατημένο ή όχι, η δουλειά του reverse engineering έχει ολοκληρωθεί και μπορούμε να προχωρήσουμε στην υλοποίηση της εφαρμογής μας για τη διαχείριση αυτού του είδους των συσκευών και την επίτευξη One Touch Backup :)

Σε αυτό το post είδαμε τα βήματα που μας έφτασαν από την πλήρη άγνοια για το πώς δουλεύει μία USB συσκευή, μέχρι του σημείου να μπορούμε να την οδηγήσουμε με λίγες γραμμές userspace κώδικα C για το Linux.

Ελπίζω να έγινε κατανοητό ότι το reverse engineering αυτού του είδους σίγουρα δεν είναι και ό,τι πιο εύκολο αλλά δεν είναι και πυρηνική επιστήμη, ειδικά για σχετικά απλές ως προς τη λειτουργία τους συσκευές.

Καλά Reversing :)

Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου