Race Condition — บัคอะไรเอ่ย เกิดบ้างไม่เกิดบ้าง

วันนี้ผมจะมาพูดถึงช่องโหว่ตัวหนึ่งที่มีเจอบ้างเป็นครั้งคราว ส่วนใหญ่เจอบนเว็บที่มี business logic ซับซ้อน. (ถ้าแค่ wordpress query database มาแสดง ไม่มีปัญหา). developer บางคนคิดว่าทำ input validation, filter และมี WAF เรียบร้อยไม่โดนโจมตีจาก hacker แน่นอน… ก็อาจจะมาตายเพราะช่องโหว่นี้ได้ครับ (อันที่จริง ผมก็เคยเกือบตายจากมันมาก่อน หนักหน่วงมาก ต้องขอบคุณ senior ที่ช่วยชีวิตไว้ 555)

Race condition ถือเป็นตัวหนึ่งที่ค่อนข้างเลวร้ายสำหรับ developer มากเลยทีเดียว เพราะ logic ของมันไม่ได้ตรงไปตรงมา-บนลงล่างชัดเจน. ไม่ได้เกิดขึ้นทุกครั้ง. บางครั้งเกิดที มี log บ้างไม่มีบ้าง. หาก็ยากแล้ว บางครั้งก็แก้ยากด้วย. จึงควรทำความเข้าใจตั้งแต่เริ่มเขียนโปรแกรมเลยครับ เพื่อป้องกันปัญหานี้.

ผมหวังว่าผู้อ่านที่เป็น developer จะเข้าใจช่องโหว่ประเภทนี้ เข้าใจว่ามันเกิดได้อย่างไร และแก้ไขหรือป้องกันได้อย่างถูกต้องในการเขียนโปรแกรมต่อๆไปครับ. ส่วนผู้อ่านที่เป็น pentester ก็หวังว่าจะสามารถเข้าใจระบบที่อาจจะมีช่องโหว่นี้ เพื่อนำไปเป็นส่วนหนึ่งในรายการที่ต้องทดสอบด้วย.

Race Condition คืออะไร

ถ้าตาม Wikipedia เลย

A race condition or race hazard is the behavior of an electronics, software, or other system where the output is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when events do not happen in the order the programmer intended.

แต่ถ้าสั้นๆในมุมมองผม มันคือการที่ มีอะไรสักอย่างหนึ่งที่ถูก worker มากกว่าหนึ่งมากระทำอะไรสักอย่างกับสิ่งนั้นพร้อมๆกัน โดยปราศจากการควบคุมเรื่องเวลา ลำดับ หรือการ lock การเข้าใช้งาน จึงเกิดปัญหานี้ขึ้น.

เนื่องจากการที่ process ไม่มีการควบคุมเรื่องลำดับที่ชัดเจน. สิ่งอื่นมันเลยมาจัดลำดับให้ ซึ่งถูกบ้างผิดบ้าง ทำให้ข้อมูลนั้น อาจจะถูกเปลี่ยนแปลง แล้วถูกบ้างผิดบ้าง กลายเป็นบัคที่เจอบ้างไม่เจอบ้าง และน่าปวดหัวมากนั่นเอง.

Worker ด้านบนคืออะไรได้บ้าง

มีได้เยอะเท่าที่จะคิดออกเลยครับ ถ้าในระบบคอมพิวเตอร์ คนส่วนใหญ่คงคิดไปที่ CPU Cores

CPU Cores

ในความเป็นจริง CPU core เพียง core เดียว ก็สามารถนับเป็นหลาย workers ได้ ด้วยความสามารถที่เรียกว่า context switching. ถ้านึกไม่ออก ให้นึกถึงคนเดียว ทำ multi-tasking น่ะครับ สลับทำงาน A และ B. แต่ทั้งงาน A และ B ดันมีการแก้ไขข้อมูล X อันเดียวกัน. โอกาสมึนๆแล้วพังย่อมมีครับ

Context Switching

หรือถ้าคนที่เคยเรียน Digital Logic มาก่อน บนสายไฟของวงจร digital ก็เกิดได้ เนื่องจากการเลื่อมกันของเวลาขณะเข้า gate

Digital Logic Gate

หรืออันที่ผมแอบพูดไปก่อนหน้านี้แล้ว และใกล้ตัวเรามาก… คนนั่นเอง. คนเราก็สามารถเป็นตัวทำให้เกิด race condition ได้ เช่น โต๊ะแลกบัตรเข้าตึก. บางครั้งพนักงานหนึ่งคน รับงานมาจาก visitors มากกว่า 1 คน ทำให้บัตรเกิดสลับกัน เป็นต้น

