deayzl's blog

[2022 Fall GoN Open Qual CTF] Bomblab - Hard writeup 본문

CTF writeup/GoN Open Qual CTF

[2022 Fall GoN Open Qual CTF] Bomblab - Hard writeup

deayzl 2022. 8. 31. 21:00

 

undefined8 main(int param_1,undefined8 *param_2)
{
  undefined8 uVar1;
  long in_FS_OFFSET;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  if (param_1 == 1) {
    stdinaddr = stdin;
  }
  else {
    if (param_1 != 2) {
      printf("Usage: %s [<input_file>]\n",*param_2);
                    /* WARNING: Subroutine does not return */
      exit(8);
    }
    stdinaddr = fopen((char *)param_2[1],"r");
    if (stdinaddr == (FILE *)0x0) {
      printf("%s: Error: Couldn\'t open %s\n",*param_2,param_2[1]);
                    /* WARNING: Subroutine does not return */
      exit(8);
    }
  }
  init();
  puts("Welcome to my fiendish little bomb. You have 6 phases with");
  puts("which to blow yourself up. Have a nice day!");
  uVar1 = InputString();
  phase1(uVar1);
  DefuseBomb();
  puts("Phase 1 defused. How about the next one?");
  uVar1 = InputString();
  phase2(uVar1);
  DefuseBomb();
  puts("That\'s number 2.  Keep going!");
  uVar1 = InputString();
  phase3(uVar1);
  DefuseBomb();
  puts("Halfway there!");
  uVar1 = InputString();
  phase4(uVar1);
  DefuseBomb();
  puts("So you got that one.  Try this one.");
  uVar1 = InputString();
  phase5(uVar1);
  DefuseBomb();
  puts("Good work!  On to the next...");
  uVar1 = InputString();
  phase6(uVar1);
  DefuseBomb();
  puts("Umm... Good job bb :)");
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

ghidra 로 디컴파일한 모습이다.

 

총 6번의 phase 가 있으며, 이 모든 phase 를 통과해야 한다.

 

phase1 : 

void phase1(char *string)
{
  int iVar1;
  byte *i;
  
  for (i = (byte *)string; *i != 0; i = i + 1) {
    *i = *i ^ 0xcc;
  }
  iVar1 = memcmp(string,&DAT_00102020,0xb);
  if (iVar1 != 0) {
    bomb();
  }
  return;
}

 

입력받은 string 에 0xcc 를 xor 값이 DAT_00102020 에 있는 값과 같아야 한다.

#phase1
data102020 = [0x83, 0x9c, 0x89, 0x82, 0x93, 0x9f, 0x89, 0x9f, 0x8d, 0x81, 0x89, 0x00]
st = b''
for i in range(0xb):
    st += chr(data102020[i] ^ 0xcc).encode()
    
print(p.recvline().decode())
print(p.recvline().decode())
p.send_raw(st + b'\n')

이렇게 하면 phase1 을 통과할 수 있다.

 

 

phase2 :

 

void phase2(char *string)
{
  long in_FS_OFFSET;
  int i;
  int intarray [6];
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  Get6Integer(string,intarray);
  if (intarray[0] < 0) {
    bomb();
  }
  for (i = 1; i < 6; i = i + 1) {
    if (intarray[i] != intarray[i + -1] + ((i + 1) * i >> 1)) {
      bomb();
    }
  }
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}​

입력한 string 으로부터 6개의 integer 를 받고, validate 하는 것을 볼 수 있다.

 

#phase2
data2 = [0, 0, 0, 0, 0, 0]
st = b'0 '
for i in range(1, 6):
    data2[i] = data2[i - 1] + ((i + 1) * i >> 1)
    st += str(data2[i]).encode() + b' '
st = st[:-1]

print(p.recvline().decode())
p.send_raw(st + b'\n')

(phase4 까지는 거기서 거기라, 딱히 설명을 하지는 않겠다)

 

 

phase3 :

void phase3(undefined8 param_1)
{
  long in_FS_OFFSET;
  uint uinteger1;
  int uinteger2;
  int local_1c;
  ulong ulong1;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  uinteger2 = 0;
  local_1c = 0;
  local_1c = __isoc99_sscanf(param_1,"%u %u",&uinteger1,&uinteger2);
  if (local_1c < 2) {
    bomb();
  }
  ulong1 = (ulong)uinteger1 * 0x1ad7e715;
  if (uinteger1 < 0xdeadbeef) {
    bomb();
  }
  if ((ulong)(uinteger1 - uinteger2) / 0x3d != (ulong1 >> 0x35) * 0x500bf) {
    bomb();
  }
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}
#phase3
uint_1 = 0xdeadbeef
uint_3 = uint_1 * 0x1ad7e715
uint_2 = uint_1 - ((uint_3 >> 0x35) * 0x500bf) * 0x3d
if((uint_1 - uint_2) / 0x3d != (uint_3 >> 0x35) * 0x500bf):
    exit(1)

print(p.recvline().decode())
p.send_raw(str(uint_1).encode() + b' ' + str(uint_2).encode() + b'\n')

 

 

phase4 :

int recursive(int param_1,int param_2,int param_3)
{
  int iVar1;
  int iVar2;
  
  if (param_1 == 0) {
    iVar1 = 0;
  }
  else if (param_1 == 1) {
    iVar1 = 1;
  }
  else {
    iVar1 = recursive(param_1 + -1,param_2,param_3);
    iVar2 = recursive(param_1 + -2,param_2,param_3);
    iVar1 = iVar1 * param_2 - iVar2 * param_3;
  }
  return iVar1;
}

void phase4(undefined8 param_1)
{
  long in_FS_OFFSET;
  int integer1;
  int integer2;
  int local_18;
  int result;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  local_18 = __isoc99_sscanf(param_1,"%d %d",&integer1,&integer2);
  if (((local_18 != 2) || (integer1 < 0)) || (integer2 < 0)) {
    bomb();
  }
  result = recursive(5,integer1,integer2);
  if (result != 0xc6be719) {
    bomb();
  }
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}
#phase4 + DefuseBomb
"""def func(n, d1, d2):
result = 0
tmp = 0

if (n == 0):
    result = 0
elif (n == 1):
    result = 1
else:
    result = func(n + -1,d1,d2)
    tmp = func(n + -2,d1,d2)
    result = result * d1 - tmp * d2
return result
num1 = 0
num2 = 0
for i in range(0, 1000):
    for j in range(0, 1000):
        value = func(5, i, j)
        if(value == 0xc6be719):
            num1 = i
            num2 = j
            break
    if(num1 != 0 and num2 != 0):
        break
            
    
print(num1)
print(num2)"""

print(p.recvline().decode())
p.send_raw(str(123).encode() + b' ' + str(456).encode() + b' c0m0r1bb\n')

마지막에 문자열 'c0m0r1bb' 을 넣어준 이유는 DefuseBomb 함수의 일부분

if (idx == 6) {
  local_6c = __isoc99_sscanf(&DAT_00303e70,"%d %d %s",local_74,local_70,local_68);
  if (local_6c == 3) {
    iVar1 = isSameString(local_68,"c0m0r1bb");
    if (iVar1 == 0) {
      puts("Curses, you\'ve found the secret phase!");
      puts("But finding it and solving it are quite different...");
      finalphase();
    }
  }
}

phase6 이 끝나고 나서 finalphase 에 들어가기 위함이다.

 

 

phase 5 :

void phase5(undefined8 param_1)
{
  int iVar1;
  long in_FS_OFFSET;
  double input;
  double f;
  double a;
  double compare;
  double b;
  double c;
  double d;
  double e;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  compare = 1e-07;
  f = 0.0;
  a = 0.0;
  iVar1 = __isoc99_sscanf(0,param_1,&%lf,&input);
  if (((iVar1 != 1) || (input < 0.0)) || (1.0 < input)) {
    bomb();
  }
  while (a < input) {
    b = (f + 1.0) * (a + 1.0);
    a = compare / 2.0 + a;
    c = ((b * compare) / 2.0 + f + 1.0) * (a + 1.0);
    d = ((c * compare) / 2.0 + f + 1.0) * (a + 1.0);
    a = compare / 2.0 + a;
    e = (compare * d + f + 1.0) * (a + 1.0);
    f = ((d + d + c + c + b + e) * compare) / 6.0 + f;
  }
  if (compare < (double)((ulong)(f - 1.604151184400547) & 0x7fffffffffffffff)) {
    bomb();
  }
  if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

문제를 풀 당시, 여기에서 시간을 좀 썼다.

 

이 코드를 그대로 복붙해서 돌려봐도 결과가 같게 나오진 않는다.
(기드라가 정확하게 디컴파일 하지 못하는 경우가 있다)

그래서 그냥 ida 를 이용해서, 이 bomb 파일의 어셈블리어를 조작해 elf 파일을 따로 만들었고

어떤 input 이 들어갈 때, 어떤 f 값이 나오는 지 리스트를 뽑았으며

0.7 근처에서 값이 작아져서, 그 근처를 조사했다.

 

대충

a = 0.68
while a <= 1.0:
    p = process('bomb.patched')
    p.recvline()
    ...
    p.send_raw(str(a).encode() + b'\n')
    f = hex_to_double(p.recvline()[2:-1])
    print('f : ' + str(f))
    if(p.recvline() == b'0x1\n'): #if sucess print 0x1 else print 0x0
    	print('bingo! a : ' + str(a))
        break
    a = compare / 2.0 + a
    a = compare / 2.0 + a
    p.close()

이런식으로 스크립트를 짜서 적절한 input 값을 찾았다.

 

#phase5
print(p.recvline().decode())
p.send_raw(str(0.7071067428588867).encode() + b'\n')

 

 

phase6 :

ulong recursive(uint param_1,int param_2,uint param_3,uint *param_4)
{
  ulong uVar1;
  
  if (param_4 == (uint *)0x0) {
    uVar1 = 0;
  }
  else if (param_3 == *param_4) {
    if (param_1 == param_2 * 2) {
      uVar1 = (ulong)param_3;
    }
    else {
      uVar1 = 0;
    }
  }
  else if (param_3 == param_4[1]) {
    if (param_1 == (param_2 * 2 | 1U)) {
      uVar1 = (ulong)param_3;
    }
    else {
      uVar1 = 0;
    }
  }
  else if ((int)param_3 < (int)*param_4) {
    uVar1 = recursive(param_1,param_2 * 3 + -1,param_3,*(undefined8 *)(param_4 + 2));
  }
  else if ((int)param_3 < (int)param_4[1]) {
    uVar1 = recursive(param_1,param_2 * 3,param_3,*(undefined8 *)(param_4 + 4));
  }
  else {
    uVar1 = recursive(param_1,param_2 * 3 + 1,param_3,*(undefined8 *)(param_4 + 6));
  }
  return uVar1;
}

void phase6(undefined8 param_1)
{
  int iVar1;
  long in_FS_OFFSET;
  int i;
  int local_4c;
  int intarray [14];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_4c = 1;
  Get12Integer(param_1,intarray);
  for (i = 0; i < 6; i = i + 1) {
    if ((intarray[i] < 1) || (0x1a < intarray[i])) {
      bomb();
    }
  }
  for (i = 0; i < 6; i = i + 1) {
    iVar1 = recursive(intarray[i],1,intarray[i + 6],&DAT_00303d20);
    local_4c = local_4c * iVar1;
  }
  if (local_4c != 0xdac3bf7) {
    bomb();
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

여기 부분도 어려워보이지만 사실 별거 아니다.

 

함수를 잘 살펴보면 건네준 intarray 값 두 개에만 dependent 한 것을 볼 수 있다.

즉, 범위를 만족하는 int 값 두 개에 대한 recursive 함수 결과 값은 루프 몇번째에서 계산이 되는지와 무관하다.

 

그럼 int 두 개를 짝지어서 다 recursive 함수 돌려서 값을 다 구하고

(ida 를 이용해 계산된 값을 출력하도록 코드패치 하여, 각각의 값을 다 구했다)

그 중 0xdac3bf7 % result == 0 인 결과 값을 뽑아내면 된다.

(범위 제한은 intarray 처음 6개에만 적용되는 것을 유의해야한다)

다 뽑아낸 뒤, 그 중에서도 곱하면 0xdac3bf7 이 되는 6개의 값을 찾으면

그 6개의 값이 되기 위한 int 페어들이 payload 이다.

 

#phase6
print(p.recvline().decode())
p.send_raw(b'12 15 2 17 18 21 7 19 23 31 41 59\n')

 

이제 마지막이다.

 

DefuseBomb : 

void DefuseBomb(void)
{
  int iVar1;
  long lVar2;
  long in_FS_OFFSET;
  undefined local_74 [4];
  undefined local_70 [4];
  int local_6c;
  undefined local_68 [88];
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  if (0 < idx) {
    if (idx == 1) {
      lVar2 = ptrace(PTRACE_TRACEME,0,0,0);
      if (lVar2 == -1) {
        bomb();
      }
    }
    if (idx == 6) {
      local_6c = __isoc99_sscanf(&phase4_string,"%d %d %s",local_74,local_70,local_68);
      if (local_6c == 3) {
        iVar1 = isSameString(local_68,"c0m0r1bb");
        if (iVar1 == 0) {
          puts("Curses, you\'ve found the secret phase!");
          puts("But finding it and solving it are quite different...");
          finalphase();
        }
      }
    }
  }
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

idx 변수는 InputString 함수로 string 을 입력 받을때마다 하나씩 늘어난다.

즉, phase6 이 끝나면 idx 는 6이 되고 finalphase 에 들어갈 기회를 얻는다.

 

phase4 에서 언급했듯이, finalphase 에 들어갈려면 phase4 때 입력받은 string 끝에 c0m0r1bb 라는 문자열을 넣어줘야한다.

 

finalphase : 

void finalphase(void)
{
  int int;
  char *string;
  long in_FS_OFFSET;
  uint i;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  string = (char *)InputString();
  bomb();
  int = atoi(string);
  int = abs(int);
  if ((double)int == 3.14) {
    puts("Wow! You\'ve defused the secret stage!");
    puts("Congratulations! You\'ve defused the bomb!");
    getflag();
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  for (i = 0; i < 0x16f; i = i + 1) {
    *(long *)(&stack0x00000000 + (long)(int)i * 8) =
         *(long *)(&DAT_00303020 + (long)(int)i * 8) + 0x2bf240;
  }
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail(string);
  }
  return;
}

이 코드를 처음 볼 때, 좀 당황했다.

입력받은 int 가 3.14 가 될 수 없기에, getflag 함수를 호출하는 것이 불가능해보였기 때문이다.

 

그래서 그냥 assembly 를 천천히 살펴보았다.

 

assembly

처음 부분에서 abs 함수의 주소와 atoi 함수의 주소 하위 1.5 바이트를 비교하는 것을 볼 수 있다.

 

그리고 나중엔 갑자기 rbp + 0x8 를 가지고 오기도 하고,

0x45dd0 을 빼는 명령어도 있다.

 

뭔가 평범하지는 않아 보여서, 더 분석하자면

 

ret 하기 전에 r9 에 finalphase + 0x90 주소값을 넣고, r10 에 bomb 함수 주소값을 넣는 것을 볼 수 있다.

getflag 함수 호출을 할 방법이, 앞의 3.14 비교 구문 밖에 없었다면 이런 이상한 어셈블리어는 없었을 것이다.

 

무엇보다 이상했던 점은 지금까지의 payload 를 다 보냈을 때, 원래대로라면 'Umm... ~' 의 문자열이 출력되어야 했을 것이다.

그러나 bomb 함수가 실행된 모습을 볼 수 있었다.

 

그래서 patchelf 로 라이브러리를 끼워 넣어준 다음, 직접 gdb 로 디버깅해보았다.

알고보니 그 이후로 rop chain 이 쭉 이어져 있는 것을 발견했다.

(입력한 값은 rdi 에 있고 r9 에는 flag 를 얻는 주소, r10 에는 bomb 함수의 주소가 있다)

 

쭉 가다보면 call r10 이 호출되게 되니, bomb 함수가 호출되는 것이었다.

 

입력한 값은 rdi 에 있으니 rdi 를 기준으로 노트에 적어가며 rop chain 을 분석해보았고 rdi 의 값이 어떤 문자열이 되어야 한다는 것을 알아내었다.

(자세한 분석 사진을 올리고 싶으나, 중간에 칼리 리눅스가 죽어버려서 다 날라갔다...)

 

이 내용을 코드로 대충 적자면

target_addr = 0x7ffff7facd80
while True:
    if([rdi] == 0x0):
        break

    #asm
    movzx eax, [rdi]
    sub eax, ecx ; ecx : 0x60041
    for i in range(2):
        shl rax, 5
        lea rax, [rax + rdx + 4] ; rdx : 0x0
        sub rax, 1
        sub rax, 1
        sub rax, 1
        sub rax, 1
    sar eax, 6
    mov rcx, rax
    mov BYTE PTR [target_addr], cl

    rdi += 1
    movzx eax, [rdi]
    sub eax, ecx ; ecx : 0x60041
    mov r8b, al
    add BYTE PTR [target_addr], r8b
    
    target_addr += 1
   
if([target_addr] == "[R0P_M4DN3SS!!]"):
	getflag()
else:
	bomb()

 

대충 이런식이다.

(정확하지는 않다)

 

첫번째 값으로 'a' 를 전달하면 결국 target_addr 에 0x0 이 전달되고,

두번째 값으로 0x41 + 원하는 값 을 전달하면

0x7ffff7facd80 주소에 원하는 문자열을 넣을 수 있다. 

 

#DefuseBomb
p.send_raw(b'a\x9ca\x93a\x71a\x91a\xa0a\x8ea\x75a\x85a\x8fa\x74a\x94a\x94a\x62a\x62a\x9ea\x41' + b'\n')

 

이렇게 하면 flag 를 얻을 수 있다.

 

 

최종 exploit 스크립트는 이렇다.

 

from pwn import *

p = remote('host3.dreamhack.games',17442)

#phase1
data102020 = [0x83, 0x9c, 0x89, 0x82, 0x93, 0x9f, 0x89, 0x9f, 0x8d, 0x81, 0x89, 0x00]
st = b''
for i in range(0xb):
    st += chr(data102020[i] ^ 0xcc).encode()
    
print(p.recvline().decode())
print(p.recvline().decode())
p.send_raw(st + b'\n')

#phase2
data2 = [0, 0, 0, 0, 0, 0]
st = b'0 '
for i in range(1, 6):
    data2[i] = data2[i - 1] + ((i + 1) * i >> 1)
    st += str(data2[i]).encode() + b' '
st = st[:-1]
print(p.recvline().decode())
p.send_raw(st + b'\n')

#phase3
uint_1 = 0xdeadbeef
uint_3 = uint_1 * 0x1ad7e715
uint_2 = uint_1 - ((uint_3 >> 0x35) * 0x500bf) * 0x3d
if((uint_1 - uint_2) / 0x3d != (uint_3 >> 0x35) * 0x500bf):
    exit(1)

print(p.recvline().decode())
p.send_raw(str(uint_1).encode() + b' ' + str(uint_2).encode() + b'\n')

#phase4 + DefuseBomb
"""def func(n, d1, d2):
result = 0
tmp = 0

if (n == 0):
    result = 0
elif (n == 1):
    result = 1
else:
    result = func(n + -1,d1,d2)
    tmp = func(n + -2,d1,d2)
    result = result * d1 - tmp * d2
return result
num1 = 0
num2 = 0
for i in range(0, 1000):
    for j in range(0, 1000):
        value = func(5, i, j)
        if(value == 0xc6be719):
            num1 = i
            num2 = j
            break
    if(num1 != 0 and num2 != 0):
        break
            
    
print(num1)
print(num2)"""

print(p.recvline().decode())
p.send_raw(str(123).encode() + b' ' + str(456).encode() + b' c0m0r1bb\n')

#phase5
print(p.recvline().decode())
p.send_raw(str(0.7071067428588867).encode() + b'\n')

#phase6
print(p.recvline().decode())
p.send_raw(b'12 15 2 17 18 21 7 19 23 31 41 59\n')

print(p.recvline().decode())
print(p.recvline().decode())

#DefuseBomb
p.send_raw(b'a\x9ca\x93a\x71a\x91a\xa0a\x8ea\x75a\x85a\x8fa\x74a\x94a\x94a\x62a\x62a\x9ea\x41' + b'\n')

p.interactive()

 

Comments