Reversing the Android client of the music service Zaycev.net and implementing api on go

Strictly speaking, this article can be attributed to reversing only with a stretch.


You are all familiar with a service like zaycev.net. I’m not mistaken, assuming that everyone at least once downloaded music from him, either through a web interface or through a mobile application.


At the request of the service representatives, I replaced the constants in the publication, if you wish, you can request them.


If you are still interested, welcome to cat.


Part one. Debriefing


Once, one of my good friends asked me to figure out how their official Android client works. After downloading the client, I started to study and uploaded the experimental in Jadx (Dex to Java decompiler). All links at the end of the article.


The first thing that catches your eye is the presence of obfuscation:



Well, it doesn’t matter, we’ll break through, not the first time after all. A quick inspection showed that the functionality we need is concentrated in the package:


package free.zaycev.net.api;

Original Authorization Code:
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 "";
        }
    }

As you can see from the code, the order of service requests is as follows:


Greeting, receiving Hello token:


https://api.zaycev.net/external/hello

What the server responds with a json object:


{
   "token":"I-fte8MSfXjw8bYFQkcq629iB6uLb5thZSoj3rGvlCPG4ZJzpgbFPylrtLDpw7L_qQ2EBeuBIMvA7BUWkwilS8IWUg3CWGwj8SCmdIU5I8M"
}

Hash calculation:


hash = md5 (helloToken + "kmskoNdkYHDnl3ol3")

Looking ahead, I’ll say that the constant wired to the program (kmskoNdkYHDnl3ol3) changes from version to version, at the moment I have seen 3 different constants:


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

Authentication, getting Access Token:


https://api.zaycev.net/external/auth?code=%s&hash=%s

What the server responds with a json object:


{
   "token":"wnfQgLZoLErwL6g_axTTTUkCcobXGLMRZS75Zozr3oC05kWNfd07Bngjpg2VRY2GgXYPaCPqSGarqki6YU278ZO6XJP4RLdNqZMqHFwv-25iH8M_R6rSna2CmnP5OuwgTuUundxiTWqI2Am5rHA2gbU8kbB9Ya0gRJ1mHhq_MpksW3R49Fm4VBDd6vYnNUWykibWmxzxvhRBhJ2dmiKJkw"
}

Check performance:


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 - temporary, valid for about a day, after which you need to request again.


After performing these simple steps, we received the Auth token, which we will need to complete requests to the service server. Time to start searching for queries that are used by the program.


A text search on " https://api.zaycev.net " returned a list of all requests.


List of API requests:


" 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 "

Part two. Let there be a code


So we came to the final stage of our research, now we have to transfer the acquired knowledge to code. We will use, as indicated, as indicated in the heading of the article, the Go language, I won’t give all the code, you can find it at the link at the end of the article.


Declare API Link Constants
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/?"
)

For implementation, we select one of the requests, for example, the request for TOP tracks, and describe the JSON object:


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 specific errors:


type ClientError struct {
    msg string
}
func (self ClientError) Error() string {
    return self.msg
}

Create a client:


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 List Query Function:


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
}

Function that performs http requests:


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
}

Function for json decode:


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
}

Login
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 count function:


func MD5Hash(text string) string {
  hasher := md5.New()
  hasher.Write([]byte(text))
  return hex.EncodeToString(hasher.Sum(nil))
}

The source is available at the links below.


PS: The code is very far from perfect. If there are thoughts on its correction and improvement - I will be glad to your requests.


References:


Jadx: https://github.com/skylot/jadx
github: https://github.com/pixfid/go-zaycevnet
zaycev.net_4.9.3_10.apk: http://bit.ly/1MZW7UA

Also popular now: