Sitemap

[Binary Exploit — EP.5–1] Introduction to Buffer Overflow

ทำไมการที่เราใส่ Input ที่มีขนาดใหญ่มากเกินไปจะสามารถ Hack โปรแกรมได้

--

หากใครยังจำตัวอย่างโค้ดจาก EP ที่แล้วที่ว่าเมื่อเราป้อนข้อมูลที่มีความยาวมากเกินไปโปรแกรมจะ Error และหยุดทำงานทันที ในบทความนี้เราจะมาไขข้อสงสัยนี้กัน

#include <stdio.h>

int main() {
char buffer[8];
printf("Please put something: ");
scanf("%s",buffer);

// For pausing the debugger
printf("Attach debugger now!");
while(getchar() != '\n');
getchar();
return 0;
}

Prerequisite

ต้องบอกไว้ก่อนว่า Windows ได้มีการวางมาตราการการป้องกันการทำ Binary Exploitation & Pwnable ไว้เยอะมากๆ เรามีความจำเป็นที่จะต้องปิดกลไก (บน VM Windows 10 32-bit ของเรา) เหล่านี้ออกก่อนในระหว่างการเรียนรู้ เพราะเราจะเริ่มเรียนจากการ Exploit แบบง่ายๆ เพื่อเข้าใจ Concept แล้วถึงค่อยมาเรียนวิธี Bypass กลไกเหล่านี้ทีหลัง

วิธีปิดก็คือให้เข้าไปที่ Microsoft Defender > App & Browser Control > “Exploit Protection > System Settings แล้วทำการปิดทุกการตั้งค่าในหน้านี้ จากนั้น Restart คอมพิวเตอร์

Press enter or click to view image in full size

ในบทความหน้าๆ เดี๋ยวเราจะมา Discuss กันอีกทีว่ามาตราการแต่ละอย่างที่ Windows ออกแบบมามันจะช่วยอะไรได้บ้าง

Stack

Stack เป็น Data Structure ที่ใช้หลักการนำข้อมูลเข้าและออกจากด้านเดียวกัน โดยการเพิ่มข้อมูลเข้าไปจะเรียกว่า Push ส่วนการนำข้อมูลออกจะเรียกว่า Pop และข้อมูลที่สามารถนำออกมาได้จะต้องเป็นข้อมูลที่อยู่บนสุดของ Stack เท่านั้น ไม่สามารถข้ามไปหยิบข้อมูลที่อยู่ด้านล่างหรือระหว่างทางได้

หากอธิบายเพียงเท่านี้อาจจะยังนึกภาพไม่ออก ลองจินตนาการถึงกระป๋อง Pringles เมื่อเราใส่แผ่นมันฝรั่งลงไปทีละแผ่นที่ปากกระป๋อง ก็จะเปรียบเสมือนการ Push ข้อมูลลงบน Stack และเมื่อจะหยิบออก เราก็สามารถหยิบได้เฉพาะแผ่นที่อยู่บนสุดก่อนเท่านั้น ซึ่งก็คือการ Pop นั่นเอง

Press enter or click to view image in full size
ขอขอบคุณภาพจาก Progamiz

รูปแบบการทำงานลักษณะนี้เรียกว่า Last In, First Out (LIFO) หรือ “ข้อมูลที่เข้ามาล่าสุด จะสามารถถูกนำออกก่อนได้เสมอ”

เดี๋ยวเราจะใช้ Source Code ด้านล่างต่อไปนี้ในการอธิบายเรื่อง Stack

#include <stdio.h>
#include <malloc.h>
int func1(int a, char b[]) {
char buffer[8];
printf("Please put something: ");
scanf("%s",buffer);
printf("You just put: %s\n", buffer);

// For pausing the debugger
printf("Attach debugger now!");
while(getchar() != '\n');
getchar();
return 0;
}

int main(){
func1(12345,"Hello");
return 0;
}

