Hackowanie ZyWALL'a 2

12.09.2013 Artykuły zyxelsiećzywallbootloaderlinkergccc

Pewnego pięknego (chyba) popołudnia wpadł mi w łapki niejaki ZyXEL ZyWALL 2… Słowem - krótki opis jak zamienić OS na kod do migania LED-em… :)

Mały disclaimer: artykuł piszę z perspektywy czasu (2014-11-25), ale ze wsteczną datą zgodną z datą opisanych działań, może więc on zawierać delikatne nieścisłości, mimo tego że podczas pisania staram się testować stary kod. Dodatkowo podczas pisania niniejszego tekstu wykonuję kolejne eksperymenty które czekały w kolejce na zrealizowanie.

Zgodnie z tym co opisałem we wstępie, pewnego pięknego popołudnia dostałem możliwość pobawienia się wspomnianym urządzeniem. W podstawowej konfiguracji pracuje on jako router i, nawiasem mówiąc, całkiem fajnie sprawdza się w tej roli. Mając do dyspozycji kilka godzin postanowiłem, że nie ograniczę się do testu wydajności czy możliwości konfiguracji, ale także, zainspirowany Sekurak Hacking Party spróbuję sprawdzić bezpieczeństwo panelu konfiguracyjnego urządzenia.

Po krótkiej analizie znalazłem informacje, mówiące o tym, że wspomniane urządzenie nie pracuje pod, jak się spodziewałem, kontrolą Linuksa, a pod kontrolą ZyNOS'a. Zmartwiło mnie to o tyle, że nie posiadam doświadczenia w szukaniu bugów w oprogramowaniu tego typu, jednak nie zniechęciło mnie to do pracy. Postanowiłem więc poszukać w Internecie informacji czy istnieje może port Linuksa na ten konkretny procesor (w moim egzemplarzu był to Samsung S3C2500B01), niestety okazało się że nie ma niczego „gotowego” - trzeba by poświęcić dość dużo czasu na napisanie brakujących fragmentów, a mijało się to z celem o tyle że urządzenie to, już w czasie przeprowadzania tego eksperymentu, nie grzeszyło wydajnością, więc prawdopodobnie byłbym jednym z bardzo niewielu użytkowników takiego portu Linuksa.

Mimo to postanowiłem zaglądnąć do bootloadera urządzenia i zobaczyć czy korzysta ze „standardowych” rozwiązań typu U-Boot. Tutaj również spotkała mnie niespodzianka w postaci nieznanego mi bootloadera Bootbase. Mimo to gdy zobaczyłem

Press any key to enter debug mode within 3 seconds.

… na porcie szeregowym, radośnie nadusiłem „Enter” na klawiaturze.

Pierwsze 3 próby nie napawały optymizmem…

ZyWALL 2> help
ERROR

ZyWALL 2> ?
ERROR

ZyWALL 2> h
ERROR

jednak dość szybko okazało się że urządzenie reaguje na komendy podobne do, znanych wszystkim, komend AT. Znalazł się nawet mały help z listą obsługiwanych poleceń.

ZyWALL 2> AT
OK

ZyWALL 2> ATHE
======= Debug Command Listing =======
AT            just answer OK
ATHE          print help
ATBAx         change baudrate. 1:38.4k, 2:19.2k, 3:9.6k 4:57.6k 5:115.2k
ATENx,(y)     set BootExtension Debug Flag (y=password)
ATSE          show the seed of password generator
ATTI(h,m,s)   change system time to hour:min:sec or show current time
ATDA(y,m,d)   change system date to year/month/day or show current date
ATDS          dump RAS stack
ATDT          dump Boot Module Common Area
ATDUx,y       dump memory contents from address x for length y
ATRBx         display the  8-bit value of address x
ATRWx         display the 16-bit value of address x
ATRLx         display the 32-bit value of address x
ATGO(x)       run program at addr x or boot router
ATGR          boot router
ATGT          run Hardware Test Program
ATRTw,x,y(,z) RAM test level w, from address x to y (z iterations)
ATSH          dump manufacturer related data in ROM
ATTD          download router configuration to PC via XMODEM
ATUR          upload router firmware to flash ROM
ATLC          upload router configuration file to flash ROM
ATXSx         xmodem select: x=0: CRC mode(default); x=1: checksum mode
ATSR          system reboot
ATDC          Disable check model mechanism
ATMU          print Multiboot client version

