Samsung Camera to Mastodon Bridge

Giving old Samsung compact and mirrorless cameras (2009-2015) a new life

Ge0rG (@ge0rg@chaos.social)

FrOSCon 2025 (16. August 17:00)

Goal: Send photos on the go

Samsung SH100 camera uploading a photo using a $20 LTE stick

Agenda

  1. Samsung WiFi cameras

  2. Reverse engineering Samsung camera API services

  3. API Implementation Project

  4. That Hotspot detection!!11

  5. API Deployment

Samsung WiFi cameras

Samsung WiFi cameras

  • WiFi-enabled Samsung cameras released from 2009 to 2015
    • ~7 Samsung NX interchangeable-lens cameras
      (NX2000, NX30, NX300, NX3000, NX mini, NX500, NX1)
    • ~20 models of compacts (models ending in β€œF”: WBxxxF, STxxxF, …)
    • two camcorder models (HMX-QF20/HMX-QF30)
  • Available on second hand market (30-300€)
Samsung NX cameras with WiFi
Samsung compact cameras with WiFi

Samsung Social Network Services

DV150F photo upload menu

SNS API

  • Central API service hosted by Samsung …until 2021
  • Authentication (HTTP with obfuscation)
  • Forwarding of pictures and videos to upstream service
  • Supported subset of services depends on camera model

Possible rationale: react to upstream API changes without firmware updates

Servers not available any more!

API Communication

πŸ“Έ ⇄ πŸ›œ ⇄ ☁️
⬍
🐍

Camera connects to WiFi / hotspot

πŸ“Έ ⇄ πŸ›œ ⇄ ☁️
⬍
🐍

Camera talks to Samsung API: login, upload

πŸ“Έ ⇄ πŸ›œ ⇝ ☠️
⬍
🐍

Samsung API is discontinued since 2021

πŸ“Έ ⇄ πŸ›œ ⇝ ☠️
β‡…
🐍

Redirect via DNS to own implementation!

πŸ“Έ ⇄ πŸ›œ ⇝ ☠️
β‡…
β“πŸβ“

But what’s the response format?

Reverse engineering Samsung camera API services

Reverse engineering Samsung API

  1. Old PCAPs of the API communication βž› one file from 2013: hotspot check
  2. Open Source βž› hahaha!!11! (sorry)
  3. Camera PCB debug ports
  4. Reverse Engineering camera firmware

NX mini debug port

Samsung ST1000/CL65 debug port

  • First WiFi camera from Samsung, no firmware downloads available
  • Debug port with UART:

ST1000 Serial Log

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 βž› must extract firmware (SDIO? flash? JTAG?)
SWG-B15 (β€œ3combo”)

NX300 / NX500 / NX1: Tizen Linux

NX300 with xteddy

Mirrorless cameras since 2013 run a
Samsung-branded Linux OS

  • OS-level root access possible
  • Runs X11, enlightenment
  • Camera UI: 5MB ELF binary
    di-camera-app 🀬
  • Some access to app logs and st test command
    • inject key presses
    • change camera parameters

  • Redirect DNS βž› ??? βž› PROFIT!
# /etc/hosts
192.168.99.1 www.ospserver.net snsgw.samsungmobile.com

Easy API: Email

POST http://www.ospserver.net/social/columbus/email?DUID=123456789033 HTTP/1.0
Authorization:OAuth *snip*                                    πŸ‘† unencrypted HTTP request
Content-Type: multipart/form-data; boundary=---------------------------7d93b9550d4a

-----------------------------7d93b9550d4a
content-disposition: form-data; name="message"; fileName="sample.txt"
content-Type: multipart/form-data;

<?xml version="1.0" encoding="UTF-8"?>                πŸ‘ˆ XML payload with image meta-data
<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*                                      πŸ‘ˆ raw payload with JPEG file

-----------------------------7d93b9550d4a                     πŸ‘ˆ no multipart terminator!
  • Only expects β€œ200 OK” as response βž› first to be implemented in 2022

