לדלג לתוכן

5.11 פורמט ELF לעומק פתרון

פתרון - פורמט ELF לעומק

פתרון תרגיל 1 - זיהוי סקשנים

נכתוב את התוכנית:

#include <stdio.h>

int initialized_var = 100;      // משתנה גלובלי מאותחל
int uninitialized_var;          // משתנה גלובלי לא מאותחל

void greet(void) {              // פונקציה נוספת
    printf("Hello!\n");         // מחרוזת קבועה
}

int main(void) {
    greet();
    printf("value = %d\n", initialized_var);
    return 0;
}

נקמפל:

gcc -g -o prog prog.c

נמצא את הסקשנים:

readelf -S prog

פלט (חלקי):

  [Nr] Name              Type             Address           Offset    Size              Flags
  ...
  [14] .text             PROGBITS         0000000000001060  00001060  0000000000000185  AX
  [16] .rodata           PROGBITS         0000000000002000  00002000  0000000000000020  A
  [24] .data             PROGBITS         0000000000004000  00003000  0000000000000014  WA
  [25] .bss              NOBITS           0000000000004014  00003014  0000000000000004  WA
  ...

עכשיו נבדוק את הסמלים:

readelf -s prog | grep -E "initialized_var|uninitialized_var|greet|main"

פלט:

    48: 0000000000004010     4 OBJECT  GLOBAL DEFAULT   24 initialized_var
    49: 0000000000004014     4 OBJECT  GLOBAL DEFAULT   25 uninitialized_var
    50: 0000000000001149    26 FUNC    GLOBAL DEFAULT   14 greet
    51: 0000000000001163    38 FUNC    GLOBAL DEFAULT   14 main

סיכום:
| אלמנט | סקשן | הסבר |
|--------|-------|-------|
| initialized_var (ערך 100) | סקשן 24 - .data | משתנה גלובלי עם ערך התחלתי |
| uninitialized_var | סקשן 25 - .bss | משתנה גלובלי ללא ערך התחלתי |
| מחרוזת "Hello!\n" | .rodata (סקשן 16) | מחרוזות קבועות נשמרות ב-rodata |
| פונקציה greet | סקשן 14 - .text | קוד בר-הרצה |
| פונקציה main | סקשן 14 - .text | קוד בר-הרצה |

אפשר גם לוודא שהמחרוזת נמצאת ב-.rodata:

objdump -s -j .rodata prog

Contents of section .rodata:
 2000 01000200 48656c6c 6f210a00 76616c75  ....Hello!..valu
 2010 65203d20 25640a00                    e = %d..

אכן, המחרוזות "Hello!\n" ו-"value = %d\n" נמצאות בסקשן .rodata.


פתרון תרגיל 2 - כתובת main ודיסאסמבלי

א. מציאת כתובת main:

readelf -s prog | grep main

    51: 0000000000001163    38 FUNC    GLOBAL DEFAULT   14 main

הכתובת של main היא 0x1163.

ב. דיסאסמבלי:

objdump -d prog | grep -A 20 "<main>"

0000000000001163 <main>:
    1163:  f3 0f 1e fa          endbr64
    1167:  55                   push   %rbp
    1168:  48 89 e5             mov    %rsp,%rbp
    116b:  b8 00 00 00 00       mov    $0x0,%eax
    1170:  e8 d4 ff ff ff       call   1149 <greet>
    1175:  8b 05 95 2e 00 00    mov    0x2e95(%rip),%eax
    117b:  89 c6                mov    %eax,%esi
    117d:  48 8d 05 8a 0e 00 00 lea    0xe8a(%rip),%rax
    1184:  48 89 c7             mov    %rax,%rdi
    1187:  b8 00 00 00 00       mov    $0x0,%eax
    118c:  e8 9f fe ff ff       call   1030 <printf@plt>
    1191:  b8 00 00 00 00       mov    $0x0,%eax
    1196:  5d                   pop    %rbp
    1197:  c3                   ret

