X đã để lộ thông tin cá nhân của 3 triệu người dùng như thế nào?

file 69

Lời mở đầu: Trong lúc trao đổi về việc công bố bài viết chi tiết thì tôi đã nhận lời với bên X là sẽ không chỉ đích danh một công ty, tổ chức nào trong bài này. Dù sao thì, những trải nghiệm thực tế trong việc phân tích và truy tìm hung thủ... à nhầm, truy tìm lỗ hổng mới là những thứ giá trị nhất mà tôi muốn chia sẻ với các bạn. Vậy nên một cái tên gọi thì đâu có gì quá quan trọng đúng không nào?!

Giới thiệu về T-Rekt

Trong bài viết này, ngoài Juno và K-20 thì còn xuất hiện một thành viên mới chưa từng xuất hiện trong các bài viết trước đây trên Siin Blog. Đó chính là T-Rekt, thành viên nhỏ tuổi nhất trong J2TeaM. Tuổi nhỏ nhưng lại thích nhắm vô những công ty, tập đoàn lớn. Báo cáo về lỗ hổng rò rỉ thông tin cá nhân của 3 triệu người dùng mới chỉ là màn ra mắt của cậu ấy thôi, mọi người hãy chờ thêm các công ty X, Y, Z nào đó chuẩn bị được nêu tên trong các bài phân tích tiếp theo của Siin nhé!

T-Rekt đã tìm ra lỗ hổng như thế nào?

Trước đây T-Rekt cũng biết đọc mã nguồn trang web và thường capture các truy vấn bằng công cụ Fiddler. Rồi một ngày nọ, cậu ấy nảy ra ý tưởng debug ứng dụng Android nên tìm đọc tài liệu và thay đổi proxy của điện thoại để có thể capture thông qua Fiddler.

Và cái ứng dụng Android đầu tiên mà T-Rekt chọn để debug chính là ứng dụng Android của X. Ngay từ lúc bắt đầu, T-Rekt đã thử đăng nhập rồi order các kiểu con đà điểu để capture được càng nhiều truy vấn càng tốt. Với những gì mà Fiddler "chộp" được, T-Rekt nhận ra có điều gì đó không ổn trong phần thông tin tài khoản người dùng. Cậu ấy còn tìm được một số bug nhỏ nữa như đổi avatar của người dùng bất kỳ chỉ bằng UserID.

Ngày hôm sau, cậu ấy gửi tin nhắn cho team nhờ hỗ trợ phân tích lỗ hổng mà cậu ấy tìm được. Cậu ấy nói "phát hiện ra một lỗ hổng trong API của X cho phép lấy thông tin tài khoản của tất cả người dùng (trừ mật khẩu)". Ngay khi đọc được tin nhắn, K-20 đã pm tôi và tối hôm đó hai đứa ngồi phân tích hệ thống X dựa trên thông tin mà T-Rekt cung cấp. Kết quả là từ lỗ hổng đó có thể truy cập vào thông tin cá nhân của hơn 3 TRIỆU TÀI KHOẢN. Những thông tin này bao gồm: họ và tên, giới tính, ngày/tháng/năm sinh, email, số điện thoại, số CMND, địa chỉ cá nhân,...

Dịch ngược ứng dụng Android của X

Tôi nhanh chóng Google và tìm được ứng dụng của X trên Google Play. Thay vì phân tích ứng dụng thông qua các truy vấn HTTP như T-Rekt đã làm thì tôi sẽ dịch ngược ứng dụng về mã Java để phân tích trực tiếp từ mã nguồn ban đầu. Do đó, không cần cài đặt ứng dụng vào điện thoại mà tôi sẽ tải tập tin APK của ứng dụng này về máy tính. Việc này khá đơn giản, chỉ với từ khóa "apk downloader" trên Google là xong.

APK là phần mở rộng từ JAR và ZIP nên chỉ cần dùng các phần mềm giải nén thông thường như WinRAR hoặc WinZIP là có thể xem được tài nguyên ẩn chứa bên trong. Cấu trúc cây thư mục:

  • assets
  • com
  • fabric
  • lib
  • META-INF
  • res
  • AndroidManifest.xml
  • classes.dex (tập tin quan trọng nhất)
  • resources.arsc

