0x007 reverse1.exe ความแตกต่างของ stack และ stack frame

0x007 reverse1.exe ความแตกต่างของ stack และ stack frame
สวัสดีครับ มาต่อกันเลย หลังจากเรา เดินทางค่อยๆ reverse PE File ที่แสนจะเรียบง่าย
ก่อนที่จะเข้าไปลงมือทำ ดู Stack และ Stack frame ใน rizin เราจะมาศึกษาความเข้าใจของ Stack กันก่อนครับ

Stack คืออะไร ?

Stack คือพื้นที่ของหน่วยความจำที่มีไว้ให้กับ Process ที่รันอยู่ มันคือพื้นที่ใน Memory ที่ถูกจัดสรรอย่างเป็นระบบ มีหลักเกณฑ์
มีระเบียบวิธี (protocol) ชัดเจน ว่าจะใช้ยังไง ส่งมอบอะไร และเลิกใช้ ยังไง เราจะเกริ่น Stack กันแค่นี้ตอนนี้

เพราะเราจะขอย้อนไปเล็กน้อยก่อนว่า
Process คือ ไฟล์โปรแกรม (ในที่นี้ไฟล์โปรแกรมคือ .exe) ที่ถูกสั่ง Execute หรือสั่งประมวลผล ภาษาบ้านๆ คือเรา ดับเบิ้ลคลิกมันขึ้นมา
จาก Program มันก็จะกลายป็น Process

ทีนี้คำถามโดยคอนเซปต์คือ "เราเขียนโปรแกรมขึ้นมากันเพื่ออะไร ?" คำตอบก็คือ "เพื่อให้เป็นไปตามที่ผู้ออกแบบ / เขียนไว้ กำหนดให้มันทำงาน" หรือ
"เพื่อให้คอมพิวเตอร์แก้ไขปัญหา ที่เกินความสามารถของมนุษย์จะทำได้"
ให้คอมพิวเตอร์ จดจำ ค่าต่างๆ ที่สมองมนุษย์ จำไม่ไหว หรือให้คอมพิวเตอร์ช่วยคำนวณ ตัวเลขปริมาณมากๆ หรือสมการยากๆ ได้ในเวลาอันรวดเร็ว และมีความแม่นยำถูกต้อง
ัมันเป็นแบบนี้มาหลายสิบปีแล้ว ร่วมๆ 40-60 ปีแล้ว และมันจะดีขึ้นไปเรื่อยๆ ถึงได้เกิดอาชีพ นักพัฒนาซอฟต์แวร์ (Software Developer) ขึ้นมา เพื่อปรับปรุงโปรแกรมให้ดีขึ้น

ในมุม Reverser เล็กๆ มุมนึง นึกภาพตามนะครับ โปรแกรมต่างๆในโลกนี้ ที่มันดี ที่มันกลายเป็น process ที่ดีนะครับ
มันย่อมมีการรองรับสิ่งต่างๆ ที่เปลี่ยนไปได้มากมาย process นั้นสามารถรันได้ในเครื่องจำนวนมากทั่วโลก แปลว่า "มันต้องมีวิธีจัดการอะไรบางอย่าง ภายในโปรแกรมที่ถูกตั้งค่าไว้ เพื่อให้รองรับการเปลี่ยนแปลงได้มาก"
เช่น โปรแกรมคำนวณดอกเบี้ย หรือ ร้อยละ / เปอร์เซ็นต์ โปรแกรมพวกนี้ สามารถคิดคำนวณค่าดอกเบี้ย หรือร้อยละได้หลายแบบ
ดอกเบี้ยรายเดือนก็ได้ รายปีก็ได้ ร้อยละ 1 ร้อยละ 77 ก็ได้ เพราะตัวแปรเปลี่ยนค่าไปได้เรื่อยๆ
แต่ถ้าหากว่า โปรแกรมเหล่านี้ "ไม่มีตัวแปรเลย" แต่เป็น "ค่าคงที่ล้วนๆ" จะเกิดอะไรขึ้น ?

หากอัตราดอกเบี้ย เกิดมีการ เปลี่ยนแปลง / เราอยากจะคำนวณร้อยละ เปอร์เซ็นต์ด้วยอัตราอื่นละ แล้วโปรแกรมก็ไม่มีตัวแปรเลย ?
แล้วโปรแกรมที่เราหามาใช้ "หรือซื้อมาใช้" เปลี่ยนแปลงค่าพวกนี้ไม่ได้เลย เกิดอะไรขึ้นครับ ?
มันก็ไม่จำเป็นต้องเป็นโปรแกรมคอมพิวเตอร์ครับ มันก็จะเป็นแค่กระดาษโพย หนาๆ ประสิทธิภาพต่ำๆ 1 ปึก แล้วเราก็ค่อยๆเอามือเปิดกระดาษหาค่าที่เราต้องการเอาจากหมื่นๆแผ่น
ดูประสิทธิภาพต่ำ และช้ามาก

ดังนั้น "ตัวแปร" ภายในโปรแกรม (variable) ที่จะถูกเขียนขึ้นมา , นักพัฒนาโปรแกรมย่อมต้องรู้ว่า นี่คือสิ่งที่ "จำเป็นอย่างยิ่งที่จะต้องให้ค่าของมันสามารถเปลี่ยนแปลงได้"
มันช่วยให้โปรแกรมมีประสิทธิภาพสูง รองรับสภาพแวดล้อมที่หลากหลาย เงื่อนไขที่มากมายต่างๆ

