[Из песочницы] Реверсинг 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", "token - " + 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, весь код приводить не буду его вы сможете найти по ссылке в конце статьи.
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 объект:
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"`
Count int `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{}
if 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)
if err = dec.Decode(result); err != nil {
return err
}
return err
}
func (zc *ZClient) Auth() (err error) {
if err = zc.checkStaticKey(); err != nil {
return err
}
return zc.hello()
}
func (zc *ZClient) hello() (err error) {
if 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) {
if 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.apk: http://bit.ly/1MZW7UA