Tôi sử dụng tiếp công cụ dex2jar để chuyển đổi classes.dex thành classes.jar rồi dịch ngược và xem cấu trúc mã nguồn bằng jd-gui.

Phân tích ứng dụng Android của X


Như vậy là chúng ta đã có mã nguồn Java của ứng dụng, nhưng có quá nhiều class trong một package, ngồi mò thì tốn thời gian. Tôi liền sử dụng chức năng tìm kiếm của jd-gui dựa theo một Endpoint mà T-Rekt đã cung cấp:

https://x-server.com/api/customer/profile/id/

... Không có kết quả nào. Thường thì một hệ thống API không chỉ có mỗi một endpoint để truy vấn nên có thể lập trình viên thường sẽ gán Base URL vào một biến hoặc hằng. Ví dụ như:

const BASE_URL = 'https://x-server.com/api/';

Rồi nối chuỗi để tạo ra các endpoint. Nghĩ vậy, tôi liền thử tìm lại với chuỗi con "customer/profile/id":

3-million-user-accounts-information

Có kết quả liền! He he. Lưu lại hai class này ra riêng một chỗ rồi mở lại với jd-gui để phân tích tiếp:

3-million-user-accounts-information

Có quá nhiều endpoint, đúng như dự đoán thì Base URL đã được lưu ở một nơi riêng biệt nên trong class này chỉ chứa toàn chuỗi con phía sau.

Các truy vấn HTTP của ứng dụng này đều được xử lý trong class XRequest.java. Tôi nhắm vào mục tiêu nhạy cảm nhất là mật khẩu nên nhấn ngay Ctrl+F và gõ từ khóa "password".

  public static ResponseData changePassword(String paramString1, String paramString2, String paramString3)
  {
    new ResponseData();
    String str = Common.getXHostByLanguage() + "api/customer/changePassword";
    HashMap localHashMap = new HashMap();
    localHashMap.put("email", paramString1);
    localHashMap.put("current_password", paramString2);
    localHashMap.put("new_password", paramString3);
    return HttpUtils.callPostApi(str, localHashMap);
  }

Method đầu tiên hiện ra là changePassword. Áp dụng kiểu mẫu chung nên nó yêu cầu một tham số là current_password (mật khẩu hiện tại). Dù rằng không thể khai thác method này để đổi mật khẩu của người dùng bất kỳ ngay được, nhưng hacker có thể thực hiện brute force tham số current_password để nhắm vào một người dùng với một địa chỉ email được chỉ định.

  public static ResponseData forgetPassword(String paramString)
  {
    String str = Common.getXHostByLanguage() + "api/customer/resetPassword";
    HashMap localHashMap = new HashMap();
    localHashMap.put("email", paramString);
    return HttpUtils.callPostApi(str, localHashMap);
  }

Method tiếp theo là forgetPassword dùng để khôi phục mật khẩu, tham số duy nhất mà nó yêu cầu là email. Method này có thể khai thác được trong trường hợp hacker đã chiếm được email của nạn nhân, từ đó chiếm tài khoản thông qua tính năng khôi phục mật khẩu.

Một vài method khác chủ yếu là truy vấn thông tin của người dùng như lịch sử giao dịch, điểm tích lũy,... Và cuối cùng, method thú vị nhất là login, có thể được dùng để thực hiện brute force.

  public static ResponseData login(String paramString1, String paramString2)
  {
    new ResponseData();
    String str = Common.getXHostByLanguage() + "api/customer/login";
    HashMap localHashMap = new HashMap();
    localHashMap.put("email", paramString1);
    localHashMap.put("password", paramString2);
    return HttpUtils.callPostApi(str, localHashMap);
  }

Method register cũng có thể dùng để spam tài khoản mới nhưng hình thức spam này không gây hứng thú lắm với các hacker vì nó chỉ mang tính chất phá phách, với script kiddies thì có lẽ lại là chuyện khác.

Tìm hết các method liên quan tới mật khẩu, tôi lướt tiếp và thấy ngay cái method quan trọng nhất của bài viết:

  public static ResponseData getUserProfile(String paramString)
  {
    new ResponseData();
    return HttpUtils.callGetApi(Common.getXHostByLanguage() + "api/customer/profile/id" + "/" + paramString);
  }

