컴퓨터를 다루는 직업을 얻기 위해선 cs지식이 필수적이야
cs는 computer science인 건 알겠는데?
말 그대로 컴퓨터 과학이고,
컴퓨터를 이용한 모든 작업과 그 기반 이론을 연구하는 학문이라는 뜻을 지녀.
이러한 컴퓨터 과학 지식을 응용하여 프로그램이나 시스템의 취약점을 발견하고 공격하는 행위를 해킹이라고 해.
예를 들어,
공격자가 메모리를 원하는 값으로 변조하는 공격을 수행하려면, 메모리에 값이 어떻게 저장되는지부터 이해해야 되겠지?
MSB(최상위 비트)는 Most Significant Bit의 축약어이고, 여러 개의 비트로 구성된 이진 데이터에서 가장 왼쪽에 있는 비트야.
LSB(최하위 비트)는 Least Significant Bit의 축약어이고, 가장 오른쪽에 있는 비트야.
"중요하다(Significant)"는 표현을 붙인 이유는
가장 왼쪽에 있는 비트가 숫자의 크기에 가장 큰 영향을 미치기 때문이고,
가장 오른쪽에 있는 비트는 숫자의 크기에 가장 작은 영향을 미치기 때문에 이런 명명이 붙어졌어.
예를 들어보자.
2진수 0b10010100의 MSB는 1이고, LSB는 0이야.
부호가 있는 데이터의 경우, MSB는 부호의 의미를 가지게 돼.
MSB가 0이면 양수이고, 1이면 음수를 나타내지.
프로그래밍 언어에서 부호(+, -)를 갖는 데이터는
signed 데이터 또는 부호가 있는 데이터라 부르고,
부호없이 양수(+)만 나타내는 데이터는 unsigned 데이터 또는 부호가 없는 데이터라고 불러.
예를 들어보자.
0b10010100가 부호가 있는 데이터라면,
MSB가 1이기 때문에 10진수로 표현하면 음수인 -108이 돼.
반면에
부호가 없는 데이터라면,
MSB인 1은 부호를 의미하지 않고, 값을 의미하므로 양수인 148이 돼.
2B 이상의 데이터는 메모리에 연속적으로 저장돼.
이떄 각 바이트가 메모리에 정렬되는 방식을 바이트 오더링이라고 불러.
바이트 오더링은 어떤 바이트부터 낮은 주소에 저장되는지에 따라 구분돼.
두 가지 방식으로, 빅 엔디안(Big Endian)과 리틀 엔디안(Little Endian)이 있어.
예를 들어
0x01234567과 같은 데이터가 있을 때,
가장 왼쪽의 0x01이 가장 큰 바이트이고,
가장 오른쪽의 0x67이 가장 작은 바이트야.
빅 엔디안은 큰 바이트부터 낮은 주소에 저장이 되고,
리틀 엔디안은 작은 바이트부터 낮은 주소에 저장이 돼.
큰 바이트, 작은 바이트 구분하는 건 평상시에 숫자 읽는 것처럼 생각하면 돼.
숫자 1234 중에서 1이 가장 큰 자리수인 것처럼
바이트 오더링은 비트의 순서가 아니라 바이트의 순서를 고려하는 것으로, 바이트 내 비트의 순서는 동일하고 바이트의 순서만 달라져.
1) 비트기준 - 0b10010100
MSB -> 1
LSB -> 0
2) 바이트기준 - 0x01234567
MSB -> 0x01
LSB -> 0x67
정교함과 정확함이 필수인 해킹 분야에서 데이터가 어떤 바이트 오더링으로 메모리에 저장되었는지 고려하는 것을 기본 중의 기본이야.
리버스 엔지니어링 분야에서는
베보리에 저장된 값을 이용해 역연산을 수행해서 어떤 정답이 되는 값을 찾아내는 작업이 매우 흔하다고 해.
앞서 살펴본 예시와 같이
리틀 엔디안 방식으로 저장된
0x01234567을 빅 엔디안으로 생각하고
0x67452301이라는 숫자로 다뤄버리면,
결국 전혀 다른 데이터를 갖고 역연산을 수행하는 꼴이 되니까 올바른 결과를 얻을 수 없게 돼.
시스템 해킹을 예로 들어보자.
공격을 위해 특정 메모리 주소에 원하는 값을 덮어씌워야 한다고 가정하자.
메모리의 낮은 주소부터 값을 쓰는 경우라면,
리틀 엔디안을 고려하여 저장하려는 값의 가장 오른쪽 바이트부터 써야 원하는 값이 제대로 저장돼.
또한 어떤 변수의 출력값을 알아내려고 한다면,
메모리의 변수 주소 영역에서 높은 주소부터 값을 읽어온다는 것을 고려해야 해.
하단의 코드는 문자열과 16진수 정수를 메모리에 저장하는 c 프로그램의 소스 코드야.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char * str = "ABCD"; // 16진수: 41424344
puts(str); // 문자열 출력 결과: ABCD
unsigned int num = 0;
num = 0x41424344;
printf("%x\n", num); // 16진수 출력 결과: 41424344
return 0;
}
문자열
str 변수 주소의 메모리를 1B씩 4B 출력한 결과야.
문자열을 메모리에 저장할 때는 바이트 오더링을 고려하지 않아.
따라서 문자열 "ABCD"는 리틀 엔디안 방식을 따르지 않고, 문자 순서 그대로 메모리에 저장돼.
0x555555556004: 0x41 0x42 0x43 0x44
문자열이 아닌 데이터 - 16진수 정수
num 변수 주소의 메모리를 1B씩 4B 출력한 결과야.
문자열이 아닌 데이터는 리틀 엔디안 방식으로 저장돼.
따라서 16진수 0x41424344는 가장 오른쪽 바이트부터 메모리의 낮은 주소에 저장돼.
num 변수 값을 출력하면 메모리에 저장된 4B에서 높은 주소부터 읽어 와서 원래 값 그대로 출력이 돼.
0x7fffffffe314: 0x44 0x43 0x42 0x41
| 비트 연산자 | 설명 |
|---|---|
| ~x | 비트가 0이면 결과는 1, 1이면 결과는 0으로 모든 비트를 반전 시킴. (NOT) |
| x ^ y | 두 비트가 같으면 결과는 0, 다르면 결과는 1임. (XOR) |
| 시프트 연산자 | 설명 |
|---|---|
| x << n | 비트는 n만큼 왼쪽으로 이동, 오른쪽 빈 칸은 모두 0으로 채움. == x * |
| x >> n (산술 시프트) | 비트를 n만큼 오른쪽으로 이동, 왼쪽 빈 칸은 MSB와 동일한 비트 값으로 채움. (양수는 양수, 음수는 음수로 부호가 유지됨.) == x / |
| x >>> n (논리 시프트) | 비트를 n만큼 오른쪽으로 이동, 왼쪽 빈 칸은 모두 0으로 채움. (음수는 부호가 유지되지 않음.) |
+) 산술 시프트 설명 부분에서 x / 가 있었는데, 예를 들어보자.
10진수 248을 4비트 우측 시프트 시키면
즉 248 >> 4는 248 ÷ 꼴이야.1)
248 ÷ = 248 ÷ 16 = 15.5
에서 소수점 이하는 버려져. -> 152)
248을 2진수로 변환하면 0b1111 1000
4비트 우측 시프트(>>4) 0b0000 11113)
변환 결과는 0b0000 1111 = 15 (10진수)가 돼.
추가적인 예를 들어보자.
< 아래 표는 16bit 크기를 가지는 데이터 273을 왼쪽으로 4bit만큼 시프트 하는 모습임. >
10진수 273 -> 2진수 0000 0001 0001 0001
4 bit만큼 왼쪽으로 이동하면, 0001 0001 0001 0000이 되고, 10진수로는 4368임.
1비트 왼쪽으로 시프트하는 행위는 데이터에 을 곱하는 것과 같아.
4비트 시프트하는 거니까 을 곱하면 마찬가지로 4368이 나온다는 사실을 알 수 있어.

