Giving old Samsung compact and mirrorless cameras (2009-2015) a new life
Ge0rG (@ge0rg@chaos.social)
FOSDEM 2025, Sat 16:00 (Embedded DevRoom)
Samsung WiFi cameras
Reverse engineering Samsung camera API services
API Re-Implementation
Possible rationale: react to upstream API changes without firmware updates
📸 ⇄ 🛜 ⇄ ☁️
⬍
🐍
Camera connects to WiFi / hotspot
📸 ⇄ 🛜 ⇄ ☁️
⬍
🐍
Camera talks to Samsung API: login, upload
📸 ⇄ 🛜 ⇝ ☠️
⬍
🐍
Samsung API was discontinued in 2021
📸 ⇄ 🛜 ⇝ ☠️
⇅
🐍
Redirect via DNS to own implementation!
📸 ⇄ 🛜 ⇝ ☠️
⇅
❓🐍❓
But what’s the response format?
Mirrorless cameras since 2013 run a Samsung-branded Linux OS
root
access possibledi-camera-app
🤬POST http://www.ospserver.net/social/columbus/email?DUID=123456789033 HTTP/1.0 Authorization:OAuth *snip* x-osp-version:v1 User-Agent: sdeClient Content-Type: multipart/form-data; boundary=---------------------------7d93b9550d4a Accept-Language: ko User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 1.1.4322; InfoPath.2; .NET CLR 2.0.50727) -----------------------------7d93b9550d4a content-disposition: form-data; name="message"; fileName="sample.txt" content-Type: multipart/form-data; <?xml version="1.0" encoding="UTF-8"?> <email><sender>Camera@samsungcamera.com</sender><receiverList><receiver>censored@censored.com</receiver> </receiverList><title><![CDATA[[Samsung Smart Camera] sent you files.]]></title> <body><![CDATA[Sent from Samsung Camera.\r\nlanguage_sh100_utf8]]></body></email> -----------------------------7d93b9550d4a content-disposition: form-data; name="binary"; fileName="SAM_4371.JPG" content-Type: image/jpeg; *snipped JPEG payload* -----------------------------7d93b9550d4a
POST http://snsgw.samsungmobile.com/facebook/auth HTTP/1.1 <?xml version="1.0" encoding="UTF-8"?> <Request Method="login" Timeout="3000" CameraCryptKey="58a4c8161c8aa7b1287bc4934a2d89fa952da1cddc5b8f3d84d34 06713a7be05f67862903b8f28f54272657432036b78e695afbe604a6ed69349ced7cf46c3e4ce587e1d56d301c544bdc2d476ac5451c eb217c2d71a2a35ce9ac1b9819e7f09475bbd493ac7700dd2e8a9a7f1ba8c601b247a70095a0b4cc3baa396eaa96648"> <UserName Value="uFK%2Fz%2BkEchpulalnJr1rBw%3D%3D"/> <Password Value="ob7Ue7q%2BkUSZFffy3%2BVfiQ%3D%3D"/> <PersistKey Use="true"/> 4p4uaaq422af3"/> <SessionKey Type="APIF"/> <CryptSessionKey Use="true" Type="SHA1" Value="//////S3mbZSAQAA/LOitv////9IIgS2UgEAAAAQBLY="/> <ApplicationKey Value="6a563c3967f147d3adfa454ef913535d0d109ba4b4584914"/> </Request>
After some days of reverse-engineering…
secureRandom()
rand()
initialized with
time()
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from base64 import b64decode from urllib.parse import unquote def decrypt_string(key, s): d = Cipher(algorithms.AES(key[0:16]), modes.CBC(key[16:])).decryptor() plaintext = d.update(s) return plaintext.decode('utf-8').rstrip('\0') def decrypt_credentials(xml): x_csk = xml.find("CryptSessionKey") x_user = xml.find("UserName") x_pw = xml.find("Password") key = b64decode(x_csk.attrib['Value']) enc_user = b64decode(unquote(x_user.attrib['Value'])) enc_pw = b64decode(unquote(x_pw.attrib['Value'])) return (key, decrypt_string(key, enc_user), decrypt_string(key, enc_pw))
$ ./decrypt.py facebook_auth.xml
User: x Password: z
$ _
@app.route('/social/columbus/email',methods = ['POST']) def sendmail(): if not 'message' in request.files: abort(400, "No 'message' in POST or unpatched Flask") xml = ET.parse(request.files['message']) sender = xml.find('sender').text name, addr = email.utils.parseaddr(sender) recipients = [e.text for e in xml.find('receiverList').findall('receiver')] title = xml.find('title').text body = xml.find('body').text.replace("\nlanguage_sh100_utf8", "") msg = Message(subject=title, sender=sender, recipients=recipients) msg.body = body for f in request.files.getlist('binary'): msg.attach(f.filename, f.mimetype, f.read()) mail.send(msg) return make_response("Yay", 200)
Plain-text HTTP, obscure API
Proof of concept, not meant for public deployment!
“Authentication” (password verification could be implemented):
SENDERS = ['Camera@samsungcamera.com', 'georg@op-co.de']
Content routing (“mastodon”, “store”, “drop”):
[ACTIONS]
facebook = "mastodon"
picasa = "store"
"ge0rg@photog.social" = "mastodon"
"store@x.com" = "store"
"test@test.example" = "drop"
"~"
as separator between post content and alt-text
for all picturesMASTODON_POSTSCRIPT
for hashtags etc…GET / HTTP/1.1 Host: www.yahoo.co.kr HTTP/1.1 301 Moved Permanently Location: https://www.yahoo.com/
www.yahoo.co.kr
or
www.msn.com
(same mechanism)
After some more days of reverse-engineering…
partialImage.o.map
Main_Image
, …)partialImage.o.map
is a symbol map for
Main_Image
DevHTTPResponseStart()
checks Yahoo response
.yahoo.*
domain"yahoo."
at position
<= 11len("https://www.") == 12
🤬
Ideal deployment platform:
samsung-nx-emailservice
dnsmasq
for API RedirectionGeneration | Support by re-implemented API service |
---|---|
DRIMeV | 🤷NX1 ✅NX500 |
DRIMeIV | ✅NX300/NX310 ✅NX2000 🤷NX30 |
DRIMeIII | ✅NX1000/NX1100 ✅EX2F |
Milbeaut | ✅NX mini |
WBxxxF compact | 🟡WB30F/WB31F/WB32F 🟡WB35F/WB36F/WB37F 🟡WB50F ✅WB150F 🟡WB200F ✅WB250F ✅WB350F/WB351F ✅WB380F ✅WB800F ✅WB850F |
bridge | 🟡WB1100F ❌WB2200F |
other compact | ❌ST1000 ✅ST150F ✅ST200F ✅SH100 ✅DV150F 🤷DV180F ✅DV300F 🤷MV900F |
camcorders | 🤷HMX-S10/S15/S16 🤷HMX-QF20 ✅HMX-QF30 |
✅ supported | 🟡 partially | ❌ reverse engineering | 🤷 hardware donation
Sources:
GET /security/sso/initialize/time
(respone in 2012 pastebin)GET /social/columbus/serviceproviders/list?DUID=*snipped*
12:04:45.225820 # [sock]nx_tcp_client_socket_unbind [0x0]
12:04:45.228948 # [sock]nx_tcp_socket_delete [0x0]
12:04:45.231220 # [sock]Close OK [0x101]
12:04:45.233046 WebEventHandler [6]
12:04:45.236441 WebCheckRelayServer : 5 ( -7 ) 0
LifeBridgeReply(0x3c8a)
LifeBridgeWaitMsg : done
12:04:45.245002 ui_msg_web_check_relay_server_state : 5
12:04:46.355718 WifiClose : 0(state 22)
WifiClose : 1 0 0 1 0)
12:04:46.361168 LifeBridgeWmCommand : send - 113
LifeBridgeWmCommand : waiting reply - 113
LifeBridgeWmCommand : end - 113 ( result : ok )
LifeBridgeWmCommand : send - 105
LifeBridgeWmCommand : waiting reply - 105
LifeBridgeWmCommand : end - 105 ( result : ok )
LifeBridgeWmCommand : send - 101
[wifilib] Delete "bcm4325_assoc" thread
12:04:46.405377 [wifilib] Delete "assoc thread queue" queue
LifeBridge? nx_bcm4325_...
? Dedicated WiFi SoC?
No
XML dumps ➛ Need to extract firmware from camera!
<Response SessionKey="{{ sessionkey }}"> <PersistKey Value="{{ persistme }}"/> <CryptSessionKey Value="{{ csk }}"/> <LoginPeopleID="{{ screenname }}"/> <OAuth URL="http://snsgw.samsungmobile.com/oauth"/> </Response>
@app.route('/<string:site>/auth',methods = ['POST']) def auth(site): d = request.get_data() if not d: abort(400, 'Empty POST payload') # sometimes sent by NX300?! xml = ET.fromstring(d) method = xml.attrib["Method"] # "login" or "logout" if method == 'logout': return "Logged out for real!" creds = samsungxml.extract_credentials(xml) # decryption if not creds['user'] in app.config['SENDERS']: return "Login failed", 401 session = mysession.load(None) session.user = creds['user'] mysession.store(session) return render_template('response-login.xml', sessionkey=session['sid'], csk=session['sid'], screenname="Samsung NX Lover")
Lack of multipart terminator ➛ Patch Flask
TCP segmentation 😡
Hotspot detection 🤬
API is running on a 192.168.x.x IP address ➛ run on 10.x.x.x! (Seriously!)
Social Media Upload Sequence
What the camera does:
POST /<site>/auth/
(username, password) ➛ sessionPOST /<site>/photo/
(filename, album, message) ➛ upload idPOST /upload/upload_id/file
(actual picture, onePOST
per file)Implications: