Cũng hơn một năm rồi kể từ khi post pick-lock Yasuo của mình ra mắt, đúng là dạy hư bọn trẻ khi mà đi đâu cũng thấy sử dụng API tràn lan. May mà Garena cũng im lặng ngắm nhìn 😄

Nhớ hồi tháng 8 năm ngoái thì có một bạn share league-profile-tool, cái này chủ yếu dùng để mod hình ảnh trên League Client, thực hiện request đến LCU và còn một tính năng nữa… đó là boost skin miễn phí trong chế độ ARAM.

[?] Boost skin (tăng điểm/bonus) trong ARAM có giá 30 RP, giúp cả team mở khóa skin ngẫu nhiên của tướng đã pick, mỗi người mỗi khác skin khi swap tướng. Kết thúc trận thì người boost sẽ nhận thêm 200 THL.

Lúc ấy, mình cố đào source của tool ra thì thấy người ta dùng API này:

POST /lol-champ-select/v1/team-boost/purchase

Thử đi thử lại nhiều lần nhưng vẫn fail, tức không boost được skin trong ARAM. Trong khi đó thì một số tool tương tự vẫn hoạt động bình thường. Mình cũng không chắc là sai chỗ nào nữa 😅

Về cơ bản thì cái nút BOOST (Tăng điểm) chỉ có thể click được khi bạn có ít nhất 40 RP, ta có thể mô tả bằng đoạn JS sau:

RPHandler.on('change', (value) => {
    $('#btn-boost').prop('disabled', value < 40)
})

$('#btn-boost').on('click', () => {
    if (RPHandler.getValue() >= 40) {
        RPHandler.pay(40)
        ARAM.boost()
    }
    else alert("Mày nghèo lắm con trai ạ!")
})
  • RPHandler để tương tác với RP, quản lý các listener
  • Dùng jQuery cho dể biểu diễn, btn-boost sẽ là ID của nút BOOST
  • Cái thông báo khi fail phải là…

Vậy là chỉ cần gọi ARAM.boost() là có ngay skin mà không tốn một RP nào cả.

Theo mình đoán thì patch 11.2 có nhiều update cân bằng ARAM, nên có thể Riot sẽ fix bug này.

Nhưng thực tế thì đến tận patch 11.4, vì patch 11.3 có chế độ ARURF và có thể boost skin tương tự như ARAM, còn lại chắc bạn cũng biết rồi đấy.

Cái gì đến cũng sẽ đến thôi, và rồi API ở trên cũng không còn dùng được nữa 😥

ARAM.boost = function() {
    if (RPHandler.getValue() < 40) return
    // ...
}

Trong FAQ của league-profile-tool cũng có ghi rõ:

Q: Where did the ARAM boost go?
A: The old method got patched, there are other new methods out there. It will not be included in this program.

Đúng là có cách hiệu quả hơn nữa đấy 😄

Nghiên cứu

Đầu tiên hãy nhìn response của cái API trên:

POST /lol-champ-select/v1/team-boost/purchase
{
  "errorCode":"RPC_ERROR",
  "httpStatus":500,
  "implementationDetails":{},
  "message":"Error response for POST /lol-lobby-team-builder/champ-select/v1/team-boost/purchase: Unable to purchase team boost"
}

Phần endpoint trong message trả về rõ là khác với API. Cũng không khó hiểu đâu, plugin lol-lobby-team-builder mới chính là base trong việc tạo team đến chọn tướng trước khi vào trận, nên mọi endpoint liên quan đều đẩy hết vào nó.

POST /lol-lobby-team-builder/champ-select/v1/team-boost/purchase

Trước patch 11.6 thì message trả về không giống như ở trên đâu:

"Error response for POST /lol-lobby-team-builder/champ-select/v1/team-boost/purchase: Unable to purchase team boost: Received status Error: INVALID_STATE instead of expected status of OK from request to teambuilder-draft:activateBattleBoostV1"

Vậy là API ở trên vẫn chưa phải là endpoint cuối cùng, nhưng mà teambuilder-draft:activateBattleBoostV1 thì chả giống REST API gì cả.

Đến đây mình thực sự không biết nên làm gì nữa, thế là quyết định dùng IDA để phân tích League Client.

Và đây mới chính là API cuối cùng để kích hoạt boost.

Nhưng đó chỉ là string và mình không có thời gian để tìm address của nó, với lại method mà không có this thì khó mà invoke nên mình tạm bỏ qua việc call trực tiếp. Hàm C++ này thực chất là một phần trong LCU core.

const char *__cdecl class Riot::MaybeType<class Riot::Failure,struct Riot::Client::ActiveBoosts::LCDS::SummonerActiveBoostsDTO>(void)

Hãy nhìn chữ LCDS, nó là viết tất của "LifeCycle Data Services", là một service proxy trong League Client cũ vẫn còn được sử dụng. Để giao tiếp được với LCDS phải thông qua giao thức RTMP, nó không giống như việc bắn request vào microservice thông qua HTTP.

Thật may mắn khi một issue mà mình từng comment có xuất hiện từ khóa này:

"Couldn't change lobby: Error creating lobby: Error response for POST /lol-login/v1/session/invoke: LCDS invoke to gameService.createPracticeGame failed…"

Dùng API này có thể invoke vào các service proxy, bao gồm cả LCDS.

POST /lol-login/v1/session/invoke

API này có 3 param như sau:

  • destination: tên của service proxy
  • method: tên phương thức cần invoke
  • args: tham số truyền vào dưới dạng JSON

Nhưng mà invoke kiểu gì? Chẳng có một tí dữ liệu nào hết.

Sau một hồi tìm kiếm thì có ngay document thần thánh 😀

Được biết thì cái docs này sinh ra từ việc dịch ngược mã nguồn client cũ.

Trong này có nhiều method, nhưng theo logic thì ta cần gọi một hàm để boost, vậy là mình chọn call.

Docs cũng có ghi rõ call cần 4 tham số truyền vào…

Tham số #1 là UUID của một sảnh chờ (lobby), được tạo ra từ
com.riotgames.platform.common.utils.GUID, đố bạn namespace này của ngôn ngữ nào?

Là ActionScript nhé, không phải Java đâu 😅

Theo như một số API khác thì có thể bỏ qua tham số này, hệ thống sẽ lấy UUID của lobby hiện tại. Nhưng theo docs thì phải là chuỗi nên mình cho chuỗi rỗng luôn.

Tiếp theo thì hai tham số #2#3 lần lượt là game modeprocedure call, có sẵn luôn đây này:

  • teambuilder-draft
  • activateBattleBoostV1

Và cuối cùng là tham số #4 là một chuỗi chứa tham số khi call, theo hàm C++ lúc nãy thì có một void bên trong, tức là không có tham số nào, vậy ta gán chuỗi rỗng.

Ta được API hoàn chỉnh như sau:

POST /lol-login/v1/session/invoke
  destination=lcdsServiceProxy
  method=call
  args=["","teambuilder-draft","activateBattleBoostV1",""]

Tại đây, ta có thể dùng curl để request, nhưng phải có port và auth token (pass).

$ curl -k -g -X POST -u riot:PASS \
  https://127.0.0.1:PORT/lol-login/v1/session/invoke \
  -d destination=lcdsServiceProxy
  -d method=call
  -d args=["","teambuilder-draft","activateBattleBoostV1",""]
  • -k để bật mode insecure
  • -g để có thể dùng dấu ngoặc trong URL

Thường thì curl có sẵn trong Windows, được build bằng MSVC. Còn những bản build bằng MingGW hay Cygwin khi request đều sẽ fail.

Bước tiếp theo là viết script để gom mọi thứ vào một.

Triển khai

Năm nay thì đổi style mới đơn giản hơn, thay vì dùng JS trên trình duyệt web thì mình chơi hẳn Batch script luôn. Với lại tương tác trực tiếp trên cmd sẽ rất tiện.