เมื่อเขียนเสร็จ โปรแกรมขนาดใหญ่ๆ ที่มีความสามารถมากมาย ทำนู่นทำนี่ได้เยอะ ย่อมมีตัวแปรเป็นร้อย พัน หรืออาจจะถึงหมื่นๆตัว

ทำยังไง หรือจะบริหารจัดการยังไงละ ถ้าโปรแกรมมีค่า ที่เปลี่ยนแปลงไปเรื่อยๆ
ตอนนี้ เป็นค่านี้ เพื่อรับเข้ามา เพื่อคำนวณหรือทำงานอะไรสักอย่าง เสร็จแล้วก็คืนค่ากลับไปยังกลไกก่อนหน้าที่เรียกใช้งาน (function / return)
แล้วอีกสักพักตัวแปรนี้ ก็อาจจะเป็นอีกค่านึงได้เช่นกัน มีการเปรียบเทียบค่า ตัดสินใจต่างๆ มากมาย ทำให้เกิดการไหลไปไหลมา (flow)
พื้นที่ที่จะรองรับเรื่องพวกนี้ได้อย่างเหมาะสมคืออะไร ? คำตอบ ณ เวลานี้ นั่นก็คือ Dynamic Memory หรือก็คือ RAM ที่เราคุ้นเคยกันดี

ซึ่งถ้าเราจะเฉพาะเจาะจงไปในเรื่อง Program / Process / variable แล้วละก็
มันก็จะเข้าสู่เรื่อง กลวิธีในการจัดการ "จอง" พื้นที่ตัวแปรในหน่วยความจำ สิ่งนี้เราจะเรียกมันว่า Stack ของ Process

เวลาเราไปที่ร้านคอมเพื่อซื้อ RAM มาติดตั้ง , พอบูตเข้า OS แล้ว ไฟล์ที่จำเป็นต่างๆ จะถูกโหลดเข้าสู่ RAM
จับจอง Address ต่างๆ ตามแต่ละหน้าที่ของ process / threads
ในหน่วยความจำ (Memory) ทุก address เป็น "เลขฐาน 16" และเรียงจาก ต่ำ ไป สูง แบบนี้:

0x00000000   <--- ที่อยู่ต่ำ (Low Address)
...
0x7fffffff   <--- ที่อยู่สูง (High Address)

การใช้ rizin เปิดไฟล์ .exe ในโหมดปกติ (เช่น rizin reverse1.exe หรือ rizin -b 64 reverse1.exe)
จะจำลอง virtual address ให้เหมือนกับที่ตอน Execute บน Windows จริงๆ

และการเปิดแบบธรรมดานี้จะทำให้เรา "เข้าใจ Stack แบบเริ่มต้น ได้ง่าย"

Checklist ความพร้อมก่อนที่จะเข้าใจ Stack และ Stack Frame
- ทุกโปรแกรมที่รัน โดยเฉพาะบน Windows ถูกสร้างขึ้นมาเพื่อแก้ไขปัญหาอะไรสักอย่าง
- Windows เป็น API Based OS ทุกโปรแกรมต้อง "เรียกใช้ API Function อย่างน้อย 1 API เสมอ"
- ไม่มี Program / Process / Thread ไหนถูกสร้างมาแบบโล่งๆ หรือ สร้างฟังก์ชั่นที่ "ไม่มีอะไรให้ทำ" ยังไงก็ต้องมี
- อย่างที่ได้อธิบายไปยืดยาวแล้วว่า "ฟังก์ชั่น คือกลไกการแก้ไขปัญหาที่โปรแกรมเมอร์เขียนมา มันมีตัวแปร"
- และการที่จะมีฟังก์ชั่นได้ ก็จะมี thread อย่างน้อย 1 thread เสมอ
- ภายใต้ 1 thread นั้นจะมีกี่ฟังก์ชั่นก็ได้ แต่ก็อย่างที่ผมเล่าไป มันต้องมีอย่างน้อย 1 เสมอ

มาดูโค้ด rever1.exe กันอีกครั้ง

#include <stdio.h>
int main() {
    int number;
    printf("Enter a number:");
    scanf("%d", &number);
    printf("Number input =%d\n",number);
    return 0;
}

เมื่อรันจริง , reverse1.exe ถูกเราดับเบิ้ลคลิก Execute ทำให้ Windows OS สร้าง Process ขึ้นมา ชื่อ reverse1.exe
จากนั้น >> เกิด Thread ขึ้นมา 1 Thread โดยอัตโนมัติ และขอ Stack จาก Kernel
จากนั้น >> Thread มีขนาด Stack ของตัวเอง (stack เฉยๆ นะ ยังไม่มี Stack Frame) Windows มอบให้ 1 MB โดยประมาณเป็นขนาด Default เลย
จากนั้น >> ฟังชั่น ก็จะสร้าง Stack frame ของตัวเอง 1 stack frame ต่อ 1 function โดยดูจาก call ของ instruction code

นี่คือข้อความนะ ยังไม่เป็นภาพ เป็นข้อความที่จะทำให้เห็นว่า Stack / Stack frame คนละอย่างกัน

