info func 명령으로 함수 목록을 봤을 때 main함수 말고 별 다른 함수 목록이 안보여서 main 함수의 어셈을 보면 다음과 같다.
0x6d30(27952)만큼의 크기에 gets함수로 입력을 받고있다.
gets함수가 끝난 main+96에 bp를 걸고 값을 입력하면 파일을 실행할 때 출력하는 buf 주소와 입력한 값이 들어간 주소가 동일함을 확인할 수 있다.
따라서 입력값에 쉘코드를 넣고 (27952(buf크기) - 쉘코드 길이) + 8(RET까지 거리)만큼 입력값을 채우고 buf의 주소를 받아서 넣으면 문제를 RET에 쉘코드가 들어간 buf주소가 들어가 쉘을 딸 수 있을 것 같다. 이를 파이썬 익스코드를 작성하고 실행하면 다음과 같이 쉘을 딸 수 있다.
Hack CTF pwnable 분야 이전 문제들에 대한 롸업은 Project H4C 게시판에 있다.
파일 명령어로 확인해보면 64비트 리눅스 실행파일임을 확인할 수 있다.
파일을 실행하면 문자열을 입력받고, Hello 입력받은 문자열 의 형태로 출력하고 있다 .
checksec로 확인해보면 NX보호기법이 적용되어 있다.
info func로 함수 정보를 보면 다음과 같다.
execve 함수가 사용됨을 확인할 수 있고, main함수 말고, callMeMaybe 함수도 있는것을 확인할 수 있다.
main함수와 callMeMaybe 함수의 어셈 코드를 확인하면 다음과 같다.
main 함수에서 0x110(272)만큼의 공간을 할당하고, scanf함수로 입력을 받고 있고, callMeMaybe함수에서 execve 함수를 실행하고 있다. main함수의 입력받는 부분에서 bof를 발생시켜 callMeMaybe함수를 실행시킬 수 있을 것 같다. 입력값으로 272(buffer) + 8(SFP)를 채우고 ret에 callMeMaybe함수의 주소를 넣으면 될 것 같다.
from pwn import *
p = remote("ctf.j0n9hyun.xyz", 3004)
pay = "A"*280
pay += p64(0x0000000000400606)
p.sendline(pay)
p.interactive()
RTL은 Retuen Address 영역에 공유 라이브러리 함수의 주소로 변경해 해당 함수를 호출하는 방식이다. 이 기법을 통해 NX 보호기법을 우회할 수 있다.
공유 라이브러리는 컴파일을 할때 링커가 실행 파일에 사용할 공유 라이브러리를 표시하면 그 라이브러리에 있는 컴파일 된 코드를 가져와 사용한다. 리눅스는 기본적으로 공유 라이브러리가 있으면 그것과 링크를 시키고, 없으면 정적 라이브러리로 링크 작업을 한다.
인텔 x86 시스템, 리눅스 커널에서는 Cdecl 호출 규약을 사용한다. 이 호출 규약은 함수의 인자값을 stack에 저장하며 오른쪽에서 왼쪽 순서로 스택에 저장한다. 함수의 반환 값은 EAX 레지스터에 저장된다. 사용된 스택 정리는 해당 함수를 호출한 함수가 정리한다.
다음과 같이 스택에 저장된 vlun 함수의 인자값들을 확인할 수 있다. vlun함수 실행전인 main+35에 breakpoint를 걸고 esp를 확인했다.
vuln 함수의 어셈 코드를 보면 다음과 같다.
push ebp로 main 함수에서 사용하던 호출 프레임을 스택에 저장한다. 이전 함수에서 사용하던 호출 프레임은 ebp 레지스터에 저장되어 있다. mov ebp, esp로 vlun 함수에서 사용할 새 호출 프레임이 ebp레지스터에 초기화 된다. ebp 레지스터를 통해 main 함수에서 전달된 인자값을 사용할 수 있다.
DWORD PTR [ebp+*] 영역으로 각각의 인자값을 확인할 수 있다.
ret2libc 기법 사용시 인자값을 전달하려면 Return Address의 4바이트 뒤에 인자 값을 전달해야 한다.
다음과 같이 코드를 작성하고 컴파일 하여 Return to Shellcode를 확인할 수 있다.
checksec로 보호기법을 보니 NX가 적용되어 있어서 쉘 코드 삽입은 불가능할 것 같았다.
info func 명령을 통해 함수 목록을 보면 다음과 같다.
main, jump, get_flag 함수가 눈에 띈다.
각 함수들의 어셈 코드는 다음과 같다.
main 함수에서 jump 함수를 호출하고 있고, jump 함수에서는 gets 함수로 사용자의 입력을 받고 있다. get_flag 함수에서는 fopen함수로 파일을 열어 fread함수로 내용을 읽어 리턴하고 있다.
아이다의 헥스레이 기능을 이용하여 위 함수들을 확인하면 다음과 같다.
jump 함수에서 v1이라는 변수로 64바이트를 할당하고, gets함수로 v1에 값을 입력받는데, 입력받는 값의 길이에 대한 검증이 없어서 bof가 발생할 수 있다. jump 함수에서 bof를 발생시켜 get_flag 함수로 리턴하도록 하면 서버의 flag 파일을 읽어올 수 있을 것 같다. 따라서 payload를 다음과 같이 입력값에 64(v1크기) + 8(sfp 크기) 만큼 입력하고 리턴에 get_flag 함수 주소를 넣어서 get_flag 함수를 실행시키도록 하면 될 것 같다. 따라서 익스 코드는 다음과 같다.
UMD CTF 후기 : 사실 가장 오랜 시간을 잡고 있던 문제는 JIE 문제 였는데 NX 보호기법이 적용되어 있지 않아서 쉘코드 삽입하는 문제라 생각하여 그 쪽으로 접근했는데 풀리지 않아서 아쉬웠다. 같이 문제푸신 팀원중 한 분이 ROP 기법으로 문제를 JIE를 포함한 다른 문제도 풀으셨는데 이것을 보고 포너블 기법 공부를 더 열심히 해야겠다는 동기 부여를 받은것 같다. JIE문제는 쉘코드를 삽입하여 푸는 방식으로 롸업이 올라온다면 한번 확인해보고 싶다.
)'; 으로 파이썬 명령어를 닫아주고 내가 원하는 다른 명령어를 실행시킬 수 있을 것 같다. 실행할 명령어를 작성한후 ;#을 붙이면 정상적으로 원하는 명령어를 실행하고 그 결과값을 볼 수 있다. #은 리눅스 쉘에서도 주석의 역할을 하기 때문에 #으로 시스템 함수에 들어가는 남은 파이썬 명령어를 주석처리하는 것이다.
ls명령어로 확인해보니 flag 파일이 있었고, cat flag 명령으로 flag를 확인할 수 있었다.
payload : )';cat flag;#
HackPack CTF 후기 : 푼 문제는 파이썬 코드를 닫고 원하는 명령어를 입력한 뒤 주석처리해서 이후 구문을 무시하여 원하는 명령어만 정상적으로 실행되도록 하여 풀었는데, 웹해킹을 공부할때 원하는 쿼리를 입력하고 주석으로 이후 구문을 무시한다는 점에서 기본적인 SQL Injection 과 비슷한 느낌을 받았다. 이 문제 외에 다른 문제들도 풀려고 시도해 보려 했지만 코드가 길어지고, 아직 포너블 기초밖에 공부가 되어있지 않다보니 꽤 오랜시간을 보고 생각나는 다양한 방법들로 구글링을 시도해 보았지만 역시 쉽지 않았다. 포너블 공부를 더 열심히 해야 겠다는 생각을 하게 되었고, 다양한 기법들을 공부해서 문제에 적용해야 겠다는 생각을 하게 되었다.
비트 필도로 사용할 멤버는 익명 구조체로 감싸주고, 비트 필드의 값을 한번에 접근할 수 있는 unsigned short형 멤버(e)를 선언하여 익명 공용체로 감싸준다. 각 비트 필드에 값을 할당하고, e를 출력하면 64020이 나온다. 각 비트 필드에 할당한 비트들을 차례로 연결하면 1111 1010000 10 100이 되어 10진수로 64020이 된다.
56.3 퀴즈
정답은 d 이다.
정답은 4이다.
정답은 e 이다.
56.4 연습문제 : 구조체로 플래그 비트 필드 만들기
비트 수를 제한해야 하므로 정답은 다음 코드와 같다.
unsigned int a : 2;
unsigned int b : 1;
unsigned int c : 6;
56.5 연습문제 : 구조체와 공용체로 플래그 비트 필드 만들기
32936을 2진수로 표현하면 1000 0000 1010 1000이다. 할당할 값들을 2진수로 표현하면 각각 1000, 10, 10, 1000 0000 이므로 비트를 맞춰주면 다음과 같다.
unsigned short a : 4;
unsigned short b : 2;
unsigned short c : 2;
unsigned short d : 8;
56.6 심사문제 : 구조체로 플래그 비트 필드 만들기
정답은 다음 코드와 같다.
struct Flags{
unsigned int a : 4;
unsigned int b : 7;
unsigned int c : 3;
};
56.7 심사문제 : 구조체와 공용체로 플래그 비트 필드 만들기
57412를 2진수로 변환하면 1110 0000 0100 0100이므로 비트수를 맞춰주면 다음과 같다.
unsigned int a : 3;
unsigned int b : 4;
unsigned int c : 7;
unsigned int d : 2;
Unit 57. 열거형 사용하기
열거형은 정수형 상수에 이름을 붙여서 코드를 이해하기 쉽도록 해준다.
57.1 열거형 정의하기
열거형은 eum키워드로 정의하고, enum 열거형이름 변수이름; 의 형태로 정의하여 사용한다.
for(INTERFACE_TYPE i = Internal; i < MaximumInterfaceType; i++)
Unit 58. 자료형 변환하기
C에서는 자료형이 같거나 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 저장하면 자동으로 형 변환이 된다. 그러나 자료형이 다르면서 크기가 작은쪽, 표현 범위가 좁은 쪽으로 저장하면 컴파일 경고가 발생할 수 있다. 자료형의 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 자동 형 변환 되는 것을 형 확장 이라 하며 암시적 형 변환이라 한다. 반대로 자료형 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 변환되는것을 형 축소라 한다. 형 축소에서 컴파일 경고가 나오지 않도록 만드는 것을 형 변환 이라 한다. 프로그래머가 강제적으로 자료현을 변환한다고 하여 명시적 형 변환이라 부르기도 한다. 형 확장은 값의 손실이 없기 때문에 컴파일러가 알아서 처리할 수 있지만 형 축소는 값의 손실이 발생하여 컴파일러가 알아서 처리할 수 없기 때문에 경고가 발생한다. 형 변환은 컴파일러에게 자료형을 변환한다는 의도를 명확하게 알려주는 것이다.
58.1 기본 자료형 변환하기
자료형을 지정하여 변환하는 것을 명시적 자료 변환이라고 하며 변수나 값 앞에 변환할 자료형을 붙인 뒤 괄호로 묶어준다.
numPtr에 메모리를 할당하고 역참조 하여 0x12345678을 저장하였다. cPtr에 int 포인터를 char 포인터로 저장하여 메모리 주소를 저장하면 cPtr은 char 포인터이므로 1바이트만 값을 가져오게 된다. 메모리 공간에는 리틀 엔디언 형식으로 값이 저장되므로 출력값은 0x78이 된다.
위와 반대로 크기가 작은 메모리 공간을 할당한 뒤 큰 자료형의 포인터로 역참조하면 의도치 않은 값을 가져올 수도 있다.
크기가 작은 메모리를 할당한 뒤 큰 자료형의 포인터로 역참조하면 컴파일러에 따라 옆의 메모리 공간을 침범하여 값을 가져올 수 도 있다. 위 코드는 2바이트 만큼 메모리를 할당했으므로 0x1234만 저장되어 있지만 4바이트 크기만큼 값을 가져오면 2바이트 크기를 벗어나서 malloc함수로 할당하지 않은 공간까지 함께 가져올 수도 있다. 할당하지 않은 공간에는 쓰레기값이 들어있다.
포인터를 다른 자료형으로 변환하면서 역참조 하려면 다음과 같이 괄호 앞에 역참조 연산자를 붙여주면 된다.
구조체 포인터 d1을 선언하고 구조체 크기만큼 메모리를 할당하여 각 멤버에 'a'와 10을 저장하였다. 이후 void ptr에 d1을 할당하였다. void포인터이기 때문에 Data 구조체의 형태를 모르는 상태 이므로 멤버에 바로 접근할 수 없기 때문에 위와 같이 ptr을 Data 구조체로 변환한 뒤 멤버에 접근해야 한다. ptr을 구조체 포인터로 변환한 뒤 멤버에 접근할 때는 자료형 변환과 포인터 전체를 다시 한 번 괄호로 묶어야 -> 연산자를 사용하여 구조체 멤버에 접근할 수 있다.
58.5 퀴즈
정답은 d 이다.
정답은 e이다.
정답은 c이다.
58.6 연습문제 : 삼각형의 넓이 구하기
base와 height가 모두 정수이므로 하나를 실수로 변환해야 한다. 정답은 다음 코드와 같다.
area = (float)base * height / 2;
58.7 포인터 변환하기
출력값이 2바이트 이므로 short 포인터로 형변환 해야 한다.
numPtr2 = (short *)nupPtr1;
58.8 연습문제 : void 포인터 변환하기
numPtr1의 크기가 uint64_t이므로 uint64_t 포인터로 변환한 뒤 역참조하여야 한다.
*(uint64_t *)ptr
58.9 연습문제 : 구조체 포인터 변환하기
정답은 다음과 같다.
((struct Person *)ptr)->name, ((struct Person *)ptr)->age
58.10 심사문제 : 소수점 이하 버리기
num1이 실수이고, 출력하는 것은 num2이고, 정수이므로 num2에 num1을 넣으면 된다.
포인터 변수에는 메모리 주소가 들어있다. 메모리 주소에 일정 숫자를 더하거나 빼면 메모리 주소가 증가, 감소한다. 포인터 연산을 통해 다른 메모리 주소에 접근할 수 있으며 메모리 주소를 손쉽게 옮겨 다닐 수 있다. 메모리 주소가 커지는 상황을 순방향으로 이동, 메모리 주소가 작아지는 상황을 역방향으로 이동이라고 한다.
59.1 포인터 연산으로 메모리 주소 조작하기
포인터 변수에 +, - 사용하여 값을 더하거나 빼고, ++, -- 연산자로 값을 증가, 감소시킬 수 있다. *, / 연산자와 실수값은 사용할 수 없다.
numPtrB는 numPtrA에 1을 더해 4바이트 만큼 순방향 이동했고, numPtrC는 numPtrA에 2를 더해서 8바이트만큼 순방향으로 이동했다. numPtrB, numPtrC도 일반 포인터이므로 역참조 연산이 가능하고, 두 값은 결국 numArr[1], numArr[2]와 같다.
다음과 같이 포인터 연산을 한 부분을 괄호로 묶어 맨 앞에 역참조 연산자를 붙여 포인터 연산과 역참조 연산을 동시에 사용할 수도 있다.
구조체 배열 d를 선언한 뒤 첫번째 요소의 메모리 주소를 ptr에 저장하였다. (ptr+1)->num1 과 같이 포인터 연산을 한뒤 괄호로 묶은 후 화살표 련산자를 사용하여 멤버에 접근할 수 있다. 이는 구조체 배열의 인덱스로 접근하는 d[1].num1과 같다. 구조체의 크기는 4바이트짜리 int형 멤버가 두 개 있으므로 8바이트이다. 따라서 포인터 연산을 하게 되면 8바이트씩 메모리 주소에서 더하거나 뺀다.
다음은 동적 메모리를 할당하여 포인터 연산을 하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Data {
int num1;
int num2;
};
int main()
{
void*ptr =malloc(sizeof(struct Data) *3);
struct Data d[3];
((struct Data *)ptr)->num1 =10;
((struct Data *)ptr)->num2 =20;
((struct Data *)ptr +1)->num1 =30;
((struct Data *)ptr +1)->num2 =40;
((struct Data *)ptr +2)->num1 =50;
((struct Data *)ptr +2)->num2 =60;
memcpy(d, ptr, sizeof(struct Data) *3);
printf("%d %d\n", d[1].num1, d[1].num2);
printf("%d %d\n", ((struct Data *)ptr +2)->num1, ((struct Data *)ptr +2)->num2);
xor 함수에서는 문자열과 길이를 인자로 받아 길이만큼 반복하며 문자열의 각 문자를 1씩 xor 연산을 한다.
main 함수의 첫번째 if문을 보면 할당연산자보다 비교연산자가 우선순위가 높아서 비교연산을 먼저 수행하면 open함수의 리턴값은 파일을 잘 읽어와서 fd 값을 반환하고, fd값은 음이 아닌 정수이기 때문에 비교 연산의 결과는 거짓이므로 fd에는 flase(0)값이 들어가고, 조건문은 수행하지 않는다.
PW_LEN+1 크기의 pw_buf를 선언하고, if 함수의 조건식에서 read함수를 실행한다. fd값이 위에서 0으로 저장되었기 때문에 fd 0 은 stdin 이라 PW_LEN만큼 pw_buf에 입력을 받는다. 입력을 받으면 크기는 0보다 커지기 때문에 not 연산으로 조건식은 거짓이 되어 조건문은 실행하지 않는다.
PW_LEN + 1 크기의 pw_buf2를 선언하고, 입력받는다. 이후 xor함수로 xor연산을 진행한다. 이후 조건문에서 xor한 pw_buf2와 pw_buf가 같다면 시스템 함수로 플래그를 출력할 수 있다.
xor 함수는 각 자리를 1로 xor 연산 하므로 1 xor 1 = 0 이 되므로 첫번째 입력값에는 0을 10개, 2번째 입력에서는 1을 10개 입력하면 플래그를 확인할 수 있다.
rand 함수를 이용해 변수 random에 값을 저장하는데, rand()함수만 사용하면 프로그램이 생성될 때 값이 정해지기 때문에 계속 실행해도 같은 값이 저장되어 있다. key를 입력받아서 key ^ random 값이 0xdeadbeef면 시스템 함수로 flag를 읽을 수 있다. random 값을 알아야 하기 때문에 gdb로 프로그램을 실행시켜 main 함수를 보면 다음과 같다.
rand 함수가 실행되고 난 후(main+18)에 breakpoint를 걸고 실행한 후에 현재 레지스터를 확인해주면 다음과 같다.
산술연산 및 함수 반환값은 rax에 들어가기 때문에 0x6b8b4567이 rand()함수로 반환된 값이다.
0x6b8b4567과 0xdeadbeef를 xor한 값이 passcode에 key로 입력되면 if 조건문을 만족시킬 수 있을 것 같다.
두 값을 xor 연산 후 10진수로 변환하면 3039230856이 나온다. random 을 실행하고 10진수로 변환한 값을 넣으면 플래그를 획득할 수 있다.