Human Reception

สิ่งที่อาจจะถูกกระทำจนเกิด race condition มีอะไรบ้าง

บางคนมองว่าแค่ interact (read/write) ก็นับเป็น race condition หมดแล้ว ถ้าไม่ได้ lock ไว้. อันนี้แล้วแต่คนมองนะครับ แต่ถ้าเอาหลักการที่น่าจะเห็นตรงกัน คือ race condition จะมีผลจริงๆ เมื่อสิ่งที่ถูกกระทำเกิดการเปลี่ยนแปลงด้วย. ดังนั้นผมจะเน้นไปที่การ change/write บนสิ่งนั้นๆนะครับ.

Computer RAM

ใกล้ตัวมากๆอย่างหนึ่งก็คือ computer memory นั่นเอง. การที่มี process/thread มากกว่า 1 เข้าใช้งาน memory ช่องเดียวกัน ทำให้ logic ที่ developer คิดเอาไว้ มันถูกแทรกแซงด้วยสิ่งอื่น ผลลัพธ์จึงออกมาผิด. ถ้าคนเขียน backend หรือ low level program/system จะเจอตัวนี้บ่อยมากเลยครับ. ส่วนในภาษาสมัยใหม่ๆ มักจะใช้ท่า duplicate on change ออกมาให้ เพื่อให้เราไม่ต้องเข้าไปยุ่งเรื่องการ control พวกนี้ให้วุ่นวาย. ทั้งนี้อย่า assume เองว่ามันจะทำให้นะครับ. ให้ไปอ่าน document หรือเรียนรู้ภาษานั้นๆให้ลึกระดับหนึ่งก่อนครับ. บางครั้งมันก็ไม่ได้ทำให้ บางครั้งมันก็ทำให้ แต่เราดันเรียกใช้ผิด มีทั้งสองเคสครับ.

เหลืออีกกรณีคือ global variables ที่มันจะชี้ไป memory ก้อนเดิมเสมอ ไม่ว่าจะอยู่ thread ใดก็ตาม. เรื่อง race condition เป็นอีกสาเหตุที่ทำให้ developer ส่วนใหญ่ เขียนไว้ว่า อย่าใช้ global variable เต็มอินเตอร์เน็ตไปหมดครับ. (สาเหตุหลักคือ manage ยากครับ)

Database

การเรียกใช้ database นี้เป็นสิ่งที่ web application จะเจอ race condition ได้สูงที่สุดในบรรดาทั้งหมดแล้ว. (เพราะตัวอื่นไม่ค่อยได้ใช้กันปะ 555)

Files

มีการเขียนลง files บน system. มีตั้งแต่ files database เล็กๆ, log, images, user binary data (PDF, docs, etc). เห็นชัดมากขึ้นเมื่อมีการจัดการกับไฟล์เยอะๆ เช่นมีการเข้ารหัส, ย้ายไฟล์ไปมา, โอนข้ามเครื่องผ่าน HTTP, FTP ต่างๆ เป็นต้น

อื่นๆอีกมหาศาลได้แก่

  • Session / token
  • URL
  • Logical process
  • … and more

ผมมองว่าการที่ developer เผลอเขียนให้เกิด race condition ขึ้นมาได้ ส่วนใหญ่เกิดจากความไม่รู้ครับ. ที่เจอบ่อยคือ ไม่ทราบว่า web server นั้นมีการจัดการรับ request จาก HTTP มา แล้วทำการแตก process/thread เข้าไปทำงาน web application ที่เราเขียนไว้อีกที. ดังนั้นเวลาเราเขียน web application จะต้องพึงคิดว่า เราไม่ใช่ worker คนเดียวที่ทำการ resources นั้นๆ และต้องมีการจัดการให้ดีครับ.

How Web Server works

ตัวอย่าง code ที่เกิด race condition และวิธีแก้ไข

ตัวอย่าง 1: คูปองลดราคา

ถ้าพูดถึง race condition บน web application แล้ว ผมคงไม่พูดถึงตัวนี้ไม่ได้. มันคือช่องใส่คูปองเพื่อลดราคาสินค้าก่อน check out cart นั่นเอง

1 SELECT * FROM user WHERE coupon_used == 0 AND user == getUserFromSession()
2 if user == null → exit
3 SELECT price FROM cart
4 $price = price * 0.5
5 UPDATE cart SET price = $price
6 UPDATE user SET coupon_used = 1 WHERE user == getUserFromSession()