แค่นี้แหละ
ใน blog นี้เราจะ
- ใช้ rizin เพื่อดู function ว่าไฟล์นี้มีฟังก์ชั่นอะไรบ้าง หลายๆคนอาจจะคิดว่ามี main() ไง
- ใช้ rizin ดู stack ว่า มองเห็นภาพ stack ยังไง


paragraph 1 : ใช้ rizin เพื่อดู function ว่าไฟล์นี้มีฟังก์ชั่นอะไรบ้าง

เราจะเปิดไฟล์ด้วย rizin แบบปกติ

keng@kengUbuntu:~/re/stack_study$ rizin -b 64 reverse1.exe
 -- The unix-like reverse engineering framework.
[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]> 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
0x140001a00   57 880  -> 841  sym._pei386_runtime_relocator
0x140001d70    3 62           sym.__mingw_raise_matherr
0x140001db0    1 12           sym.__mingw_setusermatherr
0x140001dc0   30 445  -> 390  sym._gnu_exception_handler
0x140001f80    7 112          sym.__mingwthr_run_key_dtors.part.0
0x140001ff0    6 111          sym.___w64_mingwthr_add_key_dtor
0x140002060   12 129  -> 121  sym.___w64_mingwthr_remove_key_dtor
0x1400020f0   18 242  -> 214  sym.__mingw_TLScallback
0x140002200    1 16           sym.exit
0x140002210    1 16           sym._exit
0x140002220    4 44   -> 37   sym._ValidateImageBase
0x140002250    7 80           sym._FindPESection
0x1400022a0   10 157  -> 146  sym._FindPESectionByName
0x140002340   10 128  -> 121  sym.__mingw_GetSectionForAddress
0x1400023c0    5 55   -> 49   sym.__mingw_GetSectionCount
0x140002400   11 115  -> 108  sym._FindPESectionExec
0x140002480    4 54   -> 49   sym._GetPEImageBase
0x1400024c0   11 137  -> 130  sym._IsNonwritableInCurrentImage
0x140002550   17 198  -> 182  sym.__mingw_enum_import_library_names
0x140002620    3 50           fcn.140002620
0x140002660    1 65           sym.__mingw_vfprintf
0x1400026c0    6 48           sym.optimize_alloc
0x1400026f0   12 143  -> 135  sym.release_ptrs
0x140002780    8 109  -> 100  sym.resize_wbuf
0x1400027f0   12 160  -> 153  sym.cleanup_return
0x140002890   11 140  -> 132  sym.in_ch
0x140002920    6 99   -> 89   sym.back_ch
0x140002990  971 57258 -> 16482 sym.__mingw_sformat
0x140006ad0    1 73           sym.__mingw_vfscanf
0x140006b20    1 81           sym.__mingw_vsscanf
0x140006b80    8 154  -> 128  sym.__strtof
0x140006c20   11 190  -> 169  sym.__strtold
0x140006d00   12 236  -> 218  sym.__pformat_cvt
0x140006df0    6 87   -> 77   sym.__pformat_putc
0x140006e50   25 398  -> 387  sym.__pformat_wputchars
0x140006fe0   28 324  -> 306  sym.__pformat_putchars
0x140007130    4 71   -> 68   sym.__pformat_puts
0x140007180   10 138  -> 129  sym.__pformat_emit_inf_or_nan
0x140007210  103 1581 -> 1535 sym.__pformat_xint.isra.0
0x140007840   68 940  -> 870  sym.__pformat_int.isra.0
0x140007bf0   19 334  -> 304  sym.__pformat_emit_radix_point
0x140007d40   87 1064 -> 978  sym.__pformat_emit_float
0x140008170    6 204          sym.__pformat_emit_efloat
0x140008240    6 155  -> 148  sym.__pformat_efloat
0x1400082e0   13 219  -> 205  sym.__pformat_float
0x1400083c0   22 344  -> 319  sym.__pformat_gfloat
0x140008520   83 1263 -> 1161 sym.__pformat_emit_xfloat.isra.0
0x140008a10  161 2925 -> 2883 sym.__mingw_pformat
0x140009580    4 63           sym.__rv_alloc_D2A
0x1400095c0   10 140  -> 136  sym.__nrv_alloc_D2A
0x140009650    1 39           sym.__freedtoa
0x140009680   21 429  -> 415  sym.__quorem_D2A
0x140009840  287 6403 -> 6194 sym.__gdtoa
0x14000b180   63 912  -> 839  sym.__g__fmt
0x14000b510   16 274  -> 243  sym.__add_nanbits_D2A
0x14000b640   17 274  -> 255  sym.__rshift_D2A
0x14000b760    7 65   -> 54   sym.__trailz_D2A
0x14000b7c0   15 217  -> 212  sym.dtoa_lock
0x14000b8a0    3 66   -> 61   sym.dtoa_lock_cleanup
0x14000b8f0   11 251  -> 242  sym.__Balloc_D2A
0x14000b9f0    6 99   -> 92   sym.__Bfree_D2A
0x14000ba60    8 185  -> 176  sym.__multadd_D2A
0x14000bb20    9 189  -> 175  sym.__i2b_D2A
0x14000bbe0   20 327  -> 316  sym.__mult_D2A
0x14000bd30   25 409  -> 364  sym.__pow5mult_D2A
0x14000bed0   14 302  -> 294  sym.__lshift_D2A
0x14000c000    6 96   -> 68   sym.__cmp_D2A
0x14000c060   23 470  -> 450  sym.__diff_D2A
0x14000c240   12 271  -> 260  sym.__b2d_D2A
0x14000c350   19 496  -> 482  sym.__d2b_D2A
0x14000c540    4 50           sym.__strcp_D2A
0x14000c580    7 179  -> 156  sym.__increment_D2A
0x14000c640   41 985  -> 930  sym.rvOK.constprop.0.isra.0
0x14000ca20    5 60   -> 42   sym.__decrement_D2A
0x14000ca60   10 228  -> 213  sym.__set_ones_D2A
0x14000cb50  363 8214 -> 7919 sym.__strtodg
0x14000eb80   12 416  -> 409  sym.__sum_D2A
0x14000ed40    5 56   -> 37   sym.strnlen
0x14000ed80    6 53   -> 34   sym.wcsnlen
0x14000edc0  203 2953 -> 2859 sym.__gethex_D2A
0x14000f980   69 1116 -> 1041 sym.__hexnan_D2A
0x14000fe00   13 263  -> 259  sym.__s2b_D2A
0x14000ff10    4 156  -> 153  sym.__ratio_D2A
0x14000ffb0    5 75   -> 59   sym.__match_D2A
0x140010000    7 115  -> 106  sym.__copybits_D2A
0x140010080   11 104  -> 95   sym.__any_on_D2A
0x140010100    1 11           sym.__p__fmode
0x140010110    1 11           sym.__p__commode
0x140010120    4 103  -> 98   sym._lock_file
0x140010190    4 96   -> 91   sym._unlock_file
0x1400101f0    1 8            sym.mingw_get_invalid_parameter_handler
0x140010200    1 11           sym.mingw_set_invalid_parameter_handler
0x140010210    1 31           sym.__acrt_iob_func
0x140010230    7 137  -> 128  sym.__wcrtomb_cp
0x1400102c0    1 63           sym.wcrtomb
0x140010300   17 246  -> 219  sym.wcsrtombs
0x140010400   21 331  -> 323  sym.__mbrtowc_cp
0x140010550    1 104          sym.mbrtowc
0x1400105c0   16 269  -> 246  sym.mbsrtowcs
0x1400106d0    1 88           sym.mbrlen
0x140010738    1 6            sym.___lc_codepage_func
0x140010740    1 6            sym.___mb_cur_max_func
0x140010748    1 6            sym.__getmainargs
0x140010750    1 6            sym.__iob_func
0x140010758    1 6            sym.__set_app_type
0x140010760    1 6            sym.__setusermatherr
0x140010768    1 6            sym._amsg_exit
0x140010770    1 6            sym._cexit
0x140010778    1 6            sym._errno
0x140010780    1 6            sym._initterm
0x140010788    1 6            sym._lock
0x140010790    1 6            sym._onexit
0x140010798    1 6            sym.strtoull
0x1400107a0    1 6            sym.strtoll
0x1400107a8    1 6            sym._unlock
0x1400107b0    1 6            sym.abort
0x1400107b8    1 6            sym.calloc
0x1400107c0    1 6            sym.fprintf
0x1400107c8    1 6            sym.fputc
0x1400107d0    1 6            sym.free
0x1400107d8    1 6            sym.fwrite
0x1400107e0    1 6            sym.getc
0x1400107e8    1 6            sym.isspace
0x1400107f0    1 6            sym.isxdigit
0x1400107f8    1 6            sym.localeconv
0x140010800    1 6            sym.malloc
0x140010808    1 6            sym.memcpy
0x140010810    1 6            sym.memset
0x140010818    1 6            sym.realloc
0x140010820    1 6            sym.signal
0x140010828    1 6            sym.strerror
0x140010830    1 6            sym.strlen
0x140010838    1 6            sym.strncmp
0x140010840    1 6            sym.strtol
0x140010848    1 6            sym.strtoul
0x140010850    1 6            sym.tolower
0x140010858    1 6            sym.ungetc
0x140010860    1 6            sym.vfprintf
0x140010868    1 6            sym.wcslen
0x1400108e0    2 18           sym.__mingw_sformat.cold
0x140010940    1 5            sym.register_frame_ctor
[0x1400013f0]>

