Bab 4: Dari biner ke representasi tekstual

Daftar Isi Utama

Setelah mengetahui bagaimana transformasi source code ke biner, sekarang saya akan membahas sebaliknya. Bagian ini hanya akan membahas tools-tools dasar untuk melakukan transformasi. Akan dibahas juga sedikit mengenai proteksi yang bisa dilakukan supaya proses reverse engineering menjadi lebih sulit.

Reversing Java

Saya akan memulai dengan yang mudah: reverse engineering kode Java. Ada banyak decompiler untuk Java, beberapa di antaranya:

JD-GUI merupakan aplikasi termudah karena memakai GUI. Untuk mudahnya kita gunakan program di bab sebelumnya.

class Simple {

    private static int doubleIt(int d) {
        return d*2;
    }

    public static void main(String argv[]) {
        System.out.println("hello world");
        System.out.println("double of 100 is: " + doubleIt(100));     
    }
    
}

Perhatikan bahwa untuk program sederhana, hasil decompiler hampir sama persis dengan aslinya.

import java.io.PrintStream;

class Simple
{
  private static int doubleIt(int paramInt)
  {
    return paramInt * 2;
  }
  
  public static void main(String[] paramArrayOfString)
  {
    System.out.println("hello world");
    System.out.println("double of 100 is: " + doubleIt(100));
  }
}

Jika program diproteksi dengan obfuscator, maka sebagian nama akan hilang (misalnya doubleIt bisa hilang). Obfuscator tingkat lanjut juga bisa mengenkripsi string agar lebih sulit dibaca.

Reversing .NET

Reversing .NET bisa dilakukan dengan berbagai decompiler yang ada, misalnya:

  • Telerik Decompiler
  • ilspy

Asumsinya executable .NET ini tidak diproteksi. Ada banyak protektor .NET yang butuh teknis khusus untuk membongkarnya.

Untuk source code sebelumnya

public class Simple
{
   private static int DoubleIt(int a) {
       return 2*a;
   }

   public static void Main()
   {
      System.Console.WriteLine("hello world");
      System.Console.WriteLine("double of 100 is: "  + DoubleIt(100));
   }
}

Hasil ilspy bisa dibilang sama persis dengan source code aslinya.

using System;

public class Simple
{
    private static int DoubleIt(int a)
    {
        return 2 * a;
    }

    public static void Main()
    {
        Console.WriteLine("hello world");
        Console.WriteLine("double of 100 is: " + Simple.DoubleIt(100));
    }
}

Reversing native code

Saat ini tidak ada satu tool yang bisa mengembalikan dari sembarang binary file ke bahasa C. Untuk beberapa arsitektur yang populer (seperti Intel, ARM, MIPS, Power PC) ada decompiler yang bisa mendekompilasi sebagian binary yang ada.

Decompiler

Hal-hal yang menyulitkan pembuatan decompiler native code antara lain:

  • Ada begitu banyak bahasa yang menargetkan ke machine code (seperti bisa dilihat di bab 3)
  • Untuk sebuah bahasa, bisa ada puluhan compiler (contoh: untuk Windows saja ada banyak compiler C: gcc, llvm, Watcom, msvc, Borland C, lcc, dan masih banyak lagi)
  • Ada begitu banyak arsitektur CPU (X86/AMD64/Itanium, Arm, Mips, PowerPC, SH, Xtensa dsb)
  • Ada begitu banyak sistem operasi (dan bahkan ada kode yang tidak menggunakan sistem operasi sama sekali)

Beberapa tool decompiler yang saat ini ada adalah:

  • IDA: tools standar yang digunakan profesional. IDA dapat mendekompilasi program dalam Intel X86/AMD64/ARM/PowerPC. Harga toolnya ratusan dollar (disassembler dasar saja) hingga ribuan dollar (disassembler + decompiler lengkap). IDA dapat berjalan di Windows, OS X dan Linux.
  • Hopper: tools disassembler dengan decompiler terbatas untuk Intel dan Arm. Hopper berjalan di Mac OS X dan Linux (tidak jalan di Windows)
  • Snowman decompiler open source. Akurasinya relatif rendah.

