การรันโค้ดจากระยะไกลผ่านข้อความ MIDI
(psi3.ru)- รีเวิร์สเอนจิเนียร์ชิป SWL01U ของซินธิไซเซอร์ Yamaha PSR-E433 รุ่นเก่า และใช้เพียงข้อความ USB-MIDI SysEx เพื่อเขียนโค้ดลง RAM แล้วแสดงวิดีโอ Bad Apple บน LCD
- จากการทดลองด้วย JTAG IDCODE
0x3f0f0f0fและ OpenOCD/GDB ยืนยันได้ว่าชิปทำงานเหมือนคอร์ ARM7TDMI และดัมป์ ROM ภายใน 64KiB กับเฟิร์มแวร์แฟลชภายนอก 16MiB ได้ - ภายในเฟิร์มแวร์มี เชลล์ที่ซ่อนอยู่ ซึ่งทำงานบน MIDI SysEx และหลังจาก
loginพร้อมรหัสผ่าน#0000แล้วสามารถใช้คำสั่งอ่าน/เขียนหน่วยความจำได้ - ใช้คำสั่งเขียนหน่วยความจำตามอำเภอใจเพื่อฉีดโค้ด ARM ลง RAM แล้วเขียนทับ return address บนสแตก ทำให้ รันโค้ดได้ด้วยการเล่นไฟล์ MIDI เท่านั้น โดยไม่ต้องใช้ JTAG หรือ UART
- ปรับปรุงการส่งออก LCD ผ่านการควบคุม CGRAM, การคัดลอก task table, การปิดใช้งาน display task และการเปลี่ยน shell callback จนลดปริมาณข้อมูลที่ส่งต่อเฟรมจาก 6732 ไบต์เหลือ 92 ไบต์
การสำรวจภายใน Yamaha PSR-E433
- อุปกรณ์เป้าหมายคือซินธิไซเซอร์ Yamaha PSR-E433 ที่ใช้งานมานาน และบนเมนบอร์ดมีชิปแฟลชสองตัว, ชิป RAM และชิป
YAMAHA SWL01Uพร้อมข้อความDMLCD - ข้อมูลสาธารณะเกี่ยวกับ SWL01U แทบไม่มี และมีเพียงบทความหนึ่งที่พบออนไลน์ซึ่งระบุว่าอาจใช้คอร์ CPU แบบ SuperH
- คู่มือซ่อมบำรุงของรุ่นใกล้เคียง E443 มี pinout ของ SWL01U และระบุ
TESTN,PROTN, UART สองชุดแบบสองทิศทาง และจุดทดสอบ JTAG - แนวทางเริ่มต้นมี 4 ทาง
- ปรับขา
TESTN,PROTNเพื่อตรวจสอบการเปลี่ยนแปลงของโหมดบูต - บัดกรีเข้ากับขา UART Tx เพื่อตรวจสอบ output
- อ่านรหัสระบุชิปผ่าน JTAG
- ถอดชิปแฟลชออกมาเพื่อดัมป์เฟิร์มแวร์
- ปรับขา
- เมื่อเปิดใช้งาน
TESTNซินธิไซเซอร์ไม่บูต ส่วนPROTNไม่ทำให้พฤติกรรมเปลี่ยนไป - บัดกรีเข้ากับขา UART Tx ที่ดูเหมือนไม่ได้ใช้งานโดยตรง แต่ไม่มี output ในทั้งสี่ชุดค่าผสมของ
TESTN/PROTN
พฤติกรรมแบบ ARM7TDMI ที่ JTAG เผยให้เห็น
- JTAG ต้องมีคำอธิบายวงจรละเอียดตามการใช้งานของแต่ละผู้ผลิต แต่เริ่มจากการลองอ่าน IDCODE ที่อุปกรณ์แทบทั้งหมดรองรับด้วย OpenOCD
- OpenOCD รายงาน IDCODE เป็น
0x3f0f0f0fและค่านี้ดูเหมือนจะเชื่อมโยงได้กับไมโครคอนโทรลเลอร์ฐาน ARM7 เช่นตระกูล STMicroelectronics STR7xxx หรือ Atmel SAM7xxx - เมื่อกำหนด SWL01U เป็น target
arm7tdmiของ OpenOCD ก็เชื่อมต่อได้ตามปกติ และแสดงว่าฮาร์ดแวร์มี breakpoint/watchpoint unit 2 ชุด - เมื่อหยุดและเริ่มการทำงานต่อด้วย GDB กระแสของบอร์ดเปลี่ยนอย่างคาดการณ์ได้
- ระหว่างรันประมาณ 115mA
- ระหว่างหยุดชั่วคราวประมาณ 98mA
- การเปลี่ยนแปลงของกระแสนี้เป็นสัญญาณที่ชัดเจนว่าได้หยุดและเริ่มคอร์ ARM7TDMI จริง
ดัมป์ ROM และเฟิร์มแวร์แฟลช
- ตามเอกสาร ARM7TDMI ระบุว่า reset vector อยู่ที่แอดเดรส
0และเมื่ออ่านแอดเดรส0ด้วย GDB พบคำสั่ง jump ในรูปแบบldr pc, [pc, #24] - ดัมป์ข้อมูล 16MiB จากแอดเดรส
0แล้วเปิดด้วย Cutter แต่พบว่าสตริงซ้ำทุก 64KiB- ตัวอย่าง:
SWL01U Internalซ้ำที่0x0000bfd0,0x0001bfd0,0x0002bfd0เป็นต้น
- ตัวอย่าง:
- จากรูปแบบซ้ำและสตริง จึงสรุปว่าดัมป์นี้ไม่ใช่แฟลชภายนอก แต่เป็นหน่วยความจำภายในชิป และ SWL01U มี ROM 64KiB
- เป้าหมายของการ jump จาก reset vector คือ
0x02000000และเมื่อดัมป์ 16MiB จากแอดเดรสนี้อีกครั้งก็ไม่พบการซ้ำ - ดัมป์แฟลชภายนอกมีสตริงที่เห็นได้ระหว่างใช้งานซินธิไซเซอร์
GrandPnoTr1 will be OverWritten!BogiWogi
- ผังหน่วยความจำที่ยืนยันได้มีดังนี้
- ROM ภายใน:
0x00000000, 64KiB - แฟลชภายนอก:
0x02000000, 16MiB - ตอนบูต ROM จะส่งการควบคุมต่อไปยังแฟลชภายนอกทันที
- ROM ภายใน:
เชลล์ที่ซ่อนอยู่ซึ่งพบด้วย Ghidra
- การวิเคราะห์ด้วย Cutter เพียงอย่างเดียวไม่เพียงพอ จึงเปลี่ยนไปใช้ Ghidra และทำความเข้าใจโครงสร้างเฟิร์มแวร์ด้วยการไล่ตามสตริงและ xref
- สตริงอย่าง
help,?,info,verรวมอยู่ใกล้แอดเดรสเดียวกัน และแต่ละสตริงเชื่อมกับอาร์เรย์ที่ดูเหมือนเป็นคู่ของชื่อคำสั่งกับ function pointer - ฟังก์ชันประมวลผลคำสั่งอยู่ในรูปแบบ state machine และจากสตริง
loginกับPasswd Errorยืนยันได้ว่าเป็นเชลล์ที่มีขั้นตอนล็อกอิน - การประมวลผล input ของเชลล์วนผ่าน circular buffer ขนาด 256 ไบต์และประมวลผลทีละอักขระ โดยจะรันคำสั่งเมื่อพบอักขระ
\r - flow การล็อกอินมีดังนี้
- เมื่อป้อน
loginจะแสดงpasswd? - เมื่อป้อนรหัสผ่าน
#0000จะแสดงlogin OK - หลังจากนั้นจึงรันคำสั่งได้
- เมื่อป้อน
- คำสั่งเชลล์ที่ยืนยันได้ประกอบด้วย
logout,help,?,info,verstack,perf-on,perf-off,perf-dispd,dp,d xxxxx,d/s xxxxxm ADDRESS DATA,m/b ADDRESS DATA,m/w ADDRESS DATA,m/l ADDRESS DATA
- คำสั่ง
infoส่งคืนข้อมูลต่อไปนี้DevelopName PSR-E433DevelopNumber #3341Main DevelopNumber #3341Make data & time MAY 16 2012 19:00:57J/E Select English
เชลล์ที่ทำงานบน USB-MIDI SysEx
- ฟังก์ชัน output ของเชลล์แบ่งแต่ละไบต์ออกเป็น nibble 4 บิต ฝั่งสูง/ต่ำ แล้วใส่ในไบต์แยกกัน พร้อมเติม header และ footer คงที่ไว้ด้านหน้าและหลัง
- โครงสร้างแพ็กเก็ตที่ตรงกับพรอมป์
>มีดังนี้- header:
F0 43 73 01 52 19 00 00 - payload:
03 0E 02 00 - footer:
F7
- header:
- ข้อความ MIDI SysEx เริ่มด้วย
0xF0ผ่าน manufacturer ID และ payload แล้วจบด้วย0xF7โดย payload ใส่ได้เฉพาะไบต์ที่มี MSB เป็น 0 0x43ใน header คือ manufacturer ID ของ Yamaha และโครงสร้างแพ็กเก็ตเชลล์ตรงกับรูปแบบข้อความ Yamaha SysEx- USB descriptor ของซินธิไซเซอร์มีเฉพาะ MIDI interface และไม่มีพอร์ต serial แยกต่างหาก
- เมื่อใช้สคริปต์ Python แปลงระหว่างเทอร์มินัลกับโปรโตคอลของเชลล์ ก็สามารถสนทนากับเชลล์ผ่าน USB-MIDI ได้
การรันโค้ดด้วย MIDI Shellcode
- คำสั่ง
m/l AAAAAAAA DDDDDDDD\rของเชลล์ทำการเขียนหน่วยความจำ 32 บิต โดยส่งแอดเดรสและข้อมูลเป็น ASCII เลขฐานสิบหก - แม้จะเขียน payload 4 ไบต์ แต่ปริมาณข้อมูลที่ส่งจริงเพิ่มขึ้นมาก
- ไบต์คำสั่งแต่ละตัวถูกแปลงเป็นไบต์ nibble 4 บิตสองตัว
- ข้อความ SysEx ถูกเพิ่มอีก 9 ไบต์
- ทุก 3 ไบต์ถูกห่อในแพ็กเก็ต USB-MIDI ขนาด 4 ไบต์
- การเขียน 4 ไบต์ต้องส่งไปยังซินธิไซเซอร์ 72 ไบต์
- หากรวม echo และพรอมป์ จะมีข้อมูลไปกลับทั้งหมด 396 ไบต์
- ค้นหาพื้นที่ RAM ที่ดูเหมือนไม่ได้ใช้งานแล้ววางโค้ด ARM assembly ลงไป จากนั้นเขียนทับ return address บนสแตกเพื่อรันโค้ดนั้น
- payload แรกเรียกฟังก์ชันพิมพ์สตริงภายในเฟิร์มแวร์ เพื่อแสดง
HeloWrldในพื้นที่ข้อความ 8 อักขระของ LCD - วิธีนี้ทำงานได้โดยไม่ต้องใช้ JTAG หรือ UART และสามารถรันได้เพียงใส่ข้อความลงในไฟล์ MIDI แล้วเล่นไฟล์นั้น
- มีการจัดเตรียมไฟล์ MIDI สำหรับเฟิร์มแวร์ PSR-E433 1.02 ไว้ด้วย แต่เตือนว่าหากเล่นบนอุปกรณ์ Yamaha รุ่นอื่นหรือ PSR-E433 ที่ใช้เฟิร์มแวร์เวอร์ชันอื่น อาจเกิดพฤติกรรมที่คาดเดาไม่ได้
การแสดง Bad Apple บน LCD
- คอนโทรลเลอร์ LCD ของ Yamaha PSR-E433 คือ ML9040A และโดยพื้นฐานมีโครงสร้างสำหรับประมวลผลอักขระข้อความแบบ dot matrix
- LCD ไม่ได้มีแค่พื้นที่ dot matrix แต่ยังมีการแสดงโน้ตดนตรี, พื้นที่ 7-segment, การแสดงคอร์ด และพื้นที่แสดงคีย์บอร์ดด้านล่างด้วย
- ML9040A มีหน่วยความจำสามแบบ
- DDRAM: โฮสต์บันทึกข้อมูลอักขระที่จะนำไปแสดง
- CGROM: แปลงรหัสอักขระเป็นแพตเทิร์นกราฟิก
- CGRAM: โฮสต์กำหนดอักขระผู้ใช้ได้สูงสุด 8 ตัว
- เฟิร์มแวร์จัดการ CGRAM เพื่อควบคุมองค์ประกอบการแสดงผลที่ไม่ใช่ข้อความใต้ dot matrix และสามารถใช้เส้นทางนี้เพื่อแสดงกราฟิกที่ผู้ใช้กำหนดเองได้
- พบฟังก์ชันเฟิร์มแวร์สำหรับส่งข้อมูลใด ๆ ไปยังคอนโทรลเลอร์ LCD และลองใส่แพตเทิร์นตารางหมากรุกลงใน CGRAM แต่เฟิร์มแวร์ยังคงอัปเดต CGRAM ต่อเนื่อง จึงถูกเขียนทับในไม่ช้า
ควบคุมการอัปเดตจอภาพด้วยการจัดการ RAM
- การเขียนทับแฟลชโดยตรงมีความเสี่ยงที่จะทำให้อุปกรณ์ brick ได้ จึงจำกัดการทดลองทั้งหมดไว้ที่ การจัดการ RAM เพื่อให้ย้อนกลับได้ด้วยการเปิดเครื่องใหม่
- ในเฟิร์มแวร์มีโครงสร้างที่ดูเหมือน RTOS แบบพื้นฐาน และในแฟลชมีตาราง global ที่กำหนด callback function, stack และ attribute ของ task 64 รายการ
- ตอนบูต เฟิร์มแวร์ในแฟลชจะแจ้งตำแหน่ง task table ให้ ROM และ ROM จะบันทึกตำแหน่งนั้นไว้ในตัวแปร global ของ SRAM ภายใน
- หากคัดลอก task table ไปยัง RAM แล้วเปลี่ยนให้ ROM ใช้ตารางใหม่ ก็สามารถเปลี่ยน task callback ได้โดยไม่ต้องแก้ไขแฟลช
- เปลี่ยน callback ของ display update task ให้เป็น idle callback เริ่มต้น ทำให้เฟิร์มแวร์ไม่สามารถเขียนทับ CGRAM ต่อเนื่องได้อีก
ปรับปรุงประสิทธิภาพการส่งข้อมูลและอาการภาพแตก
- การใช้งาน Bad Apple เวอร์ชันแรกทำงานได้ แต่ประสิทธิภาพการส่งข้อมูลต่ำ ทำให้ frame rate ต่ำมากและมี artifact บนภาพ
- แม้ส่งเพียง CGRAM 64 ไบต์กับการเขียนทับ return address 32 บิตต่อเฟรม ปริมาณข้อมูลที่ส่งจริงคือ 6732 ไบต์ต่อ payload 70 ไบต์
- สาเหตุหลักของประสิทธิภาพต่ำมีสองข้อ
- ข้อมูลต้องถูกห่อในรูปแบบคำสั่งเชลล์
- ซินธิไซเซอร์ echo คำสั่งทีละอักขระด้วยแพ็กเก็ต SysEx ขนาดใหญ่
- เมื่อเปลี่ยน shell task callback เป็น callback ที่สร้างเองเพื่อรับ raw data และไม่ตอบกลับ ก็สามารถตัด overhead จากการห่อคำสั่งและ echo ออกได้
- หลังจากปรับ packing เพิ่มเติม ปริมาณข้อมูลที่ส่งต่อเฟรมลดจาก 6732 ไบต์เหลือ 92 ไบต์ หรือ ลดลง 73 เท่า
- artifact ที่เหลือเกิดจากการที่การสื่อสารกับ LCD และการสแกนปุ่ม/LED บนแผงควบคุมใช้ GPIO 8 เส้นเดียวกันร่วมกัน
- การใช้งานสุดท้ายไม่เขียนไปยัง LCD โดยตรง แต่ร้องขอให้ task multiplexing ของ LCD/แผงควบคุมส่งข้อมูลที่ต้องการหลังจากสแกนแผงควบคุมเสร็จ เพื่อหลีกเลี่ยงภาพแตก
ขั้นตอนการทำงานสุดท้ายและหัวข้อที่ยังต้องวิเคราะห์
- ขั้นตอนสุดท้ายในการแสดงวิดีโอบน LCD ผ่าน MIDI มีดังนี้
- ล็อกอินเข้าเชลล์
- ใช้คำสั่งเขียนหน่วยความจำของเชลล์เพื่อบันทึกโค้ดที่จะรันลง RAM
- เขียนทับ return address บนสแตกเพื่อรันโค้ดใน RAM
- คัดลอก task table ไปยัง RAM
- แก้ไข task table ใหม่ให้ชี้ถึงกัน
- เปลี่ยนให้ ROM ใช้ task table ใหม่
- แทนที่ display task callback ด้วย idle callback เริ่มต้น
- แทนที่ shell task callback ด้วย callback ของตัวเอง
- ใน callback ของตัวเอง แกะ MIDI data แล้วส่งต่อไปยัง task multiplexing ของ LCD/แผงควบคุม
- ป้อนเฟรมวิดีโอผ่าน MIDI
- ความเข้าใจเกี่ยวกับ พื้นที่ MMIO ของ SWL01U ยังมีจำกัด และ DSP ที่แยกจากคอร์ ARM หลักก็ยังเป็นหัวข้อที่ต้องวิเคราะห์ต่อ
- เอกสารที่เกี่ยวข้อง
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
SuperH ถูกใช้ใน Sega 32X, Sega Saturn, Sega Dreamcast ด้วย และยังถูกใช้ใน Pocket PC รุ่นแรก ๆ บางรุ่นอย่าง HP Jornada ด้วย
แต่ Pocket PC ส่วนใหญ่ใช้ ARM
สมมติฐานมันแทบจะเหลวไหล แต่ที่น่าทึ่งคือทำได้จริง
มีการพูดถึง “การปรับแพ็กข้อมูลให้เหมาะสมอีกแบบหนึ่ง” เลยสงสัยว่าเขาส่งเฟรมกันอย่างไร
ถ้าดอตเมทริกซ์เป็นอักขระ 7x5 จำนวน 8 ตัว ก็รวม 280 บิต หรือกลุ่มละ 7 บิต 40 กลุ่มต่อเฟรม แต่ดูเหมือนว่าการส่งจะใช้พื้นที่เป็นสองเท่าของนั้น
สงสัยว่าเสียไปกับข้อมูลควบคุมหรือเปล่า หรือวิธีส่งอาจยังไม่ค่อยเหมาะสมเล็กน้อย
แล้วยังมีส่วนหัวและส่วนท้ายแพ็กเก็ตอีก 9 ไบต์
ในบทความเหมือนจะเขียนว่า 92 แต่คงคำนวณผิด
การหาวิธีใช้ทั้ง 7 บิตนั้นยากเกินไป เลยเลือกวิธีที่แย่กว่าคำตอบที่เหมาะที่สุดเพียงเล็กน้อยเมื่อเทียบกับวิธีเดิม
ถ้าอยากดูอัลกอริทึมที่แน่นอน แม้โค้ดยังไม่ได้จัดระเบียบ ก็ลองดูไฟล์เหล่านี้ได้: https://github.com/portasynthinca3/swl01u/blob/master/fun/bi..., https://github.com/portasynthinca3/swl01u/blob/master/fun/ba...
ถ้าคำว่า “ไม่มีประสบการณ์ด้านรีเวิร์สเอนจิเนียริงมากนัก” หมายถึงระดับนี้ แล้วคนอื่น ๆ อยู่ตรงไหนกันแน่ก็ไม่รู้
ขั้นที่ 1 คือยังใหม่และกระตือรือร้น แต่รู้ว่าตัวเองยังไม่รู้ ขั้นที่ 2 คือ “ฉันคือพระเจ้า” ขั้นที่ 3 คือช่วง “ฉันมันโง่”
บอกว่าเป็น “เชลล์โค้ด MIDI ตัวแรกของโลก” แต่บนแพลตฟอร์มหลักส่วนใหญ่ เชลล์โค้ด MIDI มีมาตั้งกว่า 20 ปีก่อนแล้ว: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=midi
แน่นอนว่าต้องเป็น SysEx
ใน MIDI มาตรฐาน SysEx ก็เหมือนอินไลน์แอสเซมเบลอร์ใน Python
แทบทุกอุปกรณ์ MIDI มีของเฉพาะผู้ผลิตที่ไม่ได้ทำเอกสารซ่อนอยู่ข้างใน
ถ้าเสียบคีย์บอร์ด MIDI แล้วเล่น Am6,9/G# จากนั้นหน้าต่างเทอร์มินัลสิทธิ์รูทเด้งขึ้นมา คงเท่มาก
เมื่อเร็ว ๆ นี้ลองหาว่าถ้าจะฟัซ MIDI ต้องใช้อะไรบ้าง และเจอข้อมูลสำหรับสร้างไฟล์ .mid เพื่อการนี้ แต่ก็ยังไม่ค่อยตรงกับที่ต้องการ
แต่มองแล้วฝั่งนี้น่าจะน่าสำรวจแทน
เสียดายพอสมควรที่ซินธิไซเซอร์สมัยนี้ดูจะใช้น้อยลงเรื่อย ๆ โดยเฉพาะ Roland
แต่ Behringer ยังรองรับค่อนข้างดี
เช่น Deepmind มีช่วง MIDI CC ที่ดีอยู่แล้ว และยังโปรแกรมได้แทบ 100% ผ่าน SysEx
เป็นงานวิจัยที่น่าทึ่ง
ทำให้นึกถึงงานวิจัยปี 2017 ที่สังเคราะห์เชลล์โค้ดลงใน โมเลกุล DNA/RNA จริง ๆ แล้วสาธิตการรันโค้ดจากระยะไกลบนเครื่องซีเควนซ์ DNA: https://www.usenix.org/conference/usenixsecurity17/technical...
กำลังจะพูดว่า “ต่อไปเป็น OSC หรือเปล่า” แต่ดูเหมือน MIDI ยังครองอยู่
แนะนำให้อ่านบทความทั้งหมด แต่คิดว่าประโยคสำคัญ ๆ คือพวกนี้
“พวกบ้า ๆ [ผู้ผลิตคีย์บอร์ด] นี่สร้างเชลล์ที่รันอยู่บนข้อความ MIDI SysEx เหนือ USB ไว้แล้ว”
“คำสั่งที่น่าสนใจที่สุดคือคำสั่ง อ่าน/เขียนหน่วยความจำตามอำเภอใจ ถ้าต้องการ ก็สามารถส่องและจิ้มหน่วยความจำของซินธิไซเซอร์ผ่าน MIDI ได้”
“ถ้าต้องการ ก็เขียนข้อความเหล่านี้ลงในไฟล์ MIDI แล้วเล่นบนซินธิไซเซอร์เหมือนไฟล์ MIDI อื่น ๆ ได้ อ้าว มีไอเดียดี ๆ ผุดขึ้นมาแล้ว…”
“หลังจากอดหลับอดนอนขุดเฟิร์มแวร์มานับไม่ถ้วน ก็พบฟังก์ชันที่ส่งข้อมูลตามอำเภอใจไปยังคอนโทรลเลอร์ LCD”
ในแง่หนึ่งนี่เหมือนได้แอบเห็น ฝันร้ายของอินเทอร์เน็ตของสรรพสิ่ง เล็กน้อย
แทบทุกอุปกรณ์อาจมีแบ็กดอร์ได้ และอาจเป็นแบ็กดอร์โง่ ๆ อย่าง #0000 ด้วยซ้ำ
การเกิด แพ็กเก็ตสูญหาย ถือเป็นเรื่องปกติสุด ๆ
สงสัยว่าสามารถแทรก เพลง MIDI ระหว่างคำสั่ง Bad Apple เพื่อให้เล่นเสียงเองไปด้วยได้ไหม
README ของรีโพซิทอรีบอกว่ามี ดัมป์อิมเมจ อยู่ แต่จริง ๆ ไม่มี
สงสัยว่านี่เป็นสถานะที่ถูกต้องหรือเปล่า
ตอนนาทีสุดท้ายเพิ่ม
*.binเข้า.gitignoreเพื่อไม่ให้รวมชิ้นส่วนโค้ดที่แอสเซมเบิลแล้ว แต่ดูเหมือนดัมป์จะถูกกันออกไปด้วยจะอัปโหลดภายในไม่กี่ชั่วโมง
ดัมป์เหล่านั้นอาจได้รับการคุ้มครองโดย ลิขสิทธิ์ของ Yamaha ดังนั้นอาจเป็นการตัดสินใจที่ดีเสียด้วยซ้ำ
ถ้ามีผู้อ่าน HN คนไหนอยู่ใน Armenia Porta จะไปพูดเรื่องนี้ที่ Hacker Embassy วันที่ 10 มกราคม
อยากให้แวะไปจริง ๆ: https://t.me/hackerembassy/17