นับจำนวนฟังก์ชั่นทั่งหมด

[0x1400013f0]> aflc
164

มีจำนวน 164 function / ก็อาจจะเริ่มสงสัยกันนะว่า "ทุกฟังก์ชั่นต้องมี stack frame เลยมั้ย มันจะกลายเป็น 164 stack frame เลยหรือเปล่า ?"
คำตอบคือ "ไม่" , แต่เราจะไม่ขยายคำตอบนี้ตอนนี้


เป้าหมายเราใน paragraph นี้คือ ใช้ rizin ดู stack ว่า มองเห็นภาพ stack ยังไง
อันดับแรกเราจะต้องรู้ก่อนว่าเราอยู่ตรงไหน โดยปกติถ้าเราเปิดไฟล์ตามปกติ แล้วใช้ aaa เพื่อบอก  rizin ให้ทำการ auto analysis ให้เรา
พอเสร็จแล้ว rizin จะพาเรามาที่ entry point เสมอ (เพราะมันหาได้จาก PE File structure) ซึ่งก็คือการ parse PE file เพื่ออำนวยความสะดวกให้เรานั่นเอง

[0x1400013f0]> afl.
0x1400013f0 entry0
[0x1400013f0]>


คำสั่ง afl.
คือคำสั่งที่ระบุว่าเราที่อยู่ address นี้ใน shell ของ rizin (ก็คือตำแหน่ง 0x1400013f0) เป็นฟังก์ชั่นอะไรของไฟล์นี้
แต่ entry0 ยังไม่ใช่ main
เราจะต้องหา main ใน process ให้เจอ เพื่อจะรู้ว่ามันทำงานอะไรบ้างยังไง ใช่มั้ยครับ

