Danger
เนื้อหาในบทความนี้เปิดเผยขั้นตอนการทำงานทั้งหมดของผู้เขียน ซึ่งเป็นการกระทำที่ไม่ได้รับอนุญาตจากทางเจ้าของเว็บไซต์ การนำไปทำตามเนื้อหาอาจทำให้ท่านกระทำผิดกฎหมาย พรบ. ว่าด้วยการกระทําความผิดทางคอมพิวเตอร์ พ.ศ. 2560
หลังจากที่รอบก่อนเว็บ ezslot โดนเราเล่นท่า Negative Transfer จนต้องแพทซ์แก้ไขกันไป วันนี้เราเลยอยากกลับไปลองดูกันใหม่ว่าจะเจออะไรอีกไหม
Withdraw
- PATH:
https://api.pocketfarm.com/api/v1/bank/withdraw - METHOD:
POST
Request Body
{
"channel": "game",
"amount": "1"
}Response Body
{
"error_code": 400,
"message": "json: cannot unmarshal string into Go struct field ActionRequest.amount of type float64",
"success": false
}เราได้ลองเปลี่ยน amount เป็น String แล้วระบบได้ Return กลับมาบอกว่ารับค่า amount เป็น float64 เท่านั้น
ตอนนี้เรารู้แล้วว่า
- ระบบเขียนด้วย Golang จาก message ที่บอกว่า Go struct field
- ค่า amount ต้องเป็น float64
ปัญหายอดนิยมของ float64 ที่คอมโบคู่กับระบบยอดเงินแบบทศนิยมสองตำแหน่ง ที่พบได้บ่อยๆก็คือ เจอเลขในรูป scientific notation (เช่น 1e-3, 1e20) ทำให้ precision เพี้ยนหรือหลุด validation หรือเอ๋อทันที่ถ้าเจอเลข 5 ลงท้าย แต่ในเคสนี้เราเจอว่ามันเอ๋อเลข 5
สิ่งที่ float64 เห็น
0.004 → 0.004000000000000000
0.005 → 0.004999999999999999
0.006 → 0.006000000000000000
หมายเหตุ
- 0.005 ไม่สามารถแทนค่าได้ตรงใน IEEE-754 binary
- float64 เก็บเป็น base-2 ไม่ใช่ base-10
- ค่าที่ใกล้ที่สุดของ 0.005 ใน binary คือ 0.004999999999999999…
หมายความว่าถ้าเราส่งยอด 0.005 ไประบบจะเข้าใจว่าส่ง 0.004999999999999999 และเมื่อคอมโบคู่กับทศนิยมสองตำแหน่ง นั่นยิ่งเพี้ยนไปกันใหญ่ เพราะมันจะปัดเศษลงมาเป็น 0.00
สิ่งที่เกิดขึ้นเมื่อถูกคอมโบคู่กับทศนิยมสองตำแหน่งด้วยวิธี truncate/floor/cast
0.004 → 0.00 // ปัดลง
0.005 → 0.00 // ปัดลง
0.006 → 0.01 // ปัดขึ้น
Impact
ผลลัพธ์ของอาการนี้คือ ระบบเชื่อสนิทใจว่ามีการถอนเงินเกิดขึ้นจริง ทั้ง ๆ ที่ยอดเงินจริงแทบไม่ขยับ เพราะมันโดนเศษทศนิยมกินไปเรียบร้อยแล้ว
ถ้าลากพฤติกรรมนี้ไปทำซ้ำ ๆ ระบบก็จะค่อย ๆ แจกเงินฟรีจาก rounding error โดยที่ทุก transaction ผ่าน validation หมด ไม่มีอะไรดูผิดปกติในสายตา backend เลย
Root Cause
ต้นตอของเรื่องทั้งหมดไม่ได้มาจากอะไรซับซ้อน แค่เอา float64 มาจับยอดเงิน แล้วหวังว่ามันจะเชื่อฟังเหมือนเลขฐานสิบ
พอค่าอย่าง 0.005 หลุดเข้า IEEE-754 จริง ๆ มันก็แกล้งลดตัวเองลงนิดนึง แล้วค่อยโดน truncate / cast ทิ้งตอนตัดทศนิยมสองตำแหน่ง สุดท้ายยอดเงินหายไปแบบเงียบ ๆ ไม่มีใครรู้ ไม่มีใครเห็น
Mitigation
กฎเหล็กของระบบการเงินคือ ถ้าเห็น float64 อยู่ใกล้ยอดเงิน แปลว่ามีคนกำลังจะซวย
ทางรอดที่ปลอดภัยกว่าคือเลิกใช้ float กับเงินตั้งแต่ต้น เก็บเป็น integer ไปเลย จะเป็นสตางค์ satoshi หรือหน่วยย่อยอะไรก็ว่ากัน หรือไม่ก็ใช้ decimal library ที่ออกแบบมาให้ไม่เล่นตลกกับเศษทศนิยม
อีกอย่างที่ควรทำคือดักตั้งแต่ด่านหน้า จำกัดทศนิยมไม่เกินสองตำแหน่ง และปัด scientific notation อย่าง 1e-3 ทิ้งไปตั้งแต่ยังไม่เข้าระบบ จะได้ไม่ต้องมานั่งไล่ผี IEEE-754 ทีหลัง



