How Buffer Overflow in SMS Response Buffer Handling Happens in C and How to Fix It
Introduction
The sm_at_sms.c file is responsible for handling incoming SMS messages in an embedded modem application — including the tricky case of concatenated SMS (long messages split across multiple parts). The sms_concat_handle() function assembles these parts into a single response buffer, sms_ctx.concat_rsp_buf. But a flaw in how three consecutive string operations wrote into that buffer created a critical, exploitable memory corruption vulnerability.
At line 127, sprintf() wrote the SMS header and first payload segment with no length limit. At line 137, strcpy() copied subsequent payload segments with no bounds check. At line 149, strcat() concatenated all segments together — again, without knowing whether the destination buffer had room. Any one of these would be a problem. Together, they compounded the overflow risk dramatically.
For developers working on embedded C, modem firmware, or any code that processes external message payloads, this is a textbook example of how "it works in testing" can mask a real-world attack surface.
The Vulnerability Explained
What Went Wrong
The vulnerable code in sms_concat_handle() used three unsafe C string functions in sequence to build a response buffer from incoming SMS data:
1. Unbounded sprintf() at line 127:
// VULNERABLE — no size limit on the write
sprintf(sms_ctx.concat_rsp_buf,
"\r\n#XSMS: \"%02d-%02d-%02d %02d:%02d:%02d "
"UTC%+03d:%02d\",\"%s\",\"%s",
header->time.year, header->time.month, header->time.day,
header->time.hour, header->time.minute, header->time.second,
header->time.utc_offset_h, header->time.utc_offset_m,
header->originating_address.address_str,
data->payload);
sprintf() writes into sms_ctx.concat_rsp_buf without knowing its size. If data->payload is larger than expected, the write overflows the buffer.
2. Unbounded strcpy() at line 137:
// VULNERABLE — copies payload with no length check
strcpy(sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
(header->concatenated.seq_number - 1) * SMS_MAX_PAYLOAD_LEN_CHARS,
data->payload);
For subsequent SMS segments (sequence number > 1), this strcpy() writes data->payload at a computed offset into the buffer. If data->payload exceeds SMS_MAX_PAYLOAD_LEN_CHARS, it overflows into whatever memory follows.
3. Unbounded strcat() at line 149:
// VULNERABLE — concatenates without knowing remaining buffer space
for (int i = 1; i < (sms_ctx.total_msgs); i++) {
strncat(sms_ctx.concat_rsp_buf,
sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
i * SMS_MAX_PAYLOAD_LEN_CHARS,
SMS_MAX_PAYLOAD_LEN_CHARS);
}
strcat(sms_ctx.concat_rsp_buf, "\"\r\n");
Even the strncat() here is dangerous — its third argument limits how many bytes are appended, but does not account for how full the destination buffer already is. And the final strcat() has no limit at all.
How an Attacker Could Exploit This
SMS protocols allow message payloads up to specific sizes, but a malicious or malformed SMS source (or a compromised radio stack) could send a payload that exceeds SMS_MAX_PAYLOAD_LEN_CHARS. Because none of the three string operations validate the incoming length against the buffer capacity, an attacker who can inject an oversized payload into the SMS processing pipeline could:
- Overflow
sms_ctx.concat_rsp_bufby sending a first SMS segment with a payload longer than the allocated space. - Corrupt adjacent stack or heap memory — depending on where
sms_ctxlives, this could overwrite return addresses, function pointers, or other critical data structures. - Achieve arbitrary code execution on the embedded device, or at minimum cause a crash (denial of service) in the modem application.
The three consecutive unsafe operations are particularly dangerous: even if the first sprintf() doesn't overflow, the strcpy() for a later segment or the strcat() loop during reassembly can still trigger corruption.
Real-World Impact
In an embedded modem context, this kind of vulnerability is especially serious. Modem firmware often runs with elevated privileges and minimal OS-level memory protections. A successful exploit could allow an attacker to:
- Hijack the modem's execution flow
- Exfiltrate data processed by the device
- Brick the device through memory corruption
- Use the modem as a pivot point in a larger attack against connected systems
The Fix
The fix replaces all three unsafe operations with their bounds-aware counterparts, and introduces an explicit buf_size calculation to ensure every write is constrained to the actual allocated buffer.
Before and After
Fix 1: sprintf() → snprintf() with explicit size
// BEFORE
sprintf(sms_ctx.concat_rsp_buf,
"\r\n#XSMS: \"%02d-%02d-%02d %02d:%02d:%02d "
"UTC%+03d:%02d\",\"%s\",\"%s",
...
data->payload);
// AFTER
snprintf(sms_ctx.concat_rsp_buf,
SM_SMS_AT_HEADER_INFO_MAX_LEN + SMS_MAX_PAYLOAD_LEN_CHARS,
"\r\n#XSMS: \"%02d-%02d-%02d %02d:%02d:%02d "
"UTC%+03d:%02d\",\"%s\",\"%s",
...
data->payload);
snprintf() accepts a maximum number of bytes to write (including the null terminator), so even an oversized data->payload cannot overflow the buffer.
Fix 2: strcpy() → snprintf("%s") with size limit
// BEFORE
strcpy(sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
(header->concatenated.seq_number - 1) * SMS_MAX_PAYLOAD_LEN_CHARS,
data->payload);
// AFTER
snprintf(sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
(header->concatenated.seq_number - 1) * SMS_MAX_PAYLOAD_LEN_CHARS,
SMS_MAX_PAYLOAD_LEN_CHARS,
"%s", data->payload);
Using snprintf() with SMS_MAX_PAYLOAD_LEN_CHARS as the size limit ensures each segment slot in the buffer can only receive exactly as much data as was allocated for it. Note the use of "%s" as the format string — this is important to avoid format string injection if data->payload ever contains % characters.
Fix 3: strncat()/strcat() → strcat_s() with computed buf_size
// BEFORE
for (int i = 1; i < (sms_ctx.total_msgs); i++) {
strncat(sms_ctx.concat_rsp_buf,
sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
i * SMS_MAX_PAYLOAD_LEN_CHARS,
SMS_MAX_PAYLOAD_LEN_CHARS);
}
strcat(sms_ctx.concat_rsp_buf, "\"\r\n");
// AFTER
uint16_t buf_size = SM_SMS_AT_HEADER_INFO_MAX_LEN +
SMS_MAX_PAYLOAD_LEN_CHARS * sms_ctx.total_msgs;
for (int i = 1; i < (sms_ctx.total_msgs); i++) {
strcat_s(sms_ctx.concat_rsp_buf, buf_size,
sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
i * SMS_MAX_PAYLOAD_LEN_CHARS);
}
strcat_s(sms_ctx.concat_rsp_buf, buf_size, "\"\r\n");
The key improvement here is the introduction of buf_size, computed as the total allocated size of concat_rsp_buf based on the header size plus one slot per expected message. strcat_s() uses this total size to prevent any write that would exceed the buffer's capacity — unlike strncat(), which only limits the appended bytes and ignores how full the destination already is.
Why Each Change Matters
| Unsafe Function | Problem | Safe Replacement | Why It's Better |
|---|---|---|---|
sprintf() |
No write limit | snprintf() with explicit size |
Truncates output if payload exceeds limit |
strcpy() |
Copies until null byte | snprintf("%s", ...) with size |
Limits copy to per-slot allocation |
strncat() |
Limits appended bytes, not total | strcat_s() with buf_size |
Limits based on total buffer capacity |
strcat() |
No limit whatsoever | strcat_s() with buf_size |
Same as above |
Prevention & Best Practices
Never Use These Functions with Untrusted Input
The C standard library functions sprintf(), strcpy(), strcat(), and gets() are sometimes called the "C hall of shame" — they have no concept of buffer size and should be treated as deprecated in any security-sensitive context:
// ❌ Never use these with external data
sprintf(buf, fmt, user_data);
strcpy(dst, user_data);
strcat(dst, user_data);
gets(buf);
// ✅ Always use bounds-checked alternatives
snprintf(buf, sizeof(buf), fmt, user_data);
snprintf(dst, sizeof(dst), "%s", user_data); // or strncpy + explicit null
strcat_s(dst, sizeof(dst), user_data); // C11 Annex K
fgets(buf, sizeof(buf), stdin);
Compute Buffer Sizes Explicitly
In the fixed code, buf_size is computed as:
uint16_t buf_size = SM_SMS_AT_HEADER_INFO_MAX_LEN +
SMS_MAX_PAYLOAD_LEN_CHARS * sms_ctx.total_msgs;
This pattern — calculating the expected maximum size from known constants and runtime values — should be standard practice whenever you're building a buffer from multiple variable-length pieces.
Use sizeof() Where Possible
For stack-allocated buffers, prefer sizeof(buf) over a hardcoded constant:
char buf[256];
snprintf(buf, sizeof(buf), "%s", input); // sizeof automatically tracks the size
Enable Compiler Warnings and Sanitizers
Modern compilers and sanitizers can catch many of these issues at build time or runtime:
# GCC/Clang: enable relevant warnings
gcc -Wall -Wformat -Wformat-overflow -Wformat-truncation ...
# AddressSanitizer: catches buffer overflows at runtime during testing
gcc -fsanitize=address ...
# For embedded: consider using a static analysis tool like cppcheck
cppcheck --enable=all sm_at_sms.c
Relevant Standards
- CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
- CWE-121: Stack-based Buffer Overflow
- OWASP: Input Validation and Output Encoding are foundational controls
- SEI CERT C Coding Standard: Rules STR31-C (guarantee storage for strings) and STR07-C (use TR 24731 for remediation)
Key Takeaways
sprintf()with a user-controlled format argument or payload is never safe — insms_concat_handle(), the SMS payload flowed directly intosprintf()with no size limit, making every incoming SMS a potential overflow trigger.- Three consecutive unsafe operations compound risk — even if one operation doesn't overflow on its own, the combination of
sprintf()+strcpy()+strcat()in the same function creates multiple attack surfaces that are harder to reason about. strncat()is not as safe as it looks — its third argument limits appended bytes, not total buffer usage;strcat_s()with the full buffer size is the correct replacement.- Always compute
buf_sizefrom constants you control — the fix's introduction ofbuf_size = SM_SMS_AT_HEADER_INFO_MAX_LEN + SMS_MAX_PAYLOAD_LEN_CHARS * sms_ctx.total_msgsmakes the buffer's capacity explicit and auditable. - Embedded and modem firmware deserves the same security rigor as application code — the lack of OS memory protections in these environments makes buffer overflows more exploitable, not less.
How Orbis AppSec Detected This
- Source: Incoming SMS message payload (
data->payload) received from the radio/modem stack and passed intosms_concat_handle()inapp/src/sm_at_sms.c - Sink: Unbounded
sprintf()at line 127,strcpy()at line 137, andstrcat()at line 149, all writing into the fixed-size buffersms_ctx.concat_rsp_buf - Missing control: No length validation of
data->payloadagainst the remaining capacity ofsms_ctx.concat_rsp_bufbefore any of the three write operations - CWE: CWE-120 (Buffer Copy without Checking Size of Input) / CWE-121 (Stack-based Buffer Overflow)
- Fix: Replaced
sprintf()withsnprintf()(explicit size),strcpy()withsnprintf("%s", ...)(per-slot size), andstrncat()/strcat()withstrcat_s()(computedbuf_size)
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Buffer overflows caused by unsafe C string functions are among the oldest and most well-understood vulnerability classes in software security — yet they continue to appear in production codebases, particularly in embedded and firmware contexts where external data (like SMS payloads) flows into fixed-size buffers. The three-function combination of sprintf(), strcpy(), and strcat() in sms_concat_handle() created a compounding overflow risk that could have allowed an attacker to corrupt memory simply by sending a crafted SMS message.
The fix is straightforward: replace every unbounded string operation with a bounds-aware alternative, compute buffer sizes explicitly from known constants, and never trust that an external payload will fit within your buffer. These habits, applied consistently, eliminate an entire class of critical vulnerabilities.
References
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-121: Stack-based Buffer Overflow
- OWASP Buffer Overflow Attack
- OWASP Input Validation Cheat Sheet
- SEI CERT C Coding Standard — STR31-C: Guarantee that storage for strings has sufficient space
- Microsoft Docs: strcat_s, wcscat_s
- Semgrep rules: dangerous-sprintf
- fix: the sms response buffer handling in sm_at_sms in sm_at_sms.c