[Binary Exploit — EP.5–1] Introduction to Buffer Overflow
ทำไมการที่เราใส่ Input ที่มีขนาดใหญ่มากเกินไปจะสามารถ Hack โปรแกรมได้
หากใครยังจำตัวอย่างโค้ดจาก EP ที่แล้วที่ว่าเมื่อเราป้อนข้อมูลที่มีความยาวมากเกินไปโปรแกรมจะ Error และหยุดทำงานทันที ในบทความนี้เราจะมาไขข้อสงสัยนี้กัน
#include <stdio.h>
int main() {
char buffer[8];
printf("Please put something: ");
scanf("%s",buffer);
// For pausing the debugger
printf("Attach debugger now!");
while(getchar() != '\n');
getchar();
return 0;
}Prerequisite
ต้องบอกไว้ก่อนว่า Windows ได้มีการวางมาตราการการป้องกันการทำ Binary Exploitation & Pwnable ไว้เยอะมากๆ เรามีความจำเป็นที่จะต้องปิดกลไก (บน VM Windows 10 32-bit ของเรา) เหล่านี้ออกก่อนในระหว่างการเรียนรู้ เพราะเราจะเริ่มเรียนจากการ Exploit แบบง่ายๆ เพื่อเข้าใจ Concept แล้วถึงค่อยมาเรียนวิธี Bypass กลไกเหล่านี้ทีหลัง
วิธีปิดก็คือให้เข้าไปที่ Microsoft Defender > App & Browser Control > “Exploit Protection > System Settings แล้วทำการปิดทุกการตั้งค่าในหน้านี้ จากนั้น Restart คอมพิวเตอร์
ในบทความหน้าๆ เดี๋ยวเราจะมา Discuss กันอีกทีว่ามาตราการแต่ละอย่างที่ Windows ออกแบบมามันจะช่วยอะไรได้บ้าง
Stack
Stack เป็น Data Structure ที่ใช้หลักการนำข้อมูลเข้าและออกจากด้านเดียวกัน โดยการเพิ่มข้อมูลเข้าไปจะเรียกว่า Push ส่วนการนำข้อมูลออกจะเรียกว่า Pop และข้อมูลที่สามารถนำออกมาได้จะต้องเป็นข้อมูลที่อยู่บนสุดของ Stack เท่านั้น ไม่สามารถข้ามไปหยิบข้อมูลที่อยู่ด้านล่างหรือระหว่างทางได้
หากอธิบายเพียงเท่านี้อาจจะยังนึกภาพไม่ออก ลองจินตนาการถึงกระป๋อง Pringles เมื่อเราใส่แผ่นมันฝรั่งลงไปทีละแผ่นที่ปากกระป๋อง ก็จะเปรียบเสมือนการ Push ข้อมูลลงบน Stack และเมื่อจะหยิบออก เราก็สามารถหยิบได้เฉพาะแผ่นที่อยู่บนสุดก่อนเท่านั้น ซึ่งก็คือการ Pop นั่นเอง
รูปแบบการทำงานลักษณะนี้เรียกว่า Last In, First Out (LIFO) หรือ “ข้อมูลที่เข้ามาล่าสุด จะสามารถถูกนำออกก่อนได้เสมอ”
เดี๋ยวเราจะใช้ Source Code ด้านล่างต่อไปนี้ในการอธิบายเรื่อง Stack
#include <stdio.h>
#include <malloc.h>
int func1(int a, char b[]) {
char buffer[8];
printf("Please put something: ");
scanf("%s",buffer);
printf("You just put: %s\n", buffer);
// For pausing the debugger
printf("Attach debugger now!");
while(getchar() != '\n');
getchar();
return 0;
}
int main(){
func1(12345,"Hello");
return 0;
}หน้าที่ของ Stack ภายใน Virtual Address Space ของโปรแกรมขณะทำงาน คือใช้เก็บข้อมูลที่เกี่ยวข้องกับการเรียกใช้งาน เริ่มใช้งาน และจบการใช้งาน Function โดยเมื่อมีการเรียกใช้ Function ข้อมูลต่าง ๆ จะถูก Push ลงบน Stack ตามลำดับดังนี้
- Function Arguments — ค่าที่ถูกส่งเข้ามาในขณะเรียกใช้งาน Function
- Return Address — เมื่อมีการเรียกใช้งาน Function โปรแกรมจะพักการทำงานของ Flow ปัจจุบันไว้ชั่วคราว แล้วกระโดดไปทำงานภายใน Function ให้เสร็จแทน ดังนั้น Address ของคำสั่งถัดไปใน Flow เดิมก่อนจะเข้า Function จึงต้องถูก Push ลงบน Stack เพื่อใช้เป็นตำแหน่งสำหรับกลับมาทำงานต่อเมื่อ Function ทำงานเสร็จสิ้นหรือ
return - Base Pointer — เป็น Address ที่ใช้เป็นจุดอ้างอิงของ Stack Frame สำหรับ Function ที่กำลังทำงานในปัจจุบัน เนื่องจากระหว่างการทำงานโปรแกรมอาจมีการเรียก Function มาก่อนหน้าแล้วหรือซ้อนกันหลายชั้น ทำให้บน Stack มีข้อมูลของหลายๆ Function ปะปนกันอยู่ Base Pointer จึงทำหน้าที่กำหนดขอบเขตของ Stack Frame เพื่อให้สามารถอ้างอิง Local Variable และข้อมูลต่าง ๆ ของ Function ปัจจุบันได้อย่างถูกต้อง
- Local Variables / Buffer — พื้นที่สำหรับเก็บ Local Variable และ Buffer ต่าง ๆ ที่ถูกสร้างขึ้นภายใน Function
ถ้าจะให้เห็นภาพก็คงจะเป็นประมาณนี้
Address ของ Stack (ที่ OS จัดสรรให้โปรแกรม) ภายใน Virtual Address Space อาจจะไม่เหมือนเดิมทุกครั้งที่เปิดโปรแกรม ดังนั้นโปรแกรมจะมีกลไกที่เรียกว่า Stack Pointer เอาไว้คอยเก็บ Address บนสุดหรือ Top ของ Stack ณ ขณะที่โปรแกรมกำลังทำงาน ดังนั้นแล้วในทุกๆ ครั้งที่โปรแกรมมีการ Pop หรือ Push ข้อมูลลงบน Stack ค่าใน Stack Pointer ก็จะต้องถูก Update เพื่อให้ชี้ไปยังตำแหน่งบนสุดของ Stack อยู่เสมอ
โดยใน Windows10 x86 ค่าของ Stack Pointer จะเก็บอยู่ใน Register ที่ชื่อว่า ESP
ใน CPU ประเภท x86–64 เมื่อมีการ Push ข้อมูลลงบน Stack ค่าของ Stack Pointer จะขยับไปยัง Address ที่มีค่าน้อยลงและในทางกลับกัน เมื่อมีการ Pop ข้อมูล ค่า Stack Pointer จะขยับไปยัง Address ที่มีค่าสูงขึ้นเสมอ
ยกตัวอย่างเช่น หาก ESP ชี้อยู่ที่ Address 0x12340000 และโปรแกรมทำการ Push ข้อมูลขนาด 8 Bytes ลงบน Stack ค่า ESP จะถูกอัปเดตเป็น 0x12340000 - 0x08 หรือ 0x1233FFF8 เพื่อชี้ไปยังตำแหน่งบนสุดของ Stack หลังจากการ Push เสร็จสิ้น
Buffer Overflow Theory
เมื่อเข้าสู่การทำงานของฟังก์ชันค่าของ Local Variable หรือ Buffer ที่อยู่ในฟังก์ชันจะถูก Push ลง Stack ด้วยเช่นกัน
int func1(int a, char b[]) {
char buffer[8]; // จองพื้นที่บน Stack 8 bytes
printf("Please put something: ");
scanf("%s",buffer); // นำ Input User ไปเก็บที่ buffer (อยู่บน Stack)
printf("You just put: %s\n", buffer);
// For pausing the debugger
printf("Attach debugger now!");
while(getchar() != '\n');
getchar();
return 0;
}จากโค้ดด้านบน เมื่อโปรแกรมเข้าสู่ Function (func1()) และพบกับบรรทัด char* buffer[8];ระบบจะจองพื้นที่บน Stack สำหรับเก็บ Local Variable ขนาด 8 Bytes จากนั้นเมื่อมาถึงคำสั่ง scanf("%s", buffer) โปรแกรมจะนำข้อมูลที่ผู้ใช้ป้อนเข้าไปเขียนลงในพื้นที่ Memory ของ buffer
อย่างไรก็ตาม จะสังเกตได้ว่าในโค้ดไม่มี Logic สำหรับการตรวจสอบหรือกำหนดขนาดของ Input ว่าห้ามเกิน 8 Bytes (หรือ 8 ตัวอักษร) อยู่เลย ดังนั้นหากผู้ใช้ป้อนข้อมูลที่ยาวเกินกว่าพื้นที่ที่จองไว้ ข้อมูลก็จะทะลุล้ำไปยัง Address ข้างเคียงจาก Address ที่เราจองเอาไว้ให้กับตัวแปร buffer นั่นเอง
หากข้อมูลที่ล้นออกไปมีขนาดมากพอจนเขียนทับข้อมูลสำคัญบน Stack เช่น Return Address เมื่อ Function ทำงานเสร็จสิ้นและพยายามจะกระโดดกลับไปที่ Flow ดั้งเดิม Program ก็จะเผลอใช้ค่าของ Return Address ที่ตอนนี้ถูก Corrupt ไปแล้วเนื่องจาก Input ของผู้ใช้งานที่ล้นออกมา จะส่งผลให้โปรแกรมไม่สามารถกลับไปได้และเกิดข้อผิดพลาด ซึ่งนี่คือหลักการพื้นฐานของการทำ Buffer Overflow บน Stack นั่นเอง
- Before Input — เมื่อโปรแกรมกันมาถึงโค้ดสำหรับการจองพื้นที่ 8 bytes (
buffer) - Normal Input — เมื่อ User ใส่ Input ที่ไม่ได้เกิน 8 bytes (8 ตัวอักษร) ก็จะพบว่าค่าของเราก็จะยังคงอยู่ในพื้นที่ของ Buffer ขนาด 8-byte และโปรแกรมก็ยังสามารถกลับไปที่ Address เดิมได้อย่างถูกต้องเพราะ Return Address ยังอยู่ปกติดี
- Overflow Input — เมื่อ User ใส่ Input ที่ขนาดใหญ่มากๆ (สมมติเป็นตัว
Aแบบเยอะๆ) ก็จะพบว่าตัวAได้รั่วออกมาจาก Buffer ที่จองเอาไว้ขนาด 8 byte และรั่วจนไปแทนที่ค่าของ Return Address ที่อยู่บน Stack ทำให้โปรแกรมเวลาจะจบ Flow จะพยายามไปที่ AddressAAAAAAAAซึ่งดูแล้วไม่น่าใช่หน้าตาของ Address ที่ถูกต้องก็จะ Error ทันที เป็นหลักฐานของการทำ Buffer Overflow
Buffer Overflow in Action
ทีนี้เราจะมาลอง Abuse Buffer Overflow แบบจริงๆ และทำการ Debug ด้วย Windbg พร้อมไปด้วย
หลังจาก Compile โปรแกรมเสร็จเรียบร้อยให้เราทำการเปิดตัวโปรแกรมผ่าน WinDbg เพื่อทำการ Debug ตอนเริ่มโปรแกรมจากนั้นก็สั่งให้โปรแกรมทำงานต่อไปก่อนเพื่อที่เราจะได้ลองใส่ Input ลงไปได้
จากนั้นทดสอบใส่ตัว A ลงไปประมาณ 16 ตัวเป็นอย่างต่ำ สาเหตุที่ต้องเป็นจำนวนนี้เพราะว่า char buffer[8] คือ Local Variable ตัวแรกที่เราจะเจอ ถ้าหากเรายังจำสิ่งที่จะ Push เข้ามาใน Stack ตอนเรียกฟังก์ชันได้ละก็
- Return Address — Address สำหรับกลับไปที่ Flow หลัก (ใน 32-bit Address จะมีขนาด 4 Byte หรือ 4 ตัวอักษร ASCII)
- Base Pointer — Address ที่ใช้สำหรับเป็นจุดอ้างอิงซึ่ง Address ก็จะกินไปอีก 4 ตัวอักษร
- และจนมาถึง
char buffer[8]ที่เป็น Local Variable ที่จะต้อง Push ลงบน Stack และกินพื้นที่ไปอีก 8 ตัวอักษร นั่นแปลว่าถ้าจะต้องใช้ตัว A อย่างน้อยประมาณ 8 + 4 + 4 = 16 ตัวเพื่อให้ A ของเรารั่วไปโดน Return Address จนพัง Flow โปรแกรม
แต่เราจะพบว่าโปรแกรมไม่ได้ Error แต่อย่าเพิ่งตกใจ นั่นเป็นเพราะว่าตัวโปรแกรมได้ดำเนินมาถึง printf(“Attach debugger now!”); ซึ่งยังไม่ได้จบการทำงานของฟังก์ชัน func1() หรือไปถึง return แต่อย่างใด สิ่งที่เราต้องทำก็คือกด Enter เพื่อให้โปรแกรมทำงานต่อจนไปถึงบรรทัด return 0
Get Pattharadanai Sanitjairak’s stories in your inbox
Join Medium for free to get updates from this writer.
หลังจากนั้นเราจะพบว่าโปรแกรมจึงจะเกิด Error ขึ้น ซึ่งเป็น Error Access violation ก็คือโปรแกรมพยายามจะเข้าถึง Memory Address ที่ไม่ได้รับอนุญาตซึ่งก็คือ AAAAAAAA ที่ถูกเรา Overflow ลงไป
หากเราสังเกตข้อความ Error จะพบว่า Register EIP มีค่าเป็น 41414141 หรือ AAAA ซึ่งก็คือข้อมูลที่เราใช้ Overflow เข้าไป หลายคนอาจสงสัยว่าทำไมค่าที่เราป้อนจึงไปปรากฏอยู่ใน EIP และเกี่ยวข้องกับการเกิด Error หรือไม่
คำตอบคือ เกี่ยวข้องโดยตรง เพราะ EIP มีหน้าที่เก็บ Address ของ Instruction ถัดไป ที่ CPU จะประมวลผล เมื่อโปรแกรมทำงานมาถึงคำสั่ง return มันจะดึง Return Address ที่เก็บอยู่บน Stack ขึ้นมาใส่ใน EIP เพื่อกลับไปทำงานต่อใน Flow หลัก แต่เนื่องจาก Return Address ถูกเรา Overflow จนถูกเขียนทับด้วย AAAA โปรแกรมจึงนำค่า 41414141 ไปใส่ใน EIP และพยายามกระโดดไปยัง Address ดังกล่าว ส่งผลให้เกิด Error ทันที เพราะ Address นี้ไม่ใช่ตำแหน่งที่สามารถรันคำสั่งได้
ทีนี้เราก็จะปิ๊งไอเดียที่ว่าแทนที่จะ Overflow ใส่ AAAA ทำไมเราไม่ใส่เป็น Address ที่ชี้ไปยังที่ๆ มีคำสั่งอันตรายอยู่ละถ้า Program ไปที่นั่นเมื่อไหร่ โค้ดอันตรายก็จะต้องทำงานแล้วเราก็จะบรรลุเป้าหมายทันที เราลองทำซ้ำอีกรอบแต่คราวนี้ลองใส่ Input เป็น AAAAAAAABBBBDEFG
AAAAAAAA— เติม Buffer เราให้เต็ม 8 Bytes ตามที่ได้ Assign ไว้BBBB— Overflow เพื่อทับ Base Pointer (จริงๆ แล้ว Base Pointer จะถูกเก็บไว้ใน RegisterEBPด้วยเช่นกันถ้าเราสังเกตภาพที่แล้วดีๆ)DEFG— สมมติว่าเป็น Address ที่มีโค้ดอันตรายอยู่ (ซึ่งจะเทียบเท่ากับ Hex44 45 46 47หรือ Address0x44454647)
เมื่อทำการทดสอบจะพบว่าเรา Overflow ไปยัง EIP ได้เหมือนเดิมแต่สิ่งที่แตกต่างจากที่เราคิดก็คือเราใส่ 44454647 ลงไปแต่ทำไมใน EIP มันถึงมีค่าเป็น 47464544 ซึ่งกลับจากค่าของเรา
Endianess
นั่นก็เพราะว่า Windows 10 x86 ของเราทำงานอยู่บน CPU x86–64 ซึ่งใช้รูปแบบการจัดเก็บข้อมูลแบบ Little-Endian ดังนั้นเมื่อข้อมูลถูกนำมาตีความเป็น Address ค่าของแต่ละ Byte ภายใน Address จะถูกอ่านจาก Byte ที่มีนัยสำคัญน้อยที่สุด (Least Significant Byte) ก่อนเสมอ (หรือกลับด้านทุกๆ 4 Byte)
ตัวอย่างเช่น หากมีข้อมูล ABCDEFGH ถูกเขียนลงในหน่วยความจำ เมื่อโปรแกรมนำข้อมูลดังกล่าวมาตีความเป็น Address ข้อมูลจะถูกแบ่งเป็นชุด ชุดละ 4 Bytes (บนระบบ 32-bit) กลายเป็น ABCD EFGH เมื่ออ่านแบบ Little-Endian ข้อมูลทุกๆ 4 Byte จะถูกสลับด้านกลายเป็น DCBA HGFE ทำให้ผลลัพธ์สุดท้ายที่โปรแกรมจะอ่านคือ DCBAHGFE ดังนั้นแล้วเวลาเราจะทำ Binary Exploitation ก็ให้ดูด้วยว่า CPU ของเครื่องที่เรากำลังโจมตีอยู่นั้นใช้ CPU เป็น Architecture อะไรเพื่อที่จะได้ทราบ Endianness และประมวลผลค่าของ Address ที่ถูกต้อง
ดังนั้น Input ที่ถูกต้องก็จะเป็น AAAAAAAABBBBGFED เพื่อที่เราจะทำการ Overflow ให้ EIP ใช้ Address 44454647 (แทนที่จะเป็น 47464544 ที่เราทำผิดพลาดไปเมื่อรอบที่แล้ว)
Buffer Overflow Attack Procedure
หลังจากที่เราสามารถควบคุมได้ว่าจะให้โปรแกรมวิ่งไปยัง Address ไหนต่อหลังจาก Overflow ซึ่งปลายทางที่โปรแกรมจะต้องไปก็คือ Address ที่มี Malicious Code ของเรานั่นเอง แต่ปัญหาคือแล้วเราจะเอา Malicious Code ไปวางได้อย่างไรก่อน ?
โปรแกรมรับข้อมูลจาก User แค่ช่องทางเดียวซึ่งก็คือ AAAAAAAABBBBGFED ที่เราใส่ลงไปเมื่อสักครู่เพียงเท่านั้น ดังนั้นจุดเดียวที่เราจะใส่อะไรลงไปได้เช่น Malicious Code ก็คือข้อความที่ต่อท้าย Input เมื่อสักครู่นั่นเอง โดยในรอบนี้เราจะลองใส่ Input ตามตัวอย่างข้างล่างลงไปใน Program
AAAAAAAA— เติมBufferให้เต็ม 8 byteBBBB— Overflow ตัว Base Pointer (EBP)GFED— Overflow ตัวReturn AddressหรือEIPโดยGFEDคือตัวแทน Addres ที่ Malicious Code เราจะอาศัยอยู่แต่ตอนนี้เรายังไม่รู้ว่ามันจะอยู่ที่ไหนแต่ขอแทนเป็นGFEDไปก่อนw00fw00f+Zอีก 100 ตัว — เป็น Malicious Code สมมติของเราที่มีขนาดประมาณ 100 byte
AAAAAAAABBBBGFEDW00fW00fZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZทำการเปิดโปรแกรมใหม่อีกครั้ง ต่อ Debugger, Conitnue Execution แล้วก็ใส่ Input เมื่อสักครู่ให้เกิด Access Violation (Buffer Overflow) อีกที
ถ้าใครยังจำคำสั่งในการค้นหา ASCII บน Memory ได้ เราสามารถใช้คำสั่งนั้นค้นหา ASCII w00fw00f ซึ่งเป็น Byte เริ่มต้นของ Malicious Code ของเรา
s -a 0 L?80000000 "W00fW00f"เราก็จะพบว่า W00fW00f หรือ Malicious Code ของเราปรากฎอยู่ใน Memory จริงๆ
ถ้าเรายังจำได้โปรแกรมจะมีสิ่งที่เรียกว่า Stack Pointer ซึ่งเป็นค่าที่จะชี้ Address ของ Top Stack และสามารถเข้าถึงได้ผ่าน Register ESP หากเราใช้คำสั่ง dd esp เพื่อทำการตรวจสอบ Byte ที่อยู่บน Address ที่เก็บอยู่ใน ESP เป็นต้นไปเราจะพบว่า Address บน ESP ก็คือจุด Start ของ Malicious Code ของเรา
จริงๆ แล้ว การที่ Function return มันคือคือการ Pop ค่าบน Stack ไปเก็บไว้ใน EIP ซึ่งค่าดังกล่าวก็คือ Return Address (นั่นจึงเป็นอีกเหตุผลว่าทำไมเราถึงสามารถทำ Buffer Overflow เพื่อควบคุม EIP ได้) โดย Return Address มีขนาด 4 Bytes และเมื่อถูก Pop ออกไปแล้ว ESP ก็จะถูกขยับเพิ่มขึ้น 4 Bytes ทำให้ชี้ไปยังตำแหน่งของ Malicious Code (ที่ต่อท้าย GFED) ของเราพอดีนั่นเอง
ดังนั้นในมุมมองของการโจมตีเราอาจจะออกแบบ Input สำหรับการโจมตีได้คร่าวๆ ประมาณนี้
Offset จำนวน 12 Byte เพื่อเติม Buffer และ Base Pointer+ Malicious Code Address (ที่จะปรากฎบนEIP) + Malicious Code (ที่อยู่ในรูปแบบของ Byte)
Conclusion
และนั่นก็คือ Procedure คร่าว ๆ ของการทำ Buffer Overflow เพื่อ Pwnable ตัว Binary Program โดยในภาพรวมจะมีขั้นตอนคร่าวๆ ดังนี้
- ส่ง Input ที่มีความยาวมากๆ ให้ Buffer Overflow แล้วตรวจสอบว่าสามารถเขียนทับ
EIPได้หรือไม่ - คำนวณจำนวน Byte ที่ต้องใช้เพื่อให้ล้นไปถึง
EIPพอดี (Offset) - นำ Address ที่ต้องการให้ตัวโปรแกรม Redirect (Address ของ Malicious Code) ไปเขียนทับ
Return AddressหรือEIP - นำ Malicious Code ต่อท้าย Payload แล้วค้นหาใน Memory ว่า Payload ของเราปรากฎอยู่ใน Address ใดใน Memory
- นำ Malicious Code จริงๆ ต่อเข้ากับ Input ของเรา อัพเดทตัว
EIPให้ชี้ไปยัง Address ของ Malicious Code ที่ต้องการเพื่อทำการ Pwnable
อย่างไรก็ตาม ตัวอย่างโค้ดที่ใช้ในบทความนี้ยังไม่สามารถนำไปทำ Pwnable ได้จริง เนื่องด้วยข้อจำกัดหลายๆ อย่าง แต่อย่างน้อยก็สามารถใช้ในการ Debug เพื่ออธิบายว่า Buffer Overflow เกิดขึ้นได้อย่างไร เกิดแล้วเราจะนำไปต่อยอดอะไรได้บ้าง รวมไปถึง Procedure ต่างๆ ที่อธิบายเอาไว้ในบทความนี้
โดยในบทความหน้าเราจะมาลอง Pwnable ตัว Binary Program ที่เป็นของจริงๆ เพื่อให้ได้ Code Execution กันดูครับ แล้วไว้พบกันใหม่ในบทความหน้าครับ
![[Starter — EP.1] Guide to Building a Professional Profile for Cybersecurity Job Applications](https://miro.medium.com/v2/resize:fit:1358/format:webp/0*xiEHtLAyY6QAcua1.png)
![[Android — SP.1] — Strings Write-Up](https://miro.medium.com/v2/resize:fit:1358/format:webp/0*54OCa3mpXRrHQQ9F.png)
![[Binary Exploit — EP.4] Introduction to C Programming](https://miro.medium.com/v2/resize:fit:1358/format:webp/1*7DUZ7eFx6Pcj0TSnlvoAUg.jpeg)
![[Binary Exploit — EP.3] Why Learning Windows Binary Exploitation Must Start with x86](https://miro.medium.com/v2/resize:fit:1358/format:webp/0*qeJB84evRbcRRyuN.jpg)




