0x009 reverse1.exe อธิบายพื้นฐาน และความเข้าใจเรื่อง Offset

สวัสดีครับ วันนี้เราจะมาทำความเข้าใจกับคำว่า 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 ที่เรากำหนด

ref : https://www.autodesk.com/blogs/autocad/wp-content/uploads/sites/35/2020/09/OffsetOptions_5_Horiz.png

ทีนี้ ในโลกของ 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

rizin-offset-example-2025-10-03 12_00_36-

 

สิ่งนี้ก็คือ 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 ได้เข้าเส้นมากพอนะครับ

เจอกันบทความต่อไปครับ