Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several changes to the code #1

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ ENV/

# Rope project settings
.ropeproject

response.json
songs/
2 changes: 0 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
MIT License

Copyright (c) 2016 Arun Shreevastava

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand Down
59 changes: 17 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,36 @@
Saavn Downloader
Saavn Downloader(Forked from drt420/Saavn-Downloader)
====
This is a downloader which i coded when i discovered a loophole in Saavn Android App (a popular music streaming app) which can be used to download any music from Saavn in mp3 format, AFAIK only Pro users can download songs but that are also DRM protected (i will tell you how broken that is), but with this any user can bypass all these protection and get DRM free music on their disk.

I sent a total of 3 mails i suppose to their only email id that i could possibly find from their website and app - "[email protected]", along with the PoC but in return i got no reply, which shows that they don't check their mails, God bless those customers, or they don't believe me.
This is a CLI Downloader for Saavn which lets you download entire playlists or even individual songs(by searching for the title).This builds on top of the upstream branch by adding the following features:

On 30th October 2016 i sent them my first mail and till the date of writing (6th December 2016) i didn't receive any mail from Saavn, so i thought to do a full disclosure along with PoC.

Analysis
====
Let me quicky take you through the process of how i did it.

* I used Burpsuite to perform Man-In-The-Middle attack to see what was really going to and from the app.
* I could see the app was receiving the encrypted urls to the songs so i became sure that decryption has to be done at the app side.
* With a quick look at the decompiled smali files and with 'grep' i could figure out that it was using DES with ECB with no IVs.
* Then it was matter of few minutes that i had a working python script which could download songs without any protection and limit.

DRM Protection Analysis
====
DRM protection used in the app is good but its not well integrated in the app.

AES-256 in CBC mode (I may be wrong) is used for DRM and is implemented in both pure Java and in a NDK library.

I don't know much of NDK reversing but from smali files i could figure out that it uses NDK library if its "available" else it uses the Java method.

And there's the catch, the app tries to load the NDK library but if its not found then it doesn't complain rather it uses the java method.

I think it uses NDK library for performance boost but i couldn't extract more information from it so i used the java method, in java method the encryption key used to decrypt the downloaded songs is again hardcoded in the app, which is not at all secure.

So a user with a Pro account can simply delete the NDK library from the apk, reinstall it and then download songs. Then he can decrypt all those downloaded songs and play it in any of his favourite player.

Prerequisite
* Actually downloading the MP3 file using UrlLib
* Adding search functionality letting you download individual songs.
* Naming the song files using title and artist
====
You need Python and BeautifulSoup4 installed for the script to work.
You need Python, BeautifulSoup4 and urllib installed for the script to work.

I used python3 but you can use python2, just make changes in the print statements if you know how to or use python3.
I used python3 but you can use python2, just make changes in the print statements and urllib import statements.

Python3 - https://www.python.org/download/releases/3.0/
BeautifulSoup4 - https://www.crummy.com/software/BeautifulSoup/bs4/doc/

Usage
====
<pre>python3 saavn_downloader.py</pre>
You can choose to either download a single song or an entire album/playlist.
Enter `s` for Song and `a` for Album
For song, enter the title of the song and the program searches Saavn for the song and automatically downloads the first result, so make sure your query is appropriate and precise.

When asked for the song url enter the url of the album from the saavn website eg. http://www.saavn.com/s/album/blah-blah
For album, enter the url of the album or playlist from the saavn website eg. http://www.saavn.com/s/album/blah-blah
OR
http://www.saavn.com/p/playlist/blah-blah


Final Note
====
This project is show that how small coding and implementation mistakes could lead to big problem.

Some ways to fix these:
Future Additions
====

* Perform link decryption at server side and not at client side.
* Don't hardcode encryption or decryption keys at the client side.
* ID3 Metadata/Tagging
* Displaying search results and allowing user to choose from them.

And another suggestion, keep an eye on those mails you might get something interesting in it :D

Disclaimer
====
This is for education purposes. I should not be held responsible for misusage of the script or damage caused because of it. Use it at your own risk.
101 changes: 101 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import requests
import bs4
from saavn_downloader import *
import sys
import urllib.request
import string
import eyed3
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0'
}
def download_album_art(url,filename):
if not os.path.exists(filename+'.jpg'):
print('Downloading cover art...')
urllib.request.urlretrieve(url, filename+'.jpg')
else:
print('Cover art already exists')
def song_select(song_links):
# n = len(songs)
# for i,song in enumerate(songs):
# print('{}) {} - {}'.format(i, song['title'], song['artists']))
# sel = input('Enter the number of the song you want to download(0 to '+str(n)+')')
# return int(sel)
n = len(song_links)
for i, link in enumerate(song_links):
title = '????'
if link.has_attr('title'):
title = link.contents[0]
print('{}) {}'.format(i, title))
sel = input('Enter the number of the song you want to download(0 to '+str(n)+')')
return int(sel)

def download(url, filename, song):
if not os.path.exists(filename+'.mp3'):
print('Downloading '+song['title']+'-'+song['artists'])
urllib.request.urlretrieve(url, filename+'.mp3')
download_album_art(song['thumbnail'], filename)
else:
print('File already exists. Skipping file...')
return
audiofile = eyed3.load(filename+'.mp3')
if(audiofile is None):
return
audiofile.tag.artist = song['artists']
audiofile.tag.album = song['album']
audiofile.tag.title = song['title']
audiofile.tag.release_date = song['year']
thumbnail = open(filename+'.jpg', "rb").read()
audiofile.tag.images.set(3, thumbnail,"image/jpeg",u"")
audiofile.tag.save()
print('Done!')
def get_song():
base_path = './songs'
base_url = 'https://www.saavn.com/search/'
query = input('Enter search query:')
url = base_url + query

