Реверсинг Android клієнта музичного сервісу Zaycev.net та імплементація api на go

Строго кажучи, до реверсингу дану статтю можна віднести тільки з натяжкою.
Всім вам знайомий такий сервіс як zaycev.net. Не помилюся, припустивши, що кожен хоч раз качав з нього музику, або через web-інтерфейс, або через мобільний додаток.
Якщо вам все ж цікаво, ласкаво просимо під кат.
Частина перша. Розбір польотів
Одного разу один мій хороший знайомий попросив розібратися як працює їх офіційний клієнт під Android. Скачавши клієнт, я приступив до вивчення і завантажив піддослідного в Jadx (Dex to Java decompiler). Всі посилання в кінці статті.
Перше, що кидається у вічі — наявність обфускации:

Ну не біда, прорвемося, не вперше ж. Побіжний огляд показав, що потрібний нам функціонал зосереджений в пакеті:
package free.zaycev.net.api;
Оригінальний Код авторизації:
public synchronized String b() {
String str;
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new IllegalThreadStateException("Method must run in not main thread!");
} else if (ae.b(ZaycevApp.a.y())) {
String str2 = "";
str2 = "";
str2 = "";
try {
str = (String) new JSONObject(g.a("https://api.zaycev.net/external/hello", false)).get("token");
if (ZaycevApp.W().equals("4pda")) {
str2 = str + "kmskoNkYHDnl3ol2";
a.a();
} else {
str2 = str + "kmskoNkYHDnl3ol2";
}
h.a("ZAuth", "easy " + str2);
str2 = a(str2);
str = new JSONObject(g.a(String.format("https://api.zaycev.net/external/auth?code=%s&hash=%s", new Object[]{str, str2}), false)).getString("token");
if (!ae.b((CharSequence) str)) {
ZaycevApp.a.e(str);
}
} catch (Exception e) {
}
str = "";
} else {
str = ZaycevApp.a.y();
}
return str;
}

private String a(String str) {
try {
MessageDigest instance = MessageDigest.getInstance("MD5");
instance.update(str.getBytes());
byte[] digest = instance.digest();
StringBuffer stringBuffer = new StringBuffer();
for (byte b : digest) {
String toHexString = Integer.toHexString(b & 255);
while (toHexString.length() < 2) {
toHexString = "0" + toHexString;
}
stringBuffer.append(toHexString);
}
return stringBuffer.toString();
} catch (Exception e) {
h.a((Object) this, e);
return "";
}
}

Як зрозуміло з коду, порядок запитів до сервісу такий:
Привітання, отримання Hello token:
https://api.zaycev.net/external/hello
На що сервер відповідає json об'єктом:
{
"token":"I-fte8MSfXjw8bYFQkcq629iB6uLb5thZSoj3rgvlcpg4zjzpgbfpylrtldpw7l_qq2ebeubimva7buwkwils8iwug3cwgwj8scmdiu5i8m"
}

Обчислення hash:
hash = md5(helloToken + "kmskoNkYHDnl3ol2")
Забігаючи вперед, скажу, що константа, зашита в програму (kmskoNkYHDnl3ol2), змінюється від версії до версії, на даний момент мені зустрічалися 3 різних константи:
android: "60kQwLlpV3jv", "kmskoNkYHDnl3ol2"
ios: "d7DVaaELv"
Автентифікація, отримання Access Token:
https://api.zaycev.net/external/auth?code=%s&hash=%s
На що сервер відповідає json об'єктом:
{
"token":"wnfQgLZoLErwL6g_axTTTUkCcobXGLMRzs75zozr3oc05kwnfd07bngjpg2vry2ggxypacpqsgarqki6yu278zo6xjp4rldnqzmqhfwv-25iH8M_R6rSna2CmnP5OuwgTuUundxiTwqi2am5rha2gbu8kbb9ya0grj1mhhq_mpksw3r49fm4vbdd6vynnuwykibwmxzxvhrbhj2dmikjkw"
}

