Samsung Camera to Mastodon Bridge

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

Ge0rG (@ge0rg@chaos.social)

FrOSCon 2025 (August 16./17.)

Abstract (hide!)

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.

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

  4. That Hotspot detection!!11

  5. API Deployment

Project History (hide!)

  • November 2015: Samsung abandons digital camera division

😴

  • January 2021: Samsung discontinues API services

  • May 2022: first implementation of e-mail API endpoint

  • November 2023: reverse-engineering of social media API

  • May 2024: reverse-engineering of “Yahoo” hotspot

  • May 2025: reverse-engineering of Samsung NX mini hotspot

Samsung WiFi cameras

Samsung WiFi cameras

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

Samsung Social Network Services

DV150F photo upload menu

SNS API

  • Central API service hosted by Samsung …until 2021
  • Authentication
  • 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

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

Two distinct APIs: 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 ➛ easy to implement (2022)

Two distinct APIs: 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")

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 🎉

Camera API Implementation

Python Flask Service

E-mail API endpoint (example)

@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 message
    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

Social Media Upload Sequence

What the camera does:

  1. POST /<site>/auth/ (username, password) ➛ session
  2. POST /<site>/photo/ (filename, album, message) ➛ upload id
  3. POST /upload/upload_id/file (actual picture, one POST per file)

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!

MASTODON_POSTSCRIPT = '#ShittyCameraChallenge #photography #Fotografie #SamsungNX'
MASTODON_VISIBILITY = 'public' # 'direct', 'private', 'unlisted', 'public'

Project Structure & Deployment

Files

  • samsungserver.py - main / API service
  • samsungxml.py - XML parsing and decryption
  • mysession.py - file-based sessions
  • mastodonlogin.py - obtain access token
  • config.toml - config and credentials

Basic Functions

  • Emails forwarded to SMTP relay / stored
  • Social media uploads stored to upload folder
  • Folder names are HMAC(recipient) or HMAC(social_username)
  • Further upload processing with inotify
Directory index of upload folder (INSECURE_DOWNLOAD=true)

Authentication & Routing

  • Plain-text HTTP, obscure API

  • 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", "drop"):

      [ACTIONS]
      facebook = "mastodon"
      picasa = "store"
      "ge0rg@photog.social" = "mastodon"
      "store@x.com" = "store"
      "test@test.example" = "drop"

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. Off-by-4N error:

     >>> pcap[0].load.splitlines()[-2]
     b'Content-Length: 486270'
     >>> sum([len(p.load) for p in pcap[1:]])
     486278
     >>> 486278 - 486270
     8
  4. Hotspot detection 🤬

Reverse engineering hotspot detection

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: 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: Certification needed

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

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.

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

  • Understanding of firmware header
  • LZSS (Lempel-Ziv-Storer-Szymanski)
  • no decompression code in boot loader?!?

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;
}
  • content shrunk from "200 OK" to ""
  • server must provide E-Tag header
  • impossible to guess
  • easy to implement once known

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 | 🟡 partially | ❌ reverse engineering | 🤷 hardware donation

Summary

Backup

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. 🤷🤷🤷