Fungsi Hash Kriptografi (Bagian 2)

Salt

Penggunaan hash untuk menyimpan password merupakan cara yang standar di berbagai aplikasi. Cara ini sederhana, tapi hash saja memiliki kelemahan:

  • jika ada dua atau lebih orang passwordnya sama, maka akan terlihat bahwa hashnya sama.
  • bisa dilakukan dictionary attack. Artinya: hash semua kata di kamus sekali, lalu cocokkan hashnya

Cara yang lebih baik adalah dengan menambahkan salt, berupa beberapa karakter random yang berbeda untuk tiap entry. Jadi misalnya jika password untuk user1 adalah 123, kita bisa menghasilkan salt random misalnya “xcd”, lalu salt ini dihash bersama password hash(“xcd”+”123”). Jika hash yang dipakai adalah MD5, maka hasilnya:

4c88e409f0d1938d4f0adf7bdc896fbe

Di dalam database atau file, kita perlu menyimpan salt yang dipakai: “xcd”, misalnya seperti ini: xcd$4c88e409f0d1938d4f0adf7bdc896fbe

Jika ada user2 yang memakai password yang sama “123”, maka kemungkinan akan mendapatkan salt yang berbeda, misalnya “cx2” yang jika dihash menjadi:

262f5f51da3e4a320f745d0cafe54a3e

Ini jauh berbeda dari hash user1 yang sama-sama memakai password 123.

Hash untuk proteksi request/response

Banyak sistem yang berkomunikasi dengan sistem lain memakai hash dengan “secret” (string rahasia) yang diketahui kedua belah pihak. Prinsip kerjanya begini: data ditambah dengan secret lalu dihash. Hash ini dipakai sebagai message authentication code (MAC).

Misalnya kita punya string request: amount=2&price=10000 (asumsi bahwa nama field terurut abjad menaik). Lalu kita memiliki key “RAHASIA”, maka kita bisa membuat hash dengan MD5(“RAHASIAamount=2&price=10000”) dan hasilnya adalah: 0bb770c5349c2fe268f47a767a6ccc8e

String ini kemudian dikirimkan ke pihak lain seperti ini:

amount=2&price=10000&hash=0bb770c5349c2fe268f47a767a6ccc8e

Pihak lain juga mengetahui kata RAHASIA tersebut sehingga bisa memverifikasi agar datanya bisa tetap tidak diubah.

Penggunaan konstruksi hash(SECRET+message) ini memiliki kelemahan yaitu adanya kemungkinan hash length extension attack. Penjelasan lebih lanjut mengenai ini bisa dibaca di wikipedia. Konstruksi hash(message+SECRET) sedikit lebih aman, tapi juga tidak terlalu aman dari yang namanya preimage attack.

HMAC

HMAC atau hash based Message Authentication Code merupakan bentuk autentikasi dengan menggunakan Hash dengan cara tertentu (tidak sekedar secret+message atau message+secret).

Definisi HMAC dari RFC 2104 adalah seperti ini:

