จาก Part 1 ที่เราได้คุยกันไป ไฟล์ reverse1.exe ที่เรา reverse ใน windows ไปแล้ว
ซึ่ง part 1 จุดประสงค์ก็เพื่อให้เห็นว่าเราทำอะไรกับมันได้บ้าง ยังไม่เน้นสอนหรือเล่าความหมายอะไร
ทำให้เห็น ว่าใช้เครื่องมือวิเคราะห์ มีกลไก ทำอย่างไร ดึงอะไรมาดูได้บ้าง และง่าย หรือยาก อย่างไร
วันนี้ใน part 2 เราจะเริ่ม reverse และอธิบายกันต่อบน linux ครับ
เพราะผมไม่ได้เอา laptop windows กลับมาบ้าน คอมบ้านผมใช้แต่ Ubuntu ก็ทำงานได้
และยังคงใช้ rizin เหมือนเดิม มันก็ Static Analyze เบื้องต้น กับโปรแกรมที่แสนจะง่ายโปรแกรมนี้ reverse1.exe
ไม่มีให้โหลดนะครับ ไปเขียนแล้วก็ compile กันเองถ้าอยากทดสอบ ไม่ถึง 10 บรรทัดเลยโปรแกรมนี้
#include <stdio.h>
int main() {
int number;
printf("Enter a number:");
scanf("%d", &number);
printf("Number input =%d\n",number);
return 0;
}
Scope การเล่าของผมเป็น Windows PE Executable File เสมอ เพราะผมถนัด
แน่นอนว่า reverse1.exe เป็นโปรแกรมที่เราเขียนมาเอง ทำให้เรารู้โค้ดโปรแกรม ว่ามันมีขั้นตอนอะไรบ้าง
reverse1.exe ::: ประกาศตัวแปรเดียว คือ int number หมายความว่า ตัวแปร int ในภาษา C เป็น Data Type ที่เป็น
เลขจำนวนเต็ม ไม่มีทศนิยม ขนาด 2 byte หรือมีค่าเท่ากับ 16 bit จะต้องเกิดการจองพื้นที่ใน memory อย่างน้อยที่สุด 2 byte แน่ๆ เพื่อให้ตัวแปรนี้ได้ทำงาน
reverse1.exe ::: มี main function ที่เรียกใช้ตัวแปร number นี้
reverse1.exe ::: มีการใช้ printf , scanf ซึ่งมาจาก stdio.h ใน console app พื้นฐานทั่วไปที่จะใช้ได้ง่ายๆ
แปลว่า , ตอนที่เรา reverse เราจะต้องมองเห็นสิ่งต่างๆ ที่เชื่อมโยงกับ source code จริงเหล่านี้
และเราจะเข้าใจสิ่งต่างๆได้ สอดคล้องกัน
มาเริ่มจากสิ่งง่ายๆ กันก่อนครับ
แต่ละ Section ของไฟล์ reverse1.exe จะไปแปลงร่างเป็น Process ยังไง และ เพื่ออะไร
เริ่มจาก standard PE file Section ประกอบด้วยอะไรบ้าง จะแบ่งเป็น 2 ส่วนใหญ่ๆ คือส่วนของ Header และส่วนของ Section ขอเขียนไว้เป็นตารางแบบนี้ครับ
เมื่อเราดับเบิ้ลคลิกไฟล์ reverse1.exe จะเกิดอะไรขึ้น ? ก็จะมีกลไกไป trig Windows Loader เพื่อให้เข้ามาทำงานเรียก และอ่านไฟล์ PE นี้เข้า Memory (ภาพอธิบายเพื่อเสริมความเข้าใจว่าแต่ละส่วนของโครงสร้าง PE File ทำอะไรกับ Memory) ภาพ diagram นี้จะเป็นขั้นตอนคร่าวๆก่อน ยังไม่ระบุ Address