Điều khó hiểu là nếu như đa số các method truy vấn thông tin khác đều có tham số usersessionid (ID phiên làm việc của người dùng hiện tại) để xác định xem ai là người đang truy vấn thì cái method lấy hồ sơ lại chỉ yêu cầu tham số duy nhất là ID người dùng. Có nghĩa là method có thể lấy nhiều thông tin về người dùng nhất thì lại bảo mật tệ nhất. Chỉ cần thay đổi ID từ URL là có thể xem thông tin của người dùng bất kỳ.

Giả sử lập trình viên không muốn dùng session ID thì cũng có thể sử dụng access token để chứng thực. Trong method đăng nhập thì access token được trả về nhưng có vẻ như nơi duy nhất nó được tận dụng sau đó chỉ là để đăng xuất người dùng ra khỏi ứng dụng:

3-million-user-accounts-information

Một ví dụ điển hình là hệ thống API của Facebook: https://graph.facebook.com/<ID> (bạn chỉ có thể truy vấn khi truyền vào tham số access_token)

facebook-graph-api

Viết mã khai thác lỗ hổng tự động

Như đã phân tích, hiện tại chúng ta có thể truy vấn thông tin của người dùng bất kỳ dựa theo ID và thực hiện brute force tài khoản dựa theo địa chỉ Email. Tôi tiến hành viết mã khai thác cho chúng bằng Python.

Vét cạn thông tin người dùng của X

Chỉ việc tạo một hàm truy vấn tới https://x-server.com/api/customer/profile/id/[USER_ID] rồi sau đó dùng vòng lặp để quét từ Min tới Max. Vấn đề bây giờ là cần tìm giá trị của Min và Max. Nếu một hacker có thời gian rảnh, anh ta có thể quét từ số 1 cho tới một số ID nào đó thật lớn. Nhưng như thế thì thật lãng phí thời gian cho những truy vấn vào ID nào đó không tồn tại.

Cách tìm cũng không khó, tôi thực hiện dò giống như khi tìm số cột để khai thác lỗi SQL Injections. Đầu tiên tôi thử với ID 10000 (vì X là hệ thống lớn nên tôi đoán số người dùng cao ngay lần đầu). Kết quả trả về mã lỗi 400 với thông báo "Id param is not provided". Chuyện gì vậy nhỉ? Rõ ràng trong URL đã có ID rồi mà.

Ngó lại mã nguồn của ứng dụng Android, trong method callGetApi có đoạn:

3-million-user-accounts-information

Hừm, mỗi truy vấn đều được thêm vào một header "X-Device". Trong tin nhắn gửi cho tôi, T-Rekt có đề cập tới vụ này nhưng lúc code PoC tôi lại quên mất. Lập tức thêm cái header đó vào, tại thời điểm này tôi còn phát hiện ra rằng giá trị của header này không nhất thiết phải là Android hoặc iOS như T-Rekt nói mà có thể đặt giá trị nào cũng được, thậm chí là một chuỗi rỗng.

Các bạn có thể hiểu là tại server X sẽ chỉ kiểm tra xem có header nào tên như vậy hay không chứ chả quan tâm xem giá trị của nó là gì, kiểu như: if (isset($header)) {...}

Tôi thử với các số ID: 10000, 100000, 1000000,... (tăng thêm từng số 0) đều trả về thông tin người dùng. Cho tới 10 triệu thì nhận được mã lỗi 404 và thông báo "The customer does not exist" (tức là ID này không tồn tại). Suy ra giá trị nằm trong khoảng: 1 triệu < Max < 10 triệu.
ro-ri-3-trieu-du-lieu-ca-nhan
Tôi lấy tiếp một giá trị ở khoảng giữa là 5 triệu, vẫn nhận được lỗi 404. Theo kinh nghiệm, tôi còn cẩn thận thay số 0 ở cuối thành 1, tránh trường hợp số chẵn 404 nhưng số lẻ lại tồn tại. Lặp đi lặp lại cho tới khi ra được Max là một số lớn hơn 3000000 người dùng.

Quá trình tìm Min cũng tương tự nhưng áp dụng với một khoảng nhỏ và ra được 1779. Sử dụng vòng lặp để quét toàn bộ:

#!/usr/bin/env python
def main():
  for uid in range(1779, 3000000):
    scan(uid)

Brute force tài khoản người dùng X