OK

ZyWALL 2>

Okej, lista jest całkiem ładna, widzimy tutaj różne niskopoziomowe rzeczy jak odczyt konkretnych rejestrów czy nawet skoki w konkretne miejsca w pamięci(!), ale prawdziwie intrygującymi opcjami są ATSE i ATEN. Z racji tego że wciąż jestem „za cienki w uszach” na reversowanie firmware tego typu urządzeń, poszperałem chwilę po Sieci i trafiłem na krótki tutorial jak wygenerować właściwy token. (Notatka po fakcie: pod tym adresem można znaleźć instrukcję jak odblokować zaawansowany tryb bez zabawy w hasła jednorazowe.) Kolejną chwilę spędziłem na implementacji powyższego w Pythonie, (Której tutaj nie zamieszczę ze względu na to że polecenia które odblokowuje mogą spowodować trwałe uszkodzenie urządzenia. Żartowałem. Nie umieszczę jej tutaj bo jest brzydka.) i moim oczom ukazało się „delikatnie” bardziej rozbudowane „menu” dostępnych poleceń (wypisałem tylko te nowe):

ATWBx,y       write address x with  8-bit value y
ATWWx,y       write address x with 16-bit value y
ATWLx,y       write address x with 32-bit value y
AT%Tx         Enable Hardware Test Program at boot up
ATBTx         block0 write enable (1=enable, other=disable)
ATWEa(,b,c,d) write MAC addr, Country code, EngDbgFlag, FeatureBit to flash ROM
ATWZa(,b,c,d) write 12 digitals MAC addr, Country code, EngDbgFlag, FeatureBit to flash ROM
ATCUx         write Country code to flash ROM
ATCB          copy from FLASH ROM to working buffer
ATCL          clear working buffer
ATSB          save working buffer to FLASH ROM
ATBU          dump manufacturer related data in working buffer
ATWMx         set MAC address in working buffer
ATCOx         set country code in working buffer
ATFLx         set EngDebugFlag in working buffer
ATSTx         set ROMRAS address in working buffer
ATSYx         set system type in working buffer
ATVDx         set vendor name in working buffer
ATPNx         set product name in working buffer
ATFEx,y,...   set feature bits in working buffer
ATMP          check & dump memMapTab
ATDOx,y       download from address x for length y to PC via XMODEM
ATUPx,y       upload to RAM address x for length y from PC via XMODEM
ATUXx(,y)     xmodem upload from flash block x to y
ATERx,y       erase flash rom from block x to y
ATWFx,y,z     copy data from addr x to flash addr y, length z
ATLOa,b,c,d   Int/Trap Log Cmd
ATBR          Reset to default Romfile

Z racji tego że nie interesowały mnie trwałe zmiany w urządzeniu zainteresowałem się opcją ATUP. No ale co można wgrać do ramu takiemu urządzeniu? Oczywiście „hello world” migający LED-ami :)

Ale od czego zacząć? Na pocztęk warto rozglądnąć się czy w ogóle wiemy gdzie możemy bezpiecznie wrzucić swój kod. Użyłem więc polecenia ATMP z „zaawansowanej” części aby dowiedzieć się jak producent zorganizował sobie przestrzeń adresową na tym urządzeniu. Na listingu poniżej zamieszczę tylko najciekawszą część mapy.

$RAM Section:
  0: BootExt(RAMBOOT), start=0000a000, len=18000
  1: BootData(RAM), start=00022000, len=8000
  2: HTPCode(RAMCODE), start=0002a000, len=36000
  3: HTPData(RAM), start=00060000, len=50000
  4: RasCode(RAMCODE), start=0002a000, len=546000
  5: RasData(RAM), start=00570000, len=A90000