เราก็ยังอยู่ที่คำสั่งเดิม คือ afl นี่แหละครับ แต่เรา grep ข้อมูลให้แคบลง ด้วยเครื่องมือภายใน (internal grep) ของ rizin shell คือตัวหนอนครับ
วิธีใช้ grep ใน rizin คือ คำสั่งแล้วตามด้วยตัวหนอน แล้วตามด้วยสิ่งที่เราจะหา

[0x140001000]> afl~main
0x140001180   26 592  -> 554  sym.__tmainCRTStartup
0x1400013d0    1 29           sym.WinMainCRTStartup
0x1400014f8    1 81           sym.main
0x140001610    3 31   -> 26   sym.__main
0x140010748    1 6            sym.__getmainargs
[0x140001000]>


afl~main ก็จะลิสต์ 164 บรรทัด แล้วเราก็ grep เอาเฉพาะบรรทัดที่มีคำว่า main เท่านั้น ก็เลยโผล่ขึ้นมาแค่นั้น
คนที่ใช้ linux command อยู่ก็คงพอจะเข้าใจได้ทันที

เอาละ มาต่อกัน
เราต้องหา main เพราะเป็นฟังก์ชั่นหลักที่ process จะทำงานจริงๆ แต่มันมีหลาย main แบบนี้จะคัดแยกยังไง ?
sym.__tmainCRTStartup กับ sym.__getmainargs เทียบกับอันอื่นๆ น่าจะตัดสองอันนี้ออกได้ก่อนเลย
เหลือ sym.WinMainCRTStartup , sym.main , sym.__main
อันไหนจะเป็นฟังก์ชั่นหลักจริงๆ ละ ?
อันดับแรก เราสามารถ "แยก CRT" ออกไปได้ครับ CRT มันย่อมาจาก C Runtime
คือเป็นสิ่งที่ Compiler และ Linker ใส่มาให้ตอน compile source code มาเป็น program

ในอนาคตเราจะเจอหลายๆครั้ง ไม่ว่าจะเป็น pe file ปกติ หรือ malware

ทีนี้มันก็จะเหลือ

0x1400014f8    1 81           sym.main
0x140001610    3 31   -> 26   sym.__main
0x140010748    1 6            sym.__getmainargs


สิ่งต่อไปที่เราจะวิเคราะห์คือ "ขนาดของ main function มันไม่ควรจะเล็กเกินไป" ก็เพราะมันคือ main ยังไงล่ะ
อย่าง sym.__getmainargs มีขนาด 6 byte ซึ่งเล็กไปมากที่จะเป็น main จริงๆ ตัดออกได้ง่ายๆ แบบนี้เลยแหละครับ

แปลว่าเราก็จะเหลือแค่
0x1400014f8    1 81           sym.main
0x140001610    3 31   -> 26   sym.__main


อันไหนล่ะที่จะเป็น main function จริงๆ โดยที่สมมติว่าเราไม่รู้ว่าโค้ดคืออะไร (สวมวิญญาณ reverser noob ๆ)

คำตอบคือย้อนไปตรง

0x140001180   26 592  -> 554  sym.__tmainCRTStartup
0x1400013d0    1 29           sym.WinMainCRTStartup


sym.WinMainCRTStartup < เป็น Runtime ของภาษา C
อีกเหมือนกัน ไม่ได้มาจากการเขียนเองของโปรแกรมเมอร์หรือ malware developer
ขั้นตอนสุดท้ายที่เราจะยืนยัน ว่า main function ถูกเรียก ก็คือ
เข้าไปดูใน sym.__tmainCRTStartup ว่าเรียกฟังก์ชั่นอะไรไปบ้าง วิธีการนี้เรียกว่าการตรวจสอบ cross references ผมจะสอนทีละขั้นเลยครับ

[0x1400013f0]> afl.
0x1400013f0 entry0


ยืนยันว่าเราอยู่ ณ ตำแหน่ง EntryPoint ยังไม่ใช่ main คำสั่งดูดีๆนะครับ afl มีจุดต่อท้ายด้วย

[0x1400013f0]> pdf~call
│           0x140001401      call  sym.__tmainCRTStartup               ; sym.__tmainCRTStartup


ใช้ pdf เพื่อหาว่า entry0 นี้ disassembly ออกมาได้อะไรบ้างโดย grep เฉพาะคำว่า call
เพราะเป็นการเรียกฟังก์ชั่น


[0x1400013f0]> s sym.__tmainCRTStartup

เราใช้คำสั่ง s ย้ายตัวเราเอง เข้าไปอยู่ใน address ของฟังก์ชั่น sym.__tmainCRTStartup (คำสั่ง s คือ มาจากคำว่า seek คือมองหาตำแหน่ง)

[0x140001180]> pdf~main
            ; CALL XREF from sym.WinMainCRTStartup @ 0x1400013e1
┌ sym.__tmainCRTStartup();
│     │╎│   ; CODE XREF from sym.__tmainCRTStartup @ 0x1400011a5
│    ││╎│   ; CODE XREF from sym.__tmainCRTStartup @ 0x1400013b9
│ ╎│╎││╎│   ; CODE XREF from sym.__tmainCRTStartup @ 0x140001372
│ ╎│╎││╎│   ; CODE XREF from sym.__tmainCRTStartup @ 0x1400013c3
│ ╎│╎││╎│   0x1400012c4      call  sym.__main                          ; sym.__main
│ ╎│╎││╎│   0x1400012ea      call  sym.main                            ; sym.main ; int main(int argc, char **argv, char **envp)


เรา internal grep อีกแล้ว แล้วเราก็หาเจอแล้ว
คำตอบที่ยืนยันได้อีกเพิ่มเติมก็คือ : ฟังก์ชัน main มักมีลักษณะ มีการรับอาร์กิวเมนต์ (argc, argv) หรือเรียกใช้ I/O functions ก็คือจะมี operation จริงๆ

ลองทำการ ย้ายตัวเองเข้าไปใน sym.main

[0x1400013f0]> s sym.main

จากนั้นก็ทำการ print disassembly function หรือ pdf

[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.140012011]               ; 0x140012011 ; "%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]        ; 0x140012014 ; "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]>

เราก็จะเห็นว่า rizin ช่วยเรา parse ข้อมูลให้เราวิเคราะห์ครับ
ขอแปะภาพไว้นะครับ เพราะนอกจาก rizin จะช่วยเราด้วย command ที่รวดเร็วแล้ว rizin ยังทำให้ shell มีความ colorize ด้วย

0x007-01-sym-main-confirmed


ทำให้เราสังเกตง่ายขึ้นอีก
สังเกตว่า
- บนสุดจะมีการ push ล่างสุดจะมีการ pop เป็นสีม่วง add , sub เป็นสีเหลือง เรื่องพวกนี้เป็นกลไกของ stack / stack frame
- call คือการเรียก function จะเป็นสีเขียว
- ret ย่อมาจากคำว่า return คือจบ function main นี้แล้ว

รู้สึกว่าเราสามารถมองภาพแยกเป็นส่วนๆ ได้แล้วใช่มั้ยครับ
ทีนี้เราเห็นฟังก์ชั่น main แล้ว เราเห็นแล้วว่ามีการเริ่มต้นและสิ้นสุดของ stack (push ตอนเริ่มฟังก์ชั่น , pop ตอนจบฟังก์ชั่น)
เราจะต้องยืนยันอีกนิดว่า "มันจะต้องมีแค่ thread เดียว"
1 stack = 1 thread เสมอ

หายังไง หรือยืนยันยังไง ?
logic ก็คือ , Process จะต้องไม่มีการสร้าง thread ขึ้นมาครับ >> หมายความว่า ถ้ามีการสร้าง thread แปลว่ามันจะมีมากกว่า 1 thread >> เราจึงต้องเข้ามาดูว่ามีการ import API อะไรบ้าง (Windows มี API ที่ไว้จัดการ Thread มากมาย)

คำสั่ง ii ครับ

[0x1400013f0]> ii?
Usage: ii[jqt]   # List imports (table mode)
| iij      # List imports (JSON mode)
| iiq      # List imports (quiet mode)
| iit      # List imports (table mode)
[0x1400013f0]> ii
nth vaddr       bind type lib          name
-------------------------------------------------------------------
1   0x140016220 NONE FUNC KERNEL32.dll DeleteCriticalSection
2   0x140016228 NONE FUNC KERNEL32.dll EnterCriticalSection
3   0x140016230 NONE FUNC KERNEL32.dll GetLastError
4   0x140016238 NONE FUNC KERNEL32.dll InitializeCriticalSection
5   0x140016240 NONE FUNC KERNEL32.dll IsDBCSLeadByteEx

ด้วยคำสั่ง ii คือการให้ rizin ช่วยเราแสดงการ imports ออกมาว่า pe file นี้ imports api อะไรออกมาบ้างจากไฟล์ไหนบ้าง
ตรงคอลัมน์ name เราก็จะเห็นชื่อ API ซึ่งโปรแกรมเมอร์จะคุ้นเคยดีเลยทีเดียว

กลับมาที่ Logic ของเราคือ process ที่มีมากกว่า 1 thread = มีมากกว่า 1 stack

ถ้าจะเขียนโปรแกรม ให้ process ทำงานหลายๆ thread ก็จะต้องเรียก api เกี่ยวกับ thread (ในระดับ advance เช่น malware โหดๆ จะเป็น multi thread เสมอ)
ดังนั้นก็จะมีการ import api เกี่ยวกับ thread เช่น CreateThread << สำคัญมากอันนี้

เพื่อจะยืนยัน ถ้ามี thread อื่นๆ เราน่าจะ grep เจอได้จาก ii ลองเลยแล้วกัน

[0x1400013f0]> ii~Thread
[0x1400013f0]> ii~hread
[0x1400013f0]>