Python Flask Service

E-mail API endpoint (shortened)

@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']) # parse XML payload 
    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) # create email
    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)
  • β€œUnpatched Flask” βž› patch Flask to accept HTTP without multipart terminator

Hard API: Social

POST http://snsgw.samsungmobile.com/facebook/auth HTTP/1.1    πŸ‘ˆ unencrypted HTTP request
                                                        
<?xml version="1.0" encoding="UTF-8"?>
<Request Method="login" Timeout="3000" CameraCryptKey="58a4c8161c8aa7b1287bc4934a2d89fa952da1cddc5b8f3d84d3406713a7be05f6786290
3b8f28f54272657432036b78e695afbe604a6ed69349ced7cf46c3e4ce587e1d56d301c544bdc2d476ac5451
ceb217c2d71a2a35ce9ac1b9819e7f09475bbd493ac7700dd2e8a9a7f1ba8c601b247a70095a0b4cc3baa396
eaa96648">                                                        πŸ‘ˆ 1024-bit hexadecimal
<UserName Value="uFK%2Fz%2BkEchpulalnJr1rBw%3D%3D"/>                πŸ‘ˆ url-encoded base64
<Password Value="ob7Ue7q%2BkUSZFffy3%2BVfiQ%3D%3D"/>
<PersistKey Use="true"/>
4p4uaaq422af3"/>
<SessionKey Type="APIF"/>
<CryptSessionKey Use="true" Type="SHA1" Value="//////S3mbZSAQAA/LOitv////9IIgS2UgEAAAAQBLY="/> πŸ‘ˆ 256-bit base64, not url-encoded
<ApplicationKey Value="6a563c3967f147d3adfa454ef913535d0d109ba4b4584914"/>
</Request>                                                         πŸ‘† 192-bit hexadecimal
  • CryptSessionKey veeeeeeeeery suspicious (base64 "//////" = 0xff 0xff…)
  • need to reverse-engineer encryption and expected response

Encryption reverse-engineering

After some days of reverse-engineering (2023)…

  • username and password: encrypted with session key (_secureRandom() 😰)
  • CameraCryptKey = RSA-encrypted session key πŸ₯΅
char *_secureRandom(int *result) {
    srand(time(0)); // initialize non-secure(!) RNG with time in seconds(!!)
    char *target = String_new(20,result); 
    String_format(target,20,"%d",rand()); // get "random"(!!!) number, format as string
    return _sha1_byte(target,result);     // return SHA1 of "random" number string
}
char *_sha1_byte(char *buffer, int *result) {
    int len = strlen(buffer);  // get length of string, throw it away
    char *shabuf = malloc(20); // allocate target buffer
    char buf[20];              // intermediate buffer on stack
    memcpy(shabuf, buf, 20);   // copy stack buffer to target buffer
    return shabuf;             // return 20 bytes from uninitialized stack(!!!!)
}
  • CryptSessionKey: session key + IV encrypted with… nothing at all!

Proof-of-Concept Decryption

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
$ _

Authentication Response

  • Response is processed by WebCheckLogin() method:
char value[512];
GetXmlString(buf,"Response SessionKey",value);
strcpy(gWeb.response_session_key,value);
GetXmlString(buf,"PersistKey Value",value);
...
  • GetXmlString() is not an XML parser
  • Buffer overflows everywere!!1!!!
  • Minimal valid response as a Flask template:
<Response SessionKey="{{ sessionkey }}">
<PersistKey Value="{{ persistkey }}"/>
<CryptSessionKey Value="{{ csk }}"/>
<LoginPeopleID="{{ screenname }}"/>
<OAuth URL="http://snsgw.samsungmobile.com/oauth"/>
</Response>

Authentication Response Generator

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

Social Media Upload Sequence

What the camera does:

  1. POST /<site>/auth/ (username, password) βž› session (only once)
  2. N * POST /<site>/photo/ (filename, album, message) βž› upload URL
  3. N * PUT ...upload_url.../file (actual picture)

Implications:

  • Server-side state required (session, message, uploaded files)
  • No advance knowledge of number of files (Seriously, Samsung?)

Interfacing with Mastodon

  • How many photos did the user send?
  • No place for photo alt-text!

    Solution:

  • Use "~" as separator between post content and alt-text for all pictures
    βž› implicit number of pictures
  • User must keep track!
  • Input box has very limited length!

Camera API Implementation Project

samsung-nx-emailservice

Basic Functions

  • Send emails via SMTP relay
  • Post pictures to Mastodon
  • Pass uploads to shell commands
  • β€œRouting” based on email address / social network

Files (500 LoC)

  • config.toml - config and credentials
  • samsungserver.py - main / API service
  • samsungxml.py - XML parsing and decryption
  • mysession.py - file-based sessions
  • mastodonlogin.py - obtain access token
Directory index of upload folder (INSECURE_DOWNLOAD=true)

Authentication & Routing

  • Project started as proof of concept, not meant for public deployment!
  • β€œAuthentication” (password verification could be implemented):
SENDERS = ['Camera@samsungcamera.com', 'georg@op-co.de']
  • Content routing rules ("mastodon", "store", "shell", "drop"):
[ACTIONS]
facebook = "mastodon"              # default profile
picasa = "mastodon.pub"            # inherited profie
"ge0rg@photog.social" = "mastodon"
"store@x.com" = "store"            # store to disk
"test@test.example" = "drop"       # do not forward

[MASTODON] # default profile
POSTSCRIPT = '#ShittyCameraChallenge #photography #Fotografie #SamsungNX'
VISIBILITY = 'unlisted' # 'direct', 'private', 'unlisted', 'public'

[MASTODON.pub] # inherits everything from [MASTODON]
VISIBILITY = 'public'

Weird Samsung network stack quirks

  1. Lack of multipart terminator βž› Patch Flask

  2. TCP segmentation 😑

    βœ… Samsung (2013): one TCP segment
    ❌ Flask (2023): two TCP segments

    βž› Patch Flask to set TCP_CORK

  3. WB2200F: Off-by-4 error

     >>> upload[0].load.splitlines()[-2]
     'Content-Length: 486270'
     >>> sum([len(p.load) for p in upload[1:]])
     486274
     >>> 486274 - 486270
     4
     >>> 

  4. Hotspot detection 🀬

Reverse engineering hotspot detection

Hotspot: Certification needed

API is running on a 192.168.x.x IP address βž› run on 10.x.x.x (Seriously!)

Hotspot progress