Đây là hình thức tấn công bằng cách tạo ra các mật khẩu ngẫu nhiên rồi đăng nhập thử, lặp đi lặp lại cho tới khi có một mật khẩu đăng nhập thành công. Với những thông tin đã phân tích, chúng ta đã biết endpoint để đăng nhập:

https://x-server.com/api/customer/login

Tham số để đăng nhập là địa chỉ email và mật khẩu. Email thì chúng ta có được từ phần vét cạn thông tin phía trên, cứ chọn ra một email bất kỳ thôi. Hoặc nếu có thời gian và một máy chủ ảo (VPS) để treo thì có thể kết hợp cả 2 hình thức tấn công cùng lúc: Dùng vòng lặp với User ID để quét ra được email > gọi tới hàm brute force với tham số là địa chỉ email đó. Thế là hacker có thể lấy đầy đủ thông tin cá nhân lẫn mật khẩu (dò ra thành công) của hơn 3 triệu tài khoản.

Timeline

19/09/2016 2:12 PMLỗ hổng được báo cáo tới X.
25/09/2016 9:07 PMDo không nhận được phản hồi nào, tôi quyết định tạo ra trang web sử dụng API của chính X để giúp mọi người kiểm tra xem họ có nằm trong số 3 triệu người có thể bị rò rỉ dữ liệu cá nhân hay không.
26/09/2016 11:18 AMGenK đăng bài viết chia sẻ về vụ rò rỉ dữ liệu cá nhân, kéo theo một loạt trang báo khác cùng đưa tin.
26/09/2016 11:03 PMTôi gửi báo cáo lần thứ hai.
27/09/2016 10:31 AMPhía X phản hồi lại và xin thông tin liên lạc để trao đổi trực tiếp với tôi.
27/09/2016 5:25 PMTôi cung cấp thông tin liên lạc như đề nghị từ X.
27/09/2016 10:09 PMĐại diện của X liên hệ với tôi. Tôi chia sẻ toàn bộ quá trình team phát hiện và phân tích lỗ hổng. Đại diện phía X ngỏ lời muốn tặng một phần thưởng cho chúng tôi (tất nhiên là chúng tôi không từ chối).
28/09/2016 11:10 PMTôi báo cáo thêm một bug nhỏ trên trang web của X. Tại thời điểm này lỗ hổng xác thực trong API của X đã được khắc phục.

Lời cảm ơn

Nhờ tác động từ phía truyền thông mà X đã có phản hồi lại báo cáo của chúng tôi sớm hơn, do đó tôi xin được gửi lời cảm ơn đến GenKPhapLuatDoiSongVitalkDaiKyNguyenSchannel và nhiều trang tin tức khác đã đưa tin về vụ việc rò rỉ dữ liệu nghiêm trọng này!
standee123

Làm thế nào để đi ngủ sớm?

sleep

Wow, ngạc nhiên chưa? Một đứa chuyên ngủ muộn viết bài về việc ngủ sớm, haha. Nhưng đúng là tớ, Siin đây. Lâu rồi mới viết blog, hehe!!

Khoảng 3, 4 tuần gần đây tớ bắt đầu đi ngủ muộn hơn... Thường thì vào 3-4 giờ sáng, có hôm là 5 giờ. Trước đây tớ cũng không ngủ sớm nhưng cũng không đến mức muộn như vậy. Và điều tệ nhất là việc ngủ muộn đang bắt đầu ảnh hưởng tới hiệu quả làm việc của cả ngày hôm đó.

Việc tự nói với bản thân "Tôi sẽ đi ngủ sớm!!" có vẻ như không có tác dụng, vậy nên tớ nghĩ mình cần "mạnh tay" hơn... Đúng rồi, xách bàn phím lên và code nào!

coding

Xác định nguyên nhân gây ngủ muộn


Hừm, con trai + cái máy tính thì làm gì mà ngủ muộn nhỉ?

- Xem JAV chứ làm gì!!?

Hầy, toàn nghĩ bậy không à =.=

Sau khi tự theo dõi bản thân thì tớ thấy có hai thứ khiến tớ thức vào khoảng giờ đó:

Code

Có điều chắc chắn là không phải riêng gì tớ mà nhiều bạn dev khác cũng nhận thấy việc code vào ban đêm sẽ tập trung hơn và (thường) có hiệu quả tốt hơn do môi trường làm việc yên tĩnh và thời điểm đó thì thường không có ai quấy rầy bạn. Đó cũng là lý do mà chúng ta hay bị gọi là đám "cú đêm".

