Mengenal Bahasa Pemrograman Lua

Di kesempatan kali ini saya akan memperkenalkan bahasa Lua. Bahasa ini sebenarnya sudah ada cukup lama (sejak 1993), tapi kurang terkenal secara umum. Lua lebih sering dijadikan bahasa scripting untuk aplikasi dan sering kali nama Lua tidak disebut sama sekali. Lua artinya “Bulan” (moon) dalam bahasa portugis. Lua diciptakan oleh Roberto Ierusalimschy, Luiz Henrique de Figueiredo, dan Waldemar Celes, anggota dari Computer Graphics Technology Group (Tecgraf) di Pontifical Catholic University of Rio de Janeiro, di Brazil.

Lua bisa dipakai untuk membuat program/aplikasi seperti bahasa lain, tapi kelebihan bahasa ini  adalah dari awal dirancang untuk diembed dalam sebuah aplikasi lain (terutama dalam bahasa sejenis C). Di-embed ini artinya bisa  bisa dipakai untuk sistem plugin atau bisa dipakai untuk mengontrol bagian business/game/app logic sementara bagian yang butuh performance tinggi memakai C atau bahasa lain. Lua juga bisa jadi file konfigurasi yang sangat fleksibel.

Lua dipakai di banyak software misalnya Adobe Lightroom, Nmap, Wireshark, dan lighttpd. Seperti saya sebutkan di awal, sering kali berbagai aplikasi menyebutkan bahasa Lua ini hanya sekilas, dan fitur aplikasinya yang lebih ditekankan dengan nama lain, misalnya dokumentasi Nmap menyebutkan tentang Nmap Scripting Engine (NSE) tapi Lua hanya disebut sekilas di tengah. Lua juga banyak dipakai di berbagai game (misalnya World Of Warcraft, Angry Birds) dan game engine. Interpreter Lua ukurannya sangat kecil sehingga bisa dipakai untuk embedded system/IOT (misalnya NodeMCU dan eLua).

Bahasa Lua

Bahasa Lua menurut saya relatif sederhana, tapi juga sangat fleksibel. Sintaksnya tidak aneh, sedikit mirip BASIC atau pascal. Contohnya seperti ini:

print "hello, berapa umur kamu?"
umur = io.read("*n")
if umur >= 17 then
   print("Halo kakak")
else
   print("Halo adik")
end

Bagi sebagian orang mungkin ini agak aneh mengingat bahasa sekarang sudah tidak lagi memakai kata-kata (“then”, “end”) tapi lebih memakai simbol ({} di C/C++/Java/.NET, indentasi di Python), tapi untuk yang dulu memakai Pascal dan BASIC ini terasa cukup wajar. Sebagian besar syntax Lua mirip dengan bahasa lain, tapi ada beberapa hal yang agak aneh. Di Lua, operator kurang dari, lebih besar, sama dengan, semuanya sama dengan bahasa lain (<, >, =) tapi untuk tidak sama dengan, syntax yang dipakai adalah ~=. Syntax untuk komentar juga agak kurang standar, memakai “–” untuk satu baris, dan memakai –[[ untuk komentar multi baris –]].

Secara umum bahasa Lua cukup mudah dibaca bahkan oleh pemula, tidak seperti bahasa Forth (yang saya bahas sebelumnya) atau bahasa fungsional seperti Scala.

Tipe primitif yang didukung Lua adalah: string, number, dan integer. Struktur data utama di Lua adalah table. Secara umum: tabel memetakan sesuatu ke sesuatu yang lain (seperti Map/Dictionary di bahasa lain). Hampir semua hal di Lua diimplementasikan dengan table. Sebuah array di Lua juga adalah tabel yang memetakan indeks dengan sebuah nilai, dengan nilai index awal default adalah 1. Masalah index ini agak mengganggu bagi saya, karena di bahasa lain biasanya array berbasis 0. Sebuah tabel bisa berisi tabel lain untuk merepresentasikan array multi dimensi.

Sebuah struktur (struct) juga bisa direpresentasikan dalam bentuk tabel (memetakan nama field sebagai string ke nilainya). Di Lua, sebuah fungsi juga bisa masuk ke dalam tabel. Contohnya kita bisa saja membuat tabel yang berisi berbagai operasi matriks yang berisi pemetaan nama operasi, misalnya “multiply” dengan implementasi fungsi perkalian matriks.

Lua juga memiliki konsep metatable, yang bisa digunakan untuk mendefinisikan operasi terhadap sebuah variabel. Dengan setmetatable(variabel, sebuahtabel), kita bisa mendefinisikan berbagai operator dan fungsi yang berlaku untuk variabel tersebut. Jadi jika kita ingin membuat sebuah variabel menjadi sebuah tipe matriks, kita bisa membuatnya dengan setmetatable(variabel1, tabel_operasi_matriks).

Lua tidak memiliki konsep OOP tapi dengan menggunakan berbagai trik table, metatable dan fleksibilitas function di Lua, kita bisa mengimplementasikan Object Oriented Programming. Saya sendiri kurang menyukai ini, kalau saya ingin memprogram dengan paradigma OO, saya akan memilih bahasa lain.

Fitur Lua lain yang menarik adalah coroutine, generalisasi dari subrutin, tapi fitur ini jarang saya pakai. Sebenarnya fitur ini powerful, tapi ketika melakukan embeding Lua ke bahasa lain (terutama C), fitur ini cukup merepotkan karena tidak mudah dipetakan ke C.

Embeding Lua ke bahasa C

Seperti sudah saya sebutkan di atas, fitur Lua adalah sangat cocok untuk diembed ke sebuah aplikasi. Sebenarnya bahasa lain seperti Python dan Ruby juga bisa diembed ke bahasa lain (misalnya C), tapi ada banyak kelebihan Lua:

  • Ukurannya sangat kecil, ratusan kilobyte. Python butuh minimal beberapa megabyte (disk space dan memori)
  • Mendukung multi thread (satu thread menjalankan instance Lua terpisah). Untuk embedding multi thread di Python sangat sulit, dan Python memiliki Global Interpreter Lock
  • Interfacing sangat mudah, baik coding manual atau dengan bantuan SWIG

Implementasi utama Lua adalah dalam bahasa C, walau ada juga Lua untuk Java. Kebanyakan bahasa memiliki build system yang kompleks dan butuh banyak library tambahan. Dalam kasus Lua: kita cuma perlu copy paste semua file .c (kecuali lua.c) ke project kita dan itu sudah cukup. Sebagai catatan: selain implementasi utama Lua, ada juga LuaJIT yang sangat cepat namun memiliki banyak batasan terutama di sistem 64 bit.

Setelah memasukkan semua file C dalam project kita, di bagian program yang butuh Lua, kita perlu menginclude beberapa file:

               
#include "lua.h"       
#include "lualib.h"       
#include "lauxlib.h"

Lalu kita bisa menginstansiasi interpreter Lua. Jika diperlukan, kita bisa membuat banyak instance (misalnya satu di setiap thread). Setelah itu biasanya kita ingin agar library standar Lua diaktifkan, jadi kita memakai luaL_openlibs.

lua_State *L = luaL_newstate(); 
luaL_openlibs(L);

Biasanya skrip Lua akan diletakkan di luar program, supaya bisa diubah dengan mudah tanpa rekompilasi. Untuk meload dari file, kita bisa memakai luaL_loadfile atau luaL_loadstring jika ingin memakai string. Di banyak Game, file lua dienkrip di file (agar tidak mudah dimodifikasi), lalu didekrip dimemor, dan diload menggunakan luaL_loadstring.

 
int err = luaL_loadfile(L, "myscript.lua");
if (err) {
       fprintf(stderr, "Failed loading file: %s\n", lua_tostring(L, -1));
       exit(1);
}

Dalam passing parameter ke Lua dan dari Lua, kita menggunakan konsep stack. Kita bisa memasukkan nilai ke stack dengan lua_pushstring/lua_pushnumber/lua_pushinteger, dan bisa mengambil nilai di stack dengan lua_tointeger, lua_tostring, lua_tonumber dan beberapa fungsi lain. Jadi ketika memanggil fungsi di Lua: kita melakukan push value ke stack, dan untuk mengambil nilai dari lua kita akses hasilnya di stack juga.

Kita bisa mencari fungsi yang diimplementasikan di Lua dengan lua_getglobal(L, "namafungsi"), ini akan mempush reference fungsi ke stack. Setelah itu kita bisa mempush nilai parameter untuk fungsi, memanggil fungsinya, dan mendapatkan hasilnya di stack.

 
lua_getglobal(L, "myfunction"); 
lua_pushinteger(L, 1); //param 1
lua_pushinteger(L, 2);  //param 2
int err = lua_pcall(L, 
    2 /*number of arguments*/, 
    1 /*number of result*/, 
    0 /*indeks ke error handler, jika 0 maka error akan dipush ke stack*/);

Membuat fungsi di C yang bisa dipanggil oleh Lua juga sangat mudah. Misalnya kita ingin membuat fungsi max yang menerima 2 double dari Lua dan mengembalikan angka terbesar, maka kita cukup membuat ini:

 
int max_for_lua(lua_State *L) 
{
  double num1 = lua_tointeger(L, 1); //param 1 dari Lua
  double num2 = lua_tointeger(L, 2); //param 2 dari Lua

  double max = num1>num2?num1:num2; //logika utama

  lua_pushnumber(L, max); //push ke stack Lua
  return 1; //hanya ada satu nilai kembalian
}

Tentunya keberadaan fungsi ini perlu diberitahukan ke Lua agar bisa diakses oleh skrip Lua. Nama fungsi di C dan Lua bisa berbeda, contohnya fungsi max_for_lua di atas bisa didaftarkan menjadi “my_max” seperti ini:

 
lua_pushcfunction(lua_state, max_for_lua);
lua_setglobal(lua_state, "my_max");

Penutup

Sering kali saya menemukan di mana saya butuh eksperimen cepat dan tidak ingin sering mengcompile ulang kode program saya. Contoh penggunaan yang baru-baru ini saya lakukan adalah ketika eksplorasi fitur tertentu di iOS. Jika saya mengandalkan objective C atau Swift saja, tiap kali saya mengubah kode (biasanya hanya logic tertentu), akan butuh beberapa detik sampai beberapa puluh detik sampai bisa menjalankan ulang aplikasinya. Walau hanya menekan tombol “run” di XCode yang terjadi di latar belakang adalah: kodenya harus dicompile, dilink, ditandangani secara digital, dicopy ke device, lalu dijalankan. Dengan memindahkan logic ke Lua, saya hanya perlu me-mount direktori dokumen aplikasi dengan ifuse, mengedit file Lua, lalu merestart aplikasinya.

Menambahkan sebuah scripting ke language ke aplikasi memang butuh waktu tapi di sisi lain juga sangat fleksibel karena skrip bisa dimodifikasi dengan cepat dan tidak seperti kode C, tidak perlu dikompilasi ulang. Jika Anda sudah beberapa kali menambahkan fitur scripting ke sebuah aplikasi, menambahkan ke aplikasi berikutnya tidak butuh waktu lama. Untuk aplikasi kompleks kita bisa memakai Simplified Wrapper and Interface Generator (SWIG) untuk mempercepat membuat interface dari C/C++ ke Lua.

Semoga tulisan singkat ini cukup untuk memperkenalkan bahasa Lua dan mungkin juga membuat Anda tertarik untuk menambahkan Lua ke aplikasi yang Anda buat.

Dari keyboard ditekan sampai muncul karakter di layar (bagian 3)

Di bagian sebelumnya sudah dibahas mengenai bagaimana layout text harus dilakukan. Setelah semuanya selesai, maka teks bisa ditampilkan ke layar. Di level aplikasi biasanya kita hanya perlu memanggil semua fungsi dasar untuk menampilkan teks. Di sisi library dan sistem operasi, masih ada langkah ekstra yang harus dilakukan. Teks perlu digambar ke sebuah buffer dan buffer perlu ditampilkan di sebuah permukaan gambar. Driver grafik akan menampilkan buffer tersebut ke layar.

Untuk aplikasi desktop, proses menampilkan teks ini di sisi programmer semudah memanggil fungsi print (mode teks) atau mengeset property tertenty. Jika kita memprogram microcontroller atau embedded system, akan lebih terasa proses berat yang harus dilakukan ketika menampilkan sesuatu. Contoh paling sederhana adalah 7 segment display.

Programmer perlu menyala matikan tiap garis dalam 7 segment display untuk menampilkan angka tertentu.

Display saat ini biasanya menggunakan akses per piksel. Mungkin yang paling sederhana adalah 8×8 dot matrix. Di sini kita perlu mengontrol tiap titik untuk menampilkan huruf/angka/bentuk yang kita mau.

Semakin canggih hardware, biasanya pekerjaan programmer high level akan semakin mudah. Contohnya adalah display LCD jenis seperti ini:

Jpeg

Demikian seri singkat kali ini. Saya akan berusaha membuat seri baru yang membahas dari segi yang lebih low level lagi.

Dari keyboard ditekan sampai muncul karakter di layar (bagian 2)

Meneruskan dari tulisan sebelumnya, sekarang kita ke bagian aplikasi. Pertama aplikasi perlu menerima “event” dari Desktop/Window environment. Event ini bermacam-macam, misalnya permintaan untuk meminimize Window, event gerakan mouse, dan event tombol keyboard ditekan.

Setelah tombol ditekan, maka aplikasi akan melakukan update pada struktur datanya. Sebagai pengingat, contoh aplikasi yang saya berikan ada Word Processor. Ini bergantung dari mode saat ini (apakah mode insert atau overwrite, apakah dokumen sifatnya read only dsb). Struktur data yang digunakan pun bisa sangat beragam, tergantung dari fitur yang dimiliki aplikasi.

Dalam sebuah word processor, berbagai algoritma perlu dijalankan sebelum tampilan diupdate di layar. Beberapa algoritmanya misalnya: hyphenation (pemotongan kata), pemeriksa ejaan (dalam kasus tertentu biasanya nanti kata tersebut perlu digaris bawahi merah), perhitungan jumlah kata.

Ketika data di memori perlu ditampilkan di layar, maka ada banyak langkah lagi yang harus dilakukan. Mulai dari pencarian font, layout setiap karakter, dsb.

Sebuah font perlu diload sebelum bisa ditampilkan di layar. Font jaman dulu merupakan font bitmap biasa, tapi font sekarang biasanya merupakan true type font yang memiliki banyak informasi untuk tiap glyph dalam font tersebut. Jika kita memiliki satu kata yang ingin ditampilkan, maka ada kemungkinan:

  • Satu huruf dipetakan ke satu glyph
  • Satu huruf dipetakan ke lebih dari satu glyph, contohnya jika ada diacritic
  • Dua huruf atau lebih dipetakan menjadi satu glyph (contohnya jika ada ligature)

Jika dokumen terdiri dari lebih dari satu bahasa maka hal ini menjadi lebih kompleks lagi, ada bahasa yang ditulis dari kiri ke kanan dan dari kanan ke kiri. Berbagai hal ini biasanya dilakukan dalam tahap yang dinamakan “text-shaping”. Setelah semuanya siap, maka teks bisa ditampilkan di layar.

Bagian berikutnya akan membahas bagaimana teks ditampilkan di layar.

Dari keyboard ditekan sampai muncul karakter di layar (bagian 1)

Seri tulisan ini akan mencoba menjelaskan apa yang terjadi dari sejak kita menekan tombol di keyboard sampai muncul huruf di layar. Untuk mempersingkat, saya akan mengambil beberapa asumsi:

  • Keyboard ditekan di sebuah aplikasi yang terbuka (jadi sudah selesai booting)
  • Sistem operasi yang dipakai modern (bukan DOS, tapi Windows/Linux/OS X)
  • Aplikasi yang akan saya contohkan adalah sebuah word processor (supaya sederhana, rencananya saya akan menulis dari mengetik di address bar sampai muncul web page)

Keyboard

Ketika tombol di tekan di keyboard, sebuah saklar akan aktif. Ada banyak jenis saklar ini (bisa dibaca di wikipedia) tapi intinya sebuah sirkuit akan tertutup ketika tombol ditekan. Karena jumlah tombol ada banyak, maka digunakan sirkuit matriks pada keyboard untuk mengurangi jumlah koneksi.

Keyboard tidak tahu apa yang tertulis di atas sebuah tombol. Tombol pertama di keyboard QWERTY adalah huruf ‘Q’, tapi di keyboard AZERTY menjadi ‘A’.  Keyboard hanya akan mengirimkan kode sebuah tombol, nanti urusan setting di sistem operasi yang akan menginterpretasikan tombol itu menjadi suatu huruf tertentu.

Sebuah keyboard memiliki prosessor, sebuah chip khusus atau microcontroller yang akan melakukan scanning pada matrix keyboard. Di keyboard lama (PS/2), jika ada tombol ditekan maka akan mengirimkan sebuah interrupt ke PC. Di keyboard USB, PC akan melakukan polling (biasanya 150Hz), artinya dalam 1 detik komputer akan bertanya 150 kali pada keyboard: apakah ada tombol yang sedang ditekan?

