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


Всем вам знаком такой сервис как 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 + "kmskoNdkYHDnl3ol3";
                    a.a();
                } else {
                    str2 = str + "kmskoNdkYHDnl3ol3";
                }
                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 + "kmskoNdkYHDnl3ol3")

Забегая вперед, скажу, что константа, зашитая в программу (kmskoNdkYHDnl3ol3), меняется от версии к версии, на данный момент мне встречались 3 разных константы:


android: "63kQw2LlpV3jv", "kmskoNdkYHDnl3ol3"
ios: "d7DVdaELf"

Аутентификация, получение 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
Поделиться с друзьями
-->

Комментарии (9)


  1. forceLain
    26.05.2016 16:21

    Имхо, проще снифить траффик, чем копаться в обфусцированном коде. Хотя, если совместить оба метода…


    1. EnterSandman
      26.05.2016 19:26

      можно. а токены как потом будете брутфорсить?


    1. 0xcffaedfe
      26.05.2016 23:50

      Проще, когда нет https, и внутренняя кухня не интересует. Да и обфускация обфускации рознь, в большинстве случаем это просто затертые оригинальные имена методов, классов. А вот когда декомпилятор вместо кода вываливает smali тогда приходится подумать, в любом случае подход зависит от задачи.


      1. forceLain
        29.05.2016 15:32

        Кстати, https не такая уж проблема, если подсунуть на wi-fi точку, к которой подключен девайс, свой сертификат и установить его в доверенные на девайсе. С эмулятором и того проще.


  1. zelenin
    26.05.2016 17:06
    +1

    второй раз за неделю вижу как под «имплементацией api» подразумевают «имплементацию клиента api», что вещи разные сами по себе, поскольку в первом случае речь должна идти о части серверной.


    1. 0xcffaedfe
      26.05.2016 23:56
      -3

      Нет, имплементация.


      1. zelenin
        26.05.2016 23:59
        +4

        да, имплементация. Имплементация чего-то — это реализация функционала этого чего-то. Что-то — это апи. Апи — это то, что крутится на сервере. Имплементация апи — реализация того, что крутится на сервере.


  1. Mayflower
    26.05.2016 18:02
    +2

    Недавно была статья как раз про разработку официального клиента zaycev.net https://habrahabr.ru/post/283274/


    1. tgsoft
      27.05.2016 13:06

      Точно. Там в ответах еще интересные моменты от сотрудников ресурса.