lap-trinh-vien

Xây dựng chương trình "chống ngủ muộn"


Đã xác định được nguyên nhân rồi thì bắt tay vào code thôi! Tớ sẽ dùng AutoIt vì chương trình này sẽ không quá phức tạp, một ngôn ngữ kịch bản như AutoIt hay AutoHotkey là quá đủ.

Chương trình sẽ bao gồm một vòng lặp với 4 hàm chính như sau:

Diagram

checkTime() sẽ đảm nhận nhiệm vụ kiểm tra xem thời gian hiện tại có thuộc khung giờ mà tớ cần phải... đang nằm ngủ hay không.

Func checkTime()
 Local $h = @HOUR
 Return $h > 0 And $h < 7
EndFunc   ;==>checkTime

Nếu trong khung giờ này, chương trình sẽ gọi tới hàm closeApps() và kiểm tra qua toàn bộ danh sách ứng dụng mà tớ đã khai báo, nếu ứng dụng nào đang mở thì đóng ứng dụng đó.

Func closeApps()
 For $app In $apps
  If WinExists($app) Then WinClose($app)
 Next
EndFunc   ;==>closeApps

Để chắc chắn các ứng dụng này đã ngừng hoạt động, chương trình gọi tiếp hàm closeProcesses(), tìm và đóng các tiến trình của ứng dụng (nếu vẫn đang chạy).

Func closeProcesses()
 For $process In $processes
  _ProcessCloseEx($process)
 Next
EndFunc   ;==>closeProcesses

Khi đã đảm bảo là không còn gì cản trở, chương trình gọi hàm shutdown() để thực hiện tắt máy tính.

Shutdown(5)

Tham số 5 mà tớ truyền vào là sự kết hợp của 2 flag:

  • 1: sử dụng chế độ tắt máy.
  • 4: ép toàn bộ ứng dụng đang chạy phải đóng để không ảnh hưởng tới quá trình tắt máy.

Các bạn sẽ nhận ra việc gọi shutdown(5) hoàn toàn tương đương với việc chạy lệnh sau trong Command Prompt (CMD) của Windows:

shutdown -s -f

Cuối cùng, một hàm main() để liên kết toàn bộ các hàm trên với nhau:

Func main()
 If checkTime() Then
  closeApps()
  Sleep(10000)
  closeProcesses()
  Sleep(5000)
  Shutdown(5) ; Shudown + Force
 EndIf
EndFunc   ;==>main

Khoan đã... Tại sao lại phải dùng những 2 hàm khác nhau với cùng một mục đích là đóng ứng dụng?

Lý do là hàm WinClose() sẽ đóng cửa sổ đúng như cách mà chúng ta nhấn vào nút X ở góc trên cùng bên phải cửa sổ. Đây là cách đóng ứng dụng an toàn, tuy nhiên nếu trong trường hợp người dùng đang viết lách hoặc sử dụng một trang web nào đó chưa lưu xong dữ liệu thì cái hộp thoại xác nhận này sẽ hiện lên:

xac-nhan-thoat


Với 50% khả năng người dùng sẽ nhấn nút "Ở lại" thì chúng ta cần phải kiểm tra lại thêm lần nữa bằng cách đóng tiến trình với hàm _ProcessCloseEx(). Đóng tiến trình là cách tắt ứng dụng một cách ép buộc, nó tương tự với việc chúng ta sử dụng Task Manager để tắt mấy cái ứng dụng bị treo vậy.

Và lưu ý đây là cách tắt ứng dụng không an toàn nên dữ liệu có thể bị mất trong trường hợp người dùng chưa kịp lưu. Đây cũng là một lý do mà vì sao hàm này nên được sử dụng sau WinClose() như một biện pháp dự phòng.

Có vẻ ổn rồi, nhưng tớ cần đăng ký chương trình vào Startup của Windows, điều đó sẽ giúp chương trình tự hoạt động hằng ngày mà mình không cần phải mở thủ công. Có hai cách để thực hiện điều này:

  • Copy file thực thi của chương trình vào thư mục Startup, thường nằm ở đường dẫn: C:\Users\<NAME>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\
  • Đăng ký đường dẫn tới file thực thi của chương trình vào Registry: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run