rizin มีตัวเลือกในการเปิดไฟล์ได้หลายแบบ ขึ้นอยู่กับจุดประสงค์ของเรา และเรารู้เรื่องราวของไฟล์นั้นมามากแค่ไหน เช่น
- รู้ว่าเป็น PE File ธรรมดาที่สมบูรณ์ (เช่น reverse1.exe ของผมเองไฟล์นี้) รู้อยู่แก่ใจ
- รู้ว่าเป็น PE File แต่อาจจะเสียหาย หรือพังมา และรันไม่ได้ หรือมีการ edit แก้ไขมา
- รู้ว่าเป็น Malware แต่ไม่ได้ pack อะไรมากมาย เป็น malware อ่อนๆ
- รู้ว่าเป็น Malware และเป็น advanced threat ถูก obfuscate หรือถูกทำ anti debug มาอย่างดี
เราจะต้องใช้การเปิด rizin <ไฟล์ pe นั้น> ใน parameter ที่แตกต่างกัน แต่มันก็แบ่งได้ใหญ่ๆเป็น สองแบบชัดเจน ดังนี้ครับ
1. กรณี: ไฟล์ PE ปกติ สมบูรณ์ รู้จักไฟล์ดี (เช่น reverse1.exe ที่เขียนเอง) หรือ ใช้ในกรณี Malware ระดับพื้นฐาน ไม่มี pack หรือ obfuscation
rizin reverse1.exe
หรือระบุ bit ให้ชัดก็ได้
rizin -b 64 reverse1.exe
เหตุผล:
ให้ Rizin ใช้ loader วิเคราะห์แบบ full-featured ซึ่งเหมาะกับการใช้คำสั่ง aaa, afl, pdf, iS, ie, iI ฯลฯ ผมจะพาไปดู เพราะไฟล์นี้เข้าเงื่อนไขแรก จะทำให้เห็นการ map memory เหมือนตอนโปรแกรมถูกรันจริง
2. กรณี: ไฟล์ PE เสียหาย / ถูกแก้ไขบางส่วน / ข้อมูลบางจุดไม่ปกติ และใช้ในกรณี Malware ระดับสูง มี obfuscation, anti-debug, หรือ technique แปลก
rizin -n reverse1.exe
ถ้าต้องการ "หยุดการ auto analysis" เราจะเพิ่ม n ไปอีกตัว แน่นอนวิธีนี้เราจะต้อง manual เองเยอะมาก ต้องเข้าใจ address ต่างๆอย่างลึกซึ้งครับ
rizin -nn 64 reverse1.exe
เหตุผล:
ไม่โหลด PE structure อัตโนมัติ (เลี่ยง crash จาก section เพี้ยน)
เหมาะกับการ ดู header เอง, หาจุดเริ่มต้นของ code, สร้าง analysis flow ด้วยมือ
ไม่ให้ Rizin วิเคราะห์อะไรอัตโนมัติ เพื่อหลีกเลี่ยง misinterpretation เหมาะกับการดู payload, stub, shellcode ดิบๆ เอาไว้เราเรียนกันไปลึกๆ ค่อยเปิดด้วยวิธีนี้
แปลว่าเราจะเลือกใช้วิธีการเปิดไฟล์จาก "เรารู้มาก หรือ รู้น้อย แค่ไหนกับไฟล์นี้" นี่คือหัวใจสำคัญเริ่มต้นของการทำ Static Analysis
ไฟล์นี้ผมรู้แล้วว่าเป็นไฟล์ปกติ เพราะผมเขียนขึ้นมาเองใช่มั้ยละ ผมก็จะทำการโหลดแบบปกตินะครับ
keng@kengUbuntu:~/re/stack_study$ rizin -b 64 reverse1.exe
-- The '?' command can be used to evaluate math expressions. Like this: '? (0x34+22)*4'
[0x1400013f0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls
[x] Analyze len bytes of instructions for references
[x] Check for classes
[x] Analyze local variables and arguments
[x] Type matching analysis for all functions
[x] Applied 0 FLIRT signatures via sigdb
[x] Propagate noreturn information
[x] Integrate dwarf function information.
[x] Resolve pointers to data sections
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x1400013f0]> iH
PE file header:
IMAGE_NT_HEADERS
Signature : 0x4550
IMAGE_FILE_HEADERS
Machine : 0x8664
NumberOfSections : 0x11
TimeDateStamp : 0x67d54c29
PointerToSymbolTable : 0x14c00
NumberOfSymbols : 0x66b
SizeOfOptionalHeader : 0xf0
Characteristics : 0x26
IMAGE_OPTIONAL_HEADERS
Magic : 0x20b
MajorLinkerVersion : 0x2
MinorLinkerVersion : 0x2b
SizeOfCode : 0xfa00
SizeOfInitializedData : 0x12800
SizeOfUninitializedData : 0xc00
AddressOfEntryPoint : 0x13f0
BaseOfCode : 0x1000
ImageBase : 0x140000000
SectionAlignment : 0x1000
FileAlignment : 0x200
MajorOperatingSystemVersion : 0x4
MinorOperatingSystemVersion : 0x0
MajorImageVersion : 0x0
MinorImageVersion : 0x0
MajorSubsystemVersion : 0x5
MinorSubsystemVersion : 0x2
Win32VersionValue : 0x0
SizeOfImage : 0x22000
SizeOfHeaders : 0x600
CheckSum : 0x218ec
Subsystem : 0x3
DllCharacteristics : 0x160
SizeOfStackReserve : 0x200000
SizeOfStackCommit : 0x1000
SizeOfHeapReserve : 0x100000
SizeOfHeapCommit : 0x1000
LoaderFlags : 0x0
NumberOfRvaAndSizes : 0x10
RICH_FIELDS
IMAGE_DIRECTORY_ENTRY_IMPORT
VirtualAddress : 0x16000
Size : 0x820
IMAGE_DIRECTORY_ENTRY_EXCEPTION
VirtualAddress : 0x13000
Size : 0x5d0
IMAGE_DIRECTORY_ENTRY_BASERELOC
VirtualAddress : 0x19000
Size : 0xa0
IMAGE_DIRECTORY_ENTRY_TLS
VirtualAddress : 0x12060
Size : 0x28
IMAGE_DIRECTORY_ENTRY_IAT
VirtualAddress : 0x16220
Size : 0x1e0
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
VirtualAddress : 0x0
Size : 0xffff
[0x1400013f0]>
มาดูคำสั่ง iS เพื่อให้ rizin ช่วยแจกแจง section ของ reverse1.exe กัน
[0x1400013f0]> iS
paddr size vaddr vsize align perm name type flags
-----------------------------------------------------------------------------------------------------------
0x00000600 0xfa00 0x140001000 0x10000 0x0 -r-x .text CNT_CODE,CNT_INITIALIZED_DATA
0x00010000 0x400 0x140011000 0x1000 0x0 -rw- .data CNT_INITIALIZED_DATA
0x00010400 0xe00 0x140012000 0x1000 0x0 -r-- .rdata CNT_INITIALIZED_DATA
0x00011200 0x600 0x140013000 0x1000 0x0 -r-- .pdata CNT_INITIALIZED_DATA
0x00011800 0x600 0x140014000 0x1000 0x0 -r-- .xdata CNT_INITIALIZED_DATA
0x00000000 0x0 0x140015000 0x1000 0x0 -rw- .bss CNT_UNINITIALIZED_DATA
0x00011e00 0xa00 0x140016000 0x1000 0x0 -rw- .idata CNT_INITIALIZED_DATA
0x00012800 0x200 0x140017000 0x1000 0x0 -rw- .CRT CNT_INITIALIZED_DATA
0x00012a00 0x200 0x140018000 0x1000 0x0 -rw- .tls CNT_INITIALIZED_DATA
0x00012c00 0x200 0x140019000 0x1000 0x0 -r-- .reloc CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00012e00 0x200 0x14001a000 0x1000 0x0 -r-- .debug_aranges CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00013000 0x1200 0x14001b000 0x2000 0x0 -r-- .debug_info CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00014200 0x200 0x14001d000 0x1000 0x0 -r-- .debug_abbrev CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00014400 0x200 0x14001e000 0x1000 0x0 -r-- .debug_line CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00014600 0x200 0x14001f000 0x1000 0x0 -r-- .debug_frame CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00014800 0x200 0x140020000 0x1000 0x0 -r-- .debug_str CNT_INITIALIZED_DATA,MEM_DISCARDABLE
0x00014a00 0x200 0x140021000 0x1000 0x0 -r-- .debug_line_str CNT_INITIALIZED_DATA,MEM_DISCARDABLE
[0x1400013f0]>
ยังไม่ต้องสนใจอะไรทั้งหมด แต่ให้สนใจเฉพาะส่วนดังนี้ครับ
paddr ย่อมาจาก Physical Address คือ ตำแหน่งในไฟล์จริงบน disk (offset) — กล่าวง่ายๆ คือ Byte offset จากต้นไฟล์ PE คือมองไปที่ไฟล์ดิบๆโดยไม่สนใจว่ามันจะถูกโหลดไปแรมตรงไหน เช่น 0x600 คือ byte ลำดับที่ 0x600 จากต้นไฟล์ PE
vaddr ย่อมาจาก Virtual Allocation คือ ตำแหน่งที่ section จะถูกแมปไว้ใน memory (Virtual Memory) เมื่อตัว OS โหลดไฟล์ PE ขึ้นมารัน มีเพราะเราเปิดไฟล์ reverse1.exe ด้วย rizin แบบเปิดปกติ (เสมือนรันบน memory)
ตอนนี้เราพร้อมที่จะทำความเข้าใจคำถามของ Main content ของ blog นี้แล้วครับ ถ้าเลื่อนไปดูข้างบน คำถามคือ แต่ละ Section ของไฟล์ reverse1.exe จะไปแปลงร่างเป็น Process ยังไง และ เพื่ออะไร |
เรารู้หน้าที่ของแต่ละส่วนของ Header และ Section แล้ว ว่ามีไว้เพื่ออะไรจากตารางความหมายที่ผมเขียนไว้ข้างบน และเรารู้แล้วว่ามี ImageBase ที่ตำแหน่ง 0x140000000 ซึ่งเป็นค่าเริ่มต้น ImageBase ที่ถูกกำหนดไว้ อารมณ์แบบ มาตรฐานทั่วไปที่รู้กัน คือ : EXE บน x64: 0x140000000 DLL บน x64: 0x180000000 หรือ แล้วแต่ compiler EXE บน x86: 0x00400000 ขยี้อีกครั้ง ถ้าแบบเร็วๆ ว่า rizin ช่วยเรายังไง
มีอีกคำนึงที่สำคัญมากคือคำว่า RVA ย่อมาจาก Relative Virtual Address ความหมายของคำนี้ก็คือ offset จาก ImageBase ถึงข้อมูลใดข้อมูลหนึ่ง RVA = vaddr - ImageBase . ดังนั้นมันจึงมี rva ได้หลายค่า , ส่วนคำว่า offset คือ สิ่งที่เปลี่ยนแปลงไปจากเดิม คำว่า "offset" ในที่นี้ ไม่ใช่ offset บนไฟล์ดิบ (raw) นะครับ — แต่หมายถึง ค่าที่เปลี่ยนจาก ImageBase ไปยังจุดหมายใน Virtual Address Space เช่น ข้อมูลใน .text, .data, .rdata หรือ section อื่นๆ ล้วนมีค่า RVA เป็นของตัวเอง แล้ว RVA ของอะไรล่ะ? — ก็ขึ้นอยู่กับว่าเรากำลังพูดถึง section ไหน หรือข้อมูลอะไร เช่น RVA ของ EntryPoint, RVA ของฟังก์ชัน, RVA ของสตริง หรือ RVA ของ Import Table ก็ได้ แล้วมันอยู่ตรงไหน? — ก็ต้อง “คำนวณจาก ImageBase” นั่นแหละ ด้วยสูตรข้างบน RVA จึงเป็นแกนกลางสำคัญที่ทำให้ PE มีความยืดหยุ่นในการถูกโหลดไปที่ไหนก็ได้ใน memory แล้วทำไมต้องมี RVA? เพราะว่า : ไฟล์ PE ยังไม่รู้ล่วงหน้าว่าตัวเองจะถูกโหลดไปที่ไหน สมมุติ EXE 2 ตัวมี ImageBase ชนกัน (เช่นทั้งคู่ใช้ 0x140000000) แล้วจะรันพร้อมกันไม่ได้ — Windows จะ "Relocate" ไฟล์หนึ่งไปที่ address อื่น ดังนั้น: เวลาเขียน pointer ลงใน PE ต้องเขียนเป็น RVA เพราะมัน “ยืดหยุ่น” ในไฟล์ของผม reverse1.exe "ถ้าไฟล์ pe นี้จะถูกโหลดเข้า memory มันจะต้อง copy .text section ที่เป็น machine code ไปใน memory ในตำแหน่งที่ถูกต้อง" เพื่อให้เข้าเส้น เราก็จะมาคำนวณจริงจาก reverse1.exe ของผมกัน: ImageBase = 0x140000000 .text section vaddr = 0x140001000 (ผมไฮไลต์สีเหลืองไว้ข้างบนแล้ว จาก output ของคำสั่ง iS) ดังนั้น: RVA ของ .text section = 0x140001000 - 0x140000000 = 0x1000 |

ลูกศรสุดท้าย = จุดเริ่มต้นของโปรแกรม เรียกว่า EntryPoint
เรายืนยัน address of EntryPoint ได้จากคำสั่ง iH ก่อนเลยครับ
หรืออีกมุมมองหนึ่ง ถ้าเปิดไฟล์ใน rizin แบบปกติ (ไม่มี -n หรือ -nn) , rizin ก็จะพาเรามายืนตรง entry point ให้เราเลยใน shell ครับ (กรอบแดงแรก) แต่ใน shell มันมี 0x14000 ข้างหน้าใช่มั้ยครับ ?
เพราะมันมาจากการ "เปลี่ยนแปลงไปจาก ImageBase" นั่นเอง (OffSet)
OffSet ! OffSet ! OffSet !
rizin จะนำเรายืนอยู่ที่ address ของ entry point ทันที (ใน shell หรือใน Cutter ก็จะชี้ตรงนั้นเลย)
แล้วทำไม address ที่เราเห็นมันไม่ใช่ 0x13f0 ตรงๆ แต่กลายเป็น 0x1400013f0 ละ ??
เพราะ rizin แสดง “Virtual Address” เต็มๆ ซึ่งมาจาก = ImageBase + RVA นั่นเองครับ
และนั่นคือ address แรกที่ CPU จะเริ่ม execute instruction ตอนโปรแกรม run จริง
บวก / ลบ กัน เปลี่ยนแปลงไป ภาษา Reverser เค้าเรียกว่า OffSet ! ครับ

ต้องขยี้เรื่อง RVA , OffSet , ให้แม่นๆ นะครับ
เจอกัน Blog ต่อไปครับ