Karena keterbatasan decompiler yang ada, umumnya ketika melakukan reverse engineering binary code, kita akan melihat pada kode assemblynya.

Untuk aplikasi sederhana dalam bahasa C, decompiler bisa memberikan output yang cukup baik, tapi jika aplikasi mulai kompleks atau ditulis dalam bahasa bukan C, hasilnya sulit dibaca.

Coba bandingkan is_serial_valid yang ada di bab 2

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int is_serial_valid(const char *user, const char *serial)
{    
    int i, total;
    for (i=0; i < strlen(user); i++) {
        total += user[i];
    }
    for (i=0; i < strlen(serial); i++) {
        total += serial[i];
    }
    return (((total + 17)*100057)%1871)==812;
}

int main(int argc, char *argv[])
{
    char *user;
    char *serial;
    if (argc<3) {
        printf("Usage: serial3 <user> <serial>\n");
        return 0;
    }
    user = argv[1];
    serial = argv[2];
    
    if (is_serial_valid(user, serial)) {
        printf("correct serial\n");
    } else {
        printf("incorrect serial\n");
    }
}

Hasil Hopper:

function is_serial_valid {
    var_28 = arg0;
    var_30 = arg1;
    var_14 = 0x0;
    while (sign_extend_64(var_14) < strlen(var_28)) {
            var_18 = var_18 + sign_extend_64(*(int8_t *)(var_28 + sign_extend_64(var_14)) & 0xff);
            var_14 = var_14 + 0x1;
    }
    var_14 = 0x0;
    while (sign_extend_64(var_14) < strlen(var_30)) {
            var_18 = var_18 + sign_extend_64(*(int8_t *)(var_30 + sign_extend_64(var_14)) & 0xff);
            var_14 = var_14 + 0x1;
    }
    rax = ((var_18 + 0x11) * 0x186d9 - ((SAR(0x8c1be99 * (var_18 + 0x11) * 0x186d9, 0x6)) - (SAR((var_18 + 0x11) * 0x186d9, 0x1f))) * 0x74f == 0x32c ? 0x1 : 0x0) & 0xff;
    return rax;
}

Dan hasil IDA (IDA) (compile ke amd64)

__int64 __fastcall is_serial_valid(const char *a1, const char *a2)
{
  int i; // [sp+18h] [bp-18h]@1
  int j; // [sp+18h] [bp-18h]@4
  int v5; // [sp+1Ch] [bp-14h]@0

  for ( i = 0; i < strlen(a1); ++i )
    v5 += a1[i];
  for ( j = 0; j < strlen(a2); ++j )
    v5 += a2[j];
  return 100057 * (v5 + 17) % 1871 == 812;
}

Ketika optimasi dinaikkan, IDA masih mampu menghasilkan kode C, namun kode mulai terlihat lebih rumit. Ini masih fungsi yang amat sangat sederhana, ketika fungsi sudah semakin rumit, output decompiler semakin kurang jelas.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ebx@0
  const char *v4; // r12@2
  const char *v5; // rbp@2
  size_t v6; // rax@2
  const char *v7; // rdx@2
  const char *v8; // rax@2
  int v9; // ecx@3
  const char *v10; // rdi@5
  size_t v11; // rax@5
  int v12; // edx@6
  int result; // eax@9

  if ( argc <= 2 )
  {
    puts("Usage: serial3 <user> <serial>");
    result = 0;
  }
  else
  {
    v4 = argv[1];
    v5 = argv[2];
    v6 = strlen(argv[1]);
    v7 = v4;
    v8 = &v4[v6];
    while ( v7 != v8 )
    {
      v9 = *v7++;
      v3 += v9;
    }
    v10 = v5;
    v11 = (size_t)&v5[strlen(v5)];
    while ( v10 != (const char *)v11 )
    {
      v12 = *v10++;
      v3 += v12;
    }
    if ( 100057 * (v3 + 17) % 1871 == 812 )
      result = puts("correct serial");
    else
      result = puts("incorrect serial");
  }
  return result;
}

