https://pwnable.kr/play.php
pwnable.kr
1. random
문제 풀이
(1)
문제에서 제시한 ssh random@pwnable.kr -p2222 로 접속 / pw 입력
(2)
ls 명령어와 ls -l 명령어를 통해 파일 목록 및 권한 확인
(3)
cat random.c 명령어를 입력해 C코드 확인
#include <stdio.h>
int main(){
unsigned int random;
random = rand(); // random value!
unsigned int key=0;
scanf("%d", &key);
if( (key ^ random) == 0xcafebabe ){
printf("Good!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
printf("Wrong, maybe you should try 2^32 cases.\n");
return 0;
}
rand() 함수를 통해 생성된 랜덤 값을 random 변수에 저장
scanf 함수를 통해 사용자 입력을 받아 key 변수에 저장
key와 random의 XOR을 계산한 값이 0xcafebabe일 경우 flag 획득
XOR(^): 두 비트가 다를 때 1, 같을 때 0이 되는 연산
rand() 함수는 프로그램 실행 시 랜덤 값이 한 번 생성되어 정해지면
프로그램을 여러번 실행하더라도 처음 생성된 랜덤 값으로 동일하게 출력됨
따라서 진정한 랜덤 값을 생성하고 싶을 경우 srand() 함수를 함께 사용해야 함
→ 해당 함수는 프로그램 실행 시 마다 다른 랜덤 값 출력
시드(seed) | 난수 생성을 시작하기 위한 초기값 |
srand() | 시드를 설정하는 함수 |
rand() | 설정된 시드 값을 기반으로 난수 생성 |
srand()의 시드를 임의의 값으로 설정 시 == 랜덤 값(난수) 동일하게 출력 (e.g. srnad(1234))
srand()의 시드를 매번 달라지는 값으로 설정 시 == 랜덤 값(난수) 상이하게 출력 (e.g. srand(time(NULL)) → 현재 시간으로 시드 설정
해당 문제는 srnad()를 사용해 시드 값을 설정하지 않았기 때문에,
rand()가 처음 생성한 랜덤값을 찾아내면 flag 획득 가능
(4)
디버깅을 통해 자세히 비교
disass main 입력
<main+33> 구간을 통해 rand 함수가 실행되고 있음
<main+38> 구간을 통해 rand 값이 [rbp-0x1c]에 저장됨을 알 수 있음
<main+41> 구간을 통해 key 값이 [rbp-0x20]에 저장됨을 알 수 있음
rand 값을 자세히 알아보기 위해 b *main+38 입력 후 다시 실행(r)
<main+38> mov DWORD PTR [rbp-0x1c],eax
eax값을 [rbp-0x1c]에 저장했기 때문에
eax에 저장된 값을 확인하기 위해 info registers eax 입력
따라서 0x6b8b4567은 rand 값임
(1804289383은 0x6b8b4567의 10진수 값)
상단의 C코드를 다시 보면
if( (key ^ random) == 0xcafebabe ){
printf("Good!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
key 값과 random 값을 XOR 연산을 취해 0xcafebabe가 나와야 함
이를 다르게 나타내면
key == 0xcafebabe ^ random과 같음
따라서 상단에서 구한 rand의 값과 문제에서의 0xcafebabe를
XOR 연산을 취하면 key값을 알아낼 수 있음
random 파일을 실행해 해당 10진수 값을 입력하면 flag 획득 가능
2. input2
문제 풀이
(1)
문제에서 제시한 ssh input2@pwnable.kr -p2222 로 접속 / pw 입력
(2)
ls 명령어와 ls -l 명령어를 통해 파일 목록 및 권한 확인
(3)
cat input2.c 명령어를 입력해 C코드 확인
#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
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
각 문단별 마지막 printf문을 살펴보면 각 stage가 있고
이를 모두 해결해야만 flag를 획득할 수 있음
각 stage별 코드를 살펴보면
[ stage1 ]
// 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");
조건1. argc(인자의 개수)가 100개여야 함
조건2. argv['A'](인자의 위치, A=65)가 "\x00"이어야 함
조건3. argv['B'](인자의 위치, B=66)가 \x20\x0a\x0d"이어야 함
argv는 list형태로 입력받기 때문에 배열을 생성하는 코드를 작성하면 됨
그러나 현재 경로(디렉토리)에서는 파일 생성 및 작성 권한이 없음
따라서 권한 부여가 되어있는 tmp 디렉토리로 이동해 익스플로잇 코드를 작성해야 함
1. cd ./tmp → tmp 디렉토리로 이동
2. mkdir stage → stage라는 디렉토리 생성
3. cd stage → stage 디렉토리로 이동
아래의 명령어를 통해 stage1이라는 파일 생성
(pwntools 이용)
from pwn import *
argv = ['a' for i in range(0,100)]
argv[65] = "\x00"
argv[66] = "\x20\x0a\x0d"
p = process(executable = '/home/input2/input2', argv=argv)
p.interactive()
문자 'a'를 0부터 99까지의 인덱스를 가지는 배열을 만듦(총 100개)
그 중
인덱스 65에 해당하는 부분에는 "\x00"으로
인덱스 66에 해당하는 부분에는 "\x20\x0a\x0d"으로 변경
그 후 '/home/input2/input2' 경로 실행
interactive를 통해 직접 입출력 가능하도록 함
stage 1 clear
[ stage2 ]
// 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");
4byte 크기의 buffer 선언
read 함수 == 리눅스에서 파일 디스크립터(fd)로부터 데이터를 읽는 함수
fd(0) == stdin이므로
read(0, buf, 4) == stdin(표준입력)으로부터 4byte만큼 읽어 buf에 저장
memcmp()는 두 메모리 블록이 같은지 비교하는 함수
조건1. stdin과 "\x00\x0a\x00\xff" 비교
fd(2) == stderr이므로
read(2, buf, 4) == stderr(표준에러)로부터 4byte만큼 읽어 buf에 저장
조건2. stderr과 "\x00\x0a\x02\xff" 비교
아래 명령어를 통해 stage2.py 파일 생성
(상단의 stage1.py를 수정해서 작성해도 되지만, 편의상 각 stage별 코드 구분을 위해 새파일에 작성)
from pwn import *
#stage 1
argv = ['a' for i in range(0,100)]
argv[65] = "\x00"
argv[66] = "\x20\x0a\x0d"
#stage 2
with open('stderr', 'wb') as f:
f.write(b"\x00\x0a\x02\xff")
p = process(executable='/home/input2/input2', argv=argv, stderr=open('stderr', 'rb'))
p.send(b"\x00\x0a\x00\xff")
p.interactive()
위의 stage 1 코드와 이어서 작성
stderr이라는 파일을 바이너리 쓰기(wb)모드로 열고
해당 파일 안에 "\x00\x0a\x02\xff"를 바이트 형식(b)으로 작성
'/home/input2/input2' 경로 실행
stderr 인자로 'stderr' 파일을 바이너리 모드로 읽어(rb) 전달
send 함수를 통해 stdin에 "\x00\x0a\x00\xff" 전달 (바이트 형식(b))
stage 2 clear
[ stage3 ]
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");
getenv() 함수는 환경 변수(environment variable)의 값을 가져오는 함수
따라서 환경변수 "\xde\xad\xbe\xef"가 가지는 값이 "\xca\xfe\xba\xbe"와 일치해야 함
아래 명령어를 통해 stage3.py 파일 생성
from pwn import *
#stage 1
argv = ['a' for i in range(0,100)]
argv[65] = "\x00"
argv[66] = "\x20\x0a\x0d"
#stage 2
with open('stderr', 'wb') as f:
f.write(b"\x00\x0a\x02\xff")
#stage 3
env = {"\xde\xad\xbe\xef" : "\xca\xfe\xba\xbe"}
p = process(executable='/home/input2/input2', argv=argv, stderr=open('stderr', 'rb'), env=env)
p.send(b"\x00\x0a\x00\xff")
p.interactive()
위의 stage 2 코드와 이어서 작성
환경변수의 값이 key값이 되도록 env라는 딕셔너리를 만든 후 process에 넣음
stage 3 clear
[ stage 4 ]
// 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");
"\x0a"라는 이름의 파일을 읽기(r) 모드로 열고
해당 파일 내용의 첫 4byte만큼 읽어 "\x00\x00\x00\x00"와 일치하는지 확인
아래 명령어를 통해 stage4.py 파일 생성
from pwn import *
#stage 1
argv = ['a' for i in range(0,100)]
argv[65] = "\x00"
argv[66] = "\x20\x0a\x0d"
#stage 2
with open('stderr', 'wb') as f:
f.write(b"\x00\x0a\x02\xff")
#stage 3
env = {"\xde\xad\xbe\xef" : "\xca\xfe\xba\xbe"}
#stage 4
with open('\x0a', 'wb') as f:
f.write(b"\x00\x00\x00\x00")
p = process(executable='/home/input2/input2', argv=argv, stderr=open('stderr', 'rb'), env=env)
p.send(b"\x00\x0a\x00\xff")
p.interactive()
위의 stage 3 코드와 이어서 작성
\x0a이라는 파일을 바이너리 쓰기(wb)모드로 열고
해당 파일 안에 "\x00\x00\x00\x00"를 바이트 형식(b)으로 작성
stage 4 clear
[ stage 5 ]
// 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");
TCP 소켓 생성을 생성해 sd라는 변수에 대입함
AF_INET == IPv4
SOCK_STREAM == 입출력을 stream 방식으로 설정
INADDR_ANY == 모든 IP에서의 연결 허용
saddr.sin_port = htons( atoi(argv['C']) );
argv['C'](인자의 위치, C=67)의 문자열을 숫자로 변환해(atoi) sin_port에 대입
이는 소켓 통신 시 포트로 사용할 예정
listen(sd, 1);
연결 대기상태로 전환 (최대 1개의 연결만 처리)
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
클라이언트의 연결 접속 요청 결과를 cd라는 변수에 대입
(cd < 0) → 연결 접속 요청 거부
if( recv(cd, buf, 4, 0) != 4 ) return 0;
소켓을 열고 클라이언트로부터 4byte의 문자열을 입력받음
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
4byte의 값이 "\xde\xad\xbe\xef"인지 확인
∴ argv['C']의 문자열을 정수형으로 변환한 값을 포트 번호로 삼아 TCP 소켓 서버를 열고
클라이언트로부터 입력받은 4byte의 값이 "\xde\xad\xbe\xef"와 일치하는지 확인
아래 명령어를 통해 stage5.py 파일 생성
rom pwn import *
#stage 1
argv = ['a' for i in range(0,100)]
argv[65] = "\x00"
argv[66] = "\x20\x0a\x0d"
argv[67] = "2025"
#stage 2
with open('stderr', 'wb') as f:
f.write(b"\x00\x0a\x02\xff")
#stage 3
env = {"\xde\xad\xbe\xef" : "\xca\xfe\xba\xbe"}
#stage 4
with open('\x0a', 'wb') as f:
f.write(b"\x00\x00\x00\x00")
p = process(executable='/home/input2/input2', argv=argv, stderr=open('stderr', 'rb'), env=env)
p.send(b"\x00\x0a\x00\xff")
#stage 5
s = remote('localhost', 2025)
s.send(b"\xde\xad\xbe\xef")
p.interactive()
위의 stage 4 코드와 이어서 작성
코드 상단에 argv[67] = "2025"를 입력해 임의의 포트 번호 생성
remote() 함수는 pwntools의 TCP 클라이언트 함수임
localhost에서 TCP 서버의 포트 2025로 접속 시도
해당 TCP 서버에 "\xde\xad\xbe\xef" 작성
stage 5 clear
(4)
모든 스테이지를 클리어했으나, flag값은 나오지 않음
Why?
현재 작업하고 있는 경로는 /tmp/stage임
flag는 /home/input2/flag 경로에 있음
해당 flag 파일을 현재 경로에 복사할 수 없기 때문에
심볼릭 링크를 활용해 /home/input2/flag 경로에 접속할 수 있도록 해줘야 함
ln -s /home/input2/flag flag
flag라는 이름의 파일을 만들어 /home/input2/flag 경로로 이동할 수 있도록 링크를 걸어둠
(바로가기 기능과 비슷)
그 후 다시 마지막 파일인 stage5.py를 실행하면 flag 획득 가능
3. leg
문제 풀이
(1)
문제에서 제시한 ssh leg@pwnable.kr -p2222 로 접속 / pw 입력
(2)
ls 명령어와 ls -l 명령어를 통해 파일 목록 및 권한 확인
(3)
해당 문제에서는 C코드가 문제에서 따로 제시되어 있기에
pwnable.kr/bin/leg.c에 접속해 C코드 확인
#include <stdio.h>
#include <fcntl.h>
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}
main 함수를 먼저 살펴보면
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}
scanf 함수를 통해 사용자 입력을 받고 이를 key라는 변수에 저장함
그 후 key1 + key2 + key3 값이 key값과 같을 경우 flag 획득 가능
key1, key2, key3 함수를 살펴보면
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
어셈블리어처럼 보임
문제에서 제시해준 asm 파일을 통해 자세히 살펴볼 예정
(4)
디버깅을 통해 자세히 비교
pwnable.kr/bin/leg.asm에 접속해 디스어셈블된 코드 확인
key1 함수 코드를 살펴보면
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.
<key1+8> 구간에서 C코드에 작성된 내용 확인 가능
<key1+12> 구간에서 r3를 r0에 저장
따라서 r0에는 pc 값이 저장됨
pc?
program counter → 다음에 실행해야 할 명령어의 주소를 가리킴
⚲ ARM 아키텍처에서의 명령어 실행 과정
fetch(명령어 로드) → decode(해석) → execute(실행)
명령어가 로드되어 실행되기까지 2단계를 거쳐야 함
ARM 아키텍처의 주소 단위는 WORD이므로,
실행까지 2WORD 단계를 거쳐야 함
1WORD = 4byte이므로, 2WORD = 8byte임
따라서
pc(다음 실행될 명령어 주소) == 현재 명령어 주소 + 8byte
현재 실행되는 명령어 주소 == pc - 8byte
즉, <key+8> 코드가 실행되는 과정이라면
fetch | decode | execute |
<key1+16> | <key1+12> | <key1+8> |
sub sp, r11, #0 | mov r0, r3 | mov r3, pc |
<key1+8> 구간으로부터 2단계를 거친 <key1+16> 구간이 pc의 값이 됨
pc의 주소(<key1+16>의 주소)는 0x00008ce4임
key2 함수 코드를 살펴보면
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.
<key2+20> 구간에서 pc를 r3에 저장
<key2+22> 구간에서 r3에 4를 더함
fetch | decode | execute |
<key2+24> | <key2+22> | <key2+20> |
push {r3} | adds r3, #4 | mov r3, pc |
<key2+20> 구간으로부터 2단계를 거친 <key2+24> 구간이 pc의 값이 됨
<key2+24>의 주소는 0x00008d08임
여기서 <key2+22> 구간에서 r3+4를 연산했으므로
pc의 주소(<key2+24>의 주소)는 0x00008d08 + 4 = 0x00008d0c임
key3 함수 코드를 살펴보면
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.
<key3+8> 구간에서 lr를 r3에 저장
<key3+12> 구간에서 r3을 r0에 저장
lr?
link register → 함수를 호출한 후 반환하는 주소 저장 (되돌아갈 함수의 주소 저장)
key3 함수 호출이 종료된 후 되돌아가는 함수는 main 함수임
main 함수의 코드를 살펴보면
..생략..
0x00008d78 <+60>: add r4, r4, r3
0x00008d7c <+64>: bl 0x8d20 <key3>
0x00008d80 <+68>: mov r3, r0
0x00008d84 <+72>: add r2, r4, r3
..생략..
<main+64> 구간에서 key3 함수 호출이 종료된 후
<main+68> 구간으로 다시 돌아옴
따라서 key3에서 lr은 <main+68>의 주소를 저장하고 있으므로
lr의 주소는 0x00008d80임
(5)
상단의 C코드의 내용을 바탕으로 key1 + key2 + key3를 계산하면
key1 | key2 | key3 |
0x00008ce4 | 0x00008d0c | 0x00008d80 |
0x8ce4 + 0x8d0c + 0x8d80 = 0x1A770
key1 + key2 + key3의 값은 key와 비교해서 일치해야 하는데,
key는 int형으로 선언되어 있음
따라서 0x1A770의 10진수 값인 108400이 key값이 됨
leg 파일을 실행해 해당 10진수 값을 입력하면 flag 획득 가능
'3. Pwnable (포너블) > 1) Write UP' 카테고리의 다른 글
[7주차] pwnable.kr 문제 풀이_pwnabless (0) | 2025.05.26 |
---|---|
[5주차] pwnable.kr 문제 풀이_pwnabless (0) | 2025.05.13 |
[2020.04.14] pw.Sly - pwnable.kr의 bof(2) (0) | 2020.04.26 |
[2020.04.07] pw.Sly - pwnable.kr의 bof(1) (0) | 2020.04.26 |