ผลลัพธ์ output ก็คือไม่มีครับ
สรุป reverse1.exe นี้เป็น program แบบ single thread แปลว่ามี 1 thread แปลว่ามี 1 stack ของ thread นี้
นี่คือ การตอบคำถามของ paragraph ที่ว่า - ใช้ rizin ดู stack ว่า มองเห็นภาพ stack ยังไง นั่นเอง
เพราะอะไร ?
เพราะ เรายืนยันว่า
== ฟังก์ชั่น main เริ่มด้วย push จบด้วย pop > ret มันคือ stack
== ไม่มี thread อื่นๆ อีก ก็แปลว่า ไม่มี stack อื่นๆ เช่นกัน
== เราแยก runtime function ออกไปได้ เพราะเรารู้ว่า CRT ย่อมาจาก C Runtime ซึ่งมาจาก Compiler / linker ไม่น่าจะใช่โค้ดโปรแกรมที่ถูกเขียนขึ้นมา
เราจึงมองเห็น main function แล้ว และ identify มันได้



paragraph 2 : ใช้ rizin อธิบาย rbp (register stack base pointer)

คราวนี้เราจะมาเน้น มากๆ แล้ว
จากฟังก์ชั่น main เราจะเห็นบรรทัดที่เขียนไว้ว่า

0x1400014f8      push  rbp
0x1400014f9      mov   rbp, rsp
0x1400014fc      sub   rsp, 0x30


โดยพฤติกรรมของ assembly แบบนี้ เราแทบจะตีขลุมเป็นส่วนใหญ่ได้เลยว่า stack เริ่มสร้างขึ้นมาแล้ว ผมรู้ได้ไงละ
ผมจะอธิบายทีละบรรทัดนะครับ

0x1400014f8      push  rbp

ที่ Instruction Address ตำแหน่ง 0x1400014f8 นี้  เกิดคำสั่งให้ทำการเตรียมค่าเดิมของ register stack base pointer เก็บไว้ก่อน นั่นก็คือการ push
พอจบฟังก์ชั่นเราจะสามารถกลับมาได้ถูกที่ ต่อไปนี้จะเรียก register stack base pointer ว่า rbp

0x1400014f9      mov   rbp, rsp

ที่ Instruction Address ตำแหน่ง 0x1400014f9 นี้
คือการสั่งให้ rsp ที่กำลัง "ชี้" stack ไปที่ไหน ให้ "ย้าย (mov)" มาชี้ที่ base stack pointer นี้ เพื่อบอกว่านี่คือฐาน ตำแหน่งนี้นะ

คำว่า mov ก็แปลตามคัว คือ "ย้าย" เราจะ ย้ายค่าจาก rsp มาเก้บไว้ใน rbp
rsp ชี้อะไรอยู่ , ย้ายมาชี้ฐาน ที่ตำแหน่ง rbp ซะ
เมื่อมีการชี้ฐานแล้ว แปลว่าหลังจากนี้
"thread ก็สามารถ เติม stack frame เข้าไปยัง stack ฐานนี้ได้ , stack จะเติบโตขึ้น
ตามการ เติม stack frame เข้าไปเป็นชั้นๆ ซ้อนๆกัน"


ผมจะอธิบายเป็นภาพเสริมด้วยครับ
และ
"หาก stack frame ทำงานจนเสร็จ ก็จะทำการ pop ออกแบบ LIFO *(Last in , First Out)"

เปรียบภาพ
Stack = แก้วใสๆ ที่คว่ำไว้ ลอยไว้กลางอากาศ มีชื่อแก้วว่า "thread 1"
Stack Frame = เป็นกล่องของฟังก์ชั่น ที่ซ้อนกันอยู่ ที่ฐานของแก้ว ซ้อนจากตำแหน่งสูง ซ้อนลงมาข้างล่าง
LIFO = เมื่อฟังก์ชั่นทำงานจบ กล่องข้างล่างต้อง pop ออกไปก่อน , LIFO

0x007-02-thread-and-stack-overview-flow

0x1400014fc      sub   rsp, 0x30


ที่ตำแหน่ง 0x1400014fc นี้คือ ตำแหน่งที่ "สูงขึ้น" กว่าตำแหน่งก่อนหน้านี้ , คำสั่งนี้คือการเติบโตครั้งแรกของ stack frame #1
ก็คือจะมีฟังก์ชั่นแรกเกิดขึ้นใน stack นี้ จึงต้องมีการจองพื้นที่ memory ให้ฟังก์ชั่นวางตัวแปรได้ ทำงานได้
จึงทำการ "ลบ" ค่าออกจาก stack ฐานไป 0x30 แปลงเป็นฐาน 10 ก็ประมาณ 48 byte
0x1400014f9 → ไม่ใช่ค่า ของ RSP แต่มันคือ address ของ instruction
RSP เก็บค่าตำแหน่ง ที่ล่าสุดใน Stack
พอ sub rsp, 0x30 → ค่าของ RSP ลดลง → Stack “โตลงล่าง” (lower memory)
 ตัวเลข Address ของ Instruction (0x1400014f9 → 0x1400014fc) กำลัง “เพิ่มขึ้น”
แต่ ค่าของ RSP กำลัง “ลดลง” → เพราะ stack เติบโตจาก สูงลงต่ำ

ตัวอย่างค่าของ RSP ตอนแรก เช่น:
  RSP = 0x0000007FFFFFFFE0
  sub rsp, 0x30
  RSP = 0x0000007FFFFFFFB0

อันนี้คือตัวอย่าง

