결론
Redis QCS 응답 또는 SQL 데이터가 비정상적으로 변경된 것이 아니라, printf(reply->element[1]->str)와 sprintf(tmpText, reply->element[1]->str)에서 발생한 Format String 취약성/오용이 직접적인 원인으로 판단된다.
특히 SQL의 LIKE 'OTRMES%', LIKE 'dr\\_%', LIKE '%ME' 등의 % 문자가 printf()의 format specifier로 해석되면서 정의되지 않은 동작이 발생했고, 그 결과 0x0p+0, 6.951058E-310 등의 비정상적인 값이 삽입된 것처럼 출력된 것이다.
따라서 Redis 서버, hiredis, redisReply, SQL 생성부를 우선 의심할 문제가 아니라 출력 및 문자열 복사 과정의 printf/sprintf 사용법을 수정하는 것이 1차적인 해결책이다.
질문)
1. 올바른 출력
printf("%s", reply->element[1]->str);
결과)
instancename like 'OTRMES%' and monname in ('ME', 'MR') )d ON
dr_re.RESNAME like 'dr\\_%' ESCAPE '\\' and dr_re.RESTYPE = 'apppms'
2. 잘못된 출력
printf(reply->element[1]->str); 출력시에
결과)
like 'OTRMES 0x0p+0nd monname in ('ME', 'MR')
re.RESNAME not like 'dr\\_ 6.951058E-310SCAPE '\\' and re.RESTYPE = 'apppms'
1. 올바른 출력과
2. 잘못된 출력으로 나타난 결과입니다. 해석과 리포트용으로 작성해주세요.
답변)
printf() 사용 방식에 따른 Redis Reply 문자열 변형 현상 분석 보고서
1. 현상 개요
Redis redisReply 구조체의 reply->element[1]->str에 저장된 SQL 문자열을 출력하는 과정에서 출력 방식에 따라 원본 문자열이 정상적으로 출력되거나 비정상적으로 변형되는 현상이 확인되었다.
동일한 reply->element[1]->str에 대하여 다음 두 가지 방식으로 출력하였다.
정상적인 출력
printf("%s", reply->element[1]->str);
출력 결과:
instancename like 'OTRMES%'
and monname in ('ME', 'MR')
)d ON
dr_re.RESNAME like 'dr\\_%' ESCAPE '\\'
and dr_re.RESTYPE = 'apppms'
비정상적인 출력
printf(reply->element[1]->str);
출력 결과:
like 'OTRMES 0x0p+0nd monname in ('ME', 'MR')
re.RESNAME not like 'dr\\_ 6.951058E-310SCAPE '\\'
and re.RESTYPE = 'apppms'
즉, Redis에서 전달받은 SQL 자체가 변경된 것이 아니라 printf() 호출 방식에 의해 문자열이 잘못 해석되고 있는 것으로 판단된다.
2. 원인 분석
문제의 핵심은 다음 코드이다.
printf(reply->element[1]->str);
C 언어의 printf()에서 첫 번째 인자는 단순한 문자열이 아니라 format string으로 취급된다.
즉,
printf("%s", reply->element[1]->str);
와
printf(reply->element[1]->str);
는 완전히 다른 의미를 가진다.
3. 정상적인 코드의 동작
다음 코드는:
printf("%s", reply->element[1]->str);
첫 번째 인자인:
를 format string으로 사용하고,
두 번째 인자인:
을 %s에 대응하는 문자열 데이터로 전달한다.
따라서 SQL 내부에 %가 존재하더라도 문제가 없다.
예를 들어 Redis 데이터가:
instancename like 'OTRMES%'
라면 %는 reply->element[1]->str의 일반적인 문자열 데이터일 뿐이다.
따라서 정상적으로:
instancename like 'OTRMES%'
가 출력된다.
4. 비정상적인 코드의 동작
반면 다음 코드는:
printf(reply->element[1]->str);
reply->element[1]->str 자체를 format string으로 사용한다.
문제는 SQL 안에 %가 포함되어 있다는 것이다.
예를 들어 원본 SQL:
instancename like 'OTRMES%'
이 printf()에 직접 전달되면:
의 %를 printf()가 format specifier의 시작으로 해석한다.
또한 SQL에는 다음과 같이 %가 여러 곳에 존재한다.
instancename like 'OTRMES%'
dr_re.RESNAME like 'dr\\_%' ESCAPE '\\'
re.RESNAME not like 'dr\\_%' ESCAPE '\\'
따라서 SQL 문자열 전체가 printf()의 format string으로 해석되면서 의도하지 않은 format 처리가 발생한다.
5. 0x0p+0, 6.951058E-310 등이 나타나는 이유
이번 현상에서 특히 중요한 부분은 다음과 같은 값이다.
그리고:
이 값들은 원본 Redis 데이터에 존재하는 값이 아니다.
printf()가 format string 안에서 %를 발견하면 그 뒤의 문자를 보고 어떤 타입의 인자를 가져올지 결정한다.
그런데 현재 코드:
printf(reply->element[1]->str);
에서는 printf()가 요구하는 추가 인자를 전달하지 않았다.
예를 들어 format string에 특정 부동소수점 형식이 해석되면 printf()는 해당 값이 인자로 전달되었다고 가정하고 호출 규약상의 레지스터/스택 영역에서 값을 읽게 된다.
그러나 실제로는 그러한 인자를 전달하지 않았기 때문에 정의되지 않은 동작(Undefined Behavior) 이 발생한다.
그 결과 우연히 메모리나 레지스터에 있던 값이 해석되어:
또는:
같은 의미 없는 값으로 출력될 수 있다.
따라서 다음과 같은 변형:
은 실제 Redis 문자열이:
였다는 의미가 아니다.
printf()가 %를 잘못 해석한 결과이다.
6. 현재 확인된 데이터 변형
원본 데이터:
instancename like 'OTRMES%'
and monname in ('ME', 'MR')
정상 출력:
printf("%s", reply->element[1]->str);
결과:
instancename like 'OTRMES%'
and monname in ('ME', 'MR')
반면:
printf(reply->element[1]->str);
결과:
like 'OTRMES 0x0p+0nd monname in ('ME', 'MR')
즉 원본의:
부분이 변형되었다.
또 다른 원본:
re.RESNAME not like 'dr\\_%' ESCAPE '\\'
정상 출력:
re.RESNAME not like 'dr\\_%' ESCAPE '\\'
비정상 출력:
re.RESNAME not like 'dr\\_ 6.951058E-310SCAPE '\\'
여기에서도 원본의 %가 존재하는 부분에서 문자열이 변형되었다.
7. 기존 sprintf() 코드와의 관계
앞서 사용한 코드:
sprintf(tmpText, reply->element[1]->str);
역시 동일한 문제가 발생한다.
printf()와 sprintf() 모두 첫 번째 인자를 format string으로 취급하기 때문이다.
따라서:
printf(reply->element[1]->str);
뿐만 아니라:
sprintf(tmpText, reply->element[1]->str);
도 잘못된 사용이다.
잘못된 코드
printf(reply->element[1]->str);
sprintf(tmpText, reply->element[1]->str);
정상적인 코드
printf("%s", reply->element[1]->str);
sprintf(tmpText, "%s", reply->element[1]->str);
또는 보다 안전하게:
snprintf(tmpText, sizeof(tmpText), "%s", reply->element[1]->str);
8. Redis 데이터 자체의 문제인지 여부
현재 현상만 놓고 보면 Redis 또는 hiredis가 SQL을 변형한 것으로 볼 근거는 없다.
동일한 reply->element[1]->str에 대해:
printf("%s", reply->element[1]->str);
를 사용했을 때:
등이 정상적으로 출력되고 있다.
이는 Redis에서 전달받은 문자열이 정상적으로 존재하고 있음을 보여준다.
따라서 문제 발생 위치는 다음과 같이 판단할 수 있다.
Redis
↓
hiredis
↓
redisReply
↓
reply->element[1]->str
↓
정상적인 문자열
↓
printf(reply->element[1]->str)
↓
❌ format string으로 잘못 해석
↓
문자열 변형 및 Undefined Behavior
9. reply->element[1]->len이 달라지는 현상과의 관계
앞서 확인한:
reply->element[1]->len = 4412
strlen(tmpText) = 4374
와 같은 현상 역시 이 문제와 연관성이 높다.
Redis의:
은 Redis에서 전달받은 문자열의 실제 길이를 나타낸다.
반면:
는 sprintf()로 변형된 문자열의 길이를 측정한다.
따라서:
sprintf(tmpText, reply->element[1]->str);
로 문자열이 변형되면:
원본 Redis 문자열
│
│ len = 4412
↓
printf/sprintf format 해석
│
│ 문자열 일부 변형
↓
tmpText
│
│ strlen = 4374
↓
4412 != 4374
가 될 수 있다.
따라서 LEN[4374]와 같은 결과가 나타난 것은 Redis 데이터의 길이가 잘못된 것이 아니라 format string 오용으로 인해 복사/출력 과정에서 문자열이 변형되었기 때문으로 판단된다.
10. 최종 판단
원인
printf(reply->element[1]->str);
및
sprintf(tmpText, reply->element[1]->str);
와 같이 외부에서 전달받은 문자열을 format string으로 직접 사용한 것이 원인이다.
특히 SQL에는:
문자가 다수 포함되어 있으므로 문제가 쉽게 발생한다.
증상
- %가 포함된 SQL 부분이 변형됨
- 0x0p+0 등의 비정상적인 값이 출력됨
- 6.951058E-310 등의 부동소수점 형태의 의미 없는 값이 출력됨
- 원본 SQL과 출력 SQL의 길이가 달라짐
- reply->element[1]->len과 strlen() 결과가 달라짐
- 결과적으로 기존 코드에서 reply(FAIL)-LEN이 발생함
정상적인 처리 방법
printf("%s", reply->element[1]->str);
문자열 복사라면:
snprintf(tmpText,
sizeof(tmpText),
"%s",
reply->element[1]->str);
또는 Redis reply의 정확한 len을 유지하려면:
memcpy(tmpText,
reply->element[1]->str,
reply->element[1]->len);
tmpText[reply->element[1]->len] = '\0';
결론
Redis QCS 응답 또는 SQL 데이터가 비정상적으로 변경된 것이 아니라, printf(reply->element[1]->str)와 sprintf(tmpText, reply->element[1]->str)에서 발생한 Format String 취약성/오용이 직접적인 원인으로 판단된다.
특히 SQL의 LIKE 'OTRMES%', LIKE 'dr\\_%', LIKE '%ME' 등의 % 문자가 printf()의 format specifier로 해석되면서 정의되지 않은 동작이 발생했고, 그 결과 0x0p+0, 6.951058E-310 등의 비정상적인 값이 삽입된 것처럼 출력된 것이다.
따라서 Redis 서버, hiredis, redisReply, SQL 생성부를 우선 의심할 문제가 아니라 출력 및 문자열 복사 과정의 printf/sprintf 사용법을 수정하는 것이 1차적인 해결책이다.