Thật may, thành viên guinness từ diễn đàn AutoIt đã viết sẵn một UDF sử dụng cả 2 cách trên và chúng ta chỉ việc đơn giản là gọi hàm ra: _StartupFolder_Install() hoặc _StartupRegistry_Install()

Và đây là toàn bộ code của chương trình:

#Region AutoIt3Wrapper directives section
#AutoIt3Wrapper_Icon=siin.ico
#AutoIt3Wrapper_Compression=4
#AutoIt3Wrapper_UseUpx=Y
#AutoIt3Wrapper_Res_Comment=Developed by Siin
#AutoIt3Wrapper_Res_Description=Developed by Siin
#AutoIt3Wrapper_Res_Fileversion=1.0.0.0
#AutoIt3Wrapper_Res_FileVersion_AutoIncrement=Y
#AutoIt3Wrapper_Res_ProductVersion=1.0.0.0
#AutoIt3Wrapper_Res_LegalCopyright=(C) 2017 Siin. All rights reserved.
#AutoIt3Wrapper_Res_Field=CompanyName|Siin Blog
#AutoIt3Wrapper_Res_Field=Website|https://siindz.blogspot.com/
#EndRegion AutoIt3Wrapper directives section

#NoTrayIcon
#include <Misc.au3>
#include '_Startup.au3'

Opt('MustDeclareVars', 1)
Opt('WinTitleMatchMode', 2)

_Singleton(@ScriptName)
_StartupRegistry_Install()

Global $apps = ['Google Chrome', 'Sublime Text']
Global $processes = ['chrome.exe', 'sublime_text.exe']

While 1
 Sleep(60000)
 main()
WEnd

Func main()
 If checkTime() Then
  closeApps()
  Sleep(10000)
  closeProcesses()
  Sleep(5000)
  Shutdown(5) ; Shudown + Force
 EndIf
EndFunc   ;==>main

Func checkTime()
 Local $h = @HOUR
 Return $h > 0 And $h < 7
EndFunc   ;==>checkTime

Func _ProcessCloseEx($sPID) ; Author: rasim
 If IsString($sPID) Then $sPID = ProcessExists($sPID)
 If Not $sPID Then Return SetError(1, 0, 0)

 Return RunWait(@ComSpec & ' /c taskkill /F /PID ' & $sPID & ' /T', @SystemDir, @SW_HIDE)
EndFunc   ;==>_ProcessCloseEx

Func closeApps()
 For $app In $apps
  If WinExists($app) Then WinClose($app)
 Next
EndFunc   ;==>closeApps

Func closeProcesses()
 For $process In $processes
  _ProcessCloseEx($process)
 Next
EndFunc   ;==>closeProcesses

Tổng kết


Vì dùng cho mục đích cá nhân nên hiện tại chương trình này khá đơn giản. Sau đây là một số gợi ý cho bạn nào muốn phát triển nó:

  • Tạo thêm GUI (giao diện người dùng) cho phép dễ dàng chọn mốc thời gian đi ngủ cũng như những chương trình nào cần tắt.
  • (*) So sánh với thời gian từ một máy chủ khác thay vì dùng Macro của AutoIt để tránh người dùng thay đổi giờ  trên máy tính > ảnh hưởng tới kết quả chương trình.
  • Phát một bản nhạc gây buồn ngủ khi gần tới giờ chẳng hạn :v
  • Tự động chặn kết nối tới Facebook, Youtube,... sau 12 giờ đêm và mở lại vào sáng hôm sau.
  • ... (tùy sức sáng tạo)

(*) Có lẽ bạn sẽ thắc mắc tại sao tớ không làm luôn phần kiểm tra thời gian? Vì đơn giản với một chương trình được tạo ra để cải thiện bản thân thì nếu mình đã không thích thì sẽ không chạy nó ngay từ đầu chứ không cần phải cheat.

Không biết là chương trình này sẽ có tác dụng hay không, tớ sẽ cập nhật kết quả sau vài hôm nữa. Dù sao thì cũng hi vọng là bài viết này có ích đối với các bạn đang muốn học AutoIT và muốn thử sức tạo ra một vài chương trình nho nhỏ hữu ích cho bản thân.