Tiếp cận reverse engineering javascript - Case study: Chatwork
Bài đăng này đã không được cập nhật trong 5 năm
Giới thiệu
Javascript là thành phần không thể thiếu của bất kỳ một web app hiện đại nào, đặc biệt là các web app ở dạng SPA (Single Page Application). Việc hiểu được flow thực hiện cũng như các hàm của client sẽ giúp ta có cái nhìn sâu sắc hơn cho về app. Tuy nhiên, với sự phát triển của các công cụ bundle, uglify, minify khiến việc đọc code js trở thành cơn ác mộng. Cho dù các browser đã có trang bị những công cụ debugger mạnh mẽ nhưng vẫn còn những hạn chế. Trong bài viết này, mình xin phép trình bày các hướng tiếp cận của mình với một case study cụ thể là Chatwork.
Bài toán
Như những người dùng của Chat++ for Chatwork, một extension vô cùng hữu ích giúp nâng cao hiệu quả công việc, đã biết, chúng ta có thể dùng phím S
để scroll xuống cuối box và đọc toàn bộ tin nhắn. Liệu có cách nào chúng ta có thể đọc toàn bộ box mà không cần phải scroll xuống cuối ko?. Cùng nghiên cứu nào #challenge_accepted
Javascript của Chatwork
- Frontend của Chatwork được viết (lại) bằng
React
. - Tất cả js của Chatwork được bundle lại vào thành một file duy nhất ở địa chỉ: https://front.chatwork.com/biwa/all/app/index.js.
- Javascript được bundle bằng Webpack ?, nghĩa là tất cả sẽ được đưa thành các
ESModule
và được load vào. Tuy nhiên, Chatwork vẫn có gán một số object global nhưCW
(object chính của app ),CS
,RM
(object của room hiện tại mà ta đang làm việc),... gán vàowindow
, điều này cho phép chúng ta thao tác được với app Chatwork từ javascript.
Các bước tiếp cận
Làm đẹp code JS
Bước đầu tiên chúng ta cần tải file JS to tổ chảng kia về, dùng các tool, ví dụ Prettier để format code dễ nhìn hơn. Phần lớn thì tên biến đã được rút ngắn, nhưng về cơ bản thì phần lớn vẫn giữ nguyên và có comment ở đầu mỗi thư viện, nên ta vẫn đọc được. Sau khi format thì được một file dài khoảng 127K+ dòng
Load file JS local thay vì file từ server
Bước này chúng ta sẽ điều hướng browser để thay vì load file js từ địa chỉ https://front.chatwork.com/biwa/all/app/index.js
thì load file local, khi đó, logic chương trình vẫn sẽ hoàn toàn giống như cũ, nhưng file js giờ đã do ta quản lý, ta có thể:
- Thêm
debugger
để breakpoint debug script (cái này thì Chrome cũng đã hỗ trợ sẵn) - Thêm
console.log
tuỳ ý - Chỉnh sửa logic, hardcode giá trị thoải con gà mái
Để làm được việc này ta cần 2 thứ:
- Server chạy HTTPS để host file JS ở local
- Công cụ giúp tự động điều hướng link JS trên trình duyệt về server local
1. Server chạy HTTPS để host file JS ở local
Tại sao phải là HTTPS? vì chatwork chạy qua HTTPS, nếu ta link đến một file JS thông qua HTTP sẽ dẫn đến lỗi mix-content
trên Chrome. Ngoài ra, ,may mắn cho chúng ta, Chatwork không có CSP Header (Content Security Policy, bạn có thể đọc thêm tại đây) nên ta có thể load JS từ source bất kỳ. Nếu có thì mọi việc sẽ trở lên rắc rối hơn rất nhiều
Mình dùng một script python đơn giản để chạy https server:
#!/usr/bin/env python3
# Ported to Python 3 by Telmo "Trooper" (telmo.trooper@gmail.com)
#
# Original code from:
# http://www.piware.de/2011/01/creating-an-https-server-in-python/
# https://gist.github.com/dergachev/7028596
#
# To generate a certificate use:
# openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl
separator = "-" * 80
port = 4443
httpd = HTTPServer(("", port), SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket(
httpd.socket,
keyfile="/Users/nguyen.anh.tien/lab/dev_tools/local-cert-generator/server.key",
certfile="/Users/nguyen.anh.tien/lab/dev_tools/local-cert-generator/server.crt",
server_side=True
)
print(separator)
print("Server running on https://localhost:" + str(port))
print(separator)
httpd.serve_forever()
Certificate thì ta có thể tự gen hoặc follow bài viết https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/ để có thể chạy https trên localhost.
Đi vào thư mục chứa file js chúng ta đã lưu và chạy script lên chúng ta được:
Server running on https://localhost:4443
Ở đây mình chạy self-sign cert nên cần vào sẵn localhost và accept việc trust cert, nếu không ta sẽ bị lỗi CERT_INVALD
khi truy cập link.
2. Tự động điều hướng link JS trên trình duyệt
Để làm việc này thì có rất nhiều cách và tool như Fiddler, Burp Suite, nhưng để cho đơn giản, mình dùng luôn một extension cho Chrome là: Requestly cho phép chỉnh sửa các request từ trình duyệt. Chúng ta thêm một rule như sau và save lại:
Giờ thì reload lại trình duyệt và kiểm tra (Nhớ chọn sẵn Disable cache để ):
vậy là đã tráo hàng thành công . Giờ ta có thể thêm debug thoải mái. Ví dụ, với webpack, thì tất cả các module sẽ được đưa vào một mảng rồi đưa vào hàng dưới.
Ở đây t
sẽ là object chứa toàn bộ các module, và hàm n
sẽ tương đương với require
. o
là index của module (ở bản build development, o
sẽ là tên của module, còn ở build production, o
là số). Thêm dòng debug như trên ta có thể check thử:
Nghiên cứu tính năng read của Chatwork
Bước tiếp theo là nghiên cứu cách Chatwork xử lý việc đọc tin nhắn như thế nào. Ta bật network lên và vào thử một room bất kỳ rồi đọc. Khi đó thấy có request:
vớ tham số:
myid: 657237
_v: 1.80a
_av: 5
ln: ja
room_id: 150077264
mid: 328590021
last_chat_id: 1242011046768562176
unread: 0
vậy là Chatwork sẽ gửi một POST
request để cập nhật thông tin. Ta chú ý đến last_chat_id
, đây chính là id của chat message cuối cùng mà chúng ta đã đọc. Vậy nếu thay đổi id này thành id cuối cùng của box, ta sẽ giải quyết được bài toán ?
Tìm kiếm thử các từ khoá read
, readAll
, post
, last_chat_id
, thấy có một số đoạn post("read",
và dẫn ta đến hai hàm khả nghi:
và
Test thử thì hàm số 1 chính cái chúng ta cần tìm.
vậy ta có thể chạy đoạn update script kiểu kiểu:
abc.post("read", {room_id: xxx, mid: yyy, last_chat_id: zzz, unread: 0}, {});
nếu như abc
được expose ra global, giờ ta cần tìm thử xem abc
là gì và có được expose ra để cho ta dùng hay không?
Đào bới tiếp trong "biển code" thì phát hiện class này được extend ra từ APIService
và có tên readAPIService
. Có thể thêm console.trace();
để sinh ra stack call đến hàm được gọi.
Cuối cùng, kiểm tra lại object global CW
thì cái chúng ta cần tìm đây rồi:
Payload cuối cùng
CW.application.domainAPIServiceContext.readAPIService.post(
"read", {
room_id: RM.id,
mid: RM.mid,
last_chat_id: RM.timeline.chat_list[RM.timeline.chat_list.length-1].id,
unread: 0
},
{}
);
Với RM
là của room hiện tại, đồng thời ta chũng lấy ra được id
và mid
cũng như chat message id cuối cùng của room để đọc, Hãy vào room có hơn 1000 message, bật F12 lên, paste đoạn kia vào xem có điều kỳ diệu nào ko nhé . Hi vọng các Chat++ Contributor sớm đưa tính năng này vào bản cập nhật tiếp theo
Tổng kết
Với cách này ta có thể debug được file js và giải quyết được bài toán. Tuy nhiên vẫn còn vài vấn đề:
- Với những script được split thành các chunk sẽ rắc rối hơn
- Việc tìm hiếm property của của object đang khá mất công, ta gặp may vì nó ko nesting nhiều lắm. Cần có công cụ giúp chúng ta duyệt qua tất cả các thuộc tính cũng như
__proto__
để tìm kiếm dễ dàng hơn - Giữa rất nhiều module như vậy, việc phân biệt giữa code thư viện và code thực sự của app là không dễ
- Xử lý cho trường hợp có header CSP
Mình vẫn đang tìm hiểu về vấn đề này, hi vọng sẽ có tiếp những tips hay ho để chia sẻ với mọi người. See ya!
All rights reserved