Đầu tiên là tạo một file có tên aram-booster.bat

@echo off & setlocal

endlocal

Mình sẽ dùng cách cũ để lấy port và auth token:

$ WMIC PROCESS WHERE name='LeagueClientUx.exe' GET commandline

Output của WMIC chứa toàn bộ commandline và vài dòng thừa, vì vậy phải tách ra và lưu nó vào biến.

@echo off & setlocal

for /f "tokens=* USEBACKQ" %%a in (
    `"WMIC PROCESS WHERE name='LeagueClientUx.exe' GET commandline | findstr ."`
) do set cmdline=%%a
  • USEBACKQ để for có thể đọc cả khoảng trắng và xuống dòng
  • Kết hợp findstr . để gom hết ouput của WMIC thành một

Khi for chạy sẽ đọc qua từng dòng, khi đó dòng cuối cùng chứa commandline sẽ được lưu vào biến cmdline.

Tiếp theo là tách port và auth token:

) do set cmdline=%%a

for %%a in (%cmdline%) do (
    for /f "tokens=1,2 delims==" %%i in (%%a) do (
        if "%%i"=="--app-port" set "port=%%j"
        if "%%i"=="--remoting-auth-token" set "pass=%%j"
    )
)
  • For thứ nhất sẽ đọc qua từng argument
  • For thứ hai sẽ split argument
  • Hai dòng if để lấy port và pass

Trong trường hợp League Client chưa được mở sẵn hoặc script không được cấp quyền Admin thì biến port chắc chắn chưa được định nghĩa, ta sẽ báo lỗi:

if not defined port goto :err
endlocal

Và label :err

:err
echo Failed to boost!
echo Please make sure League Client is opened,
echo    and run script again as Administrator.
endlocal

Mình thêm một label :end để dừng thoát và kết thúc

:end
timeout /t 5 /nobreak
endlocal

Khi request thành công, response trả về JSON có dạng:

{"body": ... }

Mình sẽ cho curl vào for để lấy response:

for /f "tokens=* USEBACKQ" %%a in (
    `curl -s -g -k -X POST -u riot:%pass% ^
        https://127.0.0.1:%port%/lol-login/v1/session/invoke ^
        -d "destination=lcdsServiceProxy" ^
        -d "method=call" ^
        -d "args=["""",""teambuilder-draft"",""activateBattleBoostV1"",""""]"`
) do set "resp=%%a"

:err
  • -s để bật silent mode, curl chỉ trả về duy nhất response
  • Response sẽ được lưu trong biến resp

Tiến hành kiểm tra response và hiện thông báo:

) do set "resp=%%a"

set success={"body"
if defined %resp% (
    if "%resp:~0,7%"=="%success%" (
        color A
        echo Your game is BOOSTED!
        goto :end
    )
)
  • Lệnh if ngoài sẽ kiểm tra biến resp được định nghĩa hay chưa
  • Lệnh if bên trong kiểm tra response
  • Biến success chứa pattern của response hợp lệ, do không dùng được dấu nháy kép trong if nên mình tách nó ra
  • Nếu thành công thì ta nhảy đến end luôn

Giờ thì tạo một trận ARAM và test thử thôi nào!

Bạn có skin và được 200 BE, còn cả team đều vui 😄

Bonus

Sau khi xong viết xong cái aram-booster này bằng Batch thì mình cũng có implement nó bằng vài ngôn ngữ khác. Bạn xem tại đây nhé: nomi-san/aram-booster.

Mình check trên Github thì thấy có nhiều repo dùng API tương tự, nhưng tất cả đều…

Thật ra họ không cần dài dòng như mình, chỉ cần dùng tool frostycpu/FinalesFunkeln và bỏ ra 40RP để boost là bắt được API call thôi 😅

FAQ

Có bị ban không?

Chắc chắn không, nhưng với Garena thì… bạn biết rồi đấy.

Có dùng được vĩnh viễn không?

Nếu có thì mình là người tốt, còn không thì là người xấu chắc rồi 😅