Jika ingin mengenal bagian ini lebih dalam, kita bisa membeli Arduino, dan keypad matrix sederhana seperti dalam gambar berikut ini.

Atau seperti ini yang lebih jelas terlihat koneksinya

Kita juga bisa membeli keycap satuan untuk keyboard mekanis, dan bahkan merangkai sendiri circuit boardnya. Microcontroller yang digunakan dalam proyek DIY seperti ini biasanya Atmega32u yang mudah diprogram.

Jika ingin membuat keyboard custom sendiri juga tidak sulit. Misalnya kita bisa membuat keyboard yang mengetik alamat email jika satu tombol ditekan. Atau mungkin kita tidak ingin memakai tombol tapi pedal.

Sistem Operasi

Input dari keyboard akan diterima oleh sistem operasi. Untuk keyboard USB, driver HID (human interface device) akan menerima data dari keyboard. Di titik ini, setiap sistem operasi memiliki jalurnya sendiri dalam menangani keyboard.

Di Linux/Unix, jika kita memakai XWindow, maka X Server akan membaca input dari /dev/input/* dan meneruskan eventnya ke berbagai Window yang muncul. Di Windows driver akan mengirimkan message ke System Message Queue. 

Shell/Desktop

Ketika sebuah tombol ditekan, maka ada Desktop manager yang akan mengatur: saat ini aplikasi yang aktif yang mana? mana yang menerima input keyboard? kadang ada lebih dari satu aplikasi yang menerima input keyboard,  ada aplikasi yang mendaftarkan diri sebagai shortcut handler (misalnya ketika tombol print screen ditekan, maka isi layar akan disimpan ke DropBox).

Berikutnya …

Di sebuah aplikasi akan ada event handler yang menangani input dari keyboard. Ini akan saya bahas di bagian berikutnya.

Membaca Source Code

Ada beberapa pertanyaan yang ditujukan ke saya yang jawabannya mudah jika sang penanya bisa membaca source code. Sayangnya skill membaca source code ini sering kali tidak dimiliki, padahal sangat diperlukan oleh programmer dan juga para reverse engineer.

Saat ini mungkin sekitar 90% pertanyaan programming bisa dicari jawabannya dengan Google, dan biasanya jawabannya akan ditemukan di situs stack overflow,  di blog seseorang, dan kadang di beberapa situs yang memang membahas topik khusus yang dicari.

Tentunya jawaban-jawaban tersebut bisa ditemukan kalau Anda bisa berbahasa Inggris. Minimal tahu beberapa kata kunci untuk mencari, dan sisanya adalah kemampuan membaca teks berbahasa Inggris. Segala macam pertanyaan sederhana sudah ada yang menanyakan, misalnya “how to split string in <java/c/javascript/..>”. Sering kali jawabannya disertai source code fungsinya, misalnya untuk pertanyaan cara split string di C yang tidak ada di library standar.

Selain berbagai jawaban di stack overflow, dan artikel di berbagai situs, ada juga berbagai buku dan dokumen yang bisa dibaca untuk mencari jawaban. Tapi ada satu sumber yang paling akurat untuk pertanyaan programming: source code. Kadang informasi dari dokumentasi bisa salah atau belum diupdate, tapi informasi dari source code tidak akan salah.

Saat ini sebagian besar software yang dipakai sudah open source. Berbagai sistem operasi sourcenya terbuka (Linux, FreeBSD, OS X, dan masih banyak lagi), compiler dan interpreter (gcc, llvm, go, php, python, dsb), editor text biasa ( emacs, notepad++, vi, dsb) atau bahkan IDE (IntelliJ, Eclipse, Visual Studio Code, dsb). Berbagai game, editor audio, konverter video, dsb semuanya ada versi open sourcenya. Berbagai library sudah ada untuk melakukan hampir apa saja, dari mulai decoding format file sampai  mengenali wajah dan suara.

Tidak semua software memiliki dokumentasi yang lengkap. Contohnya: meskipun jutaan orang memakai OS X, dan kernel OS X ini open source, sedikit sekali dokumentasi dan buku mengenai internal kernel OS X. Jika untuk software yang sangat populer saja dokumentasinya tidak banyak, tentunya masih ada ratusan ribu software lain yang dokumentasinya lebih sedikit lagi.

Dokumentasi sebuah fungsi di sebuah bahasa juga sering kali tidak menyebut detail implementasi, yang kadang penting dari sisi security, misalnya dokumentasi mt_rand dan mt_srand di PHP tidak menyebutkan bagaimana proses seeding dilakukan jika tidak dinyatakan secara eksplisit. Tentunya ini bisa ditanyakan ke stack overflow, dan dalam kasus ini ada yang menjawab untuk versi PHP yang spesifik (dan dia mendapatkan jawabannya dari source code).

Dari sisi security, kita bisa menemukan kelemahan dari membaca source code. Dalam kasus mt_rand, sudah pernah ada dua bug ditemukan (tahun 2008 dan 2011). Bug di software yang menggunakan PHP sehubungan dengan penggunaan mt_rand juga masih sering ditemukan (misalnya salah satu yang relatif baru  di Oktober 2017).

Membaca source code tanpa alasan yang kuat memang jarang dilakukan orang. Tapi ketika terpaksa, sebaiknya kita mampu membaca kode orang lain. Sering kali kita tidak perlu membaca dalam, biasanya hanya sekedar permukaan saja atau langsung spesifik ke bagian tertentu, tergantung kebutuhan. Membaca source code besar tidak bisa seperti membaca novel dari halaman pertama ke terakhir. Membaca source code besar  harus memakai berbagai pendekatan.

Pendekatan pertama adalah skimming, sekedar melihat berbagai file yang ada, lalu membuka file tersebut. Cek apakah ada komentarnya. Hal paling penting adalah mengetahui apakah file tersebut dipakai atau tidak. Sebagian file ternyata tidak dipakai, sebagian ternyata hanya dikompilasi jika fitur tertentu diaktifkan (sedangkan defaultnya fitur tersebut tidak aktif).

Hal penting lain yang bisa didapat dari sekedar skimming adalah: library apa yang dipakai oleh program ini atau program eksternal apa yang dipanggil oleh program ini? Kadang ternyata source code yang kita temukan tidak sesuai harapan karena ternyata hanya membungkus library atau command line lain.

Pendekatan berikutnya adalah menjalankan program tersebut. Ini bisa langsung dilakukan jika program tidak perlu dicompile dan tidak butuh server tertentu (contoh: sebagian program butuh server database). Jika program perlu dicompile, langkah ini juga memberikan banyak informasi. Kita jadi tahu file apa saja yang ternyata dicompile, jadi tahu library apa yang dilink, dsb.

Jika sudah bisa menjalankan program, akan lebih baik lagi jika bisa mendebug program. Beberapa program memang sangat sulit didebug, contohnya kernel Linux di Android butuh setup yang tidak mudah, plus perlu belajar memakai GDB. Tapi jika debugger bisa dipakai, maka ini akan memudahkan pemahaman program. Jika debugger tidak bisa dipakai, maka cara lain adalah menggunakan “print” debugging.

Sebagai catatan: berbagai langkah bisa dilakukan bersamaan. Mengkompilasi source code kadang butuh waktu lama (berjam-jam) atau mendownload requirement (berbagai library yang dibutuhkan) butuh waktu berjam-jam. Sementara kompilasi dilakkan,  kode bisa dibaca. Kalau kodenya terlalu besar untuk didownload (contoh: ukuran source code Qt ukurannya ratusan megabyte) bisa source code versi online yang dibaca (misalnya di github).

Sebuah IDE akan sangat membantu untuk membaca program: mencari tahu di mana sebuah fungsi didefinisikan, dari mana saja fungsi dipanggil bisa dialakukan dengan mudah. Untuk memahami program sangat besar (misalnya kernel Linux) biasanya sebuah IDE kurang bisa dipakai (terlalu lambat). Sebagai alternatif,  program cross reference (misalnya LXR) bisa dipakai.  

Jika program memiliki unit test, maka test ini juga bisa menjadi cara untuk memahami fungsi tertentu. Unit test hanya memanggil satu modul program saja untuk menguji kebenarannya, jadi kita bisa mengerti satu bagian kecil program tersebut.

Perhatikan juga bahwa kadang aplikasi yang populer sudah sangat kompleks karena memiliki banyak optimasi. Contohnya jika hanya ingin memahami membuat web server, maka bisa diawali dengan membaca source code sederhana, lalu kemudian bisa diteruskan dengan membaca yang lebih rumit. Jika Anda belum paham dasar web server, dan langsung membaca source code Apache, maka kemungkinan besar Anda akan bingung.

Beberapa hal butuh dasar teori yang baik, contohnya: memahami source code library kompresi butuh dasar teori mengenai kompresi. Memahami implementasi encoder/decoder MP3 juga akan mustahil jika tidak punya dasar teori mengenai pendengaran manusia (dan banyak konsep matematika).

Hal yang paling penting ketika membaca source code adalah: motivasi. Tanpa motivasi tertentu, membaca source code memang sangat membosankan. Motivasi membaca source code ini bisa banyak, dan tergantung masing-masing orang. Beberapa contoh hal yang pernah memotivasi saya:

  • Ada konfigurasi yang tidak jalan, dan saya tidak mengerti kenapa, sedangkan dokumentasinya kurang jelas
  • Saya ingin memahami bug tertentu (misalnya ini)
  • Saya perlu tahu implementasi algoritma tertentu
  • Saya perlu mencari bug untuk tujuan pentesting. Misalnya ada software lama tapi tidak menemukan eksploitnya di web

Semoga semua tips di atas bisa membantu Anda mencoba membaca source code orang lain.

Mengenal Bahasa Pemrograman Forth

Di posting ini saya ingin memperkenalkan bahasa pemrograman Forth. Bahasa Forth sudah ada selama 50 tahun dan cukup menarik walaupun saat ini sudah cukup jarang dipakai. Forth memakai notasi postfix, berbasis stack, dan merupakan contoh dari concatenative programming language.

Forth merupakan bahasa yang sangat sederhana dan dapat diimplementasikan di sistem dengan spesifikasi sangat rendah, bahkan juga di microcontroller. Biasanya Forth bisa diprogram secara interaktif (analoginya seperti berada di shell Python/Ruby), tapi bisa juga dikompilasi.

Sifat interaktif Forth ini seperti berbagai bahasa tingkat tinggi. Di sisi lain Forth bisa mengakses mesin secara low level dan tidak memiliki struktur data yang rumit (misalnya hash atau list),hanya dapat mengatur blok memori secara low level (seperti C). Jadi Forth berada antara bahasa tingkat tinggi dan tingkat rendah.

Di mana Forth dipakai?

Dari puluhan ribu bahasa pemrograman di dunia ini, tidak banyak bahasa pemrograman yang memiliki standard ISO, Forth merupakan salah satu bahasa yang memiliki standard ISO. Beberapa contoh bahasa dengan standar ISO adalah: JavaScript, Ruby, SQL, Pascal, C, dan C++, sementara Python, Go, Dart dsb tidak/belum punya standar ISO. Adanya standar sebuah bahasa biasanya membuat bahasa tersebut dapat dipakai di berbagai proyek yang memiliki spesifikasi sangat ketat (misalnya proyek NASA).

Di masa kejayaannya ada banyak sekali program yang ditulis dalam Forth untuk berbagai aplikasi ruang angkasa, misalnya pesawat ruang angkasa Philae yang mendarat di komet. Banyak proyek Forth yang bisa dilihat di sini.  Sebelum Apple beralih ke Intel, komputer Apple memakai Open firmware (seperti BIOS di PC) yang menggunakan Forth.

Saat ini saya belum menemukan daftar terbaru di mana Forth masih dipakai secara komersial atau dalam skala besar. Namun demikian, saya melihat masih banyak proyek open source yang aktif untuk microcontroller dan proyek IOT.

Interpreter/Compiler Forth

Saat ini ada banyak implementasi Forth untuk berbagai sistem baik komersial maupun open source. Untuk implementasi open source, daftar yang cukup lengkap bisa dilihat di Github. Awalnya Forth dirancang untuk berjalan di komputer tanpa operating system, jadi mungkin ada beberapa konsep yang agak membingungkan atau agak aneh jika dilihat dari kacamata sistem operasi saat ini.

Salah satu demo Gforth di Android

Pada artikel ini saya akan menggunakan Gforth yang dapat berjalan di Windows/Linux/OS X dan juga di Android. Saya juga akan membahas sedikit Forth untuk embedded system.

Jika Anda ingin mencoba versi Android, setelah instalasi, pergi ke settings untuk mengaktifkan permision “Storage”. Tanpa permission ini GForth tidak bisa mengekstrak contoh program ke /sdcard/gforth.

Aktifkan permission storage

Hello World

Bentuk hello world sederhana dengan gforth seperti ini.

." Hello World" cr

Perhatikan bawah tidak ada spasi antara . (titik) dan ” (petik), lalu ada spasi sebelum Hello (ini sangat penting ketika mencoba contoh tersebut). Contoh Hello World kurang menunjukkan keunikan Forth, jadi untuk contoh berikut, saya menggunakan mode interaktif Gforth untuk operasi matematika.

Untuk menambahkan dua buah angka dan mencetak hasilnya:

3 4 + .

Dalam contoh di atas, yang terjadi adalah: 3 akan dipush ke stack, 4 akan dipush ke stack, + (plus) akan mengambil 2 angka di stack dan menjumlahkan keduanya dan menaruh hasilnya di stack, . (titik) akan mengambil angka dari stack dan mencetak di layar.

Agar outputnya terlihat terpisah dari baris lain, kita bisa menambahkan cr, seperti ini (akhiri ini dengan menekan ENTER):

cr 3 4 + .

Forth memakai istilah “word” untuk semua simbol dan bilangan yang diparse oleh interpreter/compiler. Di posting ini saya tidak akan menerjemahkan “word” sebagai “kata”, supaya tidak bingung ketika membaca dokumentasi Forth.

Secara umum interpreter Forth bekerja seperti ini: interpreter akan memecah input menjadi word dan menjalankan aksi dari tiap word. Semua simbol dan bilangan yang dipisahkan spasi/enter dianggap sebagai satu word (jadi “123” adalah 1 word, “.” adalah satu word).

Aksi untuk word yang bisa dikonversi mernjadi bilangan adalah memasukkan (push) bilangan ke stack, aksi untuk operasi matematika adalah mengambil isi 2 elemen stack teratas, melakukan operasinya dan menaruh hasilnya di stack. Implementasi Forth ada yang case sensitive dan ada yang tidak. GForth merupakan contoh yang tidak, jadi besar kecil dianggap sama. Dalam Standard Forth, semua word standar harus dalam UPPERCASE.

Operasi matematika ini: (2 + 3) * 5 bisa dituliskan di forth seperti ini:

2 3 + 5 *

Sebagian orang sangat menyukai notasi postfix ini dan dulu ada banyak kalkulator yang memakai Reverse Polish Notation (RPN), bahkan semua kalkulator dari Hewlett Packard versi awal memakai notasi RPN.  Meskipun aneh, tapi notasi seperti ini hemat menekan tombol tidak butuh kurung untuk presedensi operator (seperti pada contoh di atas).

Bagaimana kita tahu apa saja yang ada di stack? kita bisa menginspeksi stack dengan word “.S”, ini akan memprint isi stack, contohnya

1 2 3 .S \ ada 3 isi stack; 1 2 3

Perhatikan backslash  adalah cara untuk membuat komentar (perlu diikuti spasi karena backslash adalah sebuah word) dan saya pakai untuk menjelaskan jadi jika ingin mengikuti contohnya di interpreter abaikan \ dan seterusnya. Untuk contoh di atas, cukup ketik:

1 2 3 .S

Jika kita jumlahkan dua isi stack teratas (2 dan 3) dengan +, lalu kita print lagi isi stacknya:

1 2 3 .S \ isi stack: 1 2 3
+ .S \ isi stack: 1 5

Top of stack menjadi 5.

Di interpreter Forth yang lain ada yang langsung mencetak isi stack di prompt (misalnya PunyForth), jadi kita tidak perlu melakukan apapun.

Ada banyak word yang mengoperasikan stack. Dalam dokumentasi  biasanya operasi dituliskan dalam bentuk komentar kurung buka ‘(‘ isi komentar lalu kurung tutup ‘)’.Komentar akan berisi stack sebelum dan sesudah word tersebut. Contohnya DUP untuk menduplikasi top of stack. Jika tadinya isi stack adalah A maka akan menjadi 2 yaitu A A. Di dokumentasi akan ditulis seperti ini:

DUP ( a -- a a)

Contoh lain, dokumentasi SWAP adalah seperti ini:

SWAP ( a b -- b a )

Dan OVER seperti ini:

OVER ( a b -- a b a )

Ada beberapa word di Forth yang sifatnya khusus, karena tidak mengoperasikan stack atau langsung dieksekusi, tapi akan melihat pada word berikutnya sampai batas tertentu. Kita mulai dari word pertama yang sifatnya khusus yaitu : (titik dua) untuk mendefinisikan word baru sampai ;

Contohnya kita ingin membuat word baru: tambahtujuh yang akan menambahkan 7 pada sebuah bilangan:

: tambahtujuh 7 + ;

Sekarang kita bisa melakukan ini

5 tambahtujuh .

dan hasilnya adalah 12. Untuk Anda yang penasaran dengan implementasi low level: Forth memiliki beberapa stack. Stack untuk data berbeda dengan stack yang dipakai untuk pemanggilan word (subrutin).

Di Forth, programmer mendefinisikan banyak word baru. Ini seperti subrutine/prosedur di sebuah bahasa. Setiap word hanya melakukan sedikit aksi. Dengan membuat word baru berdasarkan word sebelumnya, maka kita bisa membuat program yang terstruktur. Bisa dilihat bahwa di sini pendekatannya adalah bottom up.

Word : (titik dua) merupakan “defining word” dan kadang disebut juga “parsing word”, karena tidak seperti word biasa, word ini akan melihat pada input berikutnya, tidak hanya melihat stack dan mengeksekusi berdasarkan stack. Contoh defining word yang lain adalah adalah VARIABLE.

VARIABLE tanggal \ tanggal adalah variabel
12 tanggal ! \ set nilai tanggal menjadi 12
tanggal @ . \ akses variabel tanggal dan masukkan isinya ke stack, lalu print isi stack
tanggal ? \ langsung print isi variabel tanggal
7 tanggal +! \ tambahkan 7 ke nilai tanggal
tanggal ? \ sekarang tanggal menjadi 19

Konstanta juga bisa didefinisikan dengan CONSTANT.

144 CONSTANT LIMIT \ LIMIT nilainya 144

Loop yang diketahui batasnya bisa dibuat dengan

   FORMULA:
           limit index DO ... LOOP

Contoh untuk memprint kata “Bebek” 10 kali

: 10BEBEK 10 0 DO ." Bebek " CR LOOP ;
10BEBEK

Ada word bernama I (huruf i kapital) yang dipakai untuk menyalin isi stack untuk loop ke stack saat ini.

: 10BEBEK 10 0 DO I . ." Bebek " CR LOOP ; ok
10BEBEK

Jika kita mendefinisikan ulang word yang sudah ada, maka definisi terkhir yang dipakai, tapi definisi awal tidak dibuang.

Tentunya tidak semua hal bisa dikerjakan hanya dengan operasi stack. Forth juga memiliki control flow seperti IF dan LOOP. Bentuk IF di FORTH sintaksnya agak aneh dan tidak seperti di bahasa lain. Bentuknya adalah KONDISI IF AKSI THEN. Perlu diperhatikan bahwa menurut standar Forth semua control flow harus dimasukkan ke dalam definisi word

200 CONSTANT LIMIT 
: lewatbatas LIMIT > IF cr ." Lewat batas" cr THEN ;

Untuk conditional loop kita bisa memakai BEGIN UNTIL jika pemeriksaan dilakukan di akhir aksi (seperti do while{}), atau BEGIN WHILE REPEAT jika pemeriksaan dilakukan di awal (seperti while () {} ). Jika kita ingin menghentikan loop di tengah (break), kita bisa menggunakan  LEAVE.

Berikut ini contoh sederhana dalam satu baris

: bebek 0 counter ! BEGIN counter ? ." Bebek" cr  1 counter +! counter @ 10 > UNTIL ; 

Tapi itu sulit dibaca, jadi biasanya ini akan dituliskan dengan banyak word supaya lebih jelas. Word pertama “reset” untuk mengeset counter menjadi 0, word “full?” akan mengecek apakah counter sudah bernilai lebih dari 10, word “next” akan menambah counter dengan 1.

VARIABLE counter 
: reset 0 counter ! ;
: full? counter @ 10 > ;
: next 1 counter +! ;
: bebek reset BEGIN counter ? ." Bebek" cr next full? UNTIL ;

Literal string  didefinisikan dengan S”, seperti ini S" hello world" dan dicetak dengan type. Sebagai shortcut, kita bisa memakai ." string" untuk mencetak string langsung ke layar.

Kita bisa melihat definisi suatu word dengan “SEE”.

SEE lewatbatas 
: lewatbatas 200 > IF cr .\" Lewat batas" cr THEN ; ok

Yang menarik, pada gforth andaikan kodenya diimplementasikan dalam bahasa mesin, maka bahasa assemblynya akan ditampilkan.

Struktur Data

Saya tidak akan membahas panjang mengenai struktur data di Forth, karena Forth hanya memiliki primitif semacam malloc di C, dan sisanya adalah implementasi oleh programmer.

Forth bekerja dalam satuan CELL. Ukuran satu sel tergantung implementasi, tapi di Standar Forth ditentukan minimal 16 bit. Masalahnya tidak semua tipe data muat dalam 1 sel (misalnya tipe data double butuh 8 byte sedangkan integer 32 bit hanya butuh 4 byte), jadi kadang di Forth kita perlu mengoperasikan sepasang nilai di stack. Segala operasi ini dilakukan dengan Word yang berawalan dengan angka 2, misalnya 2DUP.

Word yang dipakai untuk mengalokasikan memori adalah ALLOT. Setelah memori dialokasikan, maka isinya bisa diakses dengan operasi aritmatika terhadap pointer (seperti di C). Contohnya untuk mengalokasikan Array 4 sel, lalu mengisi sel kedua dengan nilai 5:

VARIABLE a 4 CELLS ALLOT
5 a 1 CELLS + !

Ini mirip di C jika kita menggunakan aritmatika pointer.

a = (int*)malloc(4);
*(a+1) = 5;

Forth untuk embedded system

Karena Forth bisa berjalan langsung di microcontroller — termasuk juga mode interaktifnya — maka Forth memudahkan prototyping. Jika menggunakan C maka kita harus menulis program di desktop, mengcompile programnya (bisa beberapa detik hingga puluhan detik), mentransfer ke microcontroller dan melihat hasilnya. Dengan interpreter interaktif di microcontroller, maka kita bisa langsung menggunakan serial port dan langsung memprogram.

Sebagai catatan, kenyamanan ini dirasakan banyak orang sehingga sekarang ini sudah ada beberapa proyek untuk menjalankan bahasa yang lebih “modern” seperti Lua, Python dan bahkan JavaScript di berbagai microcontroller yang memiliki RAM besar. Tapi dari pengalaman saya, berbagai implementasi yang saya coba cukup lambat karena tidak dirancang dari awal untuk berjalan di embedded system. Bahasa-bahasa tersebut juga tidak dapat dijalankan di sistem yang RAM-nya sangat kecil (misalnya Forth untuk STM8S bisa berjalan dengan RAM satu kilobyte).

Wemos D1 Mini

Jika ingin mencoba Forth di embedded system, mungkin saat ini yang paling mudah dan murah adalah ESP8266, versi Wemos D1 hanya 2.75 USD dan hanya perlu kabel micro USB untuk koneksi ke komputer. Proses memasukkan interpreter Forth dan melakukan koneksi serial tidak perlu hardware lain.

Board STM8S

Board yang paling murah dan bisa menjalankan Forth setahu saya adalah STM8S yang harganya kurang dari 1 USD (termurah saat ini 0.86 USD, harga satuan, sudah termasuk ongkir dari China, bisa lebih murah jika beli banyak). Meskipun paling murah, tapi untuk memasukkan interpreter Forth kali pertama perlu memakai ST-LINK (Sekitar 2 USD) dan untuk memprogram secara interaktif butuh USB to Serial (1 USD). Setelah program berjalan, kedua hardware tersebut tidak dibutuhkan lagi. STM8 eForth bahkan mendukung background task di microcontroller ini.

Penutup

Artikel ini hanya memperkenalkan “kulitnya” Forth saja, banyak hal yang tidak saya jelaskan. Jika Anda ingin membaca lebih lanjut, ada banyak resource di Internet, misalnya bisa mulai dari ini:

 
Bahkan standard Forth juga bisa dibaca. Versi Draftnya (yang katanya 99.9% sama dengan versi finalnya) bisa dibaca di sini. Isi standarnya cukup ringkas, hanya penjelasan word yang termasuk dalam standar yang membuat dokumennya menjadi panjang.

Sebagai tambahan juga, saya menemukan ada juga seseorang di Indonesia yang memakai Forth untuk produknya: https://telinks.wordpress.com/.

Code Obfuscation

Code Obfuscation adalah salah satu bentuk proteksi agar kode sulit dibongkar orang lain. Inti dari obfuscation adalah menyamarkan/membuat kode sulit dibaca. Obfuscation bisa dilakukan manual atau dengan tool yang disebut “obfuscator”. Sementara itu dari sisi reverse engineering, proses mengembalikan dari bentuk samar ini disebut “deobfuscation”.

Arti kata “samar” di KBBI

Meskipun kata “samar” sepertinya cukup berpadanan dengan “obfuscated” saya akan tetap menggunakan istilah inggrisnya di posting ini.

Di posting ini saya hanya ingin memberikan beberapa ilustrasi nyata seperti apa obfuscation ini. Untuk para programmer, ini bisa membantu memproteksi  program, dan untuk para reverse engineer bisa berusaha memahami bagaimana obfuscation dilakukan.

Contoh obfuscated code

Obfuscation

Jika kita punya dua fungsi sederhana seperti ini:

int search(int element, int *data, int count)
{
    for (int i =0; i &amp;amp;amp;lt; count; i++) {
          if (data[i] == count)
                return i;
    }
    return -1;
}

int replace_first(int element, int replacement, int *data, int count)
{
    int pos = search(element, data, count);
    if (pos !=-1) {
        data[pos] = replacement;
        return 1;
    }
    return 0;
}


Bentuk obfuscation pertama adalah dengan mengganti nama menjadi nama lain. Ini bisa jadi nama yang sangat singkat misalnya “a”. Pada kode yang dikompilasi menjadi bahasa mesin, nama ini bahkan tidak ada lagi karena tidak diperlukan. Pada bahasa lain yang tidak dicompile jadi bahasa mesin (seperti Java dan Python), nama ini akan tetap ada.

Tanpa membaca kodenya, sudah sulit menebak apa fungsinya ini

int a(int a1, int *a2, int a3);
int b(int a1, int a2, int *a2, int a3)

Bahasa tertentu (seperti Java/C++) mendukung overloading. Beberapa fungsi bisa memiliki nama sama asalkan parameternya berbeda:

int a(int a1, int *a2, int a3);
int a(int a1, int a2, int *a2, int a3)

Cara lain adalah dengan mengubah namanya menjadi menyesatkan, misalnya kedua nama seperti di atas diubah menjadi seperti ini (perhatikan bahwa sengaja count-nya dipindah ke depan untuk menyesatkan pembacanya).

int clear(int count, int *data, int position);
int remove_range(int start, int end, int *data, int element);

Selain penggunaan nama yang menyesatkan, cara lain adalah dengan mengubah control flow program. Contoh sederhananya seperti ini, kita memanggil beberapa fungsi berurutan:

verify_license();
initialize_printer();
initialize_camera();
connect_to_database();

Jika sebuah bahasa mendukung goto maka bentuk obfuscationnya bisa seperti ini:

verify_license();
goto label2;

label1:
initialize_camera();
connect_to_database();
goto label4;

label2:
initialize_printer();
goto label1;

label4:

Jika suatu bahasa tidak mendukung goto urutan operasi bisa disamarkan dengan loop dan switch:

int order[] = {3,1,2,0};
for (int i =0; i &amp;amp;amp;lt; 4; i++) {
   switch (order[i]) {
        case 0: connect_to_database(); break;
        case 1: initialize_printer(); break;
        case 2: initialize_camera(); break;
        case 3: verify_license(); break;
   }
}

Obfuscation kecil seperti contoh di atas masih mudah dimengerti untuk kode yang pendek. Untuk kode yang besar, jumlah “case”-nya bisa puluhan, dan masing-masing nama fungsinya tidak jelas.

Beberapa obfuscation bisa mudah dilihat jika data atau string terlihat jelas, misalnya jika ada kode seperti ini, meskipun kita tidak tahu apa itu kelas x, tapi terlihat bahwa fungsi saat ini berhubungan dengan enkripsi AES:

            throw new x("AES decrypt error");

Jadi bentuk obfuscation berikutnya adalah: string encryption supaya tidak mudah mencari string di dalam program dan mempersulit pemahaman. Tentunya string ini harus bisa didekrip ketika program berjalan, hanya mempersulit pemahaman program.

            throw new x(decrypt(ConstString.ERR1));

Teknik-teknik lain juga bisa ada banyak, saya tidak akan memberikan contoh kode satu persatu. Beberapa yang bisa dilakukan misalnya

  1. Obfuscation di level bahasa mesin/bytecode
  2. Menggunakan exception untuk control flow obfuscation
  3. Menyisipkan kode sampah, misalnya mengurutkan elemen, lalu mencari elemen tengah, menjumlahkan semua elemen, lalu hasilnya tidak dipakai
  4. Menggunakan thread untuk memecah algoritma menjadi beberapa bagian sehingga lebih sulit di mengerti
  5. Mengubah logika program, misalnya menambahkan angka 2312312 di awal, lalu di akhir dikurangi lagi 2312312
  6. Menggunakan enkripsi dan/atau kompresi untuk sebagian kode program yang diload secara dinamis
  7. Memakai custom virtual machine

Perlu diperhatikan bahwa obfuscation tertentu bisa membuat program jadi lebih lambat. Sekedar mengganti nama method tidak akan membuat lebih lambat (bahkan biasanya malah membuat sedikit lebih cepat), tapi mengganti control flow biasanya membuat kode menjadi lebih lambat. Obfuscation juga membuat debugging menjadi lebih sulit (karena memang itu tujuannya). Jadi sebaiknya obfuscation hanya dilakukan di akhir development.

Saya tidak bisa menyarankan tool obfuscator tertentu karena memang jarang melakukan obfuscation pada kode saya, silakan search “obfuscator” dan nama bahasa yang Anda pakai di search engine. Tool obfuscator yang banyak saya temui adalah  Proguard (gratis) yang dipakai untuk kode Java/Android.

Di dunia Javascript ada istilah minifier, yaitu tool untuk membuat kode Javascript mejadi lebih kecil. Ini dilakukan dengan mengganti nama variabel, menghapus spasi, komentar dsb. Secara umum ini juga berfungsi sebagai obfuscator sederhana.

Deobfuscation

Secara umum tidak ada cara generik yang membuat obfuscated code bisa dibaca dengan mudah. Tapi ada beberapa tool yang bisa membantu proses deobfuscation spesifik untuk bahasa/teknologi tertentu.

Contohnya jika bertemu dengan kode JavaScript yang sudah minified, maka kita bisa memakai JavaScript beautifier. Ini tidak bisa mengembalikan nama variabel, hanya membuat teks yang sulit dibaca menjadi lebih mudah dibaca (tool beautifier ini sekarang sudah built in di Developer Tools-nya Google Chrome).

Contoh lain: ada yang membuat tool untuk mendekrip String untuk APK yang ditulis dalam Java. Tapi tool ini tidak selalu jalan untuk semua protektor. Setiap kali ada yang membuat tool untuk otomasi sesuatu, pembuat obfuscator menambahkan satu hal kecil sehingga toolnya harus diupdate (atau bahkan ditulis ulang).

Cara yang pasti berhasil adalah kombinasi manual dengan debugger dan sedikit programming.  Hal utama adalah memahami obfuscation apa yang dilakukan: apakah stringnya dienkripsi, apakah nama methodnya diubah, apakah control flow-nya berubah, dsb.

Jika sekedar nama methodnya diubah, maka kita harus membaca kodenya dan melakukan renaming untuk mendapatkan nama yang benar. Ini bisa dilakukan berdasarkan beberapa hal, misalnya:

  • string yang muncul (misalnya “AES Error” mengindikasikan AES)
  • konstanta yang dipakai (misalnya 0x9E3779B9 mengindikasikan penggunaan enkripsi XTEA)
  • fungsi yang memanggil. Misalnya jika suatu fungsi dipanggil dari fungsi AES, maka kemungkinan itu hanyalah subrutin AES
  • fungsi yang dipanggil, misalnya jika aplikasi C memanggil “system” maka kemungkinan ini fungsi menjalankan command line lain
  •  algoritma yang dipakai. Beberapa algoritma sederhana (search, sort, traversal) mudah diidentifikasi

Untuk mendapatkan gambaran sebuah program seperti menyusun sebuah puzzle. Kita bisa mulai dari bagian-bagian yang jelas. Untuk puzzle bagian yang jelas adalah pinggiran puzzle, dan bagian-bagian yang unik. Untuk program kita bisa mulai dari titik awal program (main di sistem POSIX, Activity di Android, dsb), dan titik di mana program memanggil fungsi eksternal.

Jika string dienkripsi, kita bisa membuat breakpoint di method dekrip-nya agar bisa mendapatkan hasil string-nya. Jika control flow-nya obfuscated, kita bisa membuat beberapa breakpoint untuk berhenti di tiap titik, jadi kita bisa mengetahui urutan yang sebenarnya. Selain menggunakan breakpoint dan debugger, kita juga bisa menggunakan Frida atau tool sejenis.

Penutup

Walaupun obfuscator bisa membantu melindungi program, tapi faktor keamanan lain harus tetap diperhatikan. Attacker yang gigih akan bisa membuka segala jenis obfuscation, hanya akan memperlama saja. Di kasus tertentu kombinasi obfuscator dan pentester yang kurang berpengalaman justru bisa membuat aplikasi kurang aman karena testing aplikasi kurang optimal.

Contohnya begini: aplikasi memakai enkripsi, lalu kodenya diobfuscate.  Aplikasi ini ditest oleh pentester yang tidak bisa melakukan reverse engineering terhadap kode tersebut, dan pentester menganggap aplikasi tersebut aman karena dia tidak dapat melakukan tampering terhadap nilai yang dikirimkan. Ketika aplikasi dirilis dan dibongkar oleh seorang reverse engineer yang berpengalaman, dia dapat mengubah nilai yang dikirimkan dan ternyata tidak dicek di sisi server (aplikasinya jebol).

Jadi sebaiknya: pentester diberi akses pada kode yang belum obfuscated dan kode final yang sudah. Ini akan lebih optimal karena kerja pentester lebih cepat, tidak perlu membongkar obfuscation dan lebih aman (seluruh fungsi bisa ditest dengan baik). Ini dengan asumsi bahwa pentester memiliki keahlian untuk membaca kode dengan baik.

Semoga artikel singkat ini bisa memberikan gambaran mengenai apa itu code obfuscation dan berbagai batasannya.

Mendalami Bahasa C

Saya memberikan saran agar seseorang “mendalami bahasa C” jika ingin belajar reverse engineering. Ada pertanyaan menarik yang diajukan ke saya: sedalam apa belajarnya pak? apa yang harus dipelajari. Sesuai KBBI mendalami di sini berarti: meresapi; menyelami; mempelajari (menelaah, menyelidiki) dalam-dalam.

Saya tidak akan membahas dalam mengenai kenapa seseorang perlu memahami bahasa C, singkatnya: saat ini C masih dipakai di mana-mana, dan akan terus begitu untuk beberapa belas/puluh tahun mendatang. Kernel berbagai sistem operasi ditulis dalam C, berbagai library penting masih ditulis dalam C (library kompresi, enkripsi, image encoding/decoding, dsb), dan bahkan kebanyakan bahasa pemrograman lain diimplementasikan dalam C (misalnya Ruby, Python, dan PHP).

Di awal, pelajarilah dan pahamilah semua konsep dasar dalam bahasa C. Ini seharusnya tidak makan waktu lama. Bahasa C hanya punya beberapa tipe data dasar (void, char, short, int, long, float, dan double) masing-masing bisa signed atau unsigned. Kita bisa mendefinisikan sebuah konstanta dengan const. Tipe data lain adalah enum, union, dan struct (sudah pernah saya bahas di sini) semua tipe data bisa diberi nama dengan typedef.

Hanya ada beberapa sintaks loop (while, do while, dan for, semuanya dengan break dan continue) dan conditional (if/else, goto, dan switch/case/default). Sintaks pembuatan fungsi juga cukup sederhana, hanya perlu mengingat “return” untuk mengembalikan nilai.

Konsep manajemen memori dan string (array of characters) merupakan salah satu hal yang sering membuat pemula bingung. Jika Anda sudah berhasil membuat kode yang selalu lolos valgrind (artinya tanpa warning dan tanpa memory leak), maka Anda sudah lulus dalam pelajaran ini.

Berikutnya buatlah struktur data dalam bahasa C. Mulai dari yang sederhana seperti linked list. Setelah berhasil mencontek buku/website, cobalah menuliskan ulang struktur data tanpa mencontek. Seharusnya kalau sudah paham akan bisa. Lalu cobalah memakai fitur yang lebih rumit seperti function pointer. Pastikan ini juga lolos valgrind.

Cobalah memakai berbagai library C, bisa dimulai dari memakai berbagai fungsi di library C standar. Lalu diteruskan dengan library lain, misalnya zlib untuk kompresi data, expat untuk parsing XML, png untuk dekompresi file PNG.

Cobalah juga untuk memakai lingkungan yang berbeda. Sistem operasi yang berbeda, compiler yang berbeda, IDE yang berbeda. Supaya lebih paham yang mana yang merupakan bagian dari bahasa C, dan yang mana sekedar fitur IDE atau OS yang Anda pakai.

Menurut saya seseorang bisa dianggap cukup memahami bahasa C apabila sudah menyadari bahwa bahasa C itu sangat sederhana. Pertama yang harus disadari adalah ada bahasa C dan ada libray C.

Ketika belajar C, seseorang akan diberikan program “hello world”, seperti ini:

#include <stdio.h>

int main(int argc, char *argv){
   printf("hello world");
}

Tanpa menyadari apa itu gunanya include, dari mana printf berasal, dsb. Ketika baru belajar memang kita tidak perlu tahu itu semua, tapi jika ingin mendalami, kita harus mengerti peran: preprocessor, compiler, assembler, dan linker. Kita juga perlu memahami apa itu library, dan bagaimana membuat library sendiri (pernah saya bahas di sini).

Memprogram sistem embedded tanpa sistem operasi, misalnya microcontroller akan membuat kita sadar mengenai banyak hal yang mungkin tidak terpikirkan di desktop. Misalnya pernyataan sederhana

printf("hello world\n");

Di sistem embedded tanpa layar dan tanpa keyboard, mungkin Anda akan bertanya: ke mana outputnya? bagaimana kita membaca input?. Di sini akan disadari bahwa printf bukanlah bagian dari bahasa C, tapi bagian dari library C. Bahasa C bisa digunakan dengan berbagai library (di Linux saja ada pilihan: GNU LibC, diet libc, musl, dsb).

Di sinilah salah satu kelebihan bahasa C: ketika kita ingin tahu implementasi sebuah fungsi library apapun, kita bisa melihatnya dan biasanya dalam bahasa C juga (hanya sebagian yang memakai assembly). Beda misalnya dengan PHP: jika kita ingin tahu bagaimana fungsi strlen atau split di PHP diimplementasikan, maka yang harus kita baca adalah kode dalam bahasa C (bukan PHP).

Fungsi printf di atas sudah sangat kompleks untuk dijadikan contoh, jadi saya memakai fungsi sederhana saja: strlen. Ini implemenasi generik paling sederhana dari openbsd:

http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libc/string/strlen.c?rev=1.9&content-type=text/x-cvsweb-markup

size_t
strlen(const char *str)
{
	const char *s;

	for (s = str; *s; ++s)
		;
	return (s - str);
}

Tentunya ada ribuan cara untuk mengimplementasikan strlen. dan kadang untuk optimasi digunakan assembly. Saat ini banyak compiler C yang terbuka source codenya (GCC, Clang, Watcom, dsb) dan juga semua librarynya, jadi semua internal C bisa dipelajari.

Khusus untuk yang ingin belajar C untuk reverse engineering. Pelajarilah bahwa biasanya compiler C bisa menghasilkan kode assembly langsung dalam bentuk teks, dan kita bisa membandingkan kode C dengan kode assembly yang dihasilkan compiler. Sebagian informasi mengenai cara menghasilkan teks assembly bisa dibaca di sini.

Sebagai penutup. Jika Anda sudah bisa menganggap bahasa C sebagai bahasa yang sederhana maka kemungkinan Anda sudah mendapatkan pencerahan dan sudah mulai paham bahasa C.

Matematika dan Programming

Di kala senggang saya masih menjawab pertanyaan via Facebook/Email/Telegram dan banyak calon programmer yang sudah takut sebelum belajar programming: apakah akan butuh matematika? Jawabannya singkatnya tergantung. Tergantung ingin memprogram apa dan sedalam apa.

Matematika dasar tentunya sangat diperlukan, misalnya perkalian, pembagian, penjumlahan pengurangan. Hampir di semua bidang diperlukan ilmu dasar geometri. Misalnya tentang sistem koordinat ketika menggambar grafik (atau sekedar mengatur posisi teks di sebuah halaman web).

Pengetahuan dasar ini penting, dan ini berarti anak yang masih sangat kecil dan belum memiliki dasar matematika harus berhenti dulu belajar di titik tertentu. Ini pengalaman saya dalam mengajari anak saya ketika dulu mengajari dia programming di usia 4 tahun (sekarang sudah 7 tahun). Logika boolean juga perlu dipahami, sekedar AND, OR, dan NOT sudah cukup untuk sebagian besar kasus.

Untuk pemrograman grafik, terutama grafik 3D diperlukan pemahaman matriks dan vektor. Segala operasi matrix akan terlihat secara visual ketika memprogram grafik. Matrik dan vektor juga dipakai di Machine Learning. Jika fokusnya ingin mengolah data besar, maka berbagai ilmu matematika seperti: statistika, linear programming, graph, dan banyak konsep yang rumit akan terpakai.

Kadang hampir semua topik matematika terpakai dalam satu aplikasi. Contohnya untuk membuat game yang kompleks, diperlukan berbagai matematika untuk grafik (matriks, vektor), dibutuhkan AI untuk menggerakkan musuh (yang butuh matriks, vektor, statistika, dsb), dan untuk mencari jalan terpendek atau terbaik kadang dibutuhkan teori graph, dan masih banyak lagi komponen sebuah game.

Saya masih bisa memberikan banyak contoh lain aplikasi matematika dalam programming, tapi hal yang paling penting adalah: matematika (selain topik yang paling dasar) bisa dipelajari selagi kita belajar memprogram.

Sebelum masuk ITB, saya dulu belajar programming otodidak mulai dari SMP. Ketika saya belajar pemrograman sambil belajar matematika SMP/SMU, saya merasa lebih bisa mengerti karena bisa dicoba dalam bentuk program. Contohnya konsep fungsi dan komposisi fungsi. Saya bisa membuat sebuah fungsi di dalam bentuk kode program yang memanggil fungsi lain, dan saya bisa bereksperimen dengan itu. Jadi konsep “fungsi” tidak lagi menjadi hal yang abstrak.

Simbol summation (∑) di kode program hanyalah sebuah loop penjumlahan. Demikian juga dengan ∏ yang hanya loop dengan isi perkalian.

Demikian juga dengan topik matriks dan vektor waktu SMU. Saya mendapatkan banyak ilmu justru dari buku cara membuat game 3D yang saya baca dan saya coba waktu itu. Topik di kelas terasa sangat abstrak, tapi di kode program, bisa terlihat apa gunanya memiliki matriks dan berbagai sifat matriks lainnya.

Kalau Anda tertarik lebih dalam lagi mengenai berbagai topik matematika yang nantinya akan terpakai, silakan baca posting panjang ini (Math for Programmers) yang sudah ditulis seseorang di tahun 2006. Artikel tersebut juga membahas secara detail bagaimana belajar matematika yang lebih baik.

Jadi kesimpulannya adalah: belajar programming bisa dimulai asalkan sudah memiliki pengetahuan matematika yang dasar. Belajar konsep lanjut bisa dilakukan sambil belajar programming, dan bagi sebagian orang belajar dengan cara ini lebih mudah. Tapi jika Anda sudah punya dasar matematika yang bagus tentunya programming akan lebih mudah lagi dan Anda bisa membuat program yang lebih baik lagi. Contohnya: jika Anda punya dasar matematika yang bagus untuk AI, maka Anda bisa membuat game dengan AI yang lebih baik dari yang tanpa AI.

Signature email jaman kuliah

Dulu waktu kuliah, saya punya signature email seperti ini:

main(i){putchar((i-1)["Xme]i_l"]+(i++))&&(8-i)&&main(i);} 


Ternyata masih ada beberapa orang yang ingat, dan masih banyak yang penasaran apa artinya (cuma satu kata: Yohanes) dan  kok bisa muncul seperti itu?.

Pertama, menurut standar C lama, sebuah fungsi tanpa kembalian akan mengembalikan sebuah int, dan parameter yang tanpa tipe juga adalah sebuah int, jadi fungsi di atas sama dengan:

int main(int i){
   putchar((i-1)["Xme]i_l"]+(i++))
         &&(8-i)&&
           main(i);
 } 

Perhatikan juga bahwa di C, sifat operator && adalah short circuit, artinya dalam A() && B() jika A() mengembalikan false, maka B tidak dieksekusi:

#include <stdio.h>

int A() {
   printf("Fungsi A dipanggil\n");
   return 0;
}

int B() {
   printf("Fungsi B dipanggil\n");
   return 1;
}

int main(int argc, char *argv[])
{
   if (A() && B()) {
           printf("A dan B mengembalikan TRUE\n");
   }
   return 0;
}

Bagian main di atas sama saja dengan ini:

#include <stdio.h>

int main(int argc, char *argv[])
{
   if (A()) {
       if (B()) {
           printf("A dan B mengembalikan TRUE\n");
       }
   }
   return 0;
}

Jadi kode signature saya bisa dijadikan if juga seperti ini:

int main(int i){
   if (putchar((i-1)["Xme]i_l"]+(i++))) {
           if (8-i) {
               if (main(i)) {
               }
           }
   }
 } 

Perhatikan beberapa bisa diperjelas, misalnya (8-i) akan true jika (8-i) !=0, atau selama i != 8. Karena if (main()) kosong, maka bisa dihilangkan if-nya.

int main(int i){
   if (putchar((i-1)["Xme]i_l"]+(i++))) {
           if (i!=8) {
              main(i);
           }
   }
 } 

Di C, sebuah array adalah sebuah pointer dan sebuah string adalah array of characters. Di C:

    int *array = (int *)malloc(sizeof(int)*10);
    int index = 1;
    array[index] = 10;
    //syntax array access di atas sama dengan:
    *(array + index) = 10;
    //penjumlahan sifatnya komutatif
    *(index + array) = 10;
    //jadi ini juga sama:
    index[array] = 10; 

Untuk lebih jelasnya, string saya keluarkan, dan notasinya diperbaiki:

const char *str = "Xme]i_l";
int main(int i){
   if (putchar(str[i-1]+(i++))) {
           if (i!=8) {
              main(i);
           }
   }
 } 

Kita lihat fungsi putchar di manual:

fputc, fputs, putc, putchar, puts – output of characters and strings

Di bagian return value:

fputc(), putc() and putchar() return the character written as an unsigned char cast to an int or EOF on
error.

Catatan: karena fungsi putchar tidak dideklarasikan, dan saya tidak menginclude apapun, maka dianggap kembaliannya int, dan ada warning dari compiler.

Dalam kasus saya, putchar ini akan selalu mengembalikan 1, karena saya memprint satu karakter setiap waktu. Jadi kita sederhanakan lagi:

const char *str = "Xme]i_l";
int main(int i){
   putchar(str[i-1]+(i++));
   if (i!=8) {
      main(i);
   }   
 } 

Operator ++ (post increment) akan dilakukan setelah sebuah ekspresi, jadi dalam kasus ini bisa disederhanakan:

const char *str = "Xme]i_l";
int main(int i){
   putchar(str[i-1]+i);
   i++;
   if (i!=8) {
      main(i);
   }   
 } 

Sekarang ke bagian “magic”-nya. Di sistem operasi Windows, Linux atau POSIX yang lain, ketika program dijalankan, maka program dalam C akan menerima jumlah parameter dan isi parameternya

int main(int argc, char *argv[])

Jika kita deklarasikan tanpa argv, maka hanya jumlah parameternya yang kita dapatkan. Meskipun biasanya namanya argc dan argv, nama parameternya tentunya boleh apa saja

int main(int jumlah_argumen)

Jika program dijalankan tanpa parameter, maka jumlah argumennya adalah 1, yaitu nama program saat ini (yang tidak kita pedulikan di program ini). Jadi sebenarnya program tersebut dipanggil dengan

main(1);

Perhatikan bahwa “main” adalah sebuah fungsi di C, dan seperti fungsi apapun, bisa dipanggil bebas. Dalam kasus ini, saya memanggil (rekursif) main, dengan nilai i yang ditambahkan terus.

Jika ingin kode yang sangat jelas tiap langkahnya seperti ini:

#include <stdio.h>

const char *str = "Xme]i_l";
int main(int i){
   printf("\ni = %d\n", i);
   printf("str[i-1] = %c +%d = %c\n", str[i-1], i, str[i-1] + i);
   putchar(str[i-1]+i);
   i++;
   if (i!=8) {
      main(i);
   }   
} 

Dan outputnya:

[email protected]:~$ ./a.out 

i = 1
str[i-1] = X +1 = Y
Y
i = 2
str[i-1] = m +2 = o
o
i = 3
str[i-1] = e +3 = h
h
i = 4
str[i-1] = ] +4 = a
a
i = 5
str[i-1] = i +5 = n
n
i = 6
str[i-1] = _ +6 = e
e
i = 7
str[i-1] = l +7 = s

Demikian keisengan jaman kuliah dulu. Sekedar tambahan: dulu terinspirasi dari International Obfuscated C Code Contest, kode saya ini sangat sederhana dibandingkan para pemenang IOCCC.