Bab 8: Aristektur AVR
AVR dipilih sebagai contoh Harvard Architecture (tepatnya modified Harvard Architecture). AVR dipakai di Arduino yang saat ini sudah sangat populer. Arduino bisa didapatkan dari berbagai situs seperti Aliexpress dan DX dengan harga murah sekali.
Ketika tulisan ini dibuat, di Aliexpress harga Arduino UNO 3,62 USD sudah termasuk kabel dan jika Anda sudah punya kabel printer, Arduino bisa dibeli 3,20 USD saja. Arduino lainnya yang cukup mudah dipakai adalah Arduino nano yang bisa didapat sekitar 2 USD saja (versi Atmega168P 1.92 USD, versi Atmega32P 2.14 USD). Versi Arduino pro mini lebih murah lagi, tapi Anda perlu membeli USB programmer dan USB to serial.
Dokumentasi
Dokumentasi utama adalah AVR Instruction set manual. Tiap device AVR mendukung instruksi yang tidak sama persis, tablenya bisa dilihat di Wikipedia.
Setiap chip memiliki fitur yang berbeda, misalnya ada yang mendukung USB, ada yang mendukung CAN bus, dsb. Tentunya Anda perlu juga datasheet device yang akan direverse. Dari datasheet ini Anda akan bisa menemukan panduan bagaimana mengakses tiap fitur.
Contoh, untuk Arduino Nano, datasheetnya adalah Atmega328
Bootloader
AVR bisa memiliki bootloader, baik yang sederhana dan opensource maupun yang kompleks dan closed source. Bootloader biasa yang terbuka akan mengijinkan pembacaan dan penulisan memori program mana saja. Bootloader bisa menerima atau menolak perintah apa saja dari host, jadi untuk device yang secure kemungkinan Anda tidak akan bisa membaca isi kode program maupun bootloadernya.
Dalam challenge RHME, mereka mengirimkan device dengan bootloader khusus yang sudah diprogram. Boot loader ini menerima HEX yang sudah dienkripsi, dan bootloader akan mendekrip dan menuliskan isinya ke Flash. Jadi semua file HEX mereka tidak akan bisa di reverse engineer di desktop karena terenrkrip. Di beberapa challenge mereka memberikan tantangan mudah supaya RE bisa dilakukan di desktop (ada versi terenkrip untuk di-write ke device dan versi tidak terenkrip untuk RE). Sebagai catatan: bootloader yang khusus ini juga akan menolak jika kita berusaha membaca data apapun (termasuk juga EPROM).
Tools
Compiler
Seperti telah dibahas sebelumnya, compiler sangat berguna karena kita bisa mencari tahu output dari sebuah kode dalam C.
Compiler yang ada:
- GCC-AVR (dipakai juga oleh Atmel Studio)
- IAR
Tools yang saya pakai adalah GCC. Kompilasi sederhana bisa dilakukan seperti ini:
avr-gcc -O3 test-avr1.c -mmcu=atmega328 -o test-avr1.elf
Tentunya silakan diganti dengan MCU yang yang Anda pakai. Jika ingin mendapatkan versi assembly dari program kecil yang Anda buat, gunakan -s
, misalnya:
avr-gcc -s test-avr1.c -mmcu=atmega328
Atau bisa juga Anda hasilkan file elf-nya, lalu dump menggunakan avr-objdump
avr-objdump -d test-avr1.elf
Untuk menghasilkan file .hex yang bisa diflash
avr-objcopy -j .text -j .data -O ihex test-avr1.elf test-avr1.hex
Untuk konversi dari hex ke bin
avr-objcopy -I ihex -O binary avr.hex avr.bin
Disassembler
Beberapa disassembler untuk AVR:
- IDA Pro, tapi harus hati-hati, ELF handling untuk AVR kadang ngawur
- Radare
- avr-objdump
Saya sendiri tidak memakai IDA pro, jika ingin memakai ini, coba compile sendiri program Anda lalu coba baca listingnya, dengan ini Anda akan bisa membandingkan dengan kode sendiri yang Anda compile.
Untuk melakukan reversing sederhana:
avr-objdump -d namafile.elf
Untuk reversing file .hex
avr-objdump -j .sec1 -d -m avr5 namafile.hex
Perhatikan bahwa alamat yang dipakai objdump adalah 2 kali dari yang dipakai oleh IDA. Alamat yang dipakai oleh objdump adalah penomoran memori secara absolut, sedangkan IDA memakai nomor instruksi. Jadi jika kita memiliki empat instruksi seperti ini (2 byte di kiri adalah opcodenya)
2f 92 push r2
3f 92 push r3
4f 92 push r4
5f 92 push r5
Maka alamat di IDA adalah:
0: 2f 92 push r2
1: 3f 92 push r3
2: 4f 92 push r4
3: 5f 92 push r5
Sedangkan di objdump:
0: 2f 92 push r2
2: 3f 92 push r3
4: 4f 92 push r4
6: 5f 92 push r5
Perlu diperhatikan bahwa ketika menggunakan instruksi IJMP/ICALL, penomoran IDA adalah yang benar. Jadi jika register Z bernilai 2, maka IJMP akan menuju ke instruksi push r4
dan bukan ke push r3
.
Debugger dan Simulator
Device AVR tertentu memiliki konektor JTAG sehingga bisa didebug menggunakan hardware debugger.
Simulavr bisa digunakan untuk mengemulasikan kode, terutama jika tidak menggunakan hardware khusus. Versi simulavr yang saya pakai adalah dari: https://github.com/Traumflug/simulavr (versi ini mendukung input/output serial port)
Tools lain
Flashing bisa dilakukan menggunakan program avrdude
.
Tools lain yang berguna adalah: avr-objcopy
untuk konversi dari dan ke ELF, BIN dan HEX.
Arsitektur
AVR menggunakan arsitektur harvard dengan memori space untuk kode dan data dipisah. Jadi alamat 0 yang menunjuk ke kode tidak sama dengan alamat 0 yang menunjuk ke data. Instruksi untuk mengakses data yang berupa kode program (LPM) dan instruksi untuk mengakses data di RAM berbeda (LD/ST).
Register
AVR memiliki 32 register, masing-masing berukuran 8 bit, dari R0 sampai R31.
Ada pasangan register khusus yang dipakai oleh beberapa instruksi tertentu
R31:R30 Z
R29:R28 Y
R27:R26 X
Karena register 16 bit hanya bisa mengakses memori 64 KB, maka ada register tambahan untuk device yang memiliki memori program lebih dari 64 KB. Register RAMPX, RAMPY, RAMPZ akan ditambahkan ke X, Y, Z supaya bisa mengakses hingga 16 MB.
Layout Memori
Tigapuluh dua register general purpose (R0 sampai R31) dalam AVR di map ke RAM dari 0x00 sampai 0x1F, artinya akses ke alamat memori itu sama dengan mengakses register. Register stack pointer bisa diakses di memori 0x3E (MSB stack pointer) dan 0x3D (LSB stack pointer). Untuk device yang RAM-nya 256 byte atau kurang hanya LSB yang dipakai. Register status/flag (REG) ada di memori 0x3F. Anda akan banyak melihat 3 angka tersebut di listing assembly AVR.
Alamat RAM 0x20 sampai 0x5F dipetakan ke register I/O, dan alamat 0x60-0xFF dipetakan ke extended I/O. Tiap I/O perlu dibaca khusus karena tidak mudah menjelaskannya. Data di RAM yang dipakai program dimulai dari alamat 0x100. Berikut ini memori map yang saya ambil dari datasheet Atmega328P.
Entry Point
Eksekusi dimulai dari alamat 0, atau lebih tepatnya lagi: ketika program direset, maka handler untuk interrupt "RESET" akan dieksekusi, dan kebetulan alamatnya adalah alamat 0. Umumnya di sini akan ditemui RJMP (relative jump) ke awal program.
Tergantung devicenya, alamat-alamat berikutnya adalah alamat interrupt handler (watchdog, timer, dsb). Jika tidak dipakai biasanya ini akan mengarah ke alamat yang akan "hang" (jump ke diri sendiri) atau "reset" (akan jump ke alamat 0).
Disassembly of section .text:
00000000 <__vectors>:
0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end>
4: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
8: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
10: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
14: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
18: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
1c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
20: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
24: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
28: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
2c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
30: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
34: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
38: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
3c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
40: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
44: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
48: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
4c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
50: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
54: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
58: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
5c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
60: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
64: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
Di dalam reset, hal yang pertama dilakukan biasanya adalah mengeset r1 menjadi 0, membersihkan flag, mensetup stack ke akhir memori.
eor r1, r1 ; clear R1
out 0x3f, r1 ; clear SREG
ldi r28, 0xFF ; 0x8FF (device ini memiliki memori 2048 byte)
; 256 byte pertama dimap ke register/IO
; 0x8ff-256 = 2047 (alamat dimulai dari 0)
ldi r29, 0x08 ;
out 0x3e, r29 ; Set stack pointer
out 0x3d, r28 ;
Kecuali diakses dengan instruksi khusus (LPM/ELPM/SPM), data di dalam flash/program memory tidak bisa diakses, jadi di awal biasanya ada instruksi untuk mengcopy data yang diiinisilisasi (dan string) ke RAM. Data yang diinisialisasi contohnya adalah variabel global dengan nilai awal di C, misalnya:
int a = 1;
char b[] = "123";
Dalam contoh berikut ini, saya membuat program dengan data "hello world"
. Data yang dicopy hanya 12 byte (0xc) yaitu string "hello world\0"
, di program alamatnya 0x200 dan ketika diload ke RAM pindah ke alamat 0x100. Jadi ketika reversing, jangan mencoba mencari alamat 0x200 untuk mencari bagian mana yang mengakses string "hello world"
, karena tidak akan ketemu (sudah pindah ke alamat 0x100).
<__do_copy_data>:
ldi r17, 0x01 ; 1 COUNTER
ldi r26, 0x00 ;
ldi r27, 0x01 ; Alamat RAM 0x100 (ke register X)
ldi r30, 0x00 ;
ldi r31, 0x02 ; Alamat ROM/Flash 0x200 (ke register Z)
rjmp .+4 ; 0x84 <__do_copy_data+0x10>
lpm r0, Z+ ; Load dari ROM (dan increment Z)
st X+, r0 ; tulis ke RAM (dan increment X)
cpi r26, 0x0C ; Compare counter dengan 0xC
cpc r27, r17
brne .-10 ; 0x80 <__do_copy_data+0xc>
Status Register (SREG)
Ini merupakan flag register, artinya register ini akan diupdate sebagai efek samping instruksi lain. Misalnya jika hasil pengurangan adalah nol, maka bit Z (zero) akan diset. Di AVR bit-bit ini juga bisa diubah langsung dengan instruksi khusus.
Bit yang ada:
- C (carry)
- Z (zero)
- N (negative)
- V (overflow)
- S (signed)
- H (half carry)
- T (transfer)
- I (interrupt)
Instruksi
Sebelum membaca ini, sebaiknya baca dulu bab 5 yang membahas assembly secara umum. Segala macam instruksi jump dan branch tidak akan saya bahas karena strukturnya sama dengan assembly pada umumnya. Instruksi aritmatika dasar juga seperti instruksi dalam kebanyakan prosessor lain.
AVR hanya memiliki 124 instruksi. Ini sangat sedikit dibanding Intel, dan meskipun angka 124 ini terlihat banyak, tapi sebenarnya banyak yang redundan jika dilihat dari sisi reverser, misalnya instruksi ADD
untuk penjumlahan 2 register, sedangkan ADI
untuk register dengan immediate (dengan angka langsung). Yang sejenis ini ada beberapa: SUB/SUBI
, AND/ANDI
, OR/ORI
). Kemudian ada lebih dari 20 instruksi branch yang intinya adalah melihat status register, plus 17 instruksi yang sekedar mengeset bit dalam SREG bisa dibaca sekilas saja.
Untuk device seperti Arduino UNO, Nano yang memiliki flash memory kurang dari 64 KB, kita bisa mengabaikan instruksi yang memakai prefix E (EICALL, EIJMP, ELPM). Biasanya dalam reversing instruksi SLEEP, BREAK dan WDR juga tidak perlu diperhatikan.
Ada berbagai cara mengelompokkan instruksi, menurut saya instruksi AVR bisa dikelompokkan seperti ini:
- instruksi aritmatika (ADC, ADD, ADIW, ASR, DEC, FMUL, FMULS, FMULSU, INC, MUL, MULS, NEG, SBC, SBCI, SBIW, SUB, SUBI)
- instruksi bit/logic (AND, ANDI, CBR, CLR, COM, EOR, LSL, LSR, OR, ORI, ROL, ROR, SBR, SER, SWAP)
- conditional logic (SBRC, SBRS)
- compare (CP, CPC, CPI, CPSE, TST)
- I/O (CBI, IN, OUT, SBI)
- conditional I/O (SBIC, SBIS)
- flag (BSET, BST, CLC, CLH, CLI, CLN, CLT, CLV, CLZ, SEC, SEH, SEI, SEN, SES, SET, SEV, SEZ)
- branching (BRBC, BRBS, BRCC, BRCS, BREQ, BRGE, BRHC, BRHS, BRID, BRIE, BRLO, BRLT, BRMI, BRNE, BRPL, BRSH, BRTC, BRTS, BRVC, BRVS)
- debugging (BREAK)
- call (CALL, EICALL, ICALL, RCALL, RET, RETI)
- jump (JMP, EIJMP, IJMP, RJMP)
- akses register (LDI, MOV, MOVW)
- akses ke memori program (ELPM, LPM, SPM)
- akses ke RAM (ST, STD, LD, LDD, LAT, LAS, STS, XCH)
- akses ke stack (PUSH, POP)
- enkripsi (DES)
- lain-lain (NOP, SLEEP, WDR)
Karena ukuran register hanya 8 bit, maka berbagai operasi aritmatika pada angka 16 bit akan terdiri dari beberapa instruksi. Instruksi DIV juga tidak ada jadi jika kode program kita mengandung operasi mod (%
) maka compiler akan menghasilkan fungsi built in __divmodhi4
dan __udivmodhi4
(dan di dalamnya ada loop).
Contoh sederhana lain adalah seperti ini:
int h_m_s_to_seconds(int h, int m, int s)
{
return h*3600 + m*60 + s;
}
Dalam assembly yang dioptimasi:
00000080 <h_m_s_to_seconds>:
80: 20 e1 ldi r18, 0x10 ; 16
82: 3e e0 ldi r19, 0x0E ; 14
84: fc 01 movw r30, r24
86: e2 9f mul r30, r18
88: c0 01 movw r24, r0
8a: e3 9f mul r30, r19
8c: 90 0d add r25, r0
8e: f2 9f mul r31, r18
90: 90 0d add r25, r0
92: 11 24 eor r1, r1
94: ec e3 ldi r30, 0x3C ; 60
96: e6 9f mul r30, r22
98: 90 01 movw r18, r0
9a: e7 9f mul r30, r23
9c: 30 0d add r19, r0
9e: 11 24 eor r1, r1
a0: 82 0f add r24, r18
a2: 93 1f adc r25, r19
a4: 84 0f add r24, r20
a6: 95 1f adc r25, r21
a8: 08 95 ret
Perhatikan konstanta 3600 (0xe10) diload menggunakan dua instruksi (ldi r18, 0x10
dan ldi r19, 0x0e
). Secara umum ini agak mempersulit RE karena kita jika kita cari 0xe10 tidak akan ketemu. Ini berlaku juga untuk konstanta seperti lokasi string, sehingga biasanya disassembler tidak akan otomatis menemukan reference ke string.
Calling convention
GCC untuk AVR memiliki calling convention seperti ini:
R0 scratch register (boleh dipakai sembarang)
R1 selalu 0
Ketika memanggil fungsi, R18-R27, R30, R31 bisa diubah oleh fungsi yang kita panggil, jadi pemanggil harus kita simpan jika ingin nilainya tidak berubah. Register R2–R17, R28, R29 harus disimpan oleh fungsi, jadi pemanggil bisa yakin bahwa nilainya tidak akan berubah.
Parameter selalu dipass semuanya di register, atau semuanya di memori. Calling conventionnya agak rumit:
- Register R8 sampai R25 dipakai untuk parameter dan return value
- Ukuran parameter dibulatkan ke bilangan genap berikut (jadi untuk 1 char akan dianggap memakai 2 register)
- Jika ada return value, alokasi registernya dianggap seperti parameter pertama
- Alokasi register mulai dari R26 minus ukuran register
Contoh:
int func(char a, long b)
Nilai kembalian adalah int
(2 byte): 26 - 2 = 24, jadi ini akan disimpan R24 dan R25
Ukuran a
1 byte dibulatkan jadi 2, jadi ini akan ditempatkan di R24, dan R25 akan dibiarkan tidak terpakai.
Ukuran b
adalah 4 byte, jadi akan memakai R23, R22, R21, R20
Serial I/O
Tidak seperti komputer yang outputnya ke layar monitor, I/O sebuah microcontroller bisa ke mana saja, misalnya ke LED, speaker, LCD (dengan berbagai interface yang mungkin), serial port. Dalam berbagai challenge biasanya serial port digunakan. Untuk berbagai jenis I/O hal pertama yang bisa Anda coba adalah: mencari contohnya dalam C, lalu compile dengan GCC, lalu lakukan disassembly.
Contohnya, Anda bisa melihat posting blog ini mengenai bagaimana melakukan input/output serial port. Bisa dilihat bahwa yang penting adalah: ketika input kita membaca dari UDR0, dan output dengan menulis ke UDR0. Ini kemudian bisa dicocokkan dengan MCU yang kita pakai, contohnya di Atmega 328 ini ada di alamat 0xC6 (silakan search sendiri string "UDR0" di dalam datasheet Atmega328).
Dengan mencari instruksi yang mengakses 0xC6 kita akan menemukan subrutin yang menangani input serial
ldi r24, 0xC6 ; R25:R24 -> 00C6
ldi r25, 0x00 ;
movw r30, r24 ; copy R25:R24 -> R30:R29
ld r24, Z ; baca dari serial ke r24
Dan contoh output serial
ldi r24, 0xC6 ; R25:R24 -> 00C6
ldi r25, 0x00 ;
movw r30, r24 ; -> copy R25:R24 -> R30:R29
st Z, r18 ; outputkan isi r18 ke serial
Biasanya fungsi yang memanggil serial output per karakter adalah fungsi puts
atau printf
, dan fungsi yang memanggil serial input per karakter adalah gets
atau semacamnya. Dari situ Anda bisa menelusuri balik untuk menemukan string apa yang dicetak.
Membaca ROM dengan printf
Ketika melakukan eksploitasi printf
, ada satu format string yang berguna yaitu %S
(S besar, bukan kecil) yang akan membaca dari ROM. Dengan ini kita bisa melakukan ROM dumping untuk mempelajari kode program jika tidak punya binarynya. Dokumentasi lengkap format string yang didukung AVR LIBC bisa dibaca di sini.
S Similar to the s format, except the pointer is expected to point to a program-memory (ROM) string instead of a RAM string.
Copyright © 2009-2018 Yohanes Nugroho