βœ… NX-series: just works (http://gld.samsungosp.com)
βœ… SH100: not on 192.168.x.x
❓ WBxxxF
❓ NX mini


β–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘

7/27

Do not enter personal information!

Hotspot: Yahoo?!

GET http://www.yahoo.co.kr/ HTTP/1.1

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 (2024)…

  • WB850F and EX2F firmare comes with partialImage.o.map
  • Firmware file contains partition records (bootloader, resources, Main_Image, …)
  • partialImage.o.map is a symbol map for Main_Image
  • Function DevHTTPResponseStart() checks Yahoo response
    • HTTP 200: must set a cookie on .yahoo.* domain
    • HTTP 30x Redirect: URL must contain "yahoo." at position <= 11

len("https://www.") == 12 🀬

Hotspot: Yahoo workaround

# /etc/hosts
192.168.99.1 www.ospserver.net snsgw.samsungmobile.com www.yahoo.co.kr
@app.route('/')
def home():
    host = (request.headers.get('Host') or "")
    if host == "www.yahoo.co.kr":
        response = make_response("YAHOO!", 200)
        response.set_cookie('samsung', 'hotspot', domain='.yahoo.co.kr')
        return response

Β Β 

Hotspot progress

βœ… NX-series: just works (http://gld.samsungosp.com)
βœ… SH100: not on 192.168.x.x
βœ… WBxxxF: Yahoo / MSN - cookie / redirect
❓ NX mini


β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘

21/27

Hotspot: NX mini is special

NX mini: a smol interchangeable-lens camera!
  • Sometimes refuses to connect (β€œchance 50:50!”)

  • Firmware is using unknown compression:

    Copyright (c) 2<80>^@^@5-2011, Jouni Ma^@^@linen <*@**.**>^@^@and contributors^@^B^@This program ^@^Kf^@^@ree software. Yo!  u ^@q dis^C4e it^AF/^@<9c>m^D^@odify^@^Q under theA^@ P+ms of^B^MGNU Gene^A^@ral Pub^@<bc> License^D^E versPy 2.
  • No decompression code in boot loader?!?

After some days weeks of reverse-engineering with @IgorSkochinsky (2025)…

  • Understanding of firmware header (files/partitions)
  • Full LZSS (Lempel-Ziv-Storer-Szymanski) decompressor

Fujitsu RELC

RELC = Rapid Embedded Lossless data Compression

  • used by NX mini, NX3000 and some earlier cameras

Β Β 

NX mini hotspot detection

Calls API known from 2013 PCAP:

GET http://gld.samsungosp.com/

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-type: text/html
Server: nginx/0.7.65
Content-length: 7
Connection: keep-alive

200 OK

Code in October 2014 firmware:

if ((strstr(response,"ETag: \"") == 0) ||
    (strstr(response,"Server: nginx/") == 0) ||
    (strstr(response,"Content-Length: 0") == 0)) {
        *auth_passed = -1;
        return 1;
}
  • impossible to guess
  • easy to implement once known
if host == "gld.samsungosp.com":
    return make_response("", 200, {
        'ETag': '"deadbeef"',
        'Server': 'nginx/notreally'})

Hotspot progress

βœ… NX-series: just works (http://gld.samsungosp.com)
βœ… SH100: not on 192.168.x.x
βœ… WBxxxF: Yahoo / MSN - cookie / redirect
βœ… NX mini: changed http://gld.samsungosp.com


β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ

27/27

API Deployment

10€ Debian LTE stick

Ideal deployment platform:

  • Android LTE modem with WiFi (MSM8916)
  • Will run Debian!
    βœ… Python for samsung-nx-emailservice
    βœ… NetworkManager for WiFi Hotspot
    βœ… dnsmasq for API Redirection
  • Just add USB power!

πŸ‘‰ Step-by-step instructions

Demo

Supported model list

Generation 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 | 🟑 email API only | ❌ needs more RE | 🀷 hardware donation

Summary

Backup

Abstract

Between 2009 and 2015, Samsung released over 30 WiFi-equipped compact and mirrorless camera models that could email photos or upload them to social media. In 2021, they shut down the required API services, crippling the WiFi functionality.

To revive the WiFi feature, the firmware of multiple camera models was reverse-engineered to understand the protocol and to β€œcircumvent” the WiFi hotspot detection implemented by Samsung.

Based on the gained knowledge, a FOSS implementation of the API was created using the Python Flask framework, allowing to post to Mastodon right from the camera. The project supports ~75% of the camera models, looking for hardware donations and better-skilled reverse engineers to close the gap.

It was exemplarily deployed on an inexpensive LTE modem running Debian, allowing on-the-go use of the camera WiFi.

HMX-QF20: It’s TLS SSL… v2!

  • HTTPS without certificate checks (SSLv2) πŸ₯±
  • Same XML API, credentials not encrypted obfuscated
  • SSLv2 disabled at compile time in every reasonable code base
  • πŸŽ‰ socat23: πŸ— Socat with SSL v2/3 Support πŸŽ‰

Samsung ST1000/CL65

  • First WiFi camera from Samsung, no firmware downloads available
  • API not shared with other models:
    1. GET /security/sso/initialize/time (respone in 2012 pastebin)
    2. GET /social/columbus/serviceproviders/list?DUID=*snipped*
    3. 🀷🀷🀷