< 아래는 데이터 32529를 우측으로 4bit만큼 산술 시프트하는 모습이야. >
10진수 32529 -> 2진수 0111 1111 0001 0001
우측 산술 시프트는 MSB가 1이면 1로 채우고, 0이면 0으로 채우면 돼.

< 아래 표에서 산술 시프트를 가할 데이터는 1111 1111 0001 0001이야. >
10진수 음수로는 -239이고, 양수로는 65297이야.
4비트만큼 우측 산술 시프트를 가하면, 1111 1111 1111 0001이 되고,
10진수 음수로는 -15이고, 양수로는 65521이야.

< 아래 표는 1111 1111 0001 0001에 4비트만큼 우측으로 논리 시프트를 가하는 모습이야. >
MSB에 상관없이 0으로 채우기 때문에 0000 1111 1111 0001이 돼.

< 다음은 4B 크기의 데이터인 0x12345678 (0001 0010 0011 0100 0101 0110 0111 1000) 에서 특정 비트만 가져오는 예시야. >
FF가 1111 1111 이라는 걸 갖고 가고 시작해보자.
- 하위 1 바이트만 가져오기 : 0x000000FF와 AND 연산하면 됨.
0x12345678 & 0x000000FF = 0x00000078 (0000 0000 0000 0000 0000 0000 0111 1000)
- 상위 1 바이트만 가져오기 : 24번 우측으로 논리 시프트를 수행합니다. 또는, 24번 우측으로 산술 시프트한 후 0x000000FF와 AND연산 하면 됨.
0x12345678 >>> 24 = 0x00000012 (0000 0000 0000 0000 0000 0000 0001 0010)
0x12345678 >> 24 & 0x000000FF = 0x00000012 (0000 0000 0000 0000 0000 0000 0001 0010)
- 상위에서 두 번째 바이트 가져오기 : 16번 우측으로 시프트 후 0x000000FF와 AND 연산하면 됨.
0x12345678 >> 16 & 0x000000FF = 0x00000034 (0000 0000 0000 0000 0000 0000 0011 0100)
- 하위 1바이트의 상위 4비트 가져오기 : 4번 우측으로 시프트한 후 0x0000000F와 AND 연산하면 됨.
0x12345678 >> 4 & 0x0000000F = 0x00000007 (0000 0000 0000 0000 0000 0000 0000 0111)
XOR 연산은 비트 값이 같으면 0을 반환해.
즉, 자기 자신과 XOR 연산하면 결과는 0이야.
더 나아가서, 같은 값에 어떤 값을 2번 XOR하면 원래의 값과 동일해진다는 특성을 이용할 수도 있어.
예를 들어보자.
x ^ y -> z일 때,
z ^ y
= x ^ y ^ y
= x 이야.
이러한 특성을 기반으로,
y를 key로 설정하면 간단한 암호화, 복호화가 가능해.
정보 교환을 위한 미국 표준 코드로, 문자를 숫자로 변환하는 쿤자 인코딩의 표준이 아스키 코드야.
아스키 문자 1개는 1B 크기로,
7 bit로 문자를 표현하고 1 bit는 오류 체크를 위해 사용해.
따라서 = 128가지의 문자표현이 가능하고, 각 문자는 0~127까지의 10진수 값을 가져.
아스키 코드는
미국에서 만든 표준 코드인 만큼 알파벳 대소문자, 숫자, 특수 문자, 제어 문자만을 포함하고 있어.
초반에는 이러한 코드도 문제가 없었지만, 점차 다양한 언어와 문자가 나타나면서 새로운 형식이 필요해졌고 유니코드가 등장하게 되었어.
즉,
영어 뿐만 아니라, 전세계 모든 언어의 문자에 고유한 번호를 부여하는 국제 표준 코드가
유니코드야!
아스키 코드보다 용량을 크게 확장해서 최대 32비트로 문자 1개를 표현해서,
현재 143000개 이상의 문자를 표현할 수 있어.
유니코드의 처음 128개 문자는 아스키 코드의 문자와 정확히 일치해.
즉,
유니코드 안에 아스키 코드가 있음을 알 수 있지.
유니코드 값은 U+ 뒤에 16진수를 붙여 나타내.
유니코드를 사용하는 인코딩 방식에는 UTF-8, UTF-16, UTF-32 등이 있어.
UTF 뒤의 숫자는 비트를 의미하는 건데,
UTF-8은 1~4 바이트의 가변적인 크기로 문자 1개를 표현하는 방식이야.
웹 브라우저로부터 받은 URL 문자열을 유효한 형식으로 변환하는 것을 URL 인코딩이라고 해.
URL 인코딩을 통해 문자열을 인터넷으로 전송 가능한 형식으로 변환해.
이를 통해 전송 중에 문자가 수정되거나 의도와 다르게 해석되는 것을 막을 수 있어.
허용되지 않는 문자, 즉 인코딩이 필요한 특수문자는 :/?#[]@!$&'()*+,;=%공백 이야.
URL 인코딩은 % 기호 뒤에 해 문자의 아스키 코드 16진수 값을 붙여 나타내.
공백 문자를 예로 들어보자.
URL에 공백이 포함되는 경우 + 기호 혹은 %20으로 변환돼.
여기서 %20이 URL 인코딩 결과야.
예를 들어, 문자열 Welcome, Dreamhack Beginners! :)를 URL 인코딩한 결과는 Welcome%2C%20Dreamhack%20Beginners%21%20%3A%29이야.
운영체제는 크게 커널과 셸로 나눌 수 있어.
커널(Kernel, 알맹이)은
운영체제의 핵심 기능인 하드웨어 관리를 실제로 수행하는 프로그램이야.
소프트웨어와 하드웨어 간의 커뮤니케이션을 관리하고,
시스템이 부팅될 때 메모리에 올라가서 꺼질 때까지 실행돼.
셸(Shell, 껍질)은
사용자와 운영체제의 커널 사이에서 사용자가 운영체제에 명령을 내릴 수 있도록 인터페이스 역할을 해.
1) 사용자가 셸에 명령을 입력하면, 셸이 명령어를 해석해서 커널에 요청해.
2) 커널은 명령을 수행하며 하드웨어를 조작하고, 수행 결과를 셸에 전송해.
3) 셸은 이 결과를 해석해서 사용자에게 출력해 줘.
즉, 셸은 명령어를 해석하는 역할을 해서 사용자와 운영체제가 소통할 수 있도록 해.
셸을 획득하면 명령어를 통해 원하는 작업을 수행하고 시스템을 제어할 수 있게 되는데,
일반적으로 셸을 획득하는 것을 시스템 해킹의 성공으로 여겨.
운영체제는 크게 Windows 운영체제와
UNIX/Linux 계열 운영체제로 나눌 수 있어.
Windows는 GUI 기능을 제공해서 사용자가 편리하게 사용할 수 있고,
UNIX는 벨 연구소에서 개발한 운영체제로, 대부분의 운영체제는 UNIX로부터 발전된 기술을 사용하고 있어. 그리고 사용자가 키보드로 입력하는 명령에 의해 조작되는 CUI(Character User Interface) 기반의 대화식 운영체제야.

1) 주소 클릭하고 ctrl + u해서 페이지 소스를 알아봄.

2) base64를 이용해서 디코딩함.

3) 디코딩했더니 아래 코드가 떠서 돌려봄.

4) 플래그 값이 떴음.

접근조차 되지 않아서 아래 글을 참조했다.
링크텍스트
1) ls-l 명령어를 통해 hint.txt 파일 발견

2) cat 명령어를 통해서 이 파일에 대한 내용 출력

3) cat 명령어를 통해 flag.txt 파일의 내용 확인

4) No!가 출력되어서 app.py 코드 확인

flag라는 단어 들어가 있으면 No!가 출력되기 때문에 와일드카드를 사용
5) cat 명령어를 통해 f*ag.txt 파일의 내용 확인

수고했오~