ג. הכתובת תואמת - גם readelf וגם objdump מראים שmain מתחיל ב-0x1163.

הסבר השורות:
- endbr64 - הוראת אבטחה (CET/IBT) - מסמנת שזו נקודת כניסה חוקית לקפיצה
- push %rbp / mov %rsp,%rbp - פרולוג הפונקציה, שמירת הstack frame (כזכור מפרק 3 על שפת C)
- call 1149 <greet> - קריאה לפונקציה greet
- mov 0x2e95(%rip),%eax - קריאת הערך של initialized_var (הכתובת מחושבת יחסית ל-RIP)
- mov %eax,%esi - הכנת הארגומנט השני ל-printf (הערך של initialized_var)
- lea 0xe8a(%rip),%rax / mov %rax,%rdi - הכנת הארגומנט הראשון (כתובת המחרוזת "value = %d\n")
- call 1030 <printf@plt> - קריאה ל-printf דרך PLT
- mov $0x0,%eax - ערך החזרה 0 (return 0)
- pop %rbp / ret - אפילוג הפונקציה, חזרה לקורא


פתרון תרגיל 3 - השוואה בין קומפילציה סטטית לדינמית

נקמפל את אותה תוכנית בשתי דרכים:

gcc -o dynamic_prog prog.c
gcc -static -o static_prog prog.c

א. מספר סקשנים:

readelf -S dynamic_prog | grep "There are"

There are 31 section headers, starting at offset 0x3978:

readelf -S static_prog | grep "There are"

There are 33 section headers, starting at offset 0xb2f60:

לגרסה הדינמית יש 31 סקשנים, לגרסה הסטטית יש 33.

ב. סקשנים שמופיעים רק בגרסה הדינמית:

readelf -S dynamic_prog | grep -E "\.dynsym|\.dynstr|\.plt|\.got|\.rela\.plt|\.rela\.dyn|\.interp|\.dynamic"

  [ 1] .interp           PROGBITS  ...
  [ 6] .dynsym           DYNSYM    ...
  [ 7] .dynstr           STRTAB    ...
  [10] .rela.dyn         RELA      ...
  [11] .rela.plt         RELA      ...
  [13] .plt              PROGBITS  ...
  [23] .dynamic          DYNAMIC   ...
  [24] .got              PROGBITS  ...

הסקשנים .interp, .dynsym, .dynstr, .plt, .got, .rela.plt, .rela.dyn, .dynamic קיימים רק בגרסה הדינמית. הסיבה: הסקשנים האלה נחוצים לתהליך הלינקור הדינמי - הם מכילים את המידע שהdynamic linker צריך כדי לטעון ספריות משותפות ולפתור כתובות בזמן ריצה. בגרסה הסטטית כל הקוד כבר מקובע בתוך הקובץ ואין צורך בלינקור דינמי.

ג. השוואת גדלים:

ls -la dynamic_prog static_prog

-rwxr-xr-x 1 user user   16696 dynamic_prog
-rwxr-xr-x 1 user user  879928 static_prog

הגרסה הסטטית גדולה בהרבה (כ-880KB מול 17KB) כי היא מכילה את כל קוד הlibc בתוך הקובץ עצמו, במקום להסתמך על ספריה חיצונית.

ד. בדיקה עם ldd:

ldd dynamic_prog

    linux-vdso.so.1 (0x00007ffff7fc3000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc5000)

ldd static_prog

    not a dynamic executable

הגרסה הדינמית תלויה ב-libc.so.6 ובdynamic linker. הגרסה הסטטית לא תלויה בשום ספריה חיצונית.


פתרון תרגיל 4 - רלוקציות ו-printf

נניח שיש לנו תוכנית שמשתמשת ב-printf ומקומפלת דינמית.

א+ב. מציאת הrelocation של printf:

readelf -r prog | grep printf

000000003fd0  000200000007 R_X86_64_JUMP_SL 0000000000000000 printf@GLIBC_2.2.5 + 0

ג. הסבר השדות:

שדה ערך הסבר
Offset 0x3fd0 הכתובת בGOT שבה צריך לכתוב את הכתובת האמיתית של printf
Info 000200000007 מכיל את אינדקס הסמל (0x2) ואת סוג הrelocation (0x7)
Type R_X86_64_JUMP_SLOT סוג הrelocation - "jump slot" - זה אומר שצריך למלא כתובת בGOT עבור PLT
Sym. Value 0x0 הכתובת של הסמל עדיין לא ידועה (תימצא בזמן ריצה)
Sym. Name printf@GLIBC_2.2.5 שם הסמל שצריך למצוא - printf מגרסת GLIBC 2.2.5

ד. הסבר במילים: רשומת הrelocation הזו אומרת לdynamic linker: "כשתטען את התוכנית, מצא את הכתובת של הפונקציה printf בספריית libc, וכתוב את הכתובת הזו לתוך הGOT בoffset 0x3fd0. ככה כשהתוכנית תקפוץ דרך הPLT לprintf, הPLT ייקח את הכתובת מהGOT ויקפוץ אליה."

ה. בדיקה מול הGOT:

readelf -S prog | grep got

  [23] .got              PROGBITS         0000000000003fb8  00002fb8  0000000000000048  08  WA  0   0  8

הGOT מתחיל בכתובת 0x3fb8 ותופס 0x48 בתים, כלומר מסתיים ב-0x4000.
הoffset של הrelocation הוא 0x3fd0, שנופל בתוך הטווח 0x3fb8 עד 0x4000 - אכן מדובר בכניסה בGOT.


פתרון תרגיל 5 - פונקציות constructor ו-destructor

הקוד:

#include <stdio.h>

__attribute__((constructor))
void my_init(void) {
    printf("constructor: before main\n");
}

__attribute__((destructor))
void my_cleanup(void) {
    printf("destructor: after main\n");
}

int main(void) {
    printf("main: running\n");
    return 0;
}

א. קומפילציה והרצה:

gcc -o ctor_test ctor_test.c
./ctor_test

constructor: before main
main: running
destructor: after main

סדר ההרצה: constructor קודם, אחריו main, ואחרון destructor - בדיוק כמו שציפינו.

ב. מציאת הסקשנים:

readelf -S ctor_test | grep -E "init_array|fini_array"

  [19] .init_array       INIT_ARRAY       0000000000003db8  00002db8  0000000000000010  08  WA  0   0  8
  [20] .fini_array       FINI_ARRAY       0000000000003dc8  00002dc8  0000000000000010  08  WA  0   0  8

הסקשן .init_array נמצא בכתובת 0x3db8 ותופס 0x10 בתים (16 בתים = 2 כתובות של 8 בתים כל אחת).
הסקשן .fini_array נמצא בכתובת 0x3dc8 ותופס 0x10 בתים.

ג. צפייה בתוכן הסקשן:

readelf -x .init_array ctor_test

Hex dump of section '.init_array':
  0x00003db8 00110000 00000000 49110000 00000000 ........I.......

אנחנו רואים שתי כתובות (בlittle endian):
- 0x1100 - זו כתובת הפונקציה frame_dummy (פונקציה פנימית של GCC)
- 0x1149 - זו הכתובת של הconstructor שלנו

ד. אימות הכתובת:

readelf -s ctor_test | grep my_init

    50: 0000000000001149    26 FUNC    GLOBAL DEFAULT   14 my_init

הכתובת של my_init היא 0x1149, שזה בדיוק מה שראינו בתוכן של .init_array. הruntime של C עובר על הכתובות ב-.init_array וקורא לכל פונקציה לפני שהוא קורא ל-main. באופן דומה, אחרי שmain חוזר, הוא עובר על .fini_array וקורא לפונקציות שם - כולל my_cleanup.