Sedangkan untuk Pascal, outputnya cukup sulit dibaca karena ada banyak pemanggilan fungsi library pascal di latar belakang (output IDA tetap dalam C). Untuk input seperti ini

program simple;

function double_it(x :integer ):integer;
begin
   double_it := 2*x;
end;


begin
   writeln('hello world');
   writeln('double of 100 is: ', double_it(100));
end.

Output IDA Pro adalah seperti ini, perhatikan bahwa secara default untuk sebuah instruksi writeln('string') saja ternyata ada banyak pemanggilan fungsi (fpc_write_text_shortstr, fpc_iocheck, fpc_writeln_end, lalu fpc_iocheck lagi).

// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax@1
  __int64 v4; // rbx@1
  __int64 v5; // rax@1
  __int64 v6; // rbx@1
  SMALLINT_0 v7; // ax@1
  __int16 v8; // ax@1

  fpc_initializeunits(*(_QWORD *)&argc, argv, envp);
  LODWORD(v3) = fpc_get_output(*(_QWORD *)&argc);
  v4 = v3;
  fpc_write_text_shortstr(0LL, v3, _SIMPLE__Ld1);
  fpc_iocheck(0LL);
  fpc_writeln_end(v4);
  fpc_iocheck(v4);
  LODWORD(v5) = fpc_get_output(v4);
  v6 = v5;
  fpc_write_text_shortstr(0LL, v5, &_SIMPLE__Ld2);
  v7 = fpc_iocheck(0LL);
  v8 = P_SIMPLE_DOUBLE_IT_SMALLINT__SMALLINT(v7, 100);
  fpc_write_text_sint(0LL, v6, v8);
  fpc_iocheck(0LL);
  fpc_writeln_end(v6);
  fpc_iocheck(v6);
  SYSTEM_DO_EXIT(v6);
}

Sedangkan decompilernya Hopper juga kurang jelas (misalnya: kita tidak tahu rbx isinya apa)

function PASCALMAIN {
    stack[2047] = rbx;
    FPC_INITIALIZEUNITS();
    rbx = fpc_get_output();
    FPC_WRITE_TEXT_SHORTSTR(0x0, rbx, 0x422cc0);
    fpc_iocheck();
    fpc_writeln_end(rbx);
    fpc_iocheck();
    rbx = fpc_get_output();
    FPC_WRITE_TEXT_SHORTSTR(0x0, rbx, 0x422cd0);
    fpc_iocheck();
    rax = P$SIMPLE_$$_DOUBLE_IT$SMALLINT$$SMALLINT(0x64);
    fpc_write_text_sint(0x0, rbx, sign_extend_64(rax));
    fpc_iocheck();
    fpc_writeln_end(rbx);
    fpc_iocheck();
    FPC_DO_EXIT();
    return;
}

Tentunya jika kita compile ke MIPS (banyak digunakan di router dan benda networking lain), IDA hanya bisa menampilkan disassemblynya.

Disassembler

Tidak seperti decompiler, disassembler hanya akan menampilkan kode dalam assembly dalam bentuk teks yang bisa dibaca manusia. Seperti telah ditunjukkan pada bagian sebelumnya, sudah ada disassembler sederhana yang bisa digunakan (misalnya objdump). Namun disassembler yang baik akan bisa menunjukkan lebih banyak informasi.

Beberapa disassembler yang banyak dipakai orang adalah:

  • IDA (Ada versi lama yang gratis)
  • Hopper
  • radare

Copyright © 2009-2018 Yohanes Nugroho