สวัสดีครับ วันนี้เราจะมาทำความเข้าใจกับคำว่า Offset กัน
โดยจะอิงตัวอย่างไฟล์เดิม คือ reverse1.exe
=== โค้ด ===
#include <stdio.h>
int main() {
int number;
printf("Enter a number:");
scanf("%d", &number);
printf("Number input =%d\n",number);
return 0;
}
คอมไพล์แล้ว
SHA256 = 91a12d84e2eff4a3b2752afbef7f63e5c2bb6d12787426c622e55b70527fda40
Offset คืออะไร?
แปลสั้นๆ คือ "ระยะห่าง"
อาจจะยังไม่เห็นภาพ ผมจะยกตัวอย่างให้ง่ายๆว่า เวลาเราเขียนแบบวิศวกรรม ถ้าเรามีรูปสี่เหลี่ยมอยู่และเราต้องการวาดรูปสี่เหลี่ยม "ที่อยู่ภายในรูปสี่เหลี่ยมนั้นอีกที" โดยมีระยะห่างเข้าไปข้างใน จากรูปสี่เหลี่ยมเดิม
สิ่งนี้เรียกว่าระยะ Offset ออกมาจากเส้นเดิม เราก็จะกำหนดว่า เส้นใหม่จะมีระยะห่างจากเส้นเดิมเท่าไหร่ เมื่อกำหนดเสร็จโปรแกรมก็จะสร้างเสร็จมาให้โดยอิงจากค่า Offset ที่เรากำหนด