ผู้อ่านคิดว่า race condition จะเกิดขึ้นได้อย่างไรบ้างครับ

.

.

.

เอา pentester happy-happy flow ไปก่อน (ความเป็นจริง อาจจะไม่ใช่ flow นี้)

1–1: Discount code (บน-ล่างตามเวลา ซ้าย-ขวาคือคนละ requests ที่แทรกกันอยู่)

เมื่อ web server ได้รับ request มันจะทำการสั่งให้ web application ทำงานตาม logic นั้นๆ ซึ่งอาจจะมีมากกว่า 1 request ที่ทำงานพร้อมกันอยู่. ทำให้ flow ที่ developer คิดว่าทำจากบนลงล่างเสมอ อาจจะถูกแทรกด้วย request อื่น.

จากกรณีนี้จะเห็นว่า เนื่องจากการ update user ว่าใช้ coupon ไปแล้วอยู่ล่างสุด ทำให้มีโอกาสที่คูปองลดราคาจะถูกใช้มากกว่าหนึ่งครั้ง. แทนที่จะลดครึ่งราคา สามารถลดไปได้เหลือ 25% เลย (หรือมากกว่านั้นในกรณี 2+ requests)

วิธีแก้ไข

แน่นอนว่าการปล่อยให้ workers หลายตัวใช้งาน resource เดียวก็ทำให้เกิดบัค เราก็ต้องไปจัดการการเข้าใช้งาน resource นั้นๆ ให้เป็นไปตามที่เราวางไว้ครับ.

กรณีนี้ คือการใช้ transaction ของ query เข้าช่วย.

1–2: Database transaction

Databases แต่ละตัวจะมีระบบ transaction ที่แตกต่างกัน ซึ่งมีคุณสมบัติ ACID (atomicity, consistency, isolation, durability) แตกต่างกันไปด้วยครับ ว่ามันจะจัดการกับข้อมูลเราแบบไหนอย่างไร. ผู้อ่านสามารถอ่าน ACID ได้จาก link ทั่วไปเลยครับ. แต่ถ้า ACID, transaction ของแต่ละ database เนี่ย ก็ต้องเข้าไปดูในแต่ละ database เองว่า developer เขา provide ให้เราอย่างไร. สำคัญมาก เพราะแต่ละตัวนั้นจัดการให้เราไม่เหมือนกันครับ.

อย่างเช่น SQLite บอกว่าถ้าใช้ concurrency บน database connection อันเดียวกัน ผมจะไม่สนใจนะ อย่ามาโวยวาย เป็นต้น. แต่ถ้าใช้งานปกติ transaction ก็ยังโอเคเหมือน SQL database ทั่วไปอยู่ คือ ALL or Nothing.

ส่วนในกรณีที่เป็น single query นั้น databases ส่วนใหญ่ (ยกเว้นตัวบ้าๆบ้างตัว หรือกรณี config performance เอง) จะมองว่า query นั้นเป็น transaction ในตัวมันเองอยู่แล้ว จึงไม่มีปัญหาครับ.

ดังนั้นกรณี redeem คูปองข้างบนสามารถแก้ได้ 2 แบบคือ

1. จับรวบทั้งหมดด้วย transaction ซะ. ถ้าเกิดกรณีที่ request หลายตัวมาพยายาม change บน data ก้อนเดียวกัน ก็จะมีก้อนหนึ่งที่ commit ผ่านเรียบร้อย และก้อนอื่นที่ commit error ครับ (ทั้งนี้แล้วแต่ isolation level ของแต่ละ database หรือ config ด้วย) อันนี้ผมมองว่าเป็นวิธีที่ตรงไปตรงมาที่สุดแล้ว.

BEGIN TRANSACTION;
SELECT coupon_used FROM user WHERE user = getUserFromSession();
if coupon_used == 1 → exit
SELECT price FROM cart;
$price = price * 0.5;
UPDATE cart
SET price = $price;
UPDATE user
SET coupon_used = 1
WHERE user == getUserFromSession();
COMMIT;

2. ยุบ queries หลายๆตัวให้เป็น single query. อันนี้ผมเข้าใจว่า databases ส่วนใหญ่จะมองเป็น transaction เดียวกัน. แต่ก็แนะนำให้เช็คกับ documentation ของ database ที่ท่านใช้อีกทีครับ. ข้อเสียคืออาจจะต้องปรับ database design ด้วย.