หน้าที่ของ Stack ภายใน Virtual Address Space ของโปรแกรมขณะทำงาน คือใช้เก็บข้อมูลที่เกี่ยวข้องกับการเรียกใช้งาน เริ่มใช้งาน และจบการใช้งาน Function โดยเมื่อมีการเรียกใช้ Function ข้อมูลต่าง ๆ จะถูก Push ลงบน Stack ตามลำดับดังนี้

  • Function Arguments — ค่าที่ถูกส่งเข้ามาในขณะเรียกใช้งาน Function
  • Return Address — เมื่อมีการเรียกใช้งาน Function โปรแกรมจะพักการทำงานของ Flow ปัจจุบันไว้ชั่วคราว แล้วกระโดดไปทำงานภายใน Function ให้เสร็จแทน ดังนั้น Address ของคำสั่งถัดไปใน Flow เดิมก่อนจะเข้า Function จึงต้องถูก Push ลงบน Stack เพื่อใช้เป็นตำแหน่งสำหรับกลับมาทำงานต่อเมื่อ Function ทำงานเสร็จสิ้นหรือ return
  • Base Pointer — เป็น Address ที่ใช้เป็นจุดอ้างอิงของ Stack Frame สำหรับ Function ที่กำลังทำงานในปัจจุบัน เนื่องจากระหว่างการทำงานโปรแกรมอาจมีการเรียก Function มาก่อนหน้าแล้วหรือซ้อนกันหลายชั้น ทำให้บน Stack มีข้อมูลของหลายๆ Function ปะปนกันอยู่ Base Pointer จึงทำหน้าที่กำหนดขอบเขตของ Stack Frame เพื่อให้สามารถอ้างอิง Local Variable และข้อมูลต่าง ๆ ของ Function ปัจจุบันได้อย่างถูกต้อง
  • Local Variables / Buffer — พื้นที่สำหรับเก็บ Local Variable และ Buffer ต่าง ๆ ที่ถูกสร้างขึ้นภายใน Function

ถ้าจะให้เห็นภาพก็คงจะเป็นประมาณนี้

Press enter or click to view image in full size

Address ของ Stack (ที่ OS จัดสรรให้โปรแกรม) ภายใน Virtual Address Space อาจจะไม่เหมือนเดิมทุกครั้งที่เปิดโปรแกรม ดังนั้นโปรแกรมจะมีกลไกที่เรียกว่า Stack Pointer เอาไว้คอยเก็บ Address บนสุดหรือ Top ของ Stack ณ ขณะที่โปรแกรมกำลังทำงาน ดังนั้นแล้วในทุกๆ ครั้งที่โปรแกรมมีการ Pop หรือ Push ข้อมูลลงบน Stack ค่าใน Stack Pointer ก็จะต้องถูก Update เพื่อให้ชี้ไปยังตำแหน่งบนสุดของ Stack อยู่เสมอ

โดยใน Windows10 x86 ค่าของ Stack Pointer จะเก็บอยู่ใน Register ที่ชื่อว่า ESP

ใน CPU ประเภท x86–64 เมื่อมีการ Push ข้อมูลลงบน Stack ค่าของ Stack Pointer จะขยับไปยัง Address ที่มีค่าน้อยลงและในทางกลับกัน เมื่อมีการ Pop ข้อมูล ค่า Stack Pointer จะขยับไปยัง Address ที่มีค่าสูงขึ้นเสมอ

ยกตัวอย่างเช่น หาก ESP ชี้อยู่ที่ Address 0x12340000 และโปรแกรมทำการ Push ข้อมูลขนาด 8 Bytes ลงบน Stack ค่า ESP จะถูกอัปเดตเป็น 0x12340000 - 0x08 หรือ 0x1233FFF8 เพื่อชี้ไปยังตำแหน่งบนสุดของ Stack หลังจากการ Push เสร็จสิ้น

Press enter or click to view image in full size
ตำแหน่งของ Stack Pointer (ESP) เมื่อมีการ Push หรือ Pop

Buffer Overflow Theory

เมื่อเข้าสู่การทำงานของฟังก์ชันค่าของ Local Variable หรือ Buffer ที่อยู่ในฟังก์ชันจะถูก Push ลง Stack ด้วยเช่นกัน

int func1(int a, char b[]) {
char buffer[8]; // จองพื้นที่บน Stack 8 bytes
printf("Please put something: ");
scanf("%s",buffer); // นำ Input User ไปเก็บที่ buffer (อยู่บน Stack)
printf("You just put: %s\n", buffer);

// For pausing the debugger
printf("Attach debugger now!");
while(getchar() != '\n');
getchar();
return 0;
}

จากโค้ดด้านบน เมื่อโปรแกรมเข้าสู่ Function (func1()) และพบกับบรรทัด char* buffer[8];ระบบจะจองพื้นที่บน Stack สำหรับเก็บ Local Variable ขนาด 8 Bytes จากนั้นเมื่อมาถึงคำสั่ง scanf("%s", buffer) โปรแกรมจะนำข้อมูลที่ผู้ใช้ป้อนเข้าไปเขียนลงในพื้นที่ Memory ของ buffer

อย่างไรก็ตาม จะสังเกตได้ว่าในโค้ดไม่มี Logic สำหรับการตรวจสอบหรือกำหนดขนาดของ Input ว่าห้ามเกิน 8 Bytes (หรือ 8 ตัวอักษร) อยู่เลย ดังนั้นหากผู้ใช้ป้อนข้อมูลที่ยาวเกินกว่าพื้นที่ที่จองไว้ ข้อมูลก็จะทะลุล้ำไปยัง Address ข้างเคียงจาก Address ที่เราจองเอาไว้ให้กับตัวแปร buffer นั่นเอง

หากข้อมูลที่ล้นออกไปมีขนาดมากพอจนเขียนทับข้อมูลสำคัญบน Stack เช่น Return Address เมื่อ Function ทำงานเสร็จสิ้นและพยายามจะกระโดดกลับไปที่ Flow ดั้งเดิม Program ก็จะเผลอใช้ค่าของ Return Address ที่ตอนนี้ถูก Corrupt ไปแล้วเนื่องจาก Input ของผู้ใช้งานที่ล้นออกมา จะส่งผลให้โปรแกรมไม่สามารถกลับไปได้และเกิดข้อผิดพลาด ซึ่งนี่คือหลักการพื้นฐานของการทำ Buffer Overflow บน Stack นั่นเอง

Press enter or click to view image in full size
  • Before Input — เมื่อโปรแกรมกันมาถึงโค้ดสำหรับการจองพื้นที่ 8 bytes (buffer)
  • Normal Input — เมื่อ User ใส่ Input ที่ไม่ได้เกิน 8 bytes (8 ตัวอักษร) ก็จะพบว่าค่าของเราก็จะยังคงอยู่ในพื้นที่ของ Buffer ขนาด 8-byte และโปรแกรมก็ยังสามารถกลับไปที่ Address เดิมได้อย่างถูกต้องเพราะ Return Address ยังอยู่ปกติดี
  • Overflow Input — เมื่อ User ใส่ Input ที่ขนาดใหญ่มากๆ (สมมติเป็นตัว A แบบเยอะๆ) ก็จะพบว่าตัว A ได้รั่วออกมาจาก Buffer ที่จองเอาไว้ขนาด 8 byte และรั่วจนไปแทนที่ค่าของ Return Address ที่อยู่บน Stack ทำให้โปรแกรมเวลาจะจบ Flow จะพยายามไปที่ Address AAAAAAAA ซึ่งดูแล้วไม่น่าใช่หน้าตาของ Address ที่ถูกต้องก็จะ Error ทันที เป็นหลักฐานของการทำ Buffer Overflow

Buffer Overflow in Action

ทีนี้เราจะมาลอง Abuse Buffer Overflow แบบจริงๆ และทำการ Debug ด้วย Windbg พร้อมไปด้วย

หลังจาก Compile โปรแกรมเสร็จเรียบร้อยให้เราทำการเปิดตัวโปรแกรมผ่าน WinDbg เพื่อทำการ Debug ตอนเริ่มโปรแกรมจากนั้นก็สั่งให้โปรแกรมทำงานต่อไปก่อนเพื่อที่เราจะได้ลองใส่ Input ลงไปได้

Press enter or click to view image in full size

จากนั้นทดสอบใส่ตัว A ลงไปประมาณ 16 ตัวเป็นอย่างต่ำ สาเหตุที่ต้องเป็นจำนวนนี้เพราะว่า char buffer[8] คือ Local Variable ตัวแรกที่เราจะเจอ ถ้าหากเรายังจำสิ่งที่จะ Push เข้ามาใน Stack ตอนเรียกฟังก์ชันได้ละก็

  • Return Address — Address สำหรับกลับไปที่ Flow หลัก (ใน 32-bit Address จะมีขนาด 4 Byte หรือ 4 ตัวอักษร ASCII)
  • Base Pointer — Address ที่ใช้สำหรับเป็นจุดอ้างอิงซึ่ง Address ก็จะกินไปอีก 4 ตัวอักษร
  • และจนมาถึง char buffer[8] ที่เป็น Local Variable ที่จะต้อง Push ลงบน Stack และกินพื้นที่ไปอีก 8 ตัวอักษร นั่นแปลว่าถ้าจะต้องใช้ตัว A อย่างน้อยประมาณ 8 + 4 + 4 = 16 ตัวเพื่อให้ A ของเรารั่วไปโดน Return Address จนพัง Flow โปรแกรม