{\displaystyle {\begin{aligned}\operatorname {HMAC} (K,m)&=\operatorname {H} {\Bigl (}{\bigl (}K'\oplus opad{\bigr )}\parallel \operatorname {H} {\bigl (}\left(K'\oplus ipad\right)\parallel m{\bigr )}{\Bigr )}\\K'&={\begin{cases}\operatorname {H} \left(K\right)&K{\text{ is larger than block size}}\\K&{\text{otherwise}}\end{cases}}\end{aligned}}}

Fungsi H adalah sebuah fungsi hash (misalnya SHA1, MD5 atau yang lain). K’ didefinisikan sebagai hasil H(KEY), sementara ipad dan opad hanyalah byte-byte padding (supaya ukurannya bloknya sama jika kurang dari panjang tertentu). Di RFC 2104 nilai opad adalah 0x5c dan ipad adalah 0x36. Operasi || artinya penggabungan (concatenation).

Jadi sebenarnya fungsi HMAC hanya memakai fungsi Hash biasa yang dilakukan 2 kali, plus padding di kasus tertentu. Konstruksi hmac ini lebih aman dibandingkan Hash(SECRET+data) atau hash(data+SECRET).

Tapi tetap perlu diperhatikan faktor data yang akan dihash seperti temuan saya pada bug mastercard. Meskipun HMAC sudah dipakai, tapi karena kesalahan pada representasi data, maka cara ini bisa memiliki kelemahan.

Penutup

Banyak pemula yang masih bingung dan belum memahami fungsi hash. Banyak juga yang mencampuradukkan antara hash dengan enkripsi. Karena konstruksi MAC (message authentication code) yang memakai SECRET, dikira ini adalah key untuk enkripsi. Hash sifatnya searah, sedangkan enkripsi sifatnya dua arah (jika diketahui kuncinya maka hasil enkripsi bisa dikembalikan menjadi data semula).

Fungsi Hash Kriptografi (Bagian 1)

Fungsi hash adalah fungsi yang memetakan dari suatu data (bisa string atau apapun) ke sebuah data lain yang ukurannya biasanya lebih kecil. Ada fungsi hash untuk dipakai di struktur data hash table (outputnya adalah integer) dan ada fungsi hash untuk kriptografi outputnya berupa string alfanumerik dengan panjang tertentu.

Hash Table

Sebelum menjelaskan fungsi hash kriptografi, saya jelaskan dulu contoh fungsi hash biasa karena lebih sederhana. Penggunaan fungsi hash dalam kasus ini adalah untuk menyimpan data ke hash table, supaya gampang diakses lagi.

Misalnya kita punya array yang bisa menyimpan 100 elemen. Kita ingin menyimpan sebuah string, lalu ingin bisa menemukan kembali string itu dengan cepat nantinya, tidak memulai membandingkan satu per satu dari elemen pertama. Untuk melakukan ini kita bisa memakan fungsi hash. Sebuah string akan dimasukkan ke sebuah fungsi hash, hasilnya adalah sebuah angka n dari 0-99, lalu kita masukkan string ke array di indeks itu.

Ketika kita ingin mencari apakah sebuah string ada di array atau tidak, kita hash string tersebut, lalu kita cek apakah di elemen ke-n hasil fungsi hash tersebut ada elemennya. Dalam kasus tertentu, setiap string bisa dipetakan ke slot array yang berbeda.

Tentunya ada kemungkinan lebih dari satu string dipetakan ke satu lokasi, istilahnya terjadi collision. Jika ini terjadi, maka kita bisa menyimpan elemen di indeks n+1, jika sudah ada isinya ke n+2, dst. Sebaliknya ketika mencari elemen, jika isi array di posisi itu kosong artinya elemen tidak ada. Tapi jika di posisi itu ada isinya, kita perlu mengecek elemen berikutnya, jika tidak ada maka cek n+1, n+2 dst. Kasus ini tidak ideal, tapi masih lebih baik daripada mencari dari awal sampai akhir array.

Fungsi Hash

Bagaimana mengubah sebuah string menjadi sebuah angka yang kecil (dalam range tertentu, misalnya dalam kasus di atas 0-99)? cara termudah adalah dengan melakukan sebuah operasi, misalnya penjumlahan terhadap masing-masing huruf, lalu hasilnya di-modulo 100.

Menjumlahkan string saja sangat mudah, tapi sangat mudah juga terjadi collision: dua string yang hurufnya sama tapi posisinya beda akan dihash ke nilai yang sama. Cara yang lebih baik adalah dengan menggunakan bobot, misalnya dalam Java caranya adalah dengan menggunakan formula ini:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

Contohnya jika kita punya string ABC (panjangnya 3), maka hasilnya adalah A (65) * 31*31 + B (66)*31 + C (67) = 64578. Sekarang kebetulan 2 hash yang sama lebih kecil, tapi tetap cukup mudah jika dilakukan dengan sengaja. Contohnya string “B#C” akan memiki hash yang sama dengan “ABC”. Ini bisa dicari dengan mudah karena dengan rumus di atas, jika saya ubah A menjadi B (bergeser 1) maka nilai hash akan bertambah sebesar (1*31*31 = 961), dan ini bisa dikompensasi dengan mengurangi karakter kedua sebanyak 31 (dari B menjadi #).

Sebagai catatan: fungsi hash code Java hanya akan menghasilkan angka 32 bit (akan dilakukan modulo jika lebih). Artinya jika kita punya 2^32 + 1 string yang unik, maka pasti ada setidaknya sepasang yang hashnya sama.

Fungsi hash ini belum cukup aman untuk tujuan kriptografi. Saya pernah menemui aplikasi yang memakai ini untuk fungsi hash penting dan ini merupakan kesalahan fatal.

Fungsi Hash Kriptografi

Fungsi hash kriptografi berbeda dari fungsi hash biasa, tapi memiliki kesamaan. Di dalam keduanya kita ingin “mengkompress” data menjadi bilangan kecil. Dalam hash kriptografi, kita ingin agar:

  • jika data diubah sedikit saja, hashnya akan berbeda
  • dari sebuah nilai hash X, kita tidak bisa membentuk sebuah input sedemikian sehingga hashnya adalah X

Jika tiap kali diberikan sebuah hash X dan kita selalu bisa menemukan sebuah string sedemikian hingga hashnya X tertentu maka fungsi hash tersebut berhasi dipecahkan (broken).

Kegunaan hash ini:

  • Untuk verifikasi data yang besar. Jika hashnya tidak berubah, berarti data tidak diutak-atik
  • Untuk penyimpanan password

Untuk penyimpanan password, prinsipnya seperti ini:

  • ketika password dibuat, password akan dihash dan hashnya yang disimpan
  • ketika login, password yang dimasukkan akan dihash, dan nilai hashnya akan dibandingkan

Fungsi hash kriptografi biasanya minimal memiliki output 128 bit. Artinya jika kita mencoba 340282366920938463463374607431768211456 + 1 data yang unik, maka akan ada 1 yang hashnya sama. Sebagai latihan, cobalah membagi bilangan itu dengan jumlah detik dalam setahun, lalu asumsikan 1 detik bisa melakukan berapa operasi hash, dan kalikan dengan 1 milyar komputer di dunia, hasilnya merupakan bilangan tahun yang sangat besar.

Saat ini ada banyak fungsi hash dan biasanya akan menghasilkan string biner antara 128-512 bit. Atau jika dijadikan karakter heksa: antara 32 – 128 karakter heksa. Beberapa fungsi hash yang umum adalah MD5, SHA1, SHA256, SHA512.

Detail fungsi hash tidak saya bahas di sini, dan biasanya tidak penting detailnya bagi seorang programmer. Berbagai bahasa sudah memiliki library untuk melakukan hashing.Pemahaman diperlukan jika ingin mempelajari keamanannya atau ingin mengetahui bagaimana cara kerja berbagai serangan (seperti hash length extension attack).

Mari kita lakukan hal praktis saja, berikut ini contoh berbagai string yang dihash dan outputnya, perhatikan bahwa Perbedaan sedikit saja akan menghasilkan nilai hash yang sangat berbeda.

Cracking Hash

Cara cracking sebuah hash agar kembali menjadi passsword pada dasarnya ada dua:

  • Dictionary attack
  • Bruteforce attack

Jadi diberikan sebuah kamus kata, kita hash masing-masing kata dan cek apakah hashnya sama dengan yang kita cari. Dalam brute force, kita akan mencoba semua kemungkinan karakter seperti AAAA, AAAB, dst sampai semua simbol dicoba.

Beberapa situs sudah menyimpan hash dari database password yang umum, sehingga jika kita cari hashnya, maka kita akan mendapatkan passwordnya.
Tentunya menyimpan kombinasi hash dan password butuh penyimpanan yang besar, untuk mengatasi ini bisa digunakan Rainbow table.

Penutup

Topik hashing ini belum lengkap, dan masih akan diteruskan di bagian berikutnya. Topik yang belum dibahas antara lain:

  • salting
  • kesalahan pemakaian hash
  • HMAC

Kriptografi RSA Praktis

Topik kriptografi RSA sudah dibahas di banyak artikel dan video oleh orang lain, jadi di sini saya hanya ingin memberikan mengenai topik RSA secara praktis. Praktis di sini artinya saya akan memakai tool openssl dan kode Python untuk melakukan enkripsi.

Prinsip RSA adalah: bahwa kita bisa mencari 3 bilangan: n, e, dan d sedemikian hingga relasi ini terpenuhi.

{\displaystyle (m^{e})^{d}\equiv m{\pmod {n}}}

Jika diketahui e dan n, kita tidak bisa dengan mudah mencari tahu nilai d. Dalam RSA: public key adalah n dan e, sedangkan private key adalah d dan n.

Saya tidak akan menjelaskan secara matematis bagaimana menghasilkan key RSA ini. Penjelasannya bisa dibaca di Wikipedia, tapi ada beberapa hal dasar yang perlu diketahui:

  • n adalah perkalian dari dua buah bilangan prima p dan q.
  • Jika kita bisa memfaktorkan n, maka kita bisa mencari nilai private key.
  • Saat ini tidak ada algoritma efisien untuk memfaktorkan n

Mari kita coba hasilkan key RSA yang kecil . openssl minimum bisa menghasilkan key sangat kecil, sampai 16 bit, tapi tidak bisa melakukan enkripsi dengan key 16 bit karena adanya pengecekan security ekstra (nilai e > n). Jadi untuk contoh ini saya gunakan ukuran key 32 bit:

openssl genrsa -out private.pem 32

Nanti kita akan mendapatkan key yang sangat kecil dalam format base64 , seperti ini.

-----BEGIN RSA PRIVATE KEY----- MCwCAQACBQDLlcMZAgMBAAECBQCZk2iBAgMA7bsCAwDbOwICcwcCAjUFAgJMcw== 
-----END RSA PRIVATE KEY-----

Ini hanya contoh, ketika dijalankan ulang, perintah ini akan menghasilkan key baru. Karena hanya 32 bit, ada kemungkinan key yang dihasilkan bisa sama.

Catatan penting: RSA key yang disarankan saat ini adalah 2048 bit atau lebih. Penggunaan key 32 bit di sini hanya untuk penjelasan, supaya mudah diikuti dan dihitung manual.

Kita bisa mendapatkan public keynya dengan cara seperti ini:

openssl rsa -in private.pem -pubout -out public.pem

Contoh hasilnya seperti ini:

-----BEGIN PUBLIC KEY-----
MCAwDQYJKoZIhvcNAQEBBQADDwAwDAIFAMuVwxkCAwEAAQ==
-----END PUBLIC KEY-----

Di dasar teori RSA kita diberi tahu tentang bilangan p, q, N dsb. Tapi kenapa disimpan dalam bentuk teks seperti itu? Salah satu format standar penyimpanan key adalah PEM, di mana data diencode dengan menggunakan ASN.1. Mungkin lain kali saya akan membahas detail berbagai format key, certificate, dsb, tapi untuk saat ini saya akan membahas hal praktisnya saja.

Format ASN1 bisa diparse dengan asn1parse seperti ini:

$ cat private.pem | openssl asn1parse
0:d=0 hl=2 l= 44 cons: SEQUENCE
2:d=1 hl=2 l= 1 prim: INTEGER :00
5:d=1 hl=2 l= 5 prim: INTEGER :CB95C319
12:d=1 hl=2 l= 3 prim: INTEGER :010001
17:d=1 hl=2 l= 5 prim: INTEGER :99936881
24:d=1 hl=2 l= 3 prim: INTEGER :EDBB
29:d=1 hl=2 l= 3 prim: INTEGER :DB3B
34:d=1 hl=2 l= 2 prim: INTEGER :7307
38:d=1 hl=2 l= 2 prim: INTEGER :3505
42:d=1 hl=2 l= 2 prim: INTEGER :4C73

Tapi itu sulit dimengerti, kita bisa mengekstrak berbagai nilai dengan lebih jelas seperti ini:

$ cat private.pem | openssl rsa -text
RSA Private-Key: (32 bit, 2 primes)

modulus: 3415589657 (0xcb95c319)

publicExponent: 65537 (0x10001)

privateExponent: 2576574593 (0x99936881)

prime1: 60859 (0xedbb)

prime2: 56123 (0xdb3b)

exponent1: 29447 (0x7307)

exponent2: 13573 (0x3505)

coefficient: 19571 (0x4c73)

writing RSA key

-----BEGIN RSA PRIVATE KEY-----

MCwCAQACBQDLlcMZAgMBAAECBQCZk2iBAgMA7bsCAwDbOwICcwcCAjUFAgJMcw==

-----END RSA PRIVATE KEY-----

Di sana bisa terlihat bahwa n (modulus) adalah 34155896571. Nilai e (public exponent) adalah 65537 dan nilai d (private exponent) adalah 2576574593. Dua bilangan prima p dan q ada di prime1 (p=60859) dan prime2 (q=56123). Nilai exponent1 adalah d mod (p-1) dan exponent2 adalah d mod (q-1) sedangkan coefficient adalah (inverse of q) mod p.

Berbagai nilai disimpan untuk memudahkan perhitungan walaupun redundan. Contoh: nilai exponent 1 adalah d mod (p-1) = 2576574593 mod (60859-1) = 29447, jadi sebenarnya jika exponent1 tidak disimpan, bisa dihitung dari nilai p dan q yang sudah ada. Bahkan untuk enkripsi/dekripsi saja, nilai d, e dan n sudah cukup. Nilai redundan ini kadang memudahkan atau mempercepat perhitungan tertentu dan ini merupakan hal standar yang diatur di  RFC 3447 (Section A.1.2 RSA Private Key Syntax).

Supaya lengkap, ini isinya jika kita periksa isi public key di atas:

$ cat public.pem | openssl rsa -pubin -text 
Public-Key: (32 bit)
Modulus: 3415589657 (0xcb95c319)
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MCAwDQYJKoZIhvcNAQEBBQADDwAwDAIFAMuVwxkCAwEAAQ==
-----END PUBLIC KEY-----

Sekarang kita bisa mengenkrip sebuah kata pendek. Contohnya saya buat file ini (parameter -n agar tidak ada karakter \n di file hasil):

echo -n Helo > input.txt

Atau jika dilihat representasi heksadesimalnya:

hexdump -C input.txt 
00000000 48 65 6c 6f |Helo|
00000004

Atau dalam bentuk bilangan: 0x48656c6f

Sekarang inputnya kita enkrip

cat input.txt | openssl rsautl -encrypt -pubin -raw -inkey public.pem >message.encrypted

Karena key sangat kecil (32 bit) kita hanya bisa mengenkrip data yang sangat kecil. Di sini saya memakai format “raw” (dengan adanya parameter -raw), alias tanpa padding sama sekali dan hasil datanya adalah “mentah” apa adanya tanpa header apapun. Sekarang mari kita lihat hasilnya:

$ hexdump -C message.encrypted 
00000000 1c fb ca d5 |….|
00000004

Hanya 4 byte (32 bit), atau dalam bentuk bilangan adalah: 0x1cfbcad5 atau 486263509 desimal. Rumus enkripsi RSA adalah:

 c \equiv m^e \pmod{n}

Sekarang kita bisa menghitung manual apakah benar enkripsi Helo (0x48656c6f) adalah 0x1cfbcad5. Untuk mudahnya kita gunakan Python . Di Python operator dua asterisk (**) adalah perpangkatan dan persen(%) adalah modulus :

>> print(hex(0x48656c6f**(65537) % (3415589657)))
0x1cfbcad5L

Hasilnya sesuai harapan, sama dengan hasil enkripsi openssl. Operasi perpangkatan lalu mod akan butuh waktu lama jika dilakukan pada bilangan yang lebih besar. Python punya fungsi built in untuk perpangkatan dengan modulus, seperti ini:

>> print(hex(pow(0x48656c6f, 65537, 3415589657)))
0x1cfbcad5L

Sekarang mari kita dekrip kembali message.encrypted

cat message.encrypted | openssl rsautl -decrypt -raw -inkey private.pem > result.txt

Bisa dicek bahwa output result.txt adalah Helo. Kita juga bisa mendekrip secara manual dengan Python

{\displaystyle c^{d}\equiv (m^{e})^{d}\equiv m{\pmod {n}}}

Atau dalam kode Python:

>> print(hex(pow(0x1cfbcad5, 2576574593, 3415589657)))
0x48656c6fL

Atau jika ingin dalam bentuk teks:

print(bytearray.fromhex("%08x" % (pow(0x1cfbcad5, 2576574593, 3415589657))))
Helo

Demikian artikel perkenalan RSA yang praktis. Dalam artikel ini belum dibahas mengenai hal yang penting yaitu: padding, proteksi password (key di atas tidak dienkrip) dan beberapa kelemahan RSA di kasus tertentu. Hal-hal tersebut rencananya akan saya bahas di tulisan lain.

Security dari level bit (Bagian 2: Encoding Teks)

Saat ini masih banyak user dan juga programmer yang masih bingung dengan masalah “encoding” teks (misalnya ASCII, ISO8859-1, UTF-8, UTF-16, UTF32, dsb). Ini merupakan hal dasar yang penting, baik untuk keperluan sehari-hari maupun dalam bidang security. Ada beberapa attack yang berhubungan dengan encoding teks ini.

Encoding teks dan security

Sekilas topik encoding sepertinya hal yang membosankan dari sudut pandang security, tapi ada ada banyak masalah security yang berhubungan dengan encoding teks. Beberapa contohnya:

  • phishing attack memanfaatkan Unicode character yang mirip (character ambiguity)
  • phishing menggunakan karakter khusus untuk marker RTL (right to left) dan LTR (left to right)
  • Encoding alternatif untuk membypass filter
  • Overlong UTF-8 encoding attack
  • Membuat shell code yang bisa lolos encoding tertentu
  • Buffer overflow karena kesalahan penanganan encoding

Berbagai hal di atas sulit dijelaskan jika tidak memiliki pemahaman yang baik mengenai encoding, jadi di tulisan kali ini saya akan membahas mengenai text encoding.

Hadiah ke Hong Kong yang saya menangkan tahun lalu pada dasarnya adalah masalah encoding shellcode supaya lolos dari filter UTF-8. Lengkapnya bisa dibaca di blog saya yang lain.

Sekilas mengenai berbagai encoding

Di bagian ini saya hanya akan bercerita dulu mengenai sejarah encoding. Setalah itu di bagian berikutnya saya akan menuliskan lebih detail mengenai berbagai encoding yang disinggung di bagian ini. Memahami sejarah akan menjawab pertanyaan penting: kenapa sih kok ribet banget harus ada banyak encoding? kalau dari dulu cuma ada satu kan nggak ribet.

Dulu teknologi komputer berkembang pesat dimulai dari Amerika, dan ini memberi pengaruh besar terhadap encoding teks. Ketika membaca berbagai buku atau artikel komputer biasanya dijelaskan bahwa ada tabel ASCII yang memetakan huruf ke angka (atau sebaliknya). ASCII ini merupakan singkatan dari American Standard Code for Information Interchange, dan sesuai namanya ini asalnya dari Amerika.

Sejarah ASCII ini cukup panjang, tapi singkatnya ini adalah standard yang memetakan angka ke simbol (huruf, angka, spasi, tanda baca) serta beberapa karakter kontrol (seperti tanda end of file, new line, dsb). Standard ini hanya memetakan 128 simbol (hanya butuh 7 bit, atau disebut juga sebagai 7-bit character set), karena banyak komputer awal yang memakai 8 bit, maka masih ada 128 simbol yang bisa ditambahkan (istilahnya 8-bit character set).

Berbagai negara menggunakan 128 karakter pertama dari ASCII dan sisanya dipetakan ke karakter masing-masing negara/wilayah. Jadi biasanya kode 65 berarti huruf A di negara manapun, tapi kode 150 bisa menjadi karakter yang berbeda di negara lain. Jadi tiap negara memilki “code page”, atau “character set” yang berbeda. Ada standar ISO-8859 (ISO-8859-1 sampai ISO-8859-16) yang menyatakan “character set” untuk tiap wilayah/bahasa.

Dengan cara ini kita masih bisa dengan cukup mudah mencampurkan teks dua bahasa antara bahasa Inggris dengan satu bahasa lain (misalnya Thai), tapi jika harus mencampurkan lebih dari itu makan akan jadi sangat sulit. Untuk mengatasi ini maka dibuatlah standard Unicode.

Dalam Standard Unicode, hanya ada satu tabel besar untuk semua huruf di dunia ini, setiap simbol dipetakan ke sebuah nomor code point. Tentunya tabel ini sangat besar, jadi 1 karakter tidak bisa lagi dipetakan ke 8 bit (1 byte). Salah satu cara menyimpan karakter unicode adalah dalam bentuk integer 32 bit (4 byte). Cara ini boros memori (karena 1 huruf butuh 4 byte) tapi mudah diproses.

Dalam kehidupan sehari-hari, jarang sekali kita memakai semua huruf di dunia ini dalam satu dokumen, jadi ada cara untuk menghemat memori: kita hanya memakai 16 bit saja. Karena 16 bit hanya bisa dipetakan ke 65536 karakter maka hanya sebagian unicode saja yang dipakai. Unicode dibagi menjadi banyak “plane”. Sebuah plane ini sekedar range code point saja, artinya cuma sekedar nama untuk simbol kesekian sampai kesekian. Plane yang pertama adalah Basic Multilingual Plane (BMP) yang berisi hampir semua karakter di bahasa modern saat ini. Dengan 16 bit, kita bisa memakai BMP saja, dan ada cara khusus untuk memasukkan karakter dari plane lain dengan menggunakan surrogate.

Tapi 16 bit pun kadang masih terlalu boros memori. Berbagai sistem komputer masih sangat berpusat pada American/English, ini meliputi: berbagai perintah command line, berbagai keyword di berbagai bahasa pemrograman dan berbagai standar lain. Jadi seringnya kita hanya perlu menyimpan karakter ASCII, dan sesekali baru butuh karakter bahasa lain.

Dengan pemikiran ini maka ada yang namanya encoding UTF-8 (Unicode Transformation Format-8 bit). Dengan encoding ini, karakter yang ada di tabel ASCII hanya perlu 1 byte, karakter lain di luar itu bisa memakai 2, 3, atau 4 byte. Ini istilahnya adalah variable length encoding. Sebagai catatan ada encoding lain (UTF-1 yang tidak lagi dipakai dan UTF-7 yang juga bukan standar).

Byte order marker

Dalam hampir semua komputer modern, data dibagi dalam byte dan ketika kita berurusan dengan bilangan yang lebih dari 1 byte, kita punya pilihan urutan: little endian dan big endian. Contohnya angka 299 desimal adalah 0x12b dalam heksadesimal. Di memori, ini bisa dituliskan 0x12 0x0b (little endian), atau 0x0b 0x12 (big endian).

Untuk encoding UTF16 dan UTF32, ada pilihan big endian (BE) atau little endian (LE) ketika menyimpan file. Jika data big endian dibaca menjadi little endian, maka hasilnya akan salah, misalnya angka 299 desimal tadi bisa terbaca jadi 2834 desimal. Pada teks Unicode, kita bisa menambahkan BOM (byte order marker) di awal sebuah file untuk menunjukkan file ini disimpan dalam big endian atau little endian.

UTF-32


Seperti telah disinggung di atas:

  • Ini merupakan encoding paling sederhana, hanya array of 32 bit integer
  • ini merupakan encoding paling boros memori
  • untuk mengakses karakter ke n, kita tinggal mengakses elemen integer ke n
UTF32-LE dengan Byte order marker

Kita bisa mencoba membuat file dengan BabelPad (teks editor gratis yang mendukung Unicode dengan baik) dan menyimpannya dalam UTF-32 lalu membukanya dengan editor teks. Ada beberapa variasi yang bisa dicoba:

  • little endian atau big endian
  • dengan atau tanpa byte order marker. Untuk Little endian akan ada FF FE 00 00 di awal dokumen dan untuk big endian 00 00 FE FF
UTF32-LE dengan BOM

Perhatikan urutan byte-byte untuk big endian dan little endian. Untuk teks latin, terlihat sekali boros memori karena banyak byte 00 (dalam huruf latin tiap 1 huruf hanya memakai 1 byte). Sekarang saya contohkan isi teks yang berisi bahasa lain (Thai). Terlihat bahwa karakter Thai memerlukan 2 byte, dan karakter emoji senyum bahagia memakai 3 byte.

UTF-16

Encoding UTF-16 atau UCS-2 (Universal Character Set, 2 Byte) menggunakan 2 byte (16 bit) sebagai unit penyimpanannya. Karena sebagian besar huruf di dunia ini masuk ke basic multilingual plane (BMP), maka sering kali 16 bit sudah cukup (contoh tulisan/bahasa yang masuk ke BMP: Inggris, Arab, Thai, Kanji, Bali, Sunda, Batak). Hanya di kasus tertentu kita perlu menyimpan dengan cara khusus (misalnya emoji yang baru). Di dalam Unicode surrogates.

UTF-16-LE dengan BOM

Kita bisa menyimpan teks sebelumnya dengan encoding UTF-16 dan melihat harsilnya. Terlihat bahwa untuk karakter latin dan Thai, hanya dibutuhkan 2 byte untuk satu karakter, sedangkan untuk karakter emoji dibutuhkan 4 karakter. Empat karakter ini terdiri dari high surrogate (H) dan low surrogate (L) yang perlu dipasangkan untuk merepresentasikan karakter di luar BMP. Formulanya:

0x10000 + (H – 0xD800) x 0x400 + (L – 0xDC00)

Contoh: karakter emoji di contoh tersebut adalah Face with Tears of Joy. Emoji ini memiliki Code Point 128514 atau 0x1F602 dan dengan surrogate dapat direpresentasikan sebagai: 0xD83D 0xDE02. Ini bisa dicek dengan memasukkan angka pertama dan kedua ke formula di atas:

0x10000 + (0xD83D – 0xD800) *0x400 + ( 0xDE02 – 0xDC00)

Sebaliknya kita bisa mendapatkan nilai H dan L untuk sebuah code point C dengan:

L = 0xDC00 + ((C – 0x10000) mod 0x400) H = 0xD800

UTF-16-BE dengan BOM

Di sini mulai ada masalah security: apa jadinya jika ada high surrogate yang tidak memiliki pairing low surrogate? Beberapa sistem mungkin akan mengabaikan bagian itu saja lalu meneruskan memproses datanya, sedangkan sistem lain mungkin akan menolak data yang masuk atau di kasus lain justu meneruskan data apa adanya.

Dalam encoding 16 bit, di bahasa low level seperti C biasanya untuk mengakses karakter ke N kita bisa langsung menggunakan X[n], tapi jika ada karakter surrogate ini tidak benar. Program yang berusaha mengakses dengan cara ini bisa memiliki bug. Cara yang benar biasanya dengan menggunakan iterator, menggunakan accessor khusus (charAt) atau membuat representasi 32 bit di memori agar lebih mudah diakses.

UTF-8

Saat ini UTF-8 merupakan encoding yang palign banyak digunakan, karena sangat hemat penyimpanan (untuk berbagai protokol Internet) dan kompatibel dengan ASCII (untuk 128 huruf pertama). Encoding ini sedikit lebih rumit dari yang lain: untuk code point 0 sampai 0x7f dibutuhkan hanya 1 byte, 0x80 sampai 0x7ff butuh 2 byte, 0x800 sampai 0xffff butuh 3 byte, dan sisanya butuh 4 byte.

Sebagai catatan: UTF-8 tidak memiliki endiannes, tapi kadang file teks di beri byte order mark untuk sekedar menandai bahwa file tersebut menggunakan encoding UTF-8. Adanya BOM kadang membingungkan aplikasi tertentu, jadi kadang ini perlu dihilangkan.

UTF-8 dengan BOM
Tabel diambil dari Wikipedia

Kali ini saya tidak akan membahas detail mengenai UTF-8 karena artikel ini sudah terlalu panjang, dan topik UTF-8 sendiri bisa cukup panjang. Ada beberapa artikel security yang berhubungan dengan UTF-8 ini, misalnya mengenai overly long encoding dan mengenai shellcode yang merupakan teks UTF-8 valid.

ISO 8859 dan encoding lain

Seperti telah dijelaskan sebelumnya bahwa 8 bit tidak cukup untuk mengencode semuanya, maka beberapa konversi bisa dilakukan dan beberapa tidak bisa dilakukan.

  • Teks dalam encoding ISO-8859-X apapun bisa dijadikan Unicode (dengan encoding apa saja)
  • Teks dalam encoding ISO-8859-X hanya bisa dipetakan ke ISO-8859-Y jika semua karakter di teks itu kebetulan ada di target
  • Teks Unicode yang hanya mengandung satu bahasa (atau Inggris dan satu bahasa lain) biasanya bisa diencode ke salah satu ISO-8859-X

Sekarang ini UTF-8 sudah sangat populer dan biasanya encoding lama ini hanya digunakan untuk berinteraksi dengan hardware ataupun software lama.

Penutup

Semoga tulisan ini bisa memberi penjelasan yang bisa dipahami mengenai masalah encoding teks. Saya merasa ilmu encoding teks ini sangat berguna dalam masalah security dan non security, semoga hal tersebut juga berlaku untuk Anda.

Security dari level bit (Bagian 1: Encoding Base N)

Di masa awal kuliah ilmu komputer, mahasiswa diajari berbagai macam hal dasar mengenai komputer. Hal dasar pertama adalah bahwa data bisa dikonversi menjadi bilangan (data di sini berupa teks, foto, video, suara, dsb). Lalu setiap bilangan bisa dikonversi dalam representasi biner supaya bisa diproses oleh komputer digital. Beberapa pelajaran awal arsitektur komputer akan membahas mengenai gerbang logika, lalu bagaimana gerbang logika ini bisa dipakai untuk menjumlahkan. Setelah kita bisa menjumlahkan maka kita juga bisa mengurangi (di sini akan diajarkan mengenai komplemen 2). Setelah bisa menjumlahkan, kita akan bisa mengalikan.

Biasanya mahasiswa masih mengerti konsep-konsep tersebut ketika diajarkan, walau mungkin belum bisa menyambungkan ilmunya dengan komputer yang ditemui sehari-hari. Di masa-masa akhir kuliah, berbagai topik praktis diajarkan. Di sini mahasiswa akan bisa membuat web atau aplikasi praktis. Tapi biasanya ada gap pengetahuan dari hal yang dasar di tahun pertama dengan hal praktis di tahun terakhir.

Hex editor bukan benda ajaib

Contoh yang sederhana adalah masalah manipulasi bit atau pemahaman bahwa berbagai manipulasi data bisa dipahami dari level biner. Kebanyakan orang akan bingung berhadapan dengan hex editor untuk melihat data sebuah file, dan bahkan menganggap kalau hex editor (saja) cukup untuk menghack apa saja.

Supaya praktis, di posting ini saya hanya akan membahas manipulasi bit untuk encoding base N. Di posting lain saya akan membahas encoding teks di level biner (terutama teks Unicode).

Banyak orang yang akan bingung jika diminta mengimplementasikan base64 tanpa memakai library, meskipun sering kali melihat data dalam bentuk base64 dan mengerti pemakaiannya di berbagai tempat (misalnya di aplikasi web). Dalam kebanyakan bahasa pemrograman, programmer memang tinggal pake dan tidak penting untuk tahu detailnya.

Lalu kenapa perlu tahu implementasi base64 (atau base N secara umum) di bidang security? Jika bidangnya adalah analisis malware, banyak malware memakai tabel base64 yang custom. Untuk satu topik khusus ini (base64) sudah ada banyak pembahasan, tool, dan triknya sehingga gampang dicari. Tapi jika tidak paham prinsipnya, jika ada kasus yang sedikit berbeda maka Anda akan kebingungan.

Base64 saya ambil sebagai contoh untuk satu hal yang berhubungan dengan representasi/manipulasi bit tapi masih cukup sederhana. Banyak hal security yang berhubungan dengan representasi bit, contoh praktis sederhana adalah format private key di sebuah SSL certificate. Contoh yang lebih kompleks adalah row hammer attack untuk melakukan bit-flipping pada RAM atau SPA (simple power analysis) untuk mendapatkan key RSA jika implementasinya tidak aman.

Masih kembali ke contoh praktis pemakaian base N:

  • base85: dipakai dalam format PDF
  • base64: untuk berbagai data di web (misalnya data URI di web)
  • base58: dipakai untuk bitcoin address
  • base36: untuk safe url encoding
  • base32: dipakai untuk geohash dan di beberapa game Nintendo lama
  • base16: representasi heksadesimal (hex), dipakai di mana-mana
  • base8: representasi oktal dipakai di mode/permission file di OS UNIX based

Sebenarnya semua di atas hanyalah masalah “basis bilangan”. Dalam biner kita memakai dua simbol saja: 0 dan 1. Dalam oktal kita memakai simbol dari 0 sampai 7. Dalam desimal kita memakai sepuluh simbol dari 0 sampai 9. Dalam heksadesimal, kita menyetujui untuk memakai 0 sampai 9, ditambah dengan a sampai f. Tapi untuk basis bilangan selain itu, ada banyak varian huruf yang bisa kita gunakan.

Untuk contoh berikutnya, saya akan menggunakan data ini yang akan di encode: “Hello”. Untuk memudahkan, saya akan menggunakan encoding ASCII. Di tulisan berikutnya saya akan bahas mengenai berbagai encoding teks yang lain.

Angka di bawah masing-masing huruf adalah nomor huruf tersebut dalam tabel ASCII. Di bawahnya saya tampilkan juga representasi biner dari angka tersebut. Konversi basis 2 ke basis lain yang bisa direpresentasikan dalam 2 pangkat n (8, 16, 32, 64) bisa dilakukan sangat mudah. Representasi oktal jarang digunakan untuk mengencode data, jadi saya akan langsung ke heksadesimal dan base 2 pangkat n berikutnya (base32, base64).

Heksadesimal (base 16)

Dalam heksadesimal (2 pangkat 4), tiap 4 bit kita translasikan menjadi 1 simbol.

Atau singkatnya, kata hello bisa diencode dalam heksadesimal menjadi “48656c6c6f”. Secara praktis ini bisa dilakukan dengan berbagai cara. Misalnya dengan shell command ini:

echo -n Hello| xxd -p

Dan untuk mengembalikannya

echo -n 48656c6c6f|xxd -r -p

Konversi dari biner ke heksadesimal bisa dilakukan dengan tabel semacam ini. Tapi supaya singkat saya bisa mengatakan bahwa simbol untuk base16 adalah: “0123456789abcdef”. Menggunakan string seperti ini akan lebih mudah ketika kita memakai encoding base N yang lebih besar.

Base32

Berikutnya ke base32: untuk ini kita bisa memakai simbol “ABCDEFGHIJKLMNOPQRSTUVWXYZ234567”. Kenapa dipilih karakter-karakter itu, kenapa bukan “ABCDEFGHIJKLMNOPQRSTUVWXYZ012345” (atau angka dulu dan sisanya huruf). Ini dipilih karena beberapa alasan:

  • Supaya tidak tertukar antara 0/O, 1/I
  • Dalam kasus tertentu jika 0..9 ada di depan, nanti disangka heksadesimal

Tentunya jika kita membuat aplikasi sendiri yang tidak perlu interoperasi dengan aplikasi lain, kita bisa membuat tabel sendiri. Tabel yang standar ini diambil dari http://www.rfc-editor.org/rfc/rfc4648.txt

Karena 32 merupakan 2 pangkat 5, kita bisa membagi per 5 bit. Gambar di bawah ini adalah serangkaian bit di gambar pertama, tapi dikelompokkan jadi 5 bit per group. Jadi di kotak pertama tercantum ‘01001’ atau 9 dalam desimal. Karakter ke 9 (dihitung dari 0) dari string “ABCDEFGHIJKLMNOPQRSTUVWXYZ234567” adalah huruf J. Demikian seterusnya.

Kita bisa mengecek hal di atas dengan menggunakan modul Python base64 (meski namanya base64, modul ini mendukung base 16, 32, 64, dan 85).

import base64 
print base64.b32encode("Hello") #hasilnya: JBSWY3DP 
print base64.b32decode("JBSWY3DP") #hasilnya: Hello 

Base64

Contoh base64 juga sangat serupa karena 64 bit juga merupakan 2 pangkat n, dengan n = 6. Di contoh base32 kebetulan jumlah bitnya kelipatan 5, nah bagaimana jika jumlah bit tidak kelipatan n? kita biarkan bagian terakhir kurang dari n bit. Berikut ini contohnya dalam base64:

Sebenarnya string di atas sudah cukup sebagai representasi base64, tapi dalam konvensi biasanya ditambahkan padding karakter ‘=’ jika input bukan kelipatan 3. Contohnya karakter ‘A’ jika diencode akan jadi ‘QQ==’, dan ‘AA’ jadi ‘QUE=’, tapi AAA karena kelipatan 3 jadi ‘QUFB’.

Kita bisa memakai program base64 untuk mengencode dan decode base64:

echo -n Hello| base64

Dan untuk mendecodenya (di Linux gunakan -d huruf kecil, sedangkan gunakan parameter kapital -D jika Anda memakai OS X atau BSD):

echo -n SGVsbG8= | base64 -d

Atau dalam Python:

import base64 
print base64.b64encode("Hello") #hasilnya: SGVsbG8=
print base64.b64decode("JBSWY3DP") #hasilnya: Hello

Sebagai catatan: ada banyak varian base64, tergantung dari karakter yang digunakan (lengkapnya bisa dilihat di Wikipedia: https://en.wikipedia.org/wiki/Base64)

Base58

Seperti sudah dibahas sebelumnya: jika basisnya dalam bentuk 2 pangkat n, maka ini mudah dilakukan, cukup dengan memecah per n bit. Dalam implementasinya ini biasanya dilakukan dengan menggunakan operator SHIFT dan AND. Untuk basis lain, kita bisa menggunakan pembagian berulang. Karena pembagian berulang ini kurang efisien, base yang lain biasanya dipakai untuk menyimpan data yang tidak besar (contohnya: bitcoin address memakai base58, integer dalam PDF memakai base85).

Saya akan memberikan contoh encoding base58 untuk data di atas. Pertama kita gabungkan semua digit biner, lalu konversi bilangan biner menjadi desimal. Konversi ke desimal ini agar sekedar mudah dibaca manusia saja. Angka biner 0100100001100101011011000110110001101111 dalam desimal adalah 310939249775. Kita lakukan pembagian berulang dengan 58 dan catat sisanya. Sisanya ini dipakai untuk menjadi indeks bagi tabel: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz

Hasilnya kita balik dari bawah ke atas yaitu: 9Ajdvzr. Kita bisa mengecek ini dengan modul base58 di python (perlu diinstall terpisah dengan pip install base58).

import base58 
print base58.b58encode("Hello")
print base58.b58decode("9Ajdvzr")

Penutup

Ini merupakan satu bagian saja dari berbagai tulisan dasar mengenai ilmu komputer dan security. Semoga saya bisa terus sabar menuliskan keseluruhan topiknya.

Adakah satu buku yang bisa mengajarkan topik security ini dari level bit? setahu saya tidak ada. Buku Silence on the wire memberikan overview yang bagus mengenai banyak masalah security dari level bit (bahkan membahas singkat dari mulai algabar boolean), tapi topik-topik lain perlu dipelajari di berbagai teks dasar mengenai ilmu komputer.

Ilmu komputer dan security

Seringkali ketika ingin menuliskan topik security, saya bingung mulai dari mana karena banyak sekali topik security yang butuh dasar ilmu komputer yang baik. Tanpa satu dasar yang baik, penjelasan topik security bisa jauh ke mana-mana. Selain itu saya juga sering dapat pertanyaan yang aneh-aneh.

Contoh pertanyaan konyol yang sering saya dapatkan adalah: saya diberi string dalam base64 dan ditanya apa artinya. Atau bagaimana mendecode string heksa yang adalah sebuah hash. Ini sama saja dengan bertanya: 763748 itu angka apa? bisa berupa apa saja, mungkin nomor Induk mahasiswa, mungkin sisa saldo rekening Anda, 6 digit terakhir nomoer telepon gebetan Anda, PIN iPad kakek Anda, dsb.

Kalau bisa melihat isi gembok, lebih gampang membukanya tanpa kunci

Di sisi lain ada pertanyaan yang benar, tapi sulit dijawab dengan singkat, misalnya mengenai SSL pinning. Saya tadinya ingin menuliskan tentang SSL pinning dan bagaimana caranya membypass hal tersebut. Supaya pembahasannya tuntas, saya perlu membahas banyak hal.

Hal pertama adalah cara kerja SSL, yang melibatkan penggunaan public key cryptography. Di sini perlu diketahui mengenai kunci publik, kunci privat. Kemudian perlu dijelaskan mengapa SSL pinning ini diperlukan. Lalu perlu dijelaskan mengenai certificate (yang bisa ada dalam berbagai format). Perlu dijelaskan juga bagaimana mendapatkan sertifikat SSL ini atau bagaimana membuat sendiri (untuk self signed certificate). Dari sertifikat itu, kita perlu mengekstrak informasi hash public key untuk melakukan pinning.

Itu baru sampai topik dasarnya, belum masuk ke bagian: bagaimana SSL pinning diimplementasikan. Untuk Android bisa dengan kode Java (dengan berbagai library), bisa dengan native code (misalnya dengan libcurl + openssl), bisa dengan fitur Android OS 7 ike atas (Network Security Configuration). Untuk iOS pembahasannya juga akan banyak.

Setelah itu baru bisa masuk ke topik bagaimana bypass SSL pinning. Inipun bisa dengan banyak teknik, bisa dengan patching APK/IPA, bisa dengan Frida, bisa dengan berbagai tool yang sudah ada. Biasanya 99% orang yang bertanya ke saya cuma membaca: pakai tool ini atau pakai skrip frida “universal” ini, dan hasilnya nggak jalan, dan mereka bingung harus meneruskan dari mana.

Untuk memudahkan pembahasan berbagai topik security, saya akan mulai menulis berbagai topik kecil yang terpisah. Sebagian topik ini mungkin akan terlalu dasar bagi banyak orang, tapi daripada saya harus menjelaskan ulang puluhan kali, akan lebhi baik jika saya tuliskan saja.

Rencananya topik-topik yang akan saya bahas yang praktis saja, walau kenyataannya dalam hidup ini kita perlu tahu sangat banyak hal untuk dapat mengevaluasi security sebuah sistem. Contohnya baru-baru ini saya membaca mengenai pentingnya ilmu geometri dalam menemukan bug di library grafik.