How to Deal With Strings in Python
TL;DR
Python에서서의 문자열 인코딩 방식을 설명하기 위해 ASCII와 Unicode에 대해 간단히 설명한다. Python2에서는 디폴트 인코딩 방식이 ASCII이지만 Python3에서는 UTF-8을 채택했다. UTF-8은 Unicode 인코딩 방식 중 하나로 전세계에서 널리 쓰이는 인코딩 방식이다.
Python2와 Python3에서 문자열을 다루는 방식이 바뀌었으므로, payload를 작성할 때 Python2에 익숙한 player가 Python3를 사용하기 위해서는 Unicode로 인코딩한 byte 문자열과 일반 문자열의 차이를 알고 있어야 한다.
ASCII와 Unicode
ASCII와 Unicode에 대해 간단히 알아보자.
아스키 (ASCII, American Standard Code for Information Interchange)
- 1960년대 미국에서 정의한 표준화한 부호체계
ASCII 코드는 7bit 즉, 128(=2^7)개의 고유한 값만 사용한다. 컴퓨터의 기본 저장 단위는 1byte(=8bits)인데, ASCII 코드는 7bit만 사용한다. 나머지 1bit는 Parity Bit인데, 이는 통신 에러 검출을 의미한다.
10진수로 0부터 127, 16진수로 0x00부터 0x7F의 범위로 문자열 Char 즉, 고유값을 표현할 수 있다.
7bit로 표현할 수 있는 ASCII 코드는 ‘영문’만을 표현할 수 있다. 다른 언어를 표현하기에는 7bit로는 역부족이었다. 그래서 8bits로 확장한 아스키 코드가 등장했는데, 이를 ANSI 코드라고 부르기로 했다더라.
ANSI 코드는 8bits로 표현할 수 있으니 256(=2^8)개의 값을 쓸 수 있는데, 여전히 ‘한글’을 포함한 비유럽 국가의 문자를 표현하기에는 역부족이었다. 그래서 유니코드 (Unicode)라는 전 세계 언어의 문자를 정의하기 위한 국제 표준 코드가 등장했다.
유니코드 (Unicode)
유니코드는 ASCII 코드나 ANSI 코드보다 훨씬 크게 용량을 2byte(=2^16, 65536)로 확장한 코드이다. 무려 65536(=2^16)개나 문자를 저장할 수 있었다. 참고로 유니코드 3.0버전까지는 이를 기본 언어판 (BMP, Basic Multilingual Plane)이라고 불렀다. 비트맵이 아니다.
0x0000부터 0xFFFF까지 표현이 가능하다.
하지만 이는 또 세상의 모든 언어를 감당하기가 힘들었다. 그래서 유니코드 3.0버전부터 보충 언어판 (Supplementary Plane)을 정의했다. 이때 한자가 가장 많이 할당받았다고 한다. 그런데 이것은 bit로 어떻게 표현할 수 있을까?
사실 기본 언어판에는 대행코드 영역이란 것이 존재한다. 상위 대행코드와 하위 대행코드를 조합해서 보충 언어판의 문자를 표현한다. 즉, 2byte가 꼭 한 개의 문자를 의미하는 것은 아니다. 한개를 읽어보고 대행코드이면 그 다음 한개를 더 읽어 한 문자를 완성한다.
기본 언어판의 대행코드 영역은 아래와 같다.
1 | 상위 대행코드 (High Surrogates) : D800 - DBFF |
정리하자면 유니코드는 2byte로만 표현할 수 있다.는 완전히 틀린 소리이다. 결국 유니코드는 모든 문자에 index(Unidocde code point, 고유한 값)를 부여한 것이다.
1 | 'A'라는 글자는 0x0041(U+0041)이라는 index를 가진다. |
참고로 U+라는 접두어가 붙어있으면 유니코드라는 의미이다. U+0000 ~ U+007F로 ASCII 문자를 모두 표현할 수 있다. 참고로 U+AC00 ~ U+D7AF로 한글 음절을 표현할 수 있다.
유니코드를 표현하는 방법
유니코드의 index를 표현하는 방법에 대해 알아보자. 이러한 index는 여러 형식으로 변환될 수 있는데, 그 중 개인적으로 가장 익숙한 UTF-8 인코딩을 알아봤다.
그 전에 궁금증이 있다. index를 바로 컴퓨터에 저장하면 안되는 걸까? 왜 컴퓨터에 저장하기 위해서 또 변환을 해야할까? 용량때문이다. 예를 들어, 1byte로 충분히 표현할 수 있는 A를 굳이 2byte로 저장한다면 용량 문제가 생길 것이다. 호환이 잘되고 용량을 적게 만들기 위해 유니코드의 index(숫자)를 컴퓨터에 효과적으로 할당해서 0과 1로 표현할 지 결정하는 인코딩 방식이 필요하다.
UTF-8 (가변길이 인코딩, Unicode Transformation Format)
유니코드가 인터페이스라면 UTF-8은 구현체라고 할 수 있다. UTF-8은 8bit 문자 인코딩 형식으로 유니코드 문자를 index에 따라 8bit(=1byte) ~ 4byte로 변환한다.
UTF-8은 가변바이트를 사용하기 때문에, 1byte로 표현이 충분한 A는 0x41로 표현한다. 이는 UTF-8과 ASCII 코드와 영문 영역에서는 100% 호환된다는 것을 의미한다.
Code point ↔ UTF-8 conversion
| First code point | Last code point | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
|---|---|---|---|---|---|
| U+0000 | U+007F | 0xxxxxxx | |||
| U+0080 | U+07FF | 110xxxxx | 10xxxxxx | ||
| U+0800 | U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | |
| U+10000 | U+10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
출처: https://en.wikipedia.org/wiki/UTF-8
위와 같이 유니코드 범위에 따라 UTF-8 인코딩의 byte 개수가 정해진다. 참고로 한글은 3byte로 표현된다.
Python에서서의 문자열 인코딩
컴퓨터가 저장할 수 유일한 것은 byte이다. 그래서 인코딩 즉, byte로 변환해야하는 과정을 거쳐야 한다. Python에서의 문자열 인코딩 방식을 알아보자.
Python2
Python2에서는 ASCII가 디폴트 인코딩 방법으로 되어있다. ASCII로는 한글을 표현할 수 없으므로, 한글을 표현하기 위해서 아래의 코드를 코드 맨 윗줄에 따로 명시해야한다.
1 | # -*- coding: utf-8 -*- |
Python3
Python3에서는 UTF-8이 디폴트 인코딩 방법이다. 그래서 모든 문자열은 유니코드로 처리된다. b''는 byte임을 의미한다. 디폴트로 UTF-8로 인코딩되어 byte 문자열로 표현했으므로 이것을 다시.decode()하면 UTF-8로 디코딩하여 문자열로 표현할 수 있다.
정리하면 인코딩 방식만 알면 byte 문자열을 다시 문자열로 디코딩할 수 있다는 의미다.
1 | >>> b'jir4vvit' |
요즘 Pwnable을 풀 때 Python3으로 작성하고 있다. 이것을 헷갈리고 가장 처음에 했던 실수는 아래와 같다. 지금 생각해보면 왜 이렇게 했었는지 이해가 안간다.
1 | >>> str(b'jir4vvit') |
문자열에 대한 이해를 제대로 하지 않고 저지른 실수이다. Python3니까 어디서 많이 본 b''으로 문자열을 감싸고 이걸 str()로 감싸서 p.send()했었다.
여러 시도 끝에 마음편하게 byte 문자열로 payload를 구성하기로 했다.
아래처럼 문자열로 payload를 구성하면 될 때도 있고 안될 때도 있다. 되는 건 운이 좋은 때고, 안될 때는 에러가 발생한다. (안되는 경우가 더 많을 것 같다.)
1 | payload = '' |
실제로 아래와 같은 디코드가 안된다는 에러를 받았었다.
1 | Traceback (most recent call last): |
마음편히 byte로 보내면 저런 디코딩 에러를 만날 일이 없다. pwntools는 기본적으로 byte로 처리하기 때문에 payload를 아래처럼 byte로 작성해주면 정말정말 디코딩 에러를 만날 일이 없다.
1 | payload = '' |
또한 Python2와 다르게 byte 문자열과 문자열을 혼합해서 작성할 수 없다. 일관되게 작성해야 한다.
Reference
- https://whatisthenext.tistory.com/103
- https://velog.io/@zionhann/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C-%EB%AC%B8%EC%9E%90-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0
- https://finebe.tistory.com/42
- https://velog.io/@goggling/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C%EC%99%80-UTF-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
이 글을 작성할 당시엔 댓글기능이 없다. 틀린 말이나 첨언이 있다면 메일 한 통 부탁드립니다. 메일 주소는 블로그 메인 화면 에서 찾을 수 있습니다.