Press enter or click to view image in full size

แต่เราจะพบว่าโปรแกรมไม่ได้ Error แต่อย่าเพิ่งตกใจ นั่นเป็นเพราะว่าตัวโปรแกรมได้ดำเนินมาถึง printf(“Attach debugger now!”); ซึ่งยังไม่ได้จบการทำงานของฟังก์ชัน func1() หรือไปถึง return แต่อย่างใด สิ่งที่เราต้องทำก็คือกด Enter เพื่อให้โปรแกรมทำงานต่อจนไปถึงบรรทัด return 0

Get Pattharadanai Sanitjairak’s stories in your inbox

Join Medium for free to get updates from this writer.

หลังจากนั้นเราจะพบว่าโปรแกรมจึงจะเกิด Error ขึ้น ซึ่งเป็น Error Access violation ก็คือโปรแกรมพยายามจะเข้าถึง Memory Address ที่ไม่ได้รับอนุญาตซึ่งก็คือ AAAAAAAA ที่ถูกเรา Overflow ลงไป

Press enter or click to view image in full size

หากเราสังเกตข้อความ Error จะพบว่า Register EIP มีค่าเป็น 41414141 หรือ AAAA ซึ่งก็คือข้อมูลที่เราใช้ Overflow เข้าไป หลายคนอาจสงสัยว่าทำไมค่าที่เราป้อนจึงไปปรากฏอยู่ใน EIP และเกี่ยวข้องกับการเกิด Error หรือไม่

คำตอบคือ เกี่ยวข้องโดยตรง เพราะ EIP มีหน้าที่เก็บ Address ของ Instruction ถัดไป ที่ CPU จะประมวลผล เมื่อโปรแกรมทำงานมาถึงคำสั่ง return มันจะดึง Return Address ที่เก็บอยู่บน Stack ขึ้นมาใส่ใน EIP เพื่อกลับไปทำงานต่อใน Flow หลัก แต่เนื่องจาก Return Address ถูกเรา Overflow จนถูกเขียนทับด้วย AAAA โปรแกรมจึงนำค่า 41414141 ไปใส่ใน EIP และพยายามกระโดดไปยัง Address ดังกล่าว ส่งผลให้เกิด Error ทันที เพราะ Address นี้ไม่ใช่ตำแหน่งที่สามารถรันคำสั่งได้

ทีนี้เราก็จะปิ๊งไอเดียที่ว่าแทนที่จะ Overflow ใส่ AAAA ทำไมเราไม่ใส่เป็น Address ที่ชี้ไปยังที่ๆ มีคำสั่งอันตรายอยู่ละถ้า Program ไปที่นั่นเมื่อไหร่ โค้ดอันตรายก็จะต้องทำงานแล้วเราก็จะบรรลุเป้าหมายทันที เราลองทำซ้ำอีกรอบแต่คราวนี้ลองใส่ Input เป็น AAAAAAAABBBBDEFG

  • AAAAAAAA — เติม Buffer เราให้เต็ม 8 Bytes ตามที่ได้ Assign ไว้
  • BBBB — Overflow เพื่อทับ Base Pointer (จริงๆ แล้ว Base Pointer จะถูกเก็บไว้ใน Register EBP ด้วยเช่นกันถ้าเราสังเกตภาพที่แล้วดีๆ)
  • DEFG — สมมติว่าเป็น Address ที่มีโค้ดอันตรายอยู่ (ซึ่งจะเทียบเท่ากับ Hex 44 45 46 47หรือ Address 0x44454647)

เมื่อทำการทดสอบจะพบว่าเรา Overflow ไปยัง EIP ได้เหมือนเดิมแต่สิ่งที่แตกต่างจากที่เราคิดก็คือเราใส่ 44454647 ลงไปแต่ทำไมใน EIP มันถึงมีค่าเป็น 47464544 ซึ่งกลับจากค่าของเรา

Press enter or click to view image in full size

Endianess

นั่นก็เพราะว่า Windows 10 x86 ของเราทำงานอยู่บน CPU x86–64 ซึ่งใช้รูปแบบการจัดเก็บข้อมูลแบบ Little-Endian ดังนั้นเมื่อข้อมูลถูกนำมาตีความเป็น Address ค่าของแต่ละ Byte ภายใน Address จะถูกอ่านจาก Byte ที่มีนัยสำคัญน้อยที่สุด (Least Significant Byte) ก่อนเสมอ (หรือกลับด้านทุกๆ 4 Byte)

ตัวอย่างเช่น หากมีข้อมูล ABCDEFGH ถูกเขียนลงในหน่วยความจำ เมื่อโปรแกรมนำข้อมูลดังกล่าวมาตีความเป็น Address ข้อมูลจะถูกแบ่งเป็นชุด ชุดละ 4 Bytes (บนระบบ 32-bit) กลายเป็น ABCD EFGH เมื่ออ่านแบบ Little-Endian ข้อมูลทุกๆ 4 Byte จะถูกสลับด้านกลายเป็น DCBA HGFE ทำให้ผลลัพธ์สุดท้ายที่โปรแกรมจะอ่านคือ DCBAHGFE ดังนั้นแล้วเวลาเราจะทำ Binary Exploitation ก็ให้ดูด้วยว่า CPU ของเครื่องที่เรากำลังโจมตีอยู่นั้นใช้ CPU เป็น Architecture อะไรเพื่อที่จะได้ทราบ Endianness และประมวลผลค่าของ Address ที่ถูกต้อง

Press enter or click to view image in full size
ขอขอบคุณภาพจาก Willian Wu

ดังนั้น Input ที่ถูกต้องก็จะเป็น AAAAAAAABBBBGFED เพื่อที่เราจะทำการ Overflow ให้ EIP ใช้ Address 44454647 (แทนที่จะเป็น 47464544 ที่เราทำผิดพลาดไปเมื่อรอบที่แล้ว)

Press enter or click to view image in full size

Buffer Overflow Attack Procedure

หลังจากที่เราสามารถควบคุมได้ว่าจะให้โปรแกรมวิ่งไปยัง Address ไหนต่อหลังจาก Overflow ซึ่งปลายทางที่โปรแกรมจะต้องไปก็คือ Address ที่มี Malicious Code ของเรานั่นเอง แต่ปัญหาคือแล้วเราจะเอา Malicious Code ไปวางได้อย่างไรก่อน ?

โปรแกรมรับข้อมูลจาก User แค่ช่องทางเดียวซึ่งก็คือ AAAAAAAABBBBGFED ที่เราใส่ลงไปเมื่อสักครู่เพียงเท่านั้น ดังนั้นจุดเดียวที่เราจะใส่อะไรลงไปได้เช่น Malicious Code ก็คือข้อความที่ต่อท้าย Input เมื่อสักครู่นั่นเอง โดยในรอบนี้เราจะลองใส่ Input ตามตัวอย่างข้างล่างลงไปใน Program

  • AAAAAAAA — เติม Buffer ให้เต็ม 8 byte
  • BBBB — Overflow ตัว Base Pointer (EBP)
  • GFED — Overflow ตัว Return Address หรือ EIP โดย GFED คือตัวแทน Addres ที่ Malicious Code เราจะอาศัยอยู่แต่ตอนนี้เรายังไม่รู้ว่ามันจะอยู่ที่ไหนแต่ขอแทนเป็น GFED ไปก่อน
  • w00fw00f + Z อีก 100 ตัว — เป็น Malicious Code สมมติของเราที่มีขนาดประมาณ 100 byte
AAAAAAAABBBBGFEDW00fW00fZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

ทำการเปิดโปรแกรมใหม่อีกครั้ง ต่อ Debugger, Conitnue Execution แล้วก็ใส่ Input เมื่อสักครู่ให้เกิด Access Violation (Buffer Overflow) อีกที

Press enter or click to view image in full size

ถ้าใครยังจำคำสั่งในการค้นหา ASCII บน Memory ได้ เราสามารถใช้คำสั่งนั้นค้นหา ASCII w00fw00f ซึ่งเป็น Byte เริ่มต้นของ Malicious Code ของเรา