1 UPDATE user SET cart_price = cart_price * 0.5, coupon_used = 1 WHERE coupon_used == 0 AND user == getUserFromSession()

ตัวอย่าง 2: เข้ารหัสไฟล์

เอาไฟล์ไปเข้ารหัสแล้วส่ง URL ให้ผู้ใช้ download.

1 $userData = SELECT * FROM user WHERE user == getUserFromSession();
2 Write $userData → /tmp/temp_file;
3 Encrypt(/tmp/temp_file, getUserPassword());
4 Move /tmp/temp_file to /var/www/html/user_${user_id}
5 Redirect user to www.example.com/${user_id}

ลองไล่ๆดูว่าจะเกิด race condition ยังไงได้บ้างนะครับ

.

.

.

2–1: File encryption (บน-ล่างตามเวลา ซ้าย-ขวาคือคนละ requests ที่แทรกกันอยู่)

จะเห็นว่าพอมันเรียงใหม่ มันมีโอกาสที่ A จะไปโหลดได้ข้อมูลของ B มา. อันนี้ปัญหามันเกิดจากการที่เราใช้ identifier “/tmp/temp_file” ร่วมกันระหว่าง 2+ workers นั่นเอง.

วิธีแก้

(ขอข้ามวิธีแก้ด้วยการ load file เข้ามาใน memory แล้ว encrypt แทนนะครับ. สมมติว่ามีกรณีที่ load มาบน memory ไม่ได้ละกัน จะได้ดูวิธีแก้เชิง race condition)

ตรงไปตรงมาครับ มันมีปัญหาว่าใช้ identifier ร่วมกัน ก็ให้แยกมันซะ คือพยายามให้มันไปเขียนไฟล์คนละตัว โดยใช้ identifier ที่ unique กว่านั้น เช่น

1 $userData = SELECT * FROM user WHERE user == getUserFromSession();
2 Write $userData → /tmp/temp_${user_id};
3 Encrypt(/tmp/temp_${user_id}, getUserPassword());
4 Move /tmp/temp_${user_id} to /var/www/html/user_${user_id}
5 Redirect user to www.example.com/${user_id}

กรณีนี้ A ก็จะไปเผลอโหลดของ B จาก race condition ไม่ได้ละ แต่ยังมีปัญหาถ้าในกรณีที่ A คนเดียวเปิด 2+ browser หรือยิง request อยู่. มันมีวิธีทำให้ดีกว่านี้ครับ แต่ขอให้เป็นการบ้านละกัน.

ผมคงยกตัวอย่างไว้เพียงเท่านี้ ที่ผมคิดว่าเพียงพอแล้วสำหรับให้ developer web application และ backend ส่วนใหญ่เข้าใจ. ถ้าใครอยากได้ตัวอย่างในกรณี low-level language เช่น ภาษา c, mutex lock, etc ก็ลอง request มาก่อนครับ ถ้ามีระดับหนึ่ง ผมจะเขียนแยกให้อีกที.

แต่สำหรับ pentester มันยังไม่พอ !!

pentester ถ้ากรณีของ whitebox สามารถมองเห็น source code ได้เลย ถ้าเคยเป็น developer ที่เจอปัญหาแบบนี้มาก่อน ก็จะมองออกครับ (มันมี pattern ของมันอยู่) คงไม่มีปัญหาอะไร.

สำหรับ greybox-blackbox อันนี้มีปัญหาพอสมควร คือเราจะไปรู้ได้ไงว่า function หน้าเว็บแบบนี้ มันจะไปเขียนจนเกิด race condition หลังบ้านได้ !!? คำตอบคือ ไม่รู้หรอกครับ. เขาอาจจะเขียนได้ถูกต้องแล้ว หรือเขียนผิดก็ได้ ต้องลองอย่างเดียว แต่ผมคงไม่ลอง request รัวๆใส่ทุก API ที่เว็บนั้นมีแล้วนั่งวิเคราะห์แน่ๆ เพราะไม่งั้นผมคงไม่ต้องไปทดสอบอย่างอื่นเลย. ผมจะใช้หลักการว่า ตรงไหนที่สำคัญ เช่น เกี่ยวกับการเงิน, sensitive information, ความ stable ของเว็บนั้น และมีกระบวนการเขียนข้อมูล ที่น่าจะต้อง query/access resource หลายๆ step. ผมก็จะเน้นทดสอบไปที่จุดนั้นครับ.