ทีนี้ ในโลกของ Computer Reverse Engineering (RE) คำว่า Offset นั้นมีความซับซ้อนและสำคัญยิ่งกว่านั้นครับ เพราะมันไม่ใช่แค่ระยะห่างทั่วไป แต่เป็น กุญแจสำคัญ ที่ทำให้เราสามารถระบุตำแหน่งของโค้ดและข้อมูลได้อย่างถูกต้อง ไม่ว่าโปรแกรมจะถูกโหลดไปที่หน่วยความจำตำแหน่งใดก็ตาม
ความท้าทายของ Offset ในงาน RE
สิ่งที่ทำให้การตีความ Offset ในงาน RE ไม่ใช่เรื่องง่ายโดยทันที คือเราต้องเข้าใจ บริบท (Context) ของงานว่าเรากำลังทำอะไรอยู่ เราอยู่ใน Workflow แบบไหน เพราะแต่ละบริบทจะทำให้คำว่า Base Address, Virtual Address, และ Offset มีความสัมพันธ์กันที่แตกต่างกันออกไป
เราสามารถแบ่งบริบทหลัก ๆ ที่ต้องพิจารณาในการตีความ Offset ออกเป็นกลุ่ม ๆ ดังนี้:
- สถาปัตยกรรมของ CPU (CPU Architecture): เช่น x86 (32-bit) หรือ x86_64 (64-bit) ซึ่งมีผลต่อขนาดของ Address ที่ใช้และการทำงานของฟีเจอร์ด้านความปลอดภัยอย่าง ASLR (Address Space Layout Randomization)
- วิธีการวิเคราะห์ (Methodology): เรากำลังทำงานแบบ Static Analysis (วิเคราะห์ไฟล์นิ่ง ๆ) หรือ Dynamic Analysis (ดีบักโปรแกรมขณะรัน) ซึ่งมีผลต่อว่า Base Address เป็นค่าคงที่หรือค่าที่เปลี่ยนแปลงได้
- โหมดของการเปิดไฟล์ใน Disassembler: การเปิดใน Normal Mode (ที่เครื่องมือช่วยแปลโครงสร้าง PE Header) เทียบกับการเปิดใน Raw Mode (ที่เครื่องมือมองเห็นแค่ไบนารีล้วน ๆ) จะทำให้ที่อยู่ (Address) ที่เราเห็นมีความหมายแตกต่างกัน
- บริบทในการดูข้อมูลไฟล์: การดูข้อมูลในส่วนของ PE Header และ Relocation Table จะทำให้เราเห็นถึงเหตุผลเบื้องหลังว่าทำไมโปรแกรมจึงต้องมีการอ้างอิงตำแหน่งด้วย Offset
เริ่มที่ rizin คู่ใจของพวกเรา
PS C:\Users\keng\runmalware.com> rizin.exe .\reverse1.exe
-- Most of commands accept '?' as a suffix. Use it to understand how they work 🙂
[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 : 0x6800786f
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 : 0x2d489
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]>
ลองเดาดูได้มั้ยครับ ว่าบริบทที่ผมเปิดไฟล์ reverse1.exe นี้ประกอบด้วยบริบทอะไรบ้าง ?
คำตอบ
บริบทสถาปัตยกรรม (CPU Architecture): x86_64 (64-bit)
หลักฐาน: ดูจาก IMAGE_FILE_HEADERS -> Machine : 0x8664
การตีความ: โอเคเลย! ไฟล์นี้เป็นโปรแกรม 64-bit ซึ่งแปลว่าเราต้องระวังเรื่อง ASLR (Address Space Layout Randomization) และการใช้ Offset เพื่อคำนวณที่อยู่จริงจะมีความสำคัญสูงมาก เพราะ Base Address จะไม่นิ่ง
บริบทวิธีการวิเคราะห์ (Methodology): Static Analysis
หลักฐาน: rizin เป็นเครื่องมือ Static Analysis
การตีความ: เรากำลังดูโครงสร้าง 'ในกระดาษ' ของไฟล์อยู่ , Base Address ที่เราเห็นตอนนี้คือค่าที่ถูกฝังมาในไฟล์ เป็นค่าคงที่สำหรับเซสชันนี้ ไม่ใช่ที่อยู่จริงที่ OS กำหนดให้ตอนรัน
บริบทโหมดการเปิดไฟล์ (Disassembler Mode): Normal Mode
หลักฐาน: ผมใช้คำสั่งพื้นฐาน rizin.exe .\reverse1.exe และ Rizin สามารถรันคำสั่ง aaa และ iH ได้อย่างราบรื่น
การตีความ: Rizin อ่าน PE Header และใช้ค่า ImageBase มาเป็น Base Address ของเรา ซึ่งทำให้ที่อยู่ที่เราเห็นเป็น Virtual Address ที่ถูกจัดเรียงตามที่โปรแกรมควรจะเป็น เราไม่ต้องไปนั่งหา MZ หรือจัดโครงสร้างเอง
บริบทในการดูข้อมูลไฟล์ (File Data Context): PE Header Focus
หลักฐาน: คำสั่ง iH ซึ่งเป็นการสั่งให้ Rizin แสดงข้อมูลจาก IMAGE_OPTIONAL_HEADERS และ Directory Entries
การตีความ: เรากำลังเน้นดูว่า 'Base Address' และ 'Offset' ถูกกำหนดมาจากไหน! เราสามารถชี้ให้เห็นชัดๆ ได้ว่าตัวเลขสำคัญๆ เช่น 0x140000000 มันมาจาก ImageBase ใน Header นี่แหละ
------ Break คำใหม่ๆ เยอะแยะเลย ?? ---
- Base Address
- Virtual Address
- Current Address
ตามที่เห็นนะครับ นอกจากเราจะต้องทำความเข้าใจความแตกต่างของบริบทแล้ว เราจะต้องเข้าใจ Address 3 ประเภทนี้ก่อน
ตอนนี้เราอยู่ในบริบท [x86_64 + Static Analysis + Normal Mode Opened File + PE Header Focus ]
เฉลย 3 คำนี้
Base Address
คือ : ที่อยู่เสมือนเริ่มต้น ที่ Rizin ดึงมาจากค่า ImageBase ใน PE Header (ในตัวอย่างคือ 0x140000000)
เปรียบเทียบได้ว่า : บ้านเลขที่เริ่มต้น ของหมู่บ้านโปรแกรม reverse1.exe (ถูกเขียนไว้ในผังโครงการ)
Virtual Address
คือ : ที่อยู่เสมือนของโค้ดหรือข้อมูลใดๆ ในโปรแกรม ซึ่งเป็นค่าที่ Rizin คำนวณให้แล้ว (Base Address + Offset)
เปรียบเทียบได้ว่า : บ้านเลขที่ของห้องต่างๆ ในหมู่บ้าน (ทุกอย่างมีเลขที่เฉพาะตัวในผังโครงการ)
Current Address
คือ : ตำแหน่งที่เรากำลัง 'มอง' อยู่ ใน Rizin ณ ขณะนั้น (คือตำแหน่งที่อยู่ในวงเล็บ [...]>) และเราย้ายไปได้เรื่อยๆ
เปรียบเทียบได้ว่า : ห้องที่เรายืนอยู่ หรือกำลังชี้ดูอยู่ (แค่เราย้ายจุดสนใจ)
ในบริบทของการวิเคราะห์แบบ Static Analysis นี้ คำว่า Offset มีความหมายที่ง่ายที่สุดคือ:
Offset=Virtual Address−Base Address
ยกตัวอย่างจาก PE Header ของ reverse1.exe :
เราเห็น ImageBase : 0x140000000 (Base Address)
เราเห็น AddressOfEntryPoint : 0x13f0 (นี่คือ Offset ที่ถูกตั้งชื่อไว้แล้ว)
Rizin จึงเริ่มต้นที่:
Current Address (0x1400013f0)=Base Address (0x140000000)+Offset (0x13f0)
สรุป: Offset คือ ระยะทาง ที่วัดจาก Base Address เสมอ
ยกคำสั่ง iH มาไฮไลท์สีกันอีกครั้งครับ
[0x1400013f0]> iH
PE file header:
IMAGE_NT_HEADERS
Signature : 0x4550
IMAGE_FILE_HEADERS
Machine : 0x8664
NumberOfSections : 0x11
TimeDateStamp : 0x6800786f
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 : 0x2d489
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]>
ค่า offset คือ 0x13f0 "เพราะ ตอนนี้เราอยู่ในบริบท [x86_64 + Static Analysis + Normal Mode Opened File + PE Header Focus ] "
เราจึงสื่อสารและทำความเข้าใจกับคำว่า Offset ในจังหวะนี้ แบบนี้ บริบทนี้
ถ้าเรามีบริบทอื่นๆ เราอาจจะต้องสื่อสารและตีความคำว่า Offset ต่างออกไป
เอาละ
เรายืนอยู่ที่ [0x1400013f0] เพราะเป็นจุดที่ Rizin ส่งเรามาให้มาที่นี่จุดแรก ด้วยเหตุผลที่ว่า เพราะมันคือ AddressOfEntryPoint
จุดนี้ "มันมีระยะห่าง (Offset)" ออกมาเท่านี้ เพราะมันอิง Offset จาก ImageBase
จุดที่เรายืนนี้เรียกว่า Current Address ในบริบท Rizin
เมื่อเรามายืนแล้วเราก็สามารถ ดูข้อมูลภายในนี้ได้ ด้วยคำสั่ง pdf
ซึ่งหมายความว่า Print Disassembly Function
[0x1400013f0]> pdf
;-- mainCRTStartup:
┌ entry0();
│ 0x1400013f0 sub rsp, 0x28
│ 0x1400013f4 mov rax, qword [data.140012c70] ; [0x140012c70:8]=0x140015080
│ 0x1400013fb mov dword [rax], 0
│ 0x140001401 call sym.__tmainCRTStartup ; sym.__tmainCRTStartup
│ 0x140001406 nop
│ 0x140001407 nop
│ 0x140001408 add rsp, 0x28
└ 0x14000140c ret
[0x1400013f0]>
ในโค้ด Assembly นี้เอง ที่เราสามารถชี้ให้เห็นการทำงานของ Offset ได้อย่างชัดเจน:
จุดเริ่มต้น: คำสั่งแรกคือ sub rsp, 0x28 ซึ่งอยู่ที่ 0x1400013f0 ซึ่งเป็นจุดที่เรายืนอยู่ (Base Address + Entry Point Offset)
คำสั่งภายใน: คำสั่งอื่น ๆ ที่ตามมา เช่น call sym.__tmainCRTStartup อยู่ที่ 0x140001401
การคำนวณ Offset ภายใน: แม้แต่คำสั่งนี้ก็ยังคงมี Offset ที่สัมพันธ์กับ Base Address ใหญ่ของไฟล์อยู่
Offset ของคำสั่ง call:
Offset of call=0x140001401−0x140000000
Offset of call=0x1401
ตอนนี้ผมได้อธิบาย Entry Point (โค้ดที่คอมไพเลอร์ใส่มา) ด้วยแนวคิด Base Address และ Offset ที่ชัดเจนแล้ว
การทำแบบนี้จะตอกย้ำความเข้าใจที่ว่า: "ทุกสิ่งในโปรแกรม ล้วนมี Offset ที่วัดจาก Base Address เสมอ"
ทีนี้คำถามที่อาจจะท้าทายคือ ค่า offset มันคงที่หรือไม่ อย่างไร
ย้ำในบริบทเดิมนะครับ
คำตอบในบริบท [Static Analysis + Normal Mode] คือ:
Offset นั้นคงที่ (Stable) ตราบใดที่ไฟล์ไบนารีนั้นไม่ถูกแก้ไข!
ผมจะลอง afl ให้เห็น และให้ทุกคนมอง address ทางด้านซ้าย
ตอนนี้เรายืนอยู่ที่ 0x1400013f0 นะครับ
[0x1400013f0]> afl
0x140001000 1 1 sym.__mingw_invalidParameterHandler
0x140001010 14 286 -> 269 sym.pre_c_init
0x140001130 1 73 sym.pre_cpp_init
0x140001180 26 592 -> 554 sym.__tmainCRTStartup
0x1400013d0 1 29 sym.WinMainCRTStartup
0x1400013f0 1 29 entry0
0x140001410 1 20 sym.atexit
0x140001430 1 12 sym.__gcc_register_frame
0x140001440 1 1 sym.__gcc_deregister_frame
0x140001450 1 84 sym.scanf
0x1400014a4 1 84 sym.printf
0x1400014f8 1 81 sym.main
0x140001550 4 58 sym.__do_global_dtors
0x140001590 8 114 -> 111 sym.__do_global_ctors
0x140001610 3 31 -> 26 sym.__main
0x140001630 1 3 sym._setargv
0x140001640 4 47 -> 38 sym.__dyn_tls_dtor
0x140001670 12 129 -> 118 sym.__dyn_tls_init
0x140001700 1 3 sym.__tlregdtor
0x140001710 10 248 -> 218 sym._matherr
0x140001810 1 3 sym._fpreset
0x140001820 1 112 sym.__report_error
0x140001890 16 368 -> 365 sym.mark_section_writable
...
จะเห็นว่า บางที จาก 0 ก็กระโดดมา 10 จาก 10 กระโดดมา 130
address พวกนี้คือ virtual address ที่คอมไพเลอร์วางผังเอาไว้ในโครงสร้างของ binary file
เมื่อเราจะไล่ offset เราจะต้องเข้าใจว่ามันต้องมี address ต้นทางในการเปรียบเทียบ เพราะอย่างที่เราเข้าใจกันไปก่อนหน้าแล้วว่า จะต้องมีค่าแรก ใช้เปรียบเทียบเสมอในการอ้างอิง Offset
อย่างในตัวอย่างก่อนหน้า ก็คือ base address เป็นต้นทาง
และ virtual address (เขียนย่อว่า VA) ลำดับต่อไป ที่เราจะเข้าไปดู มันมีระยะ offset ออกจาก base address เท่าไหร่ ?
ลองอีกสักตัวอย่าง
[0x1400013f0]> s 0x1400014f8
[0x1400014f8]> pdf
; CALL XREF from sym.__tmainCRTStartup @ 0x1400012ea
┌ int sym.main(int argc, char **argv, char **envp);
│ ; var int64_t var_ch @ stack - 0xc
│ 0x1400014f8 push rbp
│ 0x1400014f9 mov rbp, rsp
│ 0x1400014fc sub rsp, 0x30
│ 0x140001500 call sym.__main ; sym.__main
│ 0x140001505 lea rax, str.Enter_a_number: ; section..rdata
│ ; 0x140012000 ; "Enter a number:"
│ 0x14000150c mov rcx, rax ; const char *format
│ 0x14000150f call sym.printf ; sym.printf ; int printf(const char *format)
│ 0x140001514 lea rax, [var_ch]
│ 0x140001518 mov rdx, rax
│ 0x14000151b lea rax, data.140012010 ; 0x140012010 ; "%d"
│ 0x140001522 mov rcx, rax ; const char *format
│ 0x140001525 call sym.scanf ; sym.scanf ; int scanf(const char *format)
│ 0x14000152a mov eax, dword [var_ch]
│ 0x14000152d mov edx, eax
│ 0x14000152f lea rax, str.Number_input___d ; 0x140012013 ; "Number input =%d\n"
│ 0x140001536 mov rcx, rax ; const char *format
│ 0x140001539 call sym.printf ; sym.printf ; int printf(const char *format)
│ 0x14000153e mov eax, 0
│ 0x140001543 add rsp, 0x30
│ 0x140001547 pop rbp
└ 0x140001548 ret
[0x1400014f8]>
พอเอามาลบกันแล้ว 0x14f8 นี้คือ "ระยะห่าง" จากจุดเริ่มต้นของโปรแกรมไปจนถึงคำสั่งแรกของฟังก์ชัน main ของเรา มันเป็นค่าที่ คงที่ และ ถูกบันทึก อยู่ในโครงสร้างไฟล์ไบนารี เราจึงใช้มันอ้างอิงตำแหน่งได้อย่างแม่นยำทุกครั้งที่เปิดไฟล์นี้ใน Static Analysis
ทุกคำสั่งก็มี Offset ที่วัดจาก Base Address ใหญ่ของไฟล์อยู่เช่นกัน:
คำสั่ง call sym.printf (บรรทัดแรก) อยู่ที่ 0x14000150f
Offset ของ printf (บรรทัดแรก) คือ:
Offset=0x14000150f−0x140000000=0x150f
ไม่ว่าเราจะอ้างอิงถึงฟังก์ชันทั้งหมด (0x14f8) หรือแค่คำสั่งเดียว (0x150f) ก็ล้วนแล้วแต่มี Offset ที่คงที่ วัดจาก Base Address เสมอ
แต่ขอให้เรานึก และทบทวนบริบทย้ำอีกครั้งว่าเรากำลังมองในมุมบริบทไหน
ทีนี้ ถ้าเราสังเกตอีกที ตอนที่เรา pdf เราจะพบว่า
กรอบ ของ va นั้นๆ จะมี va ย่อยๆเรียงลงมา - ใช่มั้ย
แล้วใน va ย่อยๆนั้น ก็มี offset ที่ไม่เท่ากันของกันและกัน
เช่น
│ 0x1400014f8 push rbp
│ 0x1400014f9 mov rbp, rsp
จะเห็นว่า จาก f8 ก็ไป f9
และจาก f9 ส่งต่อไป fc
│ 0x1400014fc sub rsp, 0x30
│ 0x140001500 call sym.__main ; sym.__main
จาก 14fc ไปยัง 1500
จาก 1500 ส่งต่อไปยัง 1505
│ 0x140001505 lea rax, str.Enter_a_number: ; section..rdata

