코드게이트 2017 문제인 것 같다.
Go 로 만든 elf 파일에서 플래그를 찾는 것이 목적.
Go 에 대한 이해가 너무 필요했다.
Go 는 스택 기반 호출을 하는 언어로 함수를 호출할 때, 레지스터나 push를 쓰지 않고 스택에 데이터를 삽입 후 원하는 인자들을 그 스택 그대로 가져와서 함수호출 후에 쓴다.
또한 리턴 값을 저장하는데에 rax를 쓰지도 않는다. 확실한건 x86_64 calling convention 과는 다르다는 것이다.
이러한 부분들 때문에 IDA가 있어도 일일히 어셈블리를 보는 수 밖에 없었다. (헥스레이가 깨짐)
우선 Go에서 함수가 어떻게 동작하는지를 조금 살펴보았다.
위 사진은 Go로 작성한 소스코드, 아래는 그 소스에 대한 어셈블리 코드이다.
1번과 2번으로 소스를 나눠서 볼 수 있다.
1번 라인 구문을 어셈블리로 보면 일반 x86_64 calling convention과는 다르게 rsp를 기준으로 변수 대입 및 연산을 한다는 것을 알 수 있다.
2번 라인 함수를 호출하는 구문은 Go가 함수인자를 어떻게 전달하는지, 리턴 값을 어떻게 다루는지 볼 수 있다.
보다시피 함수에서 쓰이는 인자는 따로 push하거나 레지스터를 사용하지않고 스택에 박아두고 쓴다.
분석해보면 알겠지만 함수를 call한 함수 스택에 있는 인자 주소를 call 당한 함수에서 그대로 가져다 쓴다.(효율적인가?)
그리고 리턴 값 또한 스택에 입력하지만 간접적으로 rax를 쓰는 모습을 볼 수 있다.
이런 루틴이 효율적인지 아닌지는 Go에 대해 깊게 분석해보지 않아서 모르겠지만 이정도만 알아놓고 다시 문제로 넘어가자.
바이너리는 이런 식으로 작동한다.
로그인을 하라고 하고 Secret Member의 계정이 아니면 Print FLAG에서 반려시킨다.
Shift + F12 로 rodata에 힌트가 될만한 String이 있을까 찾아봤는데 단서는 없었다.
그래서 직접 함수를 분석했다. 처음에 Login을 해주는 함수를 보았다.
Login_status가 1이면 id와 pw를 0으로 만들어서 Logout시킨다.
반대로 Login_status가 0이라면 처음 로그인 하는 것으로 간주, main_Login에서 id와 pw를 받아와서 검증한다.
id, pw 검증과정은 별다른 기능이 없기에(단순히 길이 체크 및 안되는 문자 걸러내기) 설명하진 않는다.
메뉴로 다시 돌아와서 이제 2번을 선택했을 때 실행되는 PrintFLAG 함수를 보자.
이 부분이 우리가 눈여겨 봐야할 최종 부분이다.
처음엔 무언가 삽질을 하게 만드는 함정이 있을 줄 알았지만 함수이름 그대로 flag 를 출력시켜주는 함수였다...
다만, 플래그를 출력시키는 과정이 험난할 뿐이다.
스택기반호출 방식이다보니 헥스레이에서 생략하는 부분이 은근히 많다.
후에 스택기반호출도 알맞게 헥스레이 해주는 방법을 찾아야할 것 같다.
아무튼 인자로 받아온 user 구조체에서 id와 pw를 갖고 main_id_pw_check 함수로 SecretMember인지 체크를 한다.
그리고 헥스레이에서는 생략되었지만 SecretMember이면 src는 1이 되어 플래그를 출력하는 것으로 보여진다.
Main_id_pw_check 함수 처음이다. 위쪽에 x86_64 calling convention의 프롤로그와 비스무리하게 스택프레임을 만드는 부분이 있는 것 같은데 runtime_morestack_noctxt() 라는 함수가 실행된다. 자세한건 공부해봐야겠다.
아무튼 secret_member 검증 루틴의 반복문은 총 4개인데
첫 번째 while문 부분인데 all_bin_num이라는 변수에 id의 각 문자의 아스키코드 이진수형태(8바이트)를 문자열 형태로 계속 이어붙인다.
이러한 루틴을 계속 이어나간 후, 다음 for문을 거치는데
이 부분에서 all_bin_num에 있는 이진수 문자열들로 어떤 연산을 하는 것 같은데
세 번째 while 문에서 최종 id 인증을 거칠 때 영향을 미치는 루틴이다.
하지만 헥스레이도 깨지고… 어셈블리도 난잡해서 한 문자씩 넣어가면서 세 번째 루틴을 알맞게 통과하는지 확인하면서 진행하였다.
한 문자씩 대입해도 되겠다고 확신이 든건 이 부분에서 id 길이가 8문자라는 걸 발견했을 때이다.
cmp rdi, rsi 에서 rsi는 all_bin_num이 두 번째 루틴을 통과한 후의 길이인데 통과하기 전과 변화가 없었다.
그리고 그 길이를 진짜 id를 인증하는데 쓰일 ID_KEY배열의 길이와 대조하는 과정에서 id를 이진수 문자열로 변환한 것들의 총 길이가 64바이트(비트아님!)가 되는 걸 발견하였다.
그리고 이제 나올 Check_bit라는 변수는 PrintFLAG에서 src가 될 변수인데
ID 검증과정에서 문제가 없었다면 Check_bit는 0으로 유지될 것이다. 이를 이용해서 ID 브루트포싱(?)을 할 수 있었다.
ID를 완벽히 찾았다면 PW검증과정은 식은 죽 먹기이다.
Id랑 pw랑 한 문자씩 xor하고 그 결과가 29바이트 길이의 PW_KEY 배열이랑 같으면 통과.
그리고 헥스레이에는 안나왔지만 Check_bit가 0으로 끝나면 src에 1을 넣는 루틴으로 분기한다.
플래그가 나온다!
'<보안 study> > CTFs' 카테고리의 다른 글
[SECCON 2019] Coffee_break (0) | 2019.12.21 |
---|---|
[TokyoWesterns 2019] EASY_CRACK_ME (0) | 2019.12.21 |
[Saudi and Oman CTF]Back to basics (0) | 2019.02.14 |
[Saudi and Oman CTF]I love images (0) | 2019.02.14 |
[Evlz_ctf]find_me (0) | 2019.02.08 |