[Из песочницы] Реверсинг Android клиента музыкального сервиса Zaycev.net и имплементация api на go

Строго говоря, к реверсингу данную статью можно отнести только с натяжкой.

Всем вам знаком такой сервис как zaycev.net. Не ошибусь, предположив, что каждый хоть раз качал с него музыку, либо через web-интерфейс, либо через мобильное приложение.

Если вам все же интересно, добро пожаловать под кат.

Однажды один мой хороший знакомый попросил разобраться как работает их официальный клиент под Android. Скачав клиент, я приступил к изучению и загрузил подопытного в Jadx (Dex to Java decompiler). Все ссылки в конце статьи.

Первое, что бросается в глаза — наличие обфускации:


7c1e3bde59364e23bcd9e4dfc96ceee7.png

Ну не беда, прорвемся, не впервой ведь. Беглый осмотр показал, что нужный нам функционал сосредоточен в пакете:


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, весь код приводить не буду его вы сможете найти по ссылке в конце статьи.


Объявим константы 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"`
    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

© Habrahabr.ru