$ROM Section:
  6: BootBas(ROMIMG), start=80000000, len=6000
  7: DbgArea(ROMIMG), start=80006000, len=2000
  8: RomDir2(ROMDIR), start=80008000, len=8000
  9: BootExt(ROMIMG), start=80010030, len=173D0
 10: HTPCode(ROMBIN), start=80027400, len=7C00

Sekcję Boot odrzuciłem na samym starcie - jest tam kod bootloadera potrzebny chociażby do wgrania mojej binarki do pamięci, za to postanowiłem zarykować i użyć następnej sekcji (HTPCode).

Przygotowanie właściwej binarki

Przygotowanie właściwego pliku do umieszczenia na urządzeniu nie jest jednak rzeczą trywialną. Zacznijmy od tego, że „normalne” binarki do użycia pod systemem operacyjnym zwykle mają dość zaawansowany format wewnętrzny którego „rozpakowaniem” zajmuje się system operacyjny. W naszym przypadku do urządzenia musimy dostarczyć „goły kod”. Co więcej - musimy również powiadomić kompilator pod jakim adresem znajdzie się kod, aby mógł wygenerować odpowiednie instrukcje dla tego przypadku. Zwykle system operacyjny ma do dyspozycji układ zwany MMU, który zajmuje się eleganckim mapowaniem pamięci i programiści piszący kod dla „userlandu” mają dość dużą swobodę w zarządzaniu pamięcią. Tutaj, niestety, musimy dopasować się do zastanego środowiska.

Co więcej - nie jest to również typowa platforma, do której przygotowana jest biblioteka standardowa, czy chociaż plik z adresami poszczególnych rejestrów. Wszystkie informacje będzie trzeba czytać prosto z noty katalogowej i na bieżąco tworzyć odpowiednie biblioteki.

Licząc że producent nie był szczególnie przykładny jeśli chodzi o uprawnienia do poszczególnych części pamięci, postanowiłem wrzucić wszystkie segmenty w jedno miejsce i napisałem następujący skrypt linkera:

ENTRY(__start)
phys = 0x0002a000;
SECTIONS
{
  .text phys : AT(phys) {
    code = .;
    *(.start)
    *(.text)
    *(.rodata)
  }
  .data : AT(phys + (data - code))
  {
    data = .;
    *(.data)
  }
  .bss : AT(phys + (bss - code))
  {
    bss = .;
    *(.bss)
  }
  end = .;
}

Jak widać, skrypt jest maksymalnie uproszczony, wykorzystuję wspomnianą wcześniej sekcję zaczynającą się pod adresem 0x00022000.

Dodatkowo lekko zmodyfikowałem moje ulubione Makefile i wyprodukowałem co następuje:

OBJS=hello.o
TARGET=hello

CPU=arm940t
LD_SCRIPT=script.ld
OPTIMIZATION=0

BASE_ADDR=2a000

CFLAGS=-marm -mcpu=$(CPU) -O$(OPTIMIZATION) -Wl,--no-gc-sections -std=gnu99 -nostdlib -nodefaultlibs -nostartfiles -mbig-endian
LDFLAGS=-O$(OPTIMIZATION) -nostdlib -nodefaultlibs -nostartfiles -mbig-endian
DUMPFLAGS=-marm --adjust-vma=0x$(BASE_ADDR) --endian=big

PREFIX=arm-none-eabi

CC=$(PREFIX)-gcc
LD=$(PREFIX)-gcc
OBJCOPY=$(PREFIX)-objcopy
OBJDUMP=$(PREFIX)-objdump

%.o: %.c
        $(CC) $(CFLAGS) -c -o $@ $<

$(TARGET).elf: $(OBJS)
        $(LD) -T$(LD_SCRIPT) $(LDFLAGS) -o $(TARGET).elf $(OBJS)

%.bin: %.elf
        $(OBJCOPY) -O binary $< $@

clean:
        rm -f *.o $(TARGET).elf $(TARGET).bin