กำลังดีดใช่มั้ย ? ต่อเลยดิ ผมจะให้ดูของจริง แต่ปัญหาคือ rizin เป็น static analyze ไม่ใช่ dynamic เหมือนตอนรันจริง
การจะดูได้ก็ต้องดูใน debugger ทีละสเต็ป
ก็ต้องกลับมาหาอีกเครื่องมือเพื่อนยากของเราครับ x64dbg ทำการโหลด reverse1.exe เข้าไปซะ

0x007-03-stack-step

อธิบายขั้นตอนที่ 1 ของ x64dbg : ขั้นตอนนี้คือ
ที่ Instruction Address 00007FF7A62A14F8 ผมทำการตั้ง breakpoint ไว้ เมื่อกด F8 มาถึง ก็จะเจอคำสั่งให้ทำการ push rbp

อย่างที่อธิบายไปแล้ว ย้ำอีกรอบ มันคือการบอกว่า เก็บฐาน stack เดิมไว้นะ
ทางด้านขวาให้เราสังเกต "มันคือ memory region ของ stack thread นี้"
ทางด้านซ้ายคือ "memory region ของ PE File"
คนละส่วนกันนะครับ
rbp / rsp ของด้านขวามือจะมีขีดเส้นไว้ด้วย เพื่อให้เราสังเกตเห็นได้ง่าย

0x007-04-stack-step

อธิบายขั้นตอนที่ 2 ของ x64dbg : ขั้นตอนนี้คือ
ที่ Instruction Address 00007FF7A62A14F9 เมื่อกด F8 มาถึง ก็จะเจอคำสั่งให้ทำการ mov rbp,rsp

อย่างที่อธิบายไปแล้ว ย้ำอีกรอบ มันคือการบอกว่า ให้ stack pointer ชี้ไปที่ฐานนะ
คำสั่ง mov ในภาษา assembly คือสั่งย้ายสิ่งที่อยู่ทางขวา (rsp) มาเก็บไว้ทางซ้าย (rbp)
หรือตีความแบบ stack ก็คือ ไอ้ register stack pointer น่ะ ตอนนี้ต้องมาชี้ "ฐาน stack ปัจจุบันนี้" ได้แล้วนะ เพราะกำลังจะสร้าง stack frame

0x007-05-stack-step

อธิบายขั้นตอนที่ 3 ของ x64dbg : ขั้นตอนนี้คือ
ที่ Instruction Address 00007FF7A62A14FC เมื่อกด F8 มาถึง ก็จะเจอคำสั่งให้ทำการ sub rsp,30

อย่างที่อธิบายไปแล้ว ย้ำอีกรอบ มันคือการบอกว่า เกิด stack frame ขึ้น 30 byte
sub มาจากคำว่า subtraction แปลว่า ลบออก เพื่อให้เกิด stack frame ให้ฟังก์ชั่นทำงานได้ เติบโตลงมาข้างล่าง
ฐาน stack เดิมมีค่าเท่าไหร่ ก็ลบออกไป 30 byte ดูด้านขวา rbp / rsp นะครับ
rbp ฐาน stack ณ ปัจจุบัน = 4152BFF820
rsp ชี้ฐานมั้ย ? ณ ปัจจุบัน = 4152BFF820

โอเค เหมือนกันเป๊ะ แปลว่า ชี้ฐานแล้วจริง แล้วโดนสั่งให้เติบโต 30 byte แล้ว
กด F8 ต่อได้

0x007-07-stack-step

อธิบายขั้นตอนที่ 4 ของ x64dbg : ขั้นตอนนี้คือ
ที่ Instruction Address 00007FF7A62A1500 เมื่อกด F8 มาถึง ก็จะเจอคำสั่งให้ทำการ call reverse1.7FF7A62A1610

อย่างที่อธิบายไปแล้ว ย้ำอีกรอบ มันคือการบอกว่า เมื่อ stack frame ถูกสร้างแล้วก็พร้อมเพียงพอที่จะสามารถ call function ได้
เดิม rsp ชี้ฐานอยู่ที่ 4152BFF820
ถูกลบออกไป 30 byte จาก step เมื่อกี้ (จริงๆนับเป็น 48 byte ในเลขฐาน 10 )
ก็จะเหลือ 4152BFF7F0 จริงมั้ยก็เปิดเครื่องคิดเลขลบให้ดูเลย

0x007-06-stack-step

Stack frame นั้นคือกล่องชั้นๆ ที่ถูกเรียงกันใน Stack ของ thread อีกที

0x007-08-stack-step

เมื่อฟังก์ชั่นทำงานจบ ก็จะทำการ pop ออก จากล่างก่อน หรือ Last In ก่อน
คำสั่งก็จะกลายเป็น add rsp ขนาด เช่น add rsp, 30
rsp ก็จะถูกชี้ขยับขึ้นไปเรื่อยๆ
พอจะจบ ก็จะทำการ pop rbp และ ret คือคืนกลับไปที่ caller function ที่เรียกมา

ถ้าตามในภาพ ตัวอย่างถังสีม่วงนี้พอ add rsp,30 มันก็จะกลับไปชี้ที่ address ก่อนหน้าได้ มันก็คือกระบวนการ LIFO นั่นเองครับ

ผมหวังว่าจะพอเป็นแนวทางให้กับทุกคนได้นะครับ
เจอกันใน blog ต่อไปครับ