Kakao Talk Is Making Me LOCO
I just wanted a Beeper Bridge, instead I get to reverse engineer a proprietary chat app
Preramble
If you're not Korean, consider yourself lucky for 2 reasons:
- You've never had to look yourself in the mirror and say "in 80 years my people will not exist due to something fundamentally broken with our society and culture" [1]
- You've never had to use KakaoTalk
For the uninitiated, KakaoTalk is the predominant chat app of South Korea. It is used by 95% of South Koreans, and 97% of internet users [2]. You don't ask for someone's phone number, you grab their Kakao. It's not merely about peer to peer messaging, as it serves a far more vital function as the essential interface between South Koreans and anything in their society: shopping, government, hospitals, education, etc. If you're familiar with the relationship between WeChat and China, or WhatsApp and India, or LINE and Japan, it's pretty much the same thing but Korean.
Why do they have dominant market share? At first I assumed they were just a chaebol [3] and leveraged some classic Asia style cronyism to lock out all competitors. But no such thing exists, but then I realized the answer was much simpler: South Korea's suffocating culture of homogeneity. Nobody wants to propose using anything else, nobody wants to stick out, nobody wants to be different [4]. This is probably why Koreans have the flyest digital avatars on any MMORPG they play on; they need to stunt somewhere and it's not going to be irl.
Anyways, KakaoTalk's got a crappy UX. Probably because it's a crappy app. They've had a host of issues and exploits ranging from dark UX, to corporate censorship, to numerous security holes [5]-- my favorites of which are a one-click account-takeover [6] and the release of a "secret" chat mode that's actually not secret [7]. But I'm more referring to the general crappiness of the UX/UI, as it's missing tons of modern features, feels really sluggish, and has a heavily outdated interface; you can really tell they just open a website in the app.
This general UX/UI crappiness has frustrated me so much that I was compelled to migrate clients. With the advent of the Matrix Protocol [8], this is now possible and a reality with many of my usages of chat platforms being tied to the Beeper client, allowing you to use a nice chat client for all these various chat platforms. All you have to do is make or use a "bridge" (there's plenty for most chat platforms open source) between the chat server and your matrix server, which is exactly what it sounds like.
Setting up an existing bridge
So time to download a bridge! A google search reveals that there's only one, and it seems officially sponsored by Matrix [9]. Not in my ideal language of choice, but it looks containerized so in theory I shouldn't even need to read the code. The setup instructions ended up being very outdated for Python 3.14, but with downgrading Python and some tinkering, I got the requirements installed and the build ready. Moment of truth:
> node src/main.js
Error: Using this bridge may currently cause your KakaoTalk to be BANNED!
If you wish to use it anyways, please remove this error from source code first.
at file:///.../main.js:25:7
at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
Well, I guess that's one way to make people agree to take responsibility. At this point I was skeptical about this
"official" bridge that was last modified 4 years ago but decided to press forth. After removing the error, node-kakao is live.
> node src/main.js
[API] Starting server
[API] Now listening at /var/run/matrix-appservice-kakaotalk/rpc.sock
Great, now run the Beeper registration to register the bridge:
> bbctl proxy -r registration.yaml
Feb 20 17:50:52.321 DBG Appservice transaction websocket opened
And finally, run the bridge:
> python3 -m matrix_appservice_kakaotalk
[2026-02-23 20:27:59,738] [[email protected]] Initializing matrix-appservice-kakaotalk 0.3.0+dev.2019485c
[2026-02-23 20:27:59,740] [[email protected]] Initialization complete in 0.04 seconds
[2026-02-23 20:27:59,740] [[email protected]] Running startup actions...
[2026-02-23 20:27:59,740] [[email protected]] Starting database...
[2026-02-23 20:27:59,740] [[email protected]] Connecting to sqlite:/matrix-appservice-kakaotalk.db
[2026-02-23 20:27:59,740] [[email protected]] Request 1: register
[2026-02-23 20:27:59,741] [[email protected]] Received response 1
[2026-02-23 20:27:59,746] [[email protected]] Database at v4, not upgrading
[2026-02-23 20:27:59,746] [[email protected]] Database at v2, not upgrading
[2026-02-23 20:27:59,746] [[email protected]] Starting appservice...
[2026-02-23 20:27:59,746] [[email protected]] Starting appservice web server on localhost:11115
[2026-02-23 20:27:59,748] [[email protected]] Ensuring connectivity to homeserver
[2026-02-23 20:28:01,079] [[email protected]] Logging in with bridge bot user (using login type m.login.application_service)
[2026-02-23 20:28:01,080] [[email protected]] Connecting to sqlite:/matrix-appservice-kakaotalk.db
[2026-02-23 20:28:01,082] [[email protected]] Database at v4, not upgrading
[2026-02-23 20:28:01,083] [[email protected]] Found device ID in database: UURNPWPMUS
[2026-02-23 20:28:01,219] [[email protected]] End-to-bridge encryption support is enabled
[2026-02-23 20:28:01,220] [[email protected]] Initializing appservice bot
[2026-02-23 20:28:01,353] [[email protected]] Starting syncing
[2026-02-23 20:28:01,495] [[email protected]] Startup actions complete in 1.75 seconds, now running forever
[2026-02-23 20:28:01,606] [[email protected]] Fatal error while syncing
Traceback (most recent call last):
File "/Users/juicepack/Code/matrix-appservice-kakaotalk/.venv/lib/python3.9/site-packages/mautrix/client/syncer.py", line 368, in _try_start
await self._start(filter_data)
File "/Users/juicepack/Code/matrix-appservice-kakaotalk/.venv/lib/python3.9/site-packages/mautrix/client/syncer.py", line 396, in _start
data = await self.sync(
File "/Users/juicepack/Code/matrix-appservice-kakaotalk/.venv/lib/python3.9/site-packages/mautrix/api.py", line 383, in request
return await self._send(method, full_url, req_content, query_params, headers or {})
File "/Users/juicepack/Code/matrix-appservice-kakaotalk/.venv/lib/python3.9/site-packages/mautrix/api.py", line 258, in _send
raise make_request_error(
mautrix.errors.request.MUnknownToken: synapse sync failed with status 401 Unauthorized
Seems initially booting up the bridge was fine but "syncing" e2ee (probably key exchange) is broken. After some digging, turns out that the mautrix-python library was using a protocol version that's heavily outdated and encountering some breaking changes on Beeper's side, specifically with regards to how Beeper's hungryserv matrix server /syncs the key exchange (for e2ee). Beeper is relatively new and launched way after when the official matrix spec deprecated sync [10] in favor of a new "encrypted appservice" protocol. I decided to just upgrade mautrix-python to latest to see if that fixes my issues, hoping nothing else broke in the process:
> claude -p "Upgrade mautrix-python to latest"
Another issue I ran into was installing python-olm. The underlying lib-olm requirements had listed cmake_minimum_required(VERSION 2.8), but I had CMake 4.0 which dropped backwards compatibility with anything under cmake_minimum_required=3.5. I checked out the issues section of the official repo to see if anyone was talking about it, and indeed there were some recent discussions[11]:
Just my luck, the repo was archived within a few hours of me seeing this post-- another first for me!
I had to build an old version of CMAKE and point the project dep at it, but then I ran into another pothole that was a bit out of my comfort zone:
error: cannot assign to variable 'other_pos' with const-qualified type
'T *const'
106 | ++other_pos;
| ^ ~~~~~~~~~
/Users/juicepack/.cache/uv/sdists-v9/pypi/python-olm/3.2.16/X3tRRBz4BsjsO6T-NIgG0/src/libolm/include/olm/list.hh:102:19:
note: variable 'other_pos' declared const here
102 | T * const other_pos = other._data;
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
I have no idea what this means. I haven't coded C/C++ in 12 years, ever since Bjarne Stroustrup told me during office hours that C++ wasn't for everyone after we tried to resuscitate my lab submission. Somebody else in the now-archived repo had just posted this exact same error[12], so I know it wasn't just me. I asked to explain the issue to me-- apparently, this was actually a real bug but people are only recently encountering it now because the new Clang version on Tahoe is not as lenient about this specific anti-pattern as older compilers were. It was able to fix it in 2 lines:
Before
List<T, max_size> & operator=(List<T, max_size> const & other) {
if (this == &other) {
return *this;
}
T * this_pos = _data;
T * const other_pos = other._data; // BUG HERE
while (other_pos != other._end) {
*this_pos = *other; // BUG HERE TOO
++this_pos;
++other_pos; // FAILS: can't increment
}
_end = this_pos;
return *this;
}
After
List<T, max_size> & operator=(List<T, max_size> const & other) {
if (this == &other) {
return *this;
}
T * this_pos = _data;
T const * other_pos = other._data; // pointer can move, data is read-only
while (other_pos != other._end) {
*this_pos = *other_pos; // dereference the pointer, not the list
++this_pos;
++other_pos; // now this works
}
_end = this_pos;
return *this;
}
Moment of truth:
> rm -rf ~/.cache/uv/sdists-v9/pypi/python-olm/3.2.16/.../src/libolm/build
> uv sync --extra sqlite --extra e2be
Output:
Resolved 31 packages in 16ms
Building python-olm==3.2.16
Built python-olm==3.2.16
Prepared 1 package in 20.70s
Uninstalled 1 package in 17ms
Installed 1 package in 1ms
+ python-olm==3.2.16
- setuptools==82.0.0
...
Done!
❯ uv run python -m matrix_appservice_kakaotalk
[2026-02-23 21:20:56,683] [[email protected]] Initializing matrix-appservice-kakaotalk 0.3.0+dev.2019485c
[2026-02-23 21:20:56,684] [[email protected]] Initialization complete in 0.04 seconds
[2026-02-23 21:20:56,684] [[email protected]] Running startup actions...
[2026-02-23 21:20:56,685] [[email protected]] Starting database...
[2026-02-23 21:20:56,685] [[email protected]] Connecting to sqlite:matrix-appservice-kakaotalk.db
[2026-02-23 21:20:56,685] [[email protected]] Database connection init commands: ['PRAGMA foreign_keys = ON', 'PRAGMA journal_mode = WAL', 'PRAGMA synchronous = NORMAL', 'PRAGMA busy_timeout = 5000']
[2026-02-23 21:20:56,686] [[email protected]] Request 1: register
[2026-02-23 21:20:56,686] [[email protected]] Received response 1
[2026-02-23 21:20:56,694] [[email protected]] Database at v4, not upgrading
[2026-02-23 21:20:56,695] [[email protected]] Database at v3, not upgrading
[2026-02-23 21:20:56,695] [[email protected]] Starting appservice...
[2026-02-23 21:20:56,695] [[email protected]] Starting appservice web server on localhost:11115
[2026-02-23 21:20:56,697] [[email protected]] Ensuring connectivity to homeserver
[2026-02-23 21:20:56,697] [[email protected]] req #1: GET https://matrix.beeper.com/_hungryserv/juice/_matrix/client/versions?user_id=@sh-kakaotalkbot:beeper.local None
[2026-02-23 21:20:57,257] [[email protected]] req #1 (/versions) completed in 559.9ms with status 200
[2026-02-23 21:20:57,258] [[email protected]] req #2: GET https://matrix.beeper.com/_hungryserv/juice/_matrix/client/v3/account/whoami?user_id=@sh-kakaotalkbot:beeper.local None
[2026-02-23 21:20:57,408] [[email protected]] req #2 (/v3/account/whoami) completed in 150.1ms with status 200
Finally, the bridge initialized without any issues. Now, time to login to Kakao via the Beeper Client. All I had to do was send login <MY_EMAIL> in a chat with the Bot user on my Beeper app.
[2026-02-23 21:24:06,260] [[email protected]] Failed to log in
[2026-02-23 21:24:06,264] [[email protected]] req #22: PUT https://matrix.beeper.com/_hungryserv/juice/_matrix/client/v3/rooms/...8?user_id=@sh-kakaotalkbot:beeper.local
{
"msgtype": "m.notice",
"body": "Failed to log in: Upgrade required (-999)",
"format": "org.matrix.custom.html",
"formatted_body": "<p>Failed to log in: Upgrade required (-999)</p>\n"
}
No worries, node-kakao is using a 4 year old version number in its request to the server, I just have to masquerade as a newer version. I googled it, changed it in the config, and rebooted:
[2026-02-23 22:04:10,074] [[email protected]] req #14: PUT https://matrix.beeper.com/_hungryserv/juice/_matrix/client/v3/rooms/...user_id=@sh-kakaotalkbot:beeper.local
{
"msgtype": "m.notice",
"body": "Failed to log in: Unknown error (-400)",
"format": "org.matrix.custom.html",
"formatted_body": "<p>Failed to log in: Unknown error (-400)</p>\n"
}
Well, I had a good run-- each of these thousand cuts just to get told by Kakao to go kms. Almost certainly KakaoTalk's client-server handshake schema has gone through a backwards-incompatible change in the last 4 years, and we're missing some kind of field, or some shibboleth has changed. I should have derisked this project by making sure the underlying SDK worked. Upon googling, there were no other newer SDKs and Kakao obviously does not support botting as a first class citizen (I looked at their docs to make sure). If I'm going to have to reverse engineer this auth flow from scratch, I sure as hell am not going to do it in The Only Real Dev Language. I told to port everything to go, which is feasible because the package isn't that big or complex.
> claude -p "Port this library to go, call it 암소 because kakao sounds like cow haha.
Validate handoff by getting a cmd auth login with credentials from .env to result in a 400"
> go run amso/cmd/auth_dry_run/*.go
Running auth dry run...
Found credentials! Logging in...
Error: response code 400, body <nil>
Working Backwards
Cool, we're back where we started, but I'm finally comfortable with what I'm looking at and am eager to dive in. My goal is to get a working SDK that can login, send a message, and receive a message, in some test groupchat.
My heart sank within 5 minutes of googling. Not only is the API undocumented, but they're also using some inane proprietary wire format called LOCO that pushes BSON payloads over raw TCP sockets for chat. I didn't expect this to be easy, but I really didn't think I'd ever have to open a hex dumper after college. Luckily, I'm standing on the shoulder of korean devs who already wrote a spec on the protocol[13], so I wouldn't start from nothing. I studied up on this, heavily leaned on code in teacher mode, and perused the LOCO logic in the new go repo that was already in
node-kakao (but is now very outdated). I learned that the auth flow isn't done on LOCO and just uses regular HTTPS instead. To figure this out we'll have to reverse engineer it.
But I don't have a reverse engineering background-- everything I have ever interfaced with in my career was already publicly documented and I never had to try and hack open an API. My instinct here was to see what the payload looked like from a real client and reimplement a masquerading of it myself-- monkey see, monkey do. I know you can't just open mitmproxy on top of an android emulator because HTTPS encrypts the content, and I need to see the content in order to replicate it.
But I also knew hackers have all sorts of legerdemain to get around this kind of thing. Sure enough, there's an established practice to use some kind of code injection toolkit called Frida that can patch an app in memory in order to prevent certificate pinning from throwing an exception when it sees the fake mitmproxy certificate. This way, I can make the device "trust" the mitmproxy certificate, so we're able to organically decrypt the payloads. I was extremely skeptical this would work-- if it were that easy to find and edit raw addresses, Cheat Engine would be popular for a lot more than just giving me infinite mesos on MapleStory in 2007.
Turns out, it's not as difficult as I thought it to be. I know enough about RE that there's a lot of complexity in having to hunt the exact raw address for some custom mystery code. However, I learned that certificate pinning is pretty standardized logic, especially on a mainstream open source OS like Android, and there's only a handful of ways to do it on the platform. There's entire public Frida script suites that already just hook into all the possible classes/methods by name that can be used to prevent certificate pining from throwing an exception. I'm not quite sure how the V8/Duktape stuff here works at a lower level, but my interpretation high level is something like this:
// OkHttp's CertificatePinner.check(), one of the aforementioned standardized certificate pinning methods
Java.perform(function() {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List')
.implementation = function(hostname, peerCertificates) {
return; // Frida uses Duktape to insert a return here without throwing-- pinning bypassed
// Unedited code, that would normally pin certificates and throw if tampered with
if badPin(hostname, peerCertificates) {
throw new Exception("some error...")
}
};
});
I should revisit how this works at a lower level, but so far I'm pretty comfortable with this layer of abstraction. It's just like monkey patching some standard library that you know the app is going to call. Seems simple enough to me, we can just get started with setting up mitmproxy.
Tricking KakaoTalk into using our fake certs so we can see the payloads
Something I learned through this process is that there's multiple formats for certs and we'll need to convert the cert that mitm (PEM) into a format that Java's certificate APIs are able to read (DER). Apparently PEM is just DER wrapped in base64 with the header/footer lines added for readability. TIL!
# Start mitmproxy to generate the certs
> ls ~/.mitmproxy/mitmproxy-ca-cert.cer 2>/dev/null || (mitmproxy &; sleep 2; kill %1)
# Convert formats
> openssl x509 -inform PEM -outform DER -in ~/.mitmproxy/mitmproxy-ca-cert.pem -out /tmp/cert-der.crt
# Push the DER cert into the emulator
> adb push /tmp/cert-der.crt /data/local/tmp/cert-der.crt
# Separate terminal
> mitmweb --listen-host 0.0.0.0 --listen-port 8080
Cool, mitmproxy is running now and we've pushed its cert to the emulator. Now to get frida hooked into the emulator:
# Get the right version of frida and install it, run as root, see if it can find kakao.
> adb shell getprop ro.product.cpu abi
arm64-v8a
> uv run --with frida-tools frida --version
Built frida-tools==14.6.0
Installed 7 packages in 8ms
17.7.3
> curl -L -o /tmp/frida-server.xz "https://github.com/frida/frida/releases/download/17.7.3/frida-server-17.7.3-android-arm64.xz"
> xz -d /tmp/frida-server.xz
> adb push /tmp/frida-server /data/local/tmp/frida-server
> adb root
> adb shell chmod 755 /data/local/tmp/frida-server
# 10.0.2.2 is the emulator's alias for your host machine
> adb shell settings put global http_proxy 10.0.2.2:8080
> adb shell /data/local/tmp/frida-server &
# Validate frida can see kakao
> uv run --with frida-tools frida-ps -U | grep -i kakao
3448 KakaoTalk
Running with the frida scripts from stulle123 cited below:
> uv run --with frida-tools frida -U -l scripts/frida-kakao-hook.js -f com.kakao.talk
____
/ _ | Frida 17.7.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
Spawned `com.kakao.talk`. Resuming main thread!
[Android Emulator 5554::com.kakao.talk ]-> [*] Frida HTTP interceptor loaded
[!] OkHttp3 not found: Error: java.lang.ClassNotFoundException: Didn't find class "okhttp3.RequestBody" on path: DexPathList[[zip file "/data/app/~~9Vk0TsT3dk67HGQwYGCgug==/com.kakao.talk-Xh3Lu-3Pl0Ce0_mpOCxFsg==/base.apk", zip...
[*] HttpURLConnection.getOutputStream hooked
[*] Custom TrustManager registered
[*] SSLContext.init hooked for SSL bypass
[!] CertificatePinner bypass failed: Error: java.lang.ClassNotFoundException: Didn't find class "okhttp3.CertificatePinner" on path: DexPathList[[dex file "/data/data/com.kakao.talk/frida6191012658721975805.dex"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64]]
[!] RealCall not found
[*] URL.openConnection hooked
[*] BufferedReader.readLine hooked
[KakaoClass] com.kakao.talk.core.loco.model.protocol.GetConf$Response$TrailerHighInfo
[KakaoClass] com.kakao.talk.shortform.data.model.remote.SftApiFetchContent$Request
[KakaoClass] com.kakao.talk.activity.authenticator.auth.AuthenticatorActivity$d$a
[KakaoClass] com.kakao.talk.core.loco.a$a
[KakaoClass] com.kakao.talk.core.loco.a$b
[KakaoClass] com.kakao.talk.core.loco.a$c
[KakaoClass] com.kakao.talk.core.loco.a$d
[KakaoClass] com.kakao.talk.core.loco.a$e
[KakaoClass] com.kakao.talk.core.loco.a$f
[KakaoClass] com.kakao.adfit.common.matrix.transport.HttpTransport
[KakaoClass] com.kakao.talk.core.loco.a$g
[KakaoClass] com.kakao.talk.core.loco.model.protocol.GetConf$Response$TrailerInfo$Companion
[KakaoClass] com.kakao.talk.core.loco.a$h
[KakaoClass] com.kakao.talk.core.loco.a$i
[KakaoClass] com.kakao.talk.shortform.data.model.remote.SftApiSlotSkeleton$FeaturedNoticeBanner$a
[KakaoClass] com.kakao.talk.shortform.data.model.remote.SftApiTrackingRequest
[KakaoClass] com.kakao.talk.loco.net.exception.LocoException
# ... Deleted like 200 lines of enumeration so I dont make it scroll forever
[*] Class enumeration complete
[*] All hooks installed. Trigger login in KakaoTalk now.
====================================
[URL.openConnection] https://aem-kakao-collector.onkakao.net/api/11919/envelope/
====================================
[*] SSLContext.init intercepted - injecting permissive TrustManager
[URL.openConnection] https://aem-kakao-collector.onkakao.net/api/11919/envelope/
Okay, so a lot going on here but mostly looks good. 3 important things:
- We're able to partially enumerate the class and methods, but some like
com.kakao.talk.core.loco.a$aare obfuscated, probably to intentionally make this core protocol logic hard to understand. - In particular, I failed to find (and hook into) OkHTTP because it was obfuscated
- But I did succeed in hooking into
SSLContext.init, another ssl pinning helper, because it wasn't obfuscated, as well as URL.openConnection, because it's a core Java OS method.
[*] SSLContext.init hooked for SSL bypass
[!] CertificatePinner bypass failed: Error: java.lang.ClassNotFoundException: Didn't find class "okhttp3.
[*] SSLContext.init intercepted - injecting permissive TrustManager
[URL.openConnection] https://aem-kakao-collector.onkakao.net/api/11919/envelope/
I visualize it in my head with this mental model:
┌──────────────────────┬───────────────────────────────────┬──────────────────────────────────┐
│ Layer │ Example │ Obfuscated? │
├──────────────────────┼───────────────────────────────────┼──────────────────────────────────┤
│ Android framework │ javax.net.ssl.SSLContext │ No (OS-level) │
├──────────────────────┼───────────────────────────────────┼──────────────────────────────────┤
│ KakaoTalk's own code │ com.kakao.talk.core.loco.* │ Mostly no │
├──────────────────────┼───────────────────────────────────┼──────────────────────────────────┤
│ Third-party libs │ okhttp3.*, okio.* │ Yes │
├──────────────────────┼───────────────────────────────────┼──────────────────────────────────┤
│ Internal helpers │ single-letter packages like Qs0.b │ Yes │
└──────────────────────┴───────────────────────────────────┴──────────────────────────────────┘
This may or may not work, since only some of the hooks worked. If the app uses the SSL method that we pinned, then we'll be able to make it use our SSL certs and crack open the encrypted payloads. Only one way to find out, so let's try and login. But first, you need to see this:
POST https://katalk.kakao.com/android/account2/login HTTP/1.1
{
"id": "my-email",
"password": "my-plaintext-password-what-the-hell"
}
Damn, that's crazy. The practices you can get away with when you're a monopoly. I don't know anything about security but even I know that you're supposed to at least match hashes or use a challenge protocol, anything but plaintext. What was Kakaotalk doing before they migrated to HTTPS, could anyone on your coffee shop wifi just pwn you?
Anyways, back to the login flow.
I'm only going to include the headers once since they're large but repetitive, but we'll need this info later when we're programming the SDK since we're trying to exactly replicate a working auth flow.
POST https://katalk.kakao.com/android/account2/login HTTP/1.1
X-VC: redacted
Accept-Language: en
User-Agent: KT/26.1.3 An/16 en
Device-Info: android/16; uuid=redacted; ssaid=redacted; model=SDK_GPHONE64_ARM64; screen_resolution=1280x2628; sim=310260/1/us; uvc3=redacted
A: android/26.1.3/en
ADID: redacted
C: redacted
Content-Type: application/json
POST https://katalk.kakao.com/android/account2/phone-number HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 21529
Connection: keep-alive
Set-SS: redacted
C: redacted
Kakao: Talk
Strict-Transport-Security: max-age=31536000; includeSubDomains
JSON
{
"status": 0,
"view": "phone-number",
"viewData": {
"countries": {
"all": [
{
"iso": "AF",
"code": "93",
"name": "Afghanistan"
},
{
"iso": "AL",
"code": "355",
"name": "Albania"
},
# etc etc etc same thing for every country in the world
It's just sending us a response for the client to show us country code options. I picked US and put my phone number in.
POST https://katalk.kakao.com/android/account2/phone-number HTTP/1.1
JSON
{
"countryCode": "1",
"countryIso": "US",
"phoneNumber": "+redacted",
"termCodes": [
"COPPA_PRIVACY_POLICY_TERM"
]
}
HTTP/1.1 200 OK
{
"status": 0,
"view": "mo-send",
"viewData": {
"moNumber": "+44 7860064888",
"moMessage": "KakaoTalk redacted",
"phoneNumber": {
"countryIso": "US",
"countryCode": "1",
"pstnNumber": "redacted",
"beautifiedPstnNumber": "+1 redacted",
"nsnNumber": "redacted",
"beautifiedNsnNumber": "redacted"
}
}
"nonce": "redacted"
}
This moves you to a view that makes you send a text message to the phone number from the response, as a form of reverse authentication. Why did they do this though, can't you just spoof phone numbers? I should revisit that later.
# Tell them you sent the text, it's with a button on the UI
POST https://katalk.kakao.com/android/account2/mo-sent HTTP/1.1
# Reponse is just same as previous for some reason
POST https://katalk.kakao.com/android/account2/mo-confirm HTTP/1.1
HTTP/1.1 200 OK
{
"status": 0,
"view": "web-view",
"viewData": {
"url": "https://katalk.kakao.com/android/account2/view/confirm-device-change?lang=en&sessionToken=REDACTED&appVersion=26.1.3",
"callbackPath": "passcode/callback"
}
}
We got the session token! Yay!
Let me just start a chat to see what else I can figure out:
Okay... so the auth flow uses a certain SSL validation path and the chat app flow uses a different one? Credit given where credit due, they knew they were using a different method so they double checked the certificate again, even though one might assume after the auth flow that everything is already kosher.
After doing some more research, I learned that the "view": "web-view" part of the response is a nod to a specific standardized module in Android for showing web content and it likely uses a different SSL verification stack then the one used to login. The error message Content with a security certificate issue has been blocked in the current page is a clear sign it's not using the certs we tried to trick the app into using. I asked to add a hook for the WebView to see if I can hook into the SSL verification flow used by WebView:
try {
var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
console.log("[*] WebView SSL error bypassed for: " + error.getUrl());
handler.proceed();
};
console.log("[*] WebView SSL bypass hooked");
} catch(e) {
console.log("[!] WebView SSL bypass failed: " + e);
}
✦ ❯ uv run --with frida-tools frida -U -l scripts/frida-kakao-hook.js -f com.kakao.talk | grep WebView
[*] WebView SSL bypass hooked
...
# Nothing else happened when I logged in
No dice, I loaded the hook successfully but it never triggered. That means KakaoTalk isn't using the base android.webkit.WebViewClient. They must have a subclass that overrides onReceivedSslError itself, and Java dispatches to the subclass method, skipping our hook on the parent. Let's enumerate anything for WebView or webview within the kakao package to see if I can find it.
[Android Emulator 5554::com.kakao.talk ]->
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(c) {
if (c.indexOf("kakao") !== -1 && (c.indexOf("WebView") !== -1 || c.indexOf("webview") !== -1 || (c.indexOf("Web
") !== -1 &&
c.indexOf("Client") !== -1))) {
console.log(c);
}
},
onComplete: function() { console.log("done"); }
});
});
com.kakao.talk.net.retrofit.service.account.WebViewViewData$Companion
com.kakao.talk.net.retrofit.service.account.WebViewViewData
com.kakao.talk.widget.webview.WebViewHelper$Companion
com.kakao.talk.widget.webview.CommonWebViewListener
com.kakao.talk.web.EasyWebViewLoadBypassBehavior$DefaultLoadBypassBehavior
com.kakao.talk.webview.WebViewModuleFacadeFactory
com.kakao.talk.webview.activity.BookingWebActivity
com.kakao.talk.web.EasyWebViewLoadBypassBehavior
com.kakao.talk.net.retrofit.service.account.WebViewViewData$a
com.kakao.talk.net.retrofit.service.account.WebViewViewData$b
com.kakao.talk.module.webview.contract.WebViewModuleFacade
com.kakao.talk.web.EasyWebViewLoadBypassBehavior$DefaultLoadBypassBehavior$a
com.kakao.talk.web.EasyWebViewAllowHost$DefaultAllowHost$a
com.kakao.talk.module.webview.contract.WebViewModuleFacade$a
com.kakao.talk.widget.webview.Q
com.kakao.talk.widget.webview.WebViewHelper
com.kakao.talk.widget.webview.WebViewHelper$UrlProcessResultListener
com.kakao.talk.web.EasyWebViewAllowHost
com.kakao.talk.web.EasyWebViewAllowHost$DefaultAllowHost
com.kakao.talk.module.webview.contract.a
done
[Android Emulator 5554::com.kakao.talk ]->
Could it be? Is com.kakao.talk.widget.webview.Q our grail?
[Android Emulator 5554::com.kakao.talk ]-> Java.perform(function() { try { var cls = Java.use("com.kakao.talk.widget.webview.
Q"); console.log("Parent: " +cls.class.getSuperclass().getName()); } catch(e) { console.log(e); } });
Parent: java.lang.Object
Bummer. We're looking for a class with the parent android.webkit.WebViewClient. Let's check the rest of the ones I printed above
[Android Emulator 5554::com.kakao.talk ]-> Java.perform(function() { try { var cls = Java.use("com.kakao.talk.widget.webview.
Q"); console.log("Parent: " +cls.class.getSuperclass().getName()); } catch(e) { console.log(e); } });
Parent: java.lang.Object
[Android Emulator 5554::com.kakao.talk ]-> Java.perform(function() { try { var cls = Java.use("com.kakao.talk.widget.webview.
CommonWebViewListener");
console.log("CommonWebViewListener parent: " + cls.class.getSuperclass().getName()); } catch(e) { console.log(e); } });
TypeError: cannot read property 'getName' of null
[Android Emulator 5554::com.kakao.talk ]-> Java.perform(function() { try { var cls = Java.use("com.kakao.talk.widget.webview.
CommonWebViewListener");console.log("CommonWebViewListener parent: " + cls.class.getSuperclass().getName()); } catch(e) { con
sole.log(e); } });
TypeError: cannot read property 'getName' of null
[Android Emulator 5554::com.kakao.talk ]-> Java.perform(function() { ["com.kakao.talk.widget.webview.WebViewHelper", "com.kak
ao.talk.webview.WebViewModuleFacadeFactory",
"com.kakao.talk.webview.activity.BookingWebActivity", "com.kakao.talk.web.EasyWebViewLoadBypassBehavior",
"com.kakao.talk.web.EasyWebViewLoadBypassBehavior$DefaultLoadBypassBehavior", "com.kakao.talk.module.webview.contract.WebVi
ewModuleFacade",
"com.kakao.talk.module.webview.contract.a"].forEach(function(name) { try { var s = Java.use(name).class.getSuperclass(); co
nsole.log(name +
" -> " + (s ? s.getName() : "null")); } catch(e) { console.log(name + " -> ERROR"); } }); });
com.kakao.talk.widget.webview.WebViewHelper -> java.lang.Object
com.kakao.talk.webview.WebViewModuleFacadeFactory -> Ps0.c
com.kakao.talk.webview.activity.BookingWebActivity -> com.kakao.talk.web.EasyWebActivity
com.kakao.talk.web.EasyWebViewLoadBypassBehavior -> java.lang.Object
com.kakao.talk.web.EasyWebViewLoadBypassBehavior$DefaultLoadBypassBehavior -> com.kakao.talk.web.EasyWebViewLoadBypassBehavior
com.kakao.talk.module.webview.contract.WebViewModuleFacade -> null
com.kakao.talk.module.webview.contract.a -> java.lang.Object
[Android Emulator 5554::com.kakao.talk ]->
Nope, so we're SOL. It's probably some obfuscated name that doesn't have kakao or WebView in the names. I even tried to enumerate over all subclasses and adb just choked and died.
Decompiling the APK to find the right subclass methods to hook
According to , the next step was to decompile the apk. I can use a tool called
JADX that turns the compiled bytecode back into readable Java source. So instead of searching for names that I think overrides onReceivedSslError, we'll just decompile and look for the method that references what it's overriding by name.
Normally, byte code doesn't have the original english names of functions and methods and variables saved from the source code. But if you want to override a method in Java you do need to name it literally and literals are preserved during compilation. This is how I just learned that Java resolves a lot of things at runtime. It uses a virtual method table (vtable)[14], which looks up the method by name and signature to dispatch the call to the correct override. If the subclass renamed onReceivedSslError to a(), Java wouldn't find the override and would have fallen back to the parent's default implementation (which just calls handler.cancel() from our Frida hook). This is different from languages like C/C++ where method calls are resolved at compile time to memory addresses.
I bet if I just extract the apk (which I learned is just a zip file!) and grep for onReceivedSslError within the bytecode, I'll get tons of results:
> unzip -o /tmp/kakao-base.apk -d /tmp/kakao-full && strings /tmp/kakao-full/classes*.dex | grep onReceivedSslError
# Lots of results!
onReceivedSslError
onReceivedSslError
onReceivedSslError
onReceivedSslError
onReceivedSslError
onReceivedSslError
This is just the bytecode, not even the decompiled Java, but this quirk of the language saves us a lot of work here. I can grep -r "onReceivedSslError" on the decompiled Java and instantly find the exact obfuscated class name (like a0.b or whatever) that overrides it. Once I have that class name, I can go back to Frida and hook that specific class instead of the base android.webkit.WebViewClient. I'm hoping to find something like this:
package a0;
public class b extends android.webkit.WebViewClient {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.cancel();
}
}
Let's run the decompile and grab a coffee, as it'll take a while:
❯ jadx -d /tmp/kakao-jadx /tmp/kakao-base.apk
INFO - loading ...
INFO - processing ...
ERROR - finished with errors, count: 435
Some errors, which is fine. Compiling is lossy and there's just some stuff that gets flattened that you'll never get back. The point isn't to get perfectly running Java, but to get some semblance of what the original Java source code looked like. Let's just grep it and see what we get:
# I invert grep @Metadata because it was spammy expanded encoding of Kotlin metadata which makes looking at the output hard
grep -r "onReceivedSslError" /tmp/kakao-jadx/sources/ | grep -v "@Metadata"
/tmp/kakao-jadx/sources/PC/C16030a.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/C10/L.java: public void onReceivedSslError(final WebView view, SslErrorHandler handler, final SslError error) {
/tmp/kakao-jadx/sources/com/iap/android/mppclient/container/presenter/ACWebViewClient.java: receivedSslErrorHandler.onReceivedSslError(new ACWebView(webView), new ACSslErrorHandler(sslErrorHandler), sslError);
/tmp/kakao-jadx/sources/com/iap/android/mppclient/container/presenter/ACWebViewClient.java: public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
/tmp/kakao-jadx/sources/com/iap/android/mppclient/container/presenter/ACWebViewClient.java: super.onReceivedSslError(webView, sslErrorHandler, sslError);
/tmp/kakao-jadx/sources/com/iap/android/mppclient/container/provider/ReceivedSslErrorHandler.java: void onReceivedSslError(ContainerWebView containerWebView, ISslErrorHandler iSslErrorHandler, SslError sslError);
/tmp/kakao-jadx/sources/com/kakao/talk/kakaopay/webview/common/PayCommonWebViewActivity.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/kakaopay/webview/common/PayCommonWebViewActivity.java: super.onReceivedSslError(view, handler, error);
/tmp/kakao-jadx/sources/com/kakao/talk/kakaopay/pg/presentation/pg/PayPgWebViewActivity.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/kakaopay/billgates/presentation/billgates/PayBillgatesWebViewActivity.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/widget/webview/CommonWebLayout.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/widget/webview/CommonWebLayout.java: webViewHelper.onReceivedSslError(view, handler);
/tmp/kakao-jadx/sources/com/kakao/talk/widget/webview/WebViewHelper.java: public final void onReceivedSslError(WebView view, SslErrorHandler handler) {
/tmp/kakao-jadx/sources/com/kakao/talk/widget/CommonWebViewClient.java: public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
/tmp/kakao-jadx/sources/com/kakao/talk/widget/CommonWebViewClient.java: WebViewHelper.getInstance().onReceivedSslError(webView, sslErrorHandler);
/tmp/kakao-jadx/sources/com/kakao/talk/activity/browser/PlusMessageShareWebViewActivity.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/jordy/presentation/search/webview/JdSearchWebLayout.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/jordy/presentation/search/webview/JdSearchWebLayout.java: JdSearchWebLayout.this.webViewHelper.onReceivedSslError(view, handler);
/tmp/kakao-jadx/sources/com/kakao/talk/web/k.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/webview/activity/RewardAdWebViewActivity.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/webview/activity/RewardAdWebViewActivity.java: WebViewHelper.INSTANCE.getInstance().onReceivedSslError(view, handler);
/tmp/kakao-jadx/sources/com/kakao/talk/channel/customweb/PlusFriendCustomWebClientDelegator.java: public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
/tmp/kakao-jadx/sources/com/kakao/talk/channel/customweb/PlusFriendCustomWebClientDelegator.java: PlusFriendCustomWebClientDelegator.this.webViewHelper.onReceivedSslError(view, handler);
It seems there are multiple WebViewClient subclasses:
C10.L— extendsandroid.webkit.WebViewClientdirectlycom.kakao.talk.web.k— extendsC10Lcom.kakao.talk.widget.CommonWebViewClient— extendsWebViewClientPc.a$b(inner class inPC/C16030a.java) — extendsWebViewClient
And they mostly delegate to WebViewHelper's onReceivedSslError(view, handler) which is the exception throwing code we're trying to hook into. I actually saw this class earlier when I enumerated the classes but I guess I wrote it off because it didn't have the right package name I was looking for since the code calls this method in a roundabout way.
So I have the name now but let's just just see what the code does just in case:
grep -A 15 "public final void onReceivedSslError" /tmp/kakao-jadx/sources/com/kakao/talk/widget/webview/WebViewHelper.java
public final void onReceivedSslError(WebView view, SslErrorHandler handler) {
Intrinsics.j(view, "view");
Intrinsics.j(handler, "handler");
handler.cancel();
Context context = view.getContext();
Intrinsics.i(context, "getContext(...)");
Z.g(R.string.toast_for_ssl_warning, 0, context, 0L, 10, null);
}
Cool this is exactly what I wanted. It's final too, so I know every class call ends up here since it can't be overriden. This isn't the only flow though, there's also
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
C16030a.this.listener.B0("ProtocolError", "SSL HANDSHAKE ERROR");
handler.cancel(); // ← kills the connection directly, does NOT delegate to WebViewHelper
}
This one doesn't delegate to WebViewHelper — it calls handler.cancel() directly. And C10.L also has its own complex logic (checking isForeground, adding to a list, etc.) that doesn't go through WebViewHelper either.
I don't need to choose one though, let's just hook into all of them.
// Frida script to bypass KakaoTalk SSL pinning on Android
Java.perform(function() {
console.log("[*] Frida hook loaded");
// === SSL pinning bypass (TrustManager) ===
try {
var SSLContext = Java.use("javax.net.ssl.SSLContext");
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var TrustManager = Java.registerClass({
name: "com.frida.TrustManager",
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() { return []; }
}
});
SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(km, tm, sr) {
console.log("[*] SSLContext.init intercepted");
this.init(km, [TrustManager.$new()], sr);
};
console.log("[+] SSLContext.init hooked");
} catch(e) {
console.log("[!] SSL bypass failed: " + e);
}
// === WebView SSL bypass — hook all known WebViewClient subclasses ===
// WebViewHelper — central helper most WebViewClients delegate to
try {
var WebViewHelper = Java.use("com.kakao.talk.widget.webview.WebViewHelper");
WebViewHelper.onReceivedSslError.implementation = function(view, handler) {
console.log("[*] WebViewHelper SSL bypassed");
handler.proceed();
};
console.log("[+] WebViewHelper.onReceivedSslError hooked");
} catch(e) {
console.log("[!] WebViewHelper hook failed: " + e);
}
// Pc.a$b — auth WebView client, calls handler.cancel() directly
try {
var PcAB = Java.use("Pc.a$b");
PcAB.onReceivedSslError.implementation = function(view, handler, error) {
console.log("[*] Pc.a$b SSL bypassed");
handler.proceed();
};
console.log("[+] Pc.a$b.onReceivedSslError hooked");
} catch(e) {
console.log("[!] Pc.a$b hook failed: " + e);
}
// C10.L — WebViewClient with its own SSL handling
try {
var C10L = Java.use("C10.L");
C10L.onReceivedSslError.implementation = function(view, handler, error) {
console.log("[*] C10.L SSL bypassed");
handler.proceed();
};
console.log("[+] C10.L.onReceivedSslError hooked");
} catch(e) {
console.log("[!] C10.L hook failed: " + e);
}
// Base class catch-all
try {
var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
console.log("[*] WebViewClient SSL bypassed: " + error.getUrl());
handler.proceed();
};
console.log("[+] WebViewClient.onReceivedSslError hooked");
} catch(e) {
console.log("[!] WebViewClient hook failed: " + e);
}
console.log("[*] All hooks installed. Trigger login in KakaoTalk now.");
});
Okay, none of this worked. I still get the tampered certificate error message and a white screen, and nothing is getting hooked. I asked and the certs loading may be happening through a sealed, updatable system component at boot called Conscrypt APEX [15]. Since it's loaded into memory on boot, copying it over to the system certs dir after boot doesn't do anything. We'd have to hunt the raw memory address, the exact thing I was terrified this would lead to (albeit this happened in a roundabout way).
It was all for nothing
This isn't the end of the road for me: I have a few options I could pursue, such as downgrading the emulator OS to before the bootloader saved the certs into memory (but who knows if the app will work on that version). But this is one of those important inflection points in a developer's journey where you have to take a step back and ask what the hell we're doing here. The whole reason I went down this rabbit hole was that the node-kakao SDK returns a 400 when I try to login and I need to observe and mimic the exchange pattern on a working demo. I did that with mitmproxy, and I figured I might as well just see what else I can capture. But as I already mentioned, the LOCO protocol isn't even HTTP so mitmproxy wouldn't have captured it anyways, and I already have a lot of the core logic from blog posts, the decompiled Java, and the node-kakao SDK (which was now converted to Go for even better readability for me).
I edited amso's login code to use what we learned and edited the headers. This worked, but I learned something hilarious: Kakao enforces a single-device-only flow for mobile devices, so the flow I just reverse engineered only works if we're willing to accept that users will get logged out of their own kakao account on their phone. This is likely a huge deal breaker for most people, and this also means I can't use any of our learnings from this whole fiasco. We have to reverse engineer the Mac app, which uses some secondary device auth flow that doesn't log you out. Luckily, I don't need to repeat the reverse engineering hunt on the Mac app, as the decompiled Android app has a lot of the information I was looking for, like the Mac app's special login point and the Mac XVC format. I don't know why Mac client logic is in the Android APK, but it is.
I was really, really tired of it all by this point. I just wanted a bridge and here I am in my second traversal of getting an SDK to work. In the great words of John Carmack[16]:
> claude -p "Read the decompiled kakaotalk apk in /tmp/kakao-decompiled and the outdated
go port in this working directory. Figure out the gaps and get a login flow to return a 200 and
to generate a session token. Stretch goal is to send a message to `juicepack` which is my alt account. Use my credentials in .env"
Done! Here's what I changed:
┌──────────────────────┬──────────────────┬─────────────────────────────┐
│ What │ node-kakao │ Current (Feb 2026) │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ RSA public key │ 0xF3188... │ 0xA3B076... (rotated) │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ RSA handshake type │ 12 │ 16 │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ ticket.lsl (booking) │ string │ array of strings │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ Checkin port field │ ticket.lslp │ wifi.ports[0] │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ Response status │ packet header │ BSON body status field │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ Working agent │ win32 │ mac only │
├──────────────────────┼──────────────────┼─────────────────────────────┤
│ Device registration │ assumed pre-done │ full passcode flow required │
└──────────────────────┴──────────────────┴─────────────────────────────┘
Cool so Kakao
- rotated a key for the handshake
- they changed the format of some of the payloads
- they changed the location of some data within payloads
- swapped right shibboleth for a valid exchange
But the core logic is mostly the same for auth, and I just make some tweaks on top of it to catch up past the breaking changes in the past 4 years, and I got a working login and sending a message. I also generated an e2e test to login, send a message to self, receive a message, react, edit, delete, handle media, and more. Once this passed, I felt satisfied that I can leave this as is until it's time to implement the bridge, since that's this SDK's whole purpose anyways. Likely we'll have to massage some of the SDK to make working with the bridge more smooth, or even implement some missing functionality that I haven't anticipated yet.
What's next
I'll make a separate blog post about my journey with making the bridge, now that the SDK is working. Somehow I think anything in the future related to Kakao will end up being just as much of a clownfest.
I don't really feel like the initial foray into APK reverse engineering was a waste of time. I learned a lot, and it was fun, and I can't deny that it was exciting when I got to see the raw contents of what an app was trying to obfuscate with SSL. But if this were Work and I were trying to be efficient, I would definitely have derisked way earlier. But I'm not at work-- I'm at the Recurse Center, and what is all this time for if not to dive into rabbit holes and share my wonderful learnings :)
Reach out to me at [email protected] if you have any suggestions, comments, or feedback.
"Is South Korea Disappearing?", The New York Times, Dec 2, 2023. South Korea's fertility rate dropped to a world-low of 0.72 in 2023, far below the 2.1 replacement level needed to sustain a population. ↩︎
"Digital 2026: South Korea", DataReportal. Comprehensive overview of South Korea's digital landscape, including messaging app usage statistics. ↩︎
"Chaebol", Wikipedia. Overview of South Korea's large family-controlled industrial conglomerates that dominate the economy. ↩︎
"A Study on Social Conformity in South Korean Society", Korean Journal. Academic paper examining cultural conformity pressures in South Korean society. ↩︎
"카카오톡/문제점 및 비판", Namuwiki. A community-maintained catalogue of KakaoTalk's issues, criticisms, and controversies (in Korean). ↩︎
"KakaoTalk Account Takeover", stulle123. A security writeup detailing a one-click account takeover vulnerability in KakaoTalk. ↩︎
"KakaoTalk Secret Chat", stulle123. Analysis revealing that KakaoTalk's "secret chat" mode doesn't actually provide meaningful end-to-end encryption. ↩︎
"Matrix.org", The Matrix.org Foundation. The open standard for secure, decentralised, real-time communication. ↩︎
"KakaoTalk Bridge", Matrix.org. The official Matrix ecosystem listing for the KakaoTalk bridge. ↩︎
"MSC3202: Encrypted Appservice Protocol", Matrix Spec Proposals. The proposal that deprecated the old
/sync-based key exchange in favor of a new encrypted appservice protocol. ↩︎"CMake minimum version issue", matrix-org/olm. Discussion about
cmake_minimum_requiredbackwards compatibility breaking with newer CMake versions. ↩︎"Build error with new Clang", matrix-org/olm. Report of the same C/C++ compilation error caused by stricter compiler enforcement in newer Clang versions. ↩︎
"카카오톡 LOCO 프로토콜 분석", bpak.org (archived). Korean developer's reverse engineering analysis of KakaoTalk's proprietary LOCO wire protocol. ↩︎
"JVM Spec §5.4.3.3 - Method Resolution", Oracle. The JVM specification section explaining how Java resolves method calls at runtime using virtual method tables. ↩︎
"Conscrypt APEX Module", Android Open Source Project. Documentation on Android's updatable Conscrypt security module that handles TLS certificate validation at the system level. ↩︎
"John Carmack on OpenGL", rmitz.org. Classic collection of John Carmack's writings on graphics programming and engineering philosophy. ↩︎