debug:
        $(OBJDUMP) -d $(TARGET).elf
        @echo -e "\n----- Bin dump ------------------------------------------\n"
        $(OBJDUMP) -D -b binary $(DUMPFLAGS) $(TARGET).bin

upload:
        @printf "ATUP$(BASE_ADDR),%X\n" `wc -c < hello.bin`
        @echo "ATGO$(BASE_ADDR)"

all: hello.bin upload
aall: clean all

Faktyczne użycie powyższego skryptu wyjaśnię niżej, już przy kompilacji gotowego kodu.

Wygląda że mamy już większość rzeczy potrzebnych do kompilacji. Przejdźmy więc do napisania prostego kodu w C który zamiga naszym LED-em.

Właściwy kod w C

Zgodnie z tym co napisałem - nie dysponuję żadną biblioteką standardową, ani nagłówkami do powyższego urządzenia, więc swój kod musiałem zacząć od zdefiniowania adresów rejestrów które będę wykorzystywał. Na szczęście do migania LED-em potrzebny okazał się być tylko jeden rejestr, ponieważ port był już odpowiednio skonfigurowany i wykorzystywany przez bootloader.

Pytanie tylko jak znaleźć pod który pin GPIO podłączona jest dioda a nie spędzić masy czasu na traceowaniu ścieżek na płytce (o ile w ogóle byłoby to możliwe - płytka prawdopodobnie jest więcej niż dwuswarstwowa, więc nieinwazyjne zbadanie układu ścieżek jest nietrywialne)? Najpierw sprawdziłem konfigurację pinów jako wejścia/wyjścia:

ZyWALL 2> ATRLF0030024
F0030024: 00000000

OK

ZyWALL 2> ATRLF0030028
F0030028: 00000000

OK

ATRL odczytuje 32bitowe słowo spod danego adresu, zaś użyte adresy to odpowiednio IOPDRV1 i IOPDRV2 odpowiadające za obydwa porty GPIO procesora.

Niestety ten test nie przyniósł oczekiwanej informacji - wszystkie porty zostały skonfigurowane jako wyjścia. Ale przecież… bootloader miga jednym z LED-ów!

ZyWALL 2> ATRLF003001C
F003001C: 67FFFFAD

OK

ZyWALL 2> ATRLF003001C
F003001C: 67FFFFAF

OK

Bingo! Odczyt zawartości IOPDATA1 odpowiadającego za stan GPIO zmienia się w czasie. Zgadzałoby się to z tym co mogę zaobserwować po zachowaniu LED-a. Test dla drugiego portu nie przyniósł żadnych zmian, więc użyję tych wartości w swoim kodzie.

#include <stdint.h>

#define _BV(a) ( 1<< (a) )

#define IOPDATA1 ((volatile uint32_t*)0xF003001C)

void __start(void) __attribute__ ((section(".start")));
void blink_led(void);

void blink_led(void) {
    *IOPDATA1 = 0x67FFFFAF;
    for(volatile unsigned int i=0; i<0xFFFFF; i++){}
    *IOPDATA1 = 0x67FFFFAD;
    for(volatile unsigned int i=0; i<0xFFFFF; i++){}
}

void __start(void) {
    for(;;){
        blink_led();
    }
}

W ramach szybkiego tłumaczenia kodu, wspomnę tylko że dwie puste pętle for służą za opóźnienia, nie wykorzystuję „klasycznej” funkcji main z racji tego, że nie mam żadnego kodu inicjalizującego pozostałe sekcje kodu i mogę przystąpić od razu do wykonywania właściwych instrukcji.

Jeszcze krótka (obiecana) notatka na temat kompilacji:

arm-none-eabi-gcc -marm -mcpu=arm940t -O0 -Wl,--no-gc-sections -std=gnu99 -nostdlib -nodefaultlibs -nostartfiles -mbig-endian -c -o hello.o hello.c
arm-none-eabi-gcc -Tscript.ld -O0 -nostdlib -nodefaultlibs -nostartfiles -mbig-endian -o hello.elf hello.o
arm-none-eabi-objcopy -O binary hello.elf hello.bin