Перевіряємо працездатність:
curl -X "GET" "https://api.zaycev.net/external/track/1310964?access_token=wnfQgLZoLErwL6g_axTTTUkCcobXGLMRZS75Zozr3oC05kWNfd07Bngjpg2VRY2GgXYPaCPqSGarqki6YU278ZO6XJP4RLdNqZMqHFwv-25iH8M_R6rSna2CmnP5OuwgTuUundxiTWqI2Am5rHA2gbU8kbB9Ya0gRJ1mHhq_MpksW3R49Fm4VBDd6vYnNUWykibWmxzxvhRBhJ2dmiKJkw"

JSON-Response:
{
"track": {
"name": "Sharp Dressed Man",
"bitrate": 128,
"duration": 258,
"size": 4.08,
"created": 1333340577000,
"userId": 2750888,
"userName": "zver19",
"artistId": 272997,
"artistName": "ZZTop",
"lyrics": {},
"lyricAuthor": [],
"musicAuthor": [],
"rightPossessors": [
{
"url": "http://zaycev.net/legal/reriby",
"name": "nETB",
"pictureUrl": "http://cdnimg.zaycev.net/rp/logo/29/2954-35447.png"
}
],
"artistImageUrlSquare100": "http://cdnimg.zaycev.net/artist/2729/272997-52076.jpg",
"artistImageUrlSquare250": "http://cdnimg.zaycev.net/artist/2729/272997-86370.jpg",
"artistImageUrlTop917": null
},
"rating": 0.0,
"rbtUrl": ""
}

Auth token — тимчасовий, валиден приблизно добу після чого потрібно запитувати знову.
Виконайте ці прості дії, ми отримали Auth маркер, який нам буде потрібно для виконання запитів до сервера сервісу. Час приступати до пошуку запитів, які використовуються програмою.
Текстовий пошук по "https://api.zaycev.net" видав список всіх запитів.
Список API-запитів:
"https://api.zaycev.net/external/hello"
"https://api.zaycev.net/external/auth?code=%s&hash=%s"
"https://api.zaycev.net/external/search?query=%s&page=%s&type=%s&sort=%s&style=%s&access_token=%s"
"https://api.zaycev.net/external/autocomplete?access_token=%s&code%s"
"https://api.zaycev.net/external/top?page=%s&access_token=%s"
"https://api.zaycev.net/external/musicset/list?page=%s&access_token=%s"
"https://api.zaycev.net/external/musicset/detail?id=%s&access_token=%s"
"https://api.zaycev.net/external/genre?genre=%s&page=%s&access_token=%s"
"https://api.zaycev.net/external/artist/%d?access_token=%s"
"https://api.zaycev.net/external/track/%d?access_token=%s"
"https://api.zaycev.net/external/options?access_token=%s"
"https://api.zaycev.net/external/track/%d/download/?access_token=%s&encoded_identifier=%s"
"https://api.zaycev.net/external/track/%s/play?access_token=%s&encoded_identifier=%s"
"https://api.zaycev.net/external/bugs?access_token=%s"
"https://api.zaycev.net/external/feedback?email=%s&clientInfo=%s&text=%s&access_token=%s"
Частина друга. Так буде код
Ось ми і підійшли до фінальної стаді нашого дослідження, тепер нам доведеться перенести отримані знання в код. Використовувати ми будемо, як і зазначено, як і зазначено в заголовку статті мова Go, весь код наводити не буду його ви зможете знайти за посиланням у кінці статті.
Оголосимо константи API-посилань
const (
apiURL string = "https://api.zaycev.net/external"
helloURL string = apiURL + "/hello"
authURL string = apiURL + "/auth?"
topURL string = apiURL + "/top?"
artistURL string = apiURL + "/artist/%d?"
musicSetListURL string = apiURL + "/musicset/list?"
musicSetDetileURL string = apiURL + "/musicset/detail?"
genreURL string = apiURL + "/genre?"
trackURL string = apiURL + "/track/%d?"
autoCompleteURL string = apiURL + "/autocomplete?"
searchURL string = apiURL + "/search?"
optionsURL string = apiURL + "/options?"
playURL string = apiURL + "/track/%d/play?"
downloadURL string = apiURL + "/track/%d/download/?"
)