s -a 0 L?80000000 "W00fW00f"

เราก็จะพบว่า W00fW00f หรือ Malicious Code ของเราปรากฎอยู่ใน Memory จริงๆ

Press enter or click to view image in full size

ถ้าเรายังจำได้โปรแกรมจะมีสิ่งที่เรียกว่า Stack Pointer ซึ่งเป็นค่าที่จะชี้ Address ของ Top Stack และสามารถเข้าถึงได้ผ่าน Register ESP หากเราใช้คำสั่ง dd esp เพื่อทำการตรวจสอบ Byte ที่อยู่บน Address ที่เก็บอยู่ใน ESP เป็นต้นไปเราจะพบว่า Address บน ESP ก็คือจุด Start ของ Malicious Code ของเรา

Press enter or click to view image in full size

จริงๆ แล้ว การที่ Function return มันคือคือการ Pop ค่าบน Stack ไปเก็บไว้ใน EIP ซึ่งค่าดังกล่าวก็คือ Return Address (นั่นจึงเป็นอีกเหตุผลว่าทำไมเราถึงสามารถทำ Buffer Overflow เพื่อควบคุม EIP ได้) โดย Return Address มีขนาด 4 Bytes และเมื่อถูก Pop ออกไปแล้ว ESP ก็จะถูกขยับเพิ่มขึ้น 4 Bytes ทำให้ชี้ไปยังตำแหน่งของ Malicious Code (ที่ต่อท้าย GFED) ของเราพอดีนั่นเอง

ดังนั้นในมุมมองของการโจมตีเราอาจจะออกแบบ Input สำหรับการโจมตีได้คร่าวๆ ประมาณนี้

  • Offset จำนวน 12 Byte เพื่อเติม Buffer และ Base Pointer + Malicious Code Address (ที่จะปรากฎบน EIP) + Malicious Code (ที่อยู่ในรูปแบบของ Byte)

Conclusion

และนั่นก็คือ Procedure คร่าว ๆ ของการทำ Buffer Overflow เพื่อ Pwnable ตัว Binary Program โดยในภาพรวมจะมีขั้นตอนคร่าวๆ ดังนี้

  • ส่ง Input ที่มีความยาวมากๆ ให้ Buffer Overflow แล้วตรวจสอบว่าสามารถเขียนทับ EIP ได้หรือไม่
  • คำนวณจำนวน Byte ที่ต้องใช้เพื่อให้ล้นไปถึง EIP พอดี (Offset)
  • นำ Address ที่ต้องการให้ตัวโปรแกรม Redirect (Address ของ Malicious Code) ไปเขียนทับ Return Address หรือ EIP
  • นำ Malicious Code ต่อท้าย Payload แล้วค้นหาใน Memory ว่า Payload ของเราปรากฎอยู่ใน Address ใดใน Memory
  • นำ Malicious Code จริงๆ ต่อเข้ากับ Input ของเรา อัพเดทตัว EIP ให้ชี้ไปยัง Address ของ Malicious Code ที่ต้องการเพื่อทำการ Pwnable

อย่างไรก็ตาม ตัวอย่างโค้ดที่ใช้ในบทความนี้ยังไม่สามารถนำไปทำ Pwnable ได้จริง เนื่องด้วยข้อจำกัดหลายๆ อย่าง แต่อย่างน้อยก็สามารถใช้ในการ Debug เพื่ออธิบายว่า Buffer Overflow เกิดขึ้นได้อย่างไร เกิดแล้วเราจะนำไปต่อยอดอะไรได้บ้าง รวมไปถึง Procedure ต่างๆ ที่อธิบายเอาไว้ในบทความนี้

โดยในบทความหน้าเราจะมาลอง Pwnable ตัว Binary Program ที่เป็นของจริงๆ เพื่อให้ได้ Code Execution กันดูครับ แล้วไว้พบกันใหม่ในบทความหน้าครับ

--

--

Pattharadanai Sanitjairak
Pattharadanai Sanitjairak

ผมกำลังทำงานเป็น Business Analyst อยู่ที่ Big4 แห่งหนึ่ง ผมชอบที่จะเขียน Blog เกี่ยวกับ Cybersecurity หัวข้อยากๆ แต่เรียบเรียงให้เหมาะสำหรับคนไทยมือใหม่ครับ