Plik hello.c jest kompilowany do „standardowego” pliku .o, z tym zastrzeżeniem, że nie ma używać żadnych bibliotek standardowych, plików startowych i ograniczyć się do skompilowania kodu dla konkretnego procesora, pracującego w konkretnym trybie. Dla ułatwienia korzystam ze standardu gnu99, a dla pewności wyłączam wszelką optymalizację.

Następnie linker, z użyciem mojego, wcześniej opisanego, skryptu linkera, oraz podobnymi zastrzeżeniami do nielinkowania z bibliotekami, linkuje kod do formatu „standardowego” pliku elf.

Na samym końcu z „gotowego” pliku elf „wyciągam” sam kod w postaci binarnej. To właśnie on będzie wgrany na urządzenie.

Pierwsze próby na żywym urządzeniu

Upload…

ZyWALL 2> ATUP2a000,A0
Starting XMODEM upload (CRC mode)....
CCSending hello.bin, 1 blocks: Give your local XMODEM receive command now.
Xmodem sectors/kbytes sent:   0/ 0kRetry 0: NAK on sector
Retry 0: NAK on sector
Retry 0: NAK on sector
Retry 0: NAK on sector
Bytes Sent:    256   BPS:614

Transfer complete

Total 160 bytes received

OK

ZyWALL 2> ATGO2a000

I skok do naszego kodu… Zakończony w sumie… powodzeniem bo zakłóceniem pracy PWR LED.

Dlaczego tylko zakłóceniem? Nasz kod nie nadpisał ani tablicy przerwań ani konfiguracji urządzeń - stąd kod bootloadera dalej jest wykonywany w tle - w szczególności działają timery odpowiedzialne m.in. za miganie tym samym LED-em co my.

Pozbądźmy się więc wszelakich przeszkadzaczy… maskując wszystkie przerwania. Nie jest to może szczególnie „ładny” sposób, ale powinien być wystarczający do testów.

#include <stdint.h>

#define _BV(a) ( 1<< (a) )

#define IOPDATA1 ((volatile uint32_t*)0xF003001C)

#define INTMASK ((volatile uint32_t*)0xF0140008)

void __start(void) __attribute__ ((section(".start")));
void blink_led(void);

void blink_led(void) {
    *IOPDATA1 = 0x67FFFFAF;
    for(volatile unsigned int i=0; i<0x1FFFFF; i++){}
    *IOPDATA1 = 0x67FFFFAD;
    for(volatile unsigned int i=0; i<0x1FFFFF; i++){}
}

void __start(void) {
    *INTMASK = 0xFFFFFFFF; // Mask all interrupts

    for(;;){
        blink_led();
    }
}

I wynik:

Pełen sukces! :) (Dociekliwi mogą zmodyfikować długość opóźnienia i zweryfikować że to rzeczywiście mój kod, a nie bootloader miga diodą.)

Zakończenie

Mimo, że końcowy efekt nie jest jakiś szalenie porywający to ilość wiedzy jaką przyswoiłem podczas tego eksperymentu jest istotnie satysfakcjonująca. Co więcej - mam już pomysły na kontynuację tego eksperymentu - poprawna obsługa pozostałych sekcji kodu oraz obsłużenie USART'u z mojego kodu, najlepiej z użyciem przerwań żeby pobawić się w „manualną” modyfikację tablicy przerwań.

Mam nadzieję że artykuł Cię nie zanudził, za to sprowokował do samodzielnych eksperymentów. Dziękuję za uwagę i… do usłyszenia w następnym odcinku przygód z ZyWALL'em ;)


Przydatne linki


Z racji dużego nagromadzenia w tym artykule nazw zastrzeżonych dla innych firm muszę oznajmić, że nazwy takie jak ZyXEL, ZyWALL, ZyNOS, Samsung, ARM, Bootbase nie należą do mnie i ich użycie ma na celu jedynie lepsze zidentyfikowanie środowiska na którym pracowałem.

Artykuł został umieszczony jedynie w celach edukacyjnych. Zabraniam kopiowania całości bądź fragmentów bez mojej wyraźnej zgody.