#Scraping Saavn for first result
req = requests.get(url, headers=headers)
soup = bs4.BeautifulSoup(req.text, "lxml")
links = soup.find_all("a")
song_link = None
song_links = []
for link in links:
if link.has_attr('href')and ('s/song/' in link['href']):
song_links.append(link)

#Selecting a song from the results
song_link = song_links[song_select(song_links)]['href']
print('Downloading from:'+song_link)
downloader = SaavnDownloader(song_link)
songs = downloader.get_songs()
song = songs[0]
#Removing '/' from file name and album and downloading the file
download(song['url'], base_path+'/'+(song['title']).replace('/', '') , song)
def get_album():
base_path = './songs'
album_link = input('Please enter Album URL-\n')
album_name = input('Please enter Album Name\n(Your album will be stored under ./songs/<ALBUM_NAME>)\n')
base_path = base_path +'/'+album_name

#Making Album directory if not exists
if not os.path.exists(base_path):
os.makedirs(base_path)
downloader = SaavnDownloader(album_link)
songs = downloader.get_songs()
for song in songs:
#Removing '/' from file name and album and downloading the file
download(song['url'],base_path+'/'+(song['title']).replace('/', '') ,song)
def main():
choice = input('Do you want to download a song - s or an album/playlist - a\n')
if choice.upper() == 'A' or choice.upper()=='P':
#Album
get_album()
elif choice.upper() == 'S':
#Song
get_song()
else:
print('Incorrect choice.Exiting...')

if __name__ == '__main__':
main()
96 changes: 50 additions & 46 deletions saavn_downloader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/python3
1#!/usr/bin/python3
# coded by Arun Kumar Shreevastave - 25 Oct 2016

from bs4 import BeautifulSoup
Expand All @@ -8,48 +8,52 @@
import base64

from pyDes import *

proxy_ip = ''
# set http_proxy from environment
if('http_proxy' in os.environ):
proxy_ip = os.environ['http_proxy']

proxies = {
'http': proxy_ip,
'https': proxy_ip,
}
# proxy setup end here

headers = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0'
}
base_url = 'http://h.saavncdn.com'
json_decoder = JSONDecoder()

# Key and IV are coded in plaintext in the app when decompiled
# and its preety insecure to decrypt urls to the mp3 at the client side
# these operations should be performed at the server side.
des_cipher = des(b"38346591", ECB, b"\0\0\0\0\0\0\0\0" , pad=None, padmode=PAD_PKCS5)


input_url = raw_input('Enter the song url:').strip()

try:
res = requests.get(input_url, proxies=proxies, headers=headers)
except Exception as e:
print('Error accesssing website error: '+e)
sys.exit()


soup = BeautifulSoup(res.text,"lxml")

# Encrypted url to the mp3 are stored in the webpage
songs_json = soup.find_all('div',{'class':'hide song-json'})

for song in songs_json:
obj = json_decoder.decode(song.text)
print(obj['album'],'-',obj['title'])
enc_url = base64.b64decode(obj['url'].strip())
dec_url = des_cipher.decrypt(enc_url,padmode=PAD_PKCS5).decode('utf-8')
dec_url = base_url + dec_url.replace('mp3:audios','') + '.mp3'
print(dec_url,'\n')
class SaavnDownloader:
def __init__(self, url):
self.url = url

def get_songs(self):
proxy_ip = ''
# set http_proxy from environment
if('http_proxy' in os.environ):
proxy_ip = os.environ['http_proxy']
proxies = {
'http': proxy_ip,
'https': proxy_ip,
}
# proxy setup end here

headers = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0'
}
base_url = 'http://h.saavncdn.com'
json_decoder = JSONDecoder()

# Key and IV are coded in plaintext in the app when decompiled
# and its preety insecure to decrypt urls to the mp3 at the client side
# these operations should be performed at the server side.
des_cipher = des(b"38346591", ECB, b"\0\0\0\0\0\0\0\0" , pad=None, padmode=PAD_PKCS5)
try:
res = requests.get(self.url, proxies=proxies, headers=headers)
except Exception as e:
print(str(e))
sys.exit()
soup = BeautifulSoup(res.text,"lxml")

# Encrypted url to the mp3 are stored in the webpage
songs_json = soup.find_all('div',{'class':'hide song-json'})
song_dicts = []
for song in songs_json:
song_dict = {}
obj = json_decoder.decode(song.text)
song_dict['album'] = obj['album']
song_dict['artists'] = obj['singers']
song_dict['year'] = obj['year']
song_dict['title'] = obj['title']
song_dict['thumbnail'] = obj['image_url']
enc_url = base64.b64decode(obj['url'].strip())
dec_url = des_cipher.decrypt(enc_url,padmode=PAD_PKCS5).decode('utf-8')
dec_url = base_url + dec_url.replace('mp3:audios','') + '.mp3'
song_dict['url'] = dec_url
song_dicts.append(song_dict)
return song_dicts