Для імплементації виберемо один із запитів, наприклад, запит TOP треків, і опишемо JSON об'єкт:
ZTop struct
type ZTop struct {
Page int `json:"page"`
PagesCount int `json:"pagesCount"`
Tracks []struct {
Active bool `json:"active"`
ArtistID int `json:"artistId"`
ArtistImageURLSquare100 string `json:"artistImageUrlSquare100"`
ArtistImageURLSquare250 string `json:"artistImageUrlSquare250"`
ArtistImageURLTop917 string `json:"artistImageUrlTop917"`
ArtistName string `json:"artistName"`
Bitrate int `json:"bitrate"`
Block bool `json:"block"`
Int Count `json:"count"`
Date int64 `json:"date"`
Duration string `json:"duration"`
HasRingBackTone bool `json:"hasRingBackTone"`
ID int `json:"id"`
LastStamp int `json:"lastStamp"`
Phantom bool `json:"phantom"`
Size float64 `json:"size"`
Track string `json:"track"`
UserID int `json:"userId"`
} `json:"tracks"`
}

Помилки специфічні для api:
type ClientError struct {
msg string
}

func (self ClientError) Error() string {
return self.msg
}

Створимо клієнт:
type ZClient struct {
client *http.Client
helloToken string
accessToken string
staticKey string
}

func NewZClient(httpClient *http.Client, token, sKey string) *ZClient {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &ZClient{client: httpClient, accessToken: token, staticKey: sKey}
}

Функція запиту Top списку:
func (zc *ZClient) Top(page int) (r *ZTop, err error) {
r = &ZTop{}
якщо err = zc.checkAccessToken(); err != nil {
return r, err
}
values := url.Values{}
values.Add("page", strconv.Itoa(page))
values.Add("access_token", zc.accessToken)

if err := zc.fetchApiJson(topURL, values, r); err != nil {
return r, err
}
return r, err
}

Функція, яка виконує http запити:
func (zc *ZClient) makeApiGetRequest(fullUrl string, values url.Values) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", fullUrl+values.Encode(), nil)
if err != nil {
return resp, err
}
resp, err = zc.client.Do(req)
if err != nil {
return resp, err
}
if resp.StatusCode != 200 {
var msg string = fmt.Sprintf("Unexpected status code: %d", resp.StatusCode)
resp.Write(os.Stdout)
return resp, ClientError{msg: msg}
}
return resp, nil
}

Функція для декода json:
func (zc *ZClient) fetchApiJson(actionUrl string, values url.Values, result interface{}) (err error) {
var resp *http.Response
resp, err = zc.makeApiGetRequest(actionUrl, values)
if err != nil {
return err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
якщо err = dec.Decode(result); err != nil {
return err
}
return err
}

Авторизація
func (zc *ZClient) Auth() (err error) {
якщо err = zc.checkStaticKey(); err != nil {
return err
}
return zc.hello()
}

func (zc *ZClient) hello() (err error) {
якщо err = zc.checkStaticKey(); err != nil {
return err
}
t := &ZToken{}
if err := zc.fetchApiJson(helloURL, url.Values{}, t); err != nil {
return err
}
zc.helloToken = t.Token
return zc.auth()
}

func (zc *ZClient) auth() (err error) {

якщо err = zc.checkHelloToken(); err != nil {
return err
}
r := &ZToken{}
hash := MD5Hash(zc.helloToken + zc.staticKey)
values := url.Values{}
values.Add("code", zc.helloToken)
values.Add("hash", hash)
if err := zc.fetchApiJson(authURL, values, r); err != nil {
return err
}
zc.accessToken = r.Token
return err
}

Функція підрахунку md5:
func MD5Hash(text string) string {
hasher := md5.New()
hasher.Write([]byte(text))
return hex.EncodeToString(hasher.Sum(nil))
}

Ісходник доступний за наведеними нижче посиланнями.
P. S.: Код дуже далекий від досконалості. Якщо є думки щодо його виправлення і поліпшення — буду радий вашим реквестам.
Посилання:
Jadx: https://github.com/skylot/jadx
github: https://github.com/pixfid/go-zaycevnet
zaycev.net_4.9.3_10.апк: http://bit.ly/1MZW7UA
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.