Строго говоря, к реверсингу данную статью можно отнести только с натяжкой.
Всем вам знаком такой сервис как 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:
На что сервер отвечает json объектом:
{
"token":"I-fte8MSfXjw8bYFQkcq629iB6uLb5thZSoj3rGvlCPG4ZJzpgbFPylrtLDpw7L_qQ2EBeuBIMvA7BUWkwilS8IWUg3CWGwj8SCmdIU5I8M"
}
Вычисление hash:
hash = md5(helloToken + "kmskoNkYHDnl3ol2")
Забегая вперед, скажу, что константа, зашитая в программу (kmskoNkYHDnl3ol2), меняется от версии к версии, на данный момент мне встречались 3 разных константы:
android: "60kQwLlpV3jv", "kmskoNkYHDnl3ol2"
ios: "d7DVaaELv"
Аутентификация, получение Access Token:
На что сервер отвечает 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
Автор: 0xcffaedfe