ถ้าจำตัวอย่างคูปองลดราคาได้ ผมจัด sequence ให้ผู้อ่านเห็นได้ง่ายๆ ว่ามันมีปัญหา. แต่ในความเป็นจริง ตอนเรียกใช้ อาจจะพบว่า sequence เป็นแบบนี้ครับ

Discount code — Non happy flow

อันนี้เป็น flow ที่มีโอกาสเป็นไปได้สูงทีเดียว เพราะช่วง query นั้นเกิด IO ซึ่งเป็นการทำงานที่ช้า ทำให้มักจะเกิด context switching ไปทำอีกงานหนึ่ง มันเลยเป็นซ้าย-ขวา-ซ้าย-ขวา แบบในรูป. การทำงานแบบในรูป ถ้าเราเห็นโค้ด คงส่ายหน้าว่า race condition เต็มๆ แต่ถ้าทดสอบแบบ blackbox จะพบว่า output — price ออกมาเรียบร้อยถูกต้องดี.

แต่มันเป็นบัคที่ exploit ได้ และส่งผลกับการเงินชัดเจน เราจะแก้ปัญหานี้อย่างไรเพื่อให้ทดสอบแล้วเจอบัคนี้ได้ง่ายขึ้น ?

  1. Try again — ลองใหม่ไปเรื่อยๆครับ. ถ้าของมันมีบัค สักวันมันก็ต้องบัค.
  2. Put more workers there ? 3–4 threads ? — ถ้าสามารถใส่ threads/process เพิ่มเข้าไปได้ ก็ใส่เพิ่มเข้าไปครับ. ยิ่งเยอะ มันจะทำให้ sequence ออกมา หลายๆแบบ. ในกรณีที่มีบัค เราจะเห็นชัดเจนเลยว่าค่ามันแปลกไปครับ. วิธีนี้ใช้ได้ง่ายกับ web server ที่ปกติมีหลาย threads อยู่แล้ว.
  3. Increase workload of the server / computer — ในบางกรณีที่เครื่องนั้นมีความเร็ว IO ที่สูงมาก หรือไม่ได้ใช้ IO ขนาดนั้น และ/หรือมี gap ที่จะเกิด race condition น้อยมาก. การเพิ่ม workload ให้กับ server เช่น IO Workload (เขียน disk รัวๆ), CPU workload (request เยอะๆ / DDoS), etc. จะทำให้เกิด context switching ในจุดที่แปลกๆมากขึ้น และอาจจะทำให้เราเห็นบัคซ่อนได้. ผมเห็นบางเว็บตอนแรกก็ไม่มีปัญหา race condition อะไร, พอวันดีคืนดี migrate server จน delay ช้ามาก race condition ก็โผล่ออกมาบาน.
  4. Help from other API (inotify, program hook, etc.) — ใช้การ interrupt เข้าช่วย. ลองคิดว่าถ้าเราสามารถหยุดที่บรรทัดนี้ได้ จังหวะนี้ได้ จะเกิด race condition แน่ๆ. เราก็ทำอันนั้นให้เป็นจริง เช่น แทนที่จะ run program บ่อยๆ เวลามั่วๆ ก็ใช้ inotify ที่เอามา detect filesystem change แทน ซึ่งแม่นยำกว่า. หรือถ้ากรณีที่เราสามารถ pause program ในจังหวะต่างๆได้ เช่น gdb, ptrace, etc. ก็จะช่วยให้เราสามารถ simulate race condition ได้ง่ายขึ้นครับ.

บทส่งท้าย

ผมไม่คิดว่าที่ผมเขียนมาทั้งหมดนี้จะอธิบายได้ครอบคลุมทั้งหมด เพราะ race condition ถือเป็นหัวข้อใหญ่ข้อหนึ่งเลยทีเดียว แต่ผมก็หวังว่าทั้ง developer และ pentester จะเข้าใจหลักการคร่าวๆและนำไปต่อยอดใช้ประโยชน์ได้ครับ

ทั้งนี้อยากให้ผมเพิ่มเติมตรงไหน หรือแก้ไขตรงไหน แจ้งได้ทันทีครับ

อยากทิ้งท้ายไว้ว่า computer มันเป็น deterministic machine. ทุกอย่างที่มันทำนั้นมีที่มาที่ไปทั้งหมด. อย่าเพิ่งดีใจกรณีที่เจอบัคแต่พอรันใหม่อีกพันรอบแล้วไม่เจอแล้ว เพราะมันไม่ได้หายไปไหน และอาจจะโผล่มาอีกทีในวันที่เราฉิบหายได้ครับ.

- July 30, 2018 -