소프트웨어 개발보안 가이드 분석(2021) : 포맷 스트링 삽입
외부로부터 입력된 값을 검증하지 않고 입출력 ·함수의 포맷 문자열로 그대로 사용하는 경우 발생할 수 있는 보안약점이다.
공격자는 포맷 문자열을 이용하여 취약한 프로세스를 공격하거나 메모리 내용을 읽거나 쓸 수 있다.
그 결과, 공격자는 취약한 프로세스의 권한을 취득하여 임의의 코드를 실행할 수 있다.
소프트웨어 개발보안가이드(2021), 한국인터넷진흥원
printf, sprintf 등과 같은 함수에서 사용하는 서식 문자열의 종류는 매우 다양합니다. 다음 표는 본문의 이해를 돕기 위해 자주 사용되는 서식 문자열을 가져왔습니다.
식별자 | 출력 형식 |
%d | 10진수 정수 출력 |
%x | 16진수 정수 출력 |
%s | 문자열 출력 |
%n | 이전까지 출력한 총 바이트 수를 지정한 변수 |
%p | 포인트의 주소 값 |
포맷 스트링 삽입이란
포맷 스트링 삽입은 주로 C, C++, 및 기타 언어에서 발생하는 보안 취약점 중 하나로, printf, sprintf와 같은 함수에서 발생할 수 있습니다. 이 취약점은 사용자 입력이 서식 문자열로 직접 전달될 때 발생하며, 공격자가 이를 악용하여 프로그램의 실행 흐름을 조작할 수 있습니다. printf, sprintf와 같은 함수들은 서식 문자열과 그에 대응하는 인자들을 받아들이는데 사용자 입력이 서식 문자열로 전달될 때, 공격자가 입력한 데이터에 서식 문자열을 포함시키면 메모리 내의 중요한 정보를 노출시키거나 수정될 수 있고 프로그램이 예기치 않게 동작할 수 있습니다.
공격 메커니즘
포맷 스트링 삽입 공격은 주로 사용자 입력이 포맷 문자열로 사용되는 경우 발생합니다. 공격자는 입력 데이터에 포맷 문자열을 삽입하여 서버가 이를 해석할 때 예기치 않은 동작을 유발할 수 있습니다. 공격자는 서식 문자열에 %x, %p와 같은 포맷 지정자를 사용하여 메모리 주소를 출력하거나 변경할 수 있습니다. 또한, %n과 같은 포맷 지정자를 사용하여 프로그램의 실행 흐름을 조작할 수도 있습니다. 이를 통해 공격자는 스택에 존재하는 프로그램의 변수를 조작하거나 함수의 반환 주소를 변경하여 데이터를 읽거나 쓸 수 있으며, 서비스 거부(DoS) 공격을 수행할 수도 있습니다.
이러한 공격은 보안상의 심각한 위험을 초래할 수 있으며, 프로그램의 기밀성과 무결성을 침해할 수 있습니다. 포맷 스트링 삽입을 방지하기 위해서는 사용자 입력을 적절히 검증하고 이스케이프 하여 안전하게 처리해야 합니다. 또한, 포맷 문자열을 사용할 때는 안전한 포맷 지정자를 사용하고, 사용자 입력을 직접 포맷 문자열에 삽입하지 않도록 주의해야 합니다.
취약한 웹 애플리케이션의 예
public class VulnerableServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
// 사용자 입력을 포맷하여 화면에 출력
1 response.getWriter().println("Welcome, " + username);
}
}
- 1 : 사용자로부터 입력된 username 값이 그대로 출력되고 있습니다.
- 사용자 입력에 대한 적절한 검증을 수행하고 있지 않기 때문에 포맷 스트링 공격에 취약하여 만약 사용자가 포맷 스트링을 입력한다면, 예기치 않은 결과가 발생할 수 있습니다.
시큐어코딩 적용 방법
public class SecureServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
1 if (isValidUsername(username)) {
2 String safeUsername = escapeHtml(username);
response.getWriter().println("Safe" + safeUsername);
} else {
response.getWriter().println("Invalid username");
}
}
private boolean isValidUsername(String username) {
return Pattern.matches("[a-zA-Z0-9]+", username);
}
private String escapeHtml(String input) {
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
}
- 1 : isValiUsername 메서드를 통해 사용자 이름은 알파벳과 숫자만 허용하도록 검증합니다.
- 2 : escapeHtml : 사용자 입력을 HTML 이스케이프 하여 안전하게 출력합니다.
- 결과적으로 위의 코드에서는 사용자 입력을 화면에 출력할 때, 안전한 방법으로 처리하여 포맷 스트링 삽입 공격을 방지합니다. 이렇게 함으로써 사용자 입력이 서버에서 안전하게 처리되고, 포맷 스트링 삽입 공격을 방어할 수 있습니다.
포맷 스트링 삽입 공격을 방지하기 위해서는 사용자 입력을 적절히 검증하고, 포맷 문자열에 사용되는 데이터를 안전하게 처리해야 합니다. 예를 들어, 사용자 입력을 화면에 출력할 때 printf 함수 대신에 안전한 함수를 사용하거나, 사용자 입력을 형식화하기 전에 적절한 이스케이프를 수행해야 합니다.
- 사용자 입력의 검증 및 필터링 : 사용자로부터 받은 입력을 적절히 검증하고, 필요한 경우에는 필터링하여 안전한 형태로 변환합니다. 이를 통해 악의적인 포맷 지정자가 포함된 입력을 걸러낼 수 있습니다.
- 문자열 이스케이프 : 사용자 입력을 출력하기 전에 적절한 문자열 이스케이프 함수를 사용하여 안전하게 처리합니다. 이를 통해 입력에 포함된 특수 문자나 포맷 지정자를 문자 그대로 처리하여 취약점을 방지할 수 있습니다.
- 안전한 함수 사용 : 가능한 한 서식 문자열을 사용하지 않고, 대신 다른 방법을 사용하여 데이터를 출력합니다. 예를 들어, println 메서드를 사용하여 문자열을 출력하는 것이 서식 문자열을 사용하는 것보다 안전합니다.