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

в 10:39, , рубрики: Go, java, reverse engineering, реверс-инжиниринг

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

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

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

Часть первая. Разбор полетов

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

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

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

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

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

Автор: 0xcffaedfe

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js