สิ่งนี้ก็คือ Offset เหมือนกัน และมันคือ Offset ในระดับคำสั่ง (Instruction Level)
เมื่อเรา pdf (Print Disassembly Function) โค้ดออกมา เรากำลังดู "กรอบของ Virtual Address" ของฟังก์ชัน นั้นๆ (ในที่นี้ก็คือ function main) อยู่ ซึ่งภายในกรอบใหญ่นี้ จะประกอบด้วย "Virtual Address ย่อยๆ" เรียงต่อกันไปเรื่อย ๆ และแต่ละ VA ย่อย ๆ นั้นคือที่อยู่ของ คำสั่ง Assembly แต่ละคำสั่งนั่นเอง
เหตุผลที่ Address ไม่เท่ากัน (The Non-Uniform Offset) ?
การที่ Address กระโดดไป 1 บ้าง, 3 บ้าง, หรือ 4 บ้าง ไม่ได้แปลว่า Offset ไม่คงที่ แต่เป็นเพราะ ขนาดของคำสั่ง Assembly (Instruction Size) ไม่เท่ากันครับ
คำสั่ง Assembly มีขนาดไม่เท่ากัน (Variable-Length Instructions)
ในสถาปัตยกรรม x86/x64 ที่เรากำลังวิเคราะห์อยู่ คำสั่ง Assembly แต่ละคำสั่งไม่ได้ใช้จำนวนไบต์เท่ากัน
บางคำสั่งใช้แค่ 1 ไบต์ เช่น push rbp
บางคำสั่งใช้ 3-5 ไบต์ เช่น คำสั่งที่มีการอ้างอิงถึง Register หรือ Address
บางคำสั่งอาจใช้ถึง 10-15 ไบต์ เช่น คำสั่งที่มี Operand เป็นค่าคงที่ขนาดใหญ่
0x1400014f8push rbp
ขนาดคำสั่ง = 1
ผลที่เกิดการเปลี่ยนแปลงค่า VA : จาก F8 -> F9 (กระโดด 1)
0x1400014f9 mov rbp, rsp
ขนาดคำสั่ง = 3
ผลที่เกิดการเปลี่ยนแปลงค่า VA : จาก F9 -> FC (กระโดด 3)
0x1400014fc sub rsp, 0x30
ขนาดคำสั่ง = 4
ผลที่เกิดการเปลี่ยนแปลงค่า VA : จาก FC -> 1500 (กระโดด 4)
0x140001500 call sym.__main 5 จาก 1500 -> 1505 (กระโดด 5)
ขนาดคำสั่ง = 5
ผลที่เกิดการเปลี่ยนแปลงค่า VA : จาก 1500 -> 1505 (กระโดด 1)
นี่คือบริบทของ offset เราจะใช้มุมมองที่ละเอียดพวกนี้ ในการเชื่อมโยงกลับไปที่บริบท Offset ใหญ่
Offset ระดับคำสั่ง: คือ ระยะห่าง จากคำสั่งก่อนหน้า เช่น จาก 0x1400014f8 ไป 0x1400014f9 คือ Offset 1
Offset ระดับไฟล์: คำสั่ง mov rbp, rsp ที่ 0x1400014f9 มี Offset ที่คงที่ วัดจาก Base Address ของไฟล์เสมอ
Offset=0x1400014f9−0x140000000=0x14f9
0x140000000 คือ BaseAddress
การที่เราเห็น Virtual Address ย่อย ๆ เรียงกันลงมา ก็คือการที่ Rizin กำลัง คำนวณ Base Address + Offset ของไฟล์ ให้เราเห็นในทุก ๆ บรรทัด เพื่อให้เรารู้ว่าคำสั่งนั้น ๆ อยู่ที่ตำแหน่งใดใน Virtual Address (VA)
หวังว่าคงเห็นภาพ บริบทของ Offset ได้เข้าเส้นมากพอนะครับ
เจอกันบทความต่อไปครับ