본문 바로가기
Wargame Write-Up/Reversing.kr

[Reversing.kr] (Revering) SimpleVM 풀이

by snwo 2021. 2. 1.

ELF32 파일이다. 아이다로 열면 entry point 에러가 난다.

gdb 로 열면 이상한곳에 와있다. 실행시키면 Access Denied 가 출력되고 끝난다.

strace 로 확인한 결과이다. getuid32() 함수를 호출해 uid 를 가져온 뒤, Access Denied 가 출력되는걸 보면,

root 로 실행해야한다는 합리적의심이 가능하다!

 

sudo strace ./SimpleVM

getuid32() 결과가 0이고, Input 이라고 출력되는 것을 볼 수 있다. helloworld! 을 입력했더니 Wrong 이라 뜬다.

 

패킹된 것같은데 프로세스를 덤프해서 정적분석해보자.


procdump 리눅스버전을 쓸 수도 있고, gcore 으로 간단하게 할 수 있다.

 

터미널 2개를 띄우자 (터미널1, 터미널2 로 이름짓겠다)

 

터미널 1에서 $ sudo ./SimpleVM 을 실행해 입력하지말고 기다린다.

터미널 2에서 $ ps -al 으로 SimpleVM 의 PID 를 확인한 뒤, $ sudo gcore (PID) 를 입력해 코어파일을 덤프하자.

이제 아이다로 열어서 분석하면 된다!

 

이 때 부모프로세스를 덤프하던, 자식프로세스를 덤프하던 상관이 없다.


core 파일을 ida 로 열어서 분석하자. 

shift+f12 를 눌러 string table 에서 "input :" 문자열을 클릭해, XREF (문자열이 참조된곳) 으로 이동한다.

 

main+6E 라고 써진 부분이 main 함수이다.

실제로 열어보면 0x804556 으로 되어있을 것이다. (직접 수정했기 때문)

plt 가 다 깨져있어 함수 이름을 확인할 수 없다.

aslr 끄고 다시 덤프해서 gdb로 함수주소를 확인할까 했는데, 생각보다 함수가 별로 없어

그냥 strace 결과 보고 수정하면 된다.

 

1
2
3
4
5
6
7
8
9
  if ( getuid32() )
  {
    v11 = 0;
    for ( i = 1; i <= 14; ++i )
    {
      v10 = byte_804B060[i - 1] ^ i;
      write(1, &v10, 1);                        // Access Denied!
    }
  }

cs

 

uid 를 가져와 root 인지 비교하는 부분이다. root 아니면 Access Denied 가 출력됐던 부분이ㄷ ㅏ.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    write(1, "Input : ", 8);
    pipe(&v5);
    if ( v0 != -1 && (pipe(&v7), v1 != -1) )    // pipe 생성으로 자식프로세스와 통신
    {
      v9 = clone();
      if ( v9 == -1 )
      {
        v11 = 0;
        for ( i = 1; i <= 6; ++i )
        {
          v10 = byte_804B06E[i - 1] ^ i;
          write(1, &v10, 1);                    // Error
        }
      }

cs

 

root 라면, 이 부분이 실행된다. pipe 함수를 v5, v7 을 각각 인자로

pipe 통신하기위해 준비하고, clone 함수로 자식프로세스를 생성한다. 실패하면 Error

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
      else                                      // child process
      {
        v12 = 0;
        v13 = 0;
        v14 = 0;
        read(0, &v12, 10);
        if ( v14 )
        {
          v11 = 0;
          for ( i = 1; i <= 6; ++i )
          {
            v10 = byte_804B074[i - 1] ^ i;
            write(1, &v10, 1);                  // Wrong
          }
        }
        else
        {
          write(v6, &v12, 9);
          for ( i = 0; i <= 199; ++i )
          {
            v2 = ROL_3(*(i + 0x804B0A0), 3);
            *(i + 0x804B0A0) = v2;
          }
          sub_8048410();
          write(v6, &input, 200);               // v12에 읽고, 0x804b0a0 암호화하고
                                                // 두 개를 v6에 출력 (fd[1])
        }
      }
    }

cs

 

 

fd 0 에서 입력을 받고, 0x804B0A0 ~ 0x804B168 값들을 ROL 3 돌린다.

그리고 v6 에 출력하는데 (fd 1)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
      else if ( v9 )                            // parent process
      {
        v15 = 0;
        v16 = 0;
        v17 = 0;
        read(v5, &v15, 9);
        read(v5, &input, 200);
        for ( i = 0; i <= 199; ++i )
          *(i + 0x804B0A0) ^= 0x20u;
        input = v15;
        input_4 = v16;
        for ( i = 0; i <= 199; ++i )
          *(i + 0x804B0A0) ^= 0x10u;
        if ( vm() == 1 )
        {
          v11 = 0;
          for ( i = 1; i <= 6; ++i )
          {
            v10 = byte_804B074[i - 1] ^ i;
            write(1, &v10, 1);                  // true
          }
        }
        else
        {
          v11 = 0;
          for ( i = 1; i <= 6; ++i )
          {
            v10 = byte_804B074[i - 1] ^ i;      // false
            write(1, &v10, 1);
          }
        }
      }

cs

 

v6에 출력된 값을, 부모프로세스에서 받아와, 0x804B0A0 영역을 XOR 20 하고,

0x804B0A0 ~ A8 에 내 입력값 8바이트를 넣고, 다시 0x804B0A0 영역을 XOR 10 한다.

 

그리고 vm(내가 명명) 함수를 호출해 리턴값이 1인지 검사한다.


Input :  을 출력하고, read 함수가 호출된 상태에서 덤프해온 파일이니, 

'그' 영역은, 아직 암호화하지 않은 상태이다.

 

 

HexView 에서 그 영역에 커서를 올리고

ainsetin 님 감사합니다 !

Byte(here()+i) 으로 값을 가져올 수 있다.

 

1
2
3
4
5
6
7
8
ar=[4, 4, 4, 4, 4, 4, 4, 4, 4, 69, 69, 196, 196, 4, 4, 101, 68, 228, 8, 196, 4, 228, 68, 228, 37, 228, 4, 228, 37, 68, 68, 228, 200, 196, 36, 228, 68, 228, 68, 228, 36, 228, 37, 68, 68, 228, 166, 196, 68, 228, 68, 228, 192, 228, 68, 228, 37, 68, 68, 228, 228, 196, 100, 228, 68, 228, 161, 228, 100, 228, 37, 68, 68, 228, 141, 196, 132, 228, 68, 228, 64, 228, 132, 228, 37, 68, 68, 228, 104, 196, 164, 228, 68, 228, 228, 228, 164, 228, 37, 68, 68, 228, 11, 196, 196, 228, 68, 228, 6, 228, 196, 228, 37, 68, 68, 4, 36, 101, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
def ROL3(d):
        return ((d<<3)|(d>>5))&0xff
 
for i in range(200):
        ar[i]=ROL3(ar[i])^0x30
 
print(ar)

[16, 16, 16, 16, 16, 16, 16, 16, 16, 26, 26, 22, 22, 16, 16, 27, 18, 23, 112, 22, 
16, 23, 18, 23, 25, 23, 16, 23, 25, 18, 18, 23, 118, 22, 17, 23, 18, 23, 18, 23, 
17, 23, 25, 18, 18, 23, 5, 22, 18, 23, 18, 23, 54, 23, 18, 23, 25, 18, 18, 23, 23,
 22, 19, 23, 18, 23, 61, 23, 19, 23, 25, 18, 18, 23, 92, 22, 20, 23, 18, 23, 50, 23,
 20, 23, 25, 18, 18, 23, 115, 22, 21, 23, 18, 23, 23, 23, 21, 23, 25, 18, 18, 23, 104,
 22, 22, 23, 18, 23, 0, 23, 22, 23, 25, 18, 18, 16, 17, 27, 16, 16, 16, 16, 16, 16, 16,
 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
 16, 16, 16, 16, 16, 16, 16, 16]


cs

 

이제 vm 함수를 분석하자.


함수 이름은 임의로 지정한 것이다. init 함수로 그 영역에서 값을 가져와 알맞은 함수들로 switch 한다.

덤프 뜬거라 그 영역들의 값을 그대로 가져와 디컴파일해줘서, 일일이 어셈코드를 분석했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def init():
    global ar
    ar[240]=ar[ar[9]^0x10]^0x10
    ar[9]=(ar[9]^0x10)+1^0x10
 
while 1:
    print(f"this is {ar[9]}")
    init()
    print(f"okay {ar[240]}")
    if ar[240]==2:     #mov
        init()
        ar[248]=ar[240]
        init()
        ar[244]=ar[240]
        ar[240]=ar[248]
        ar[248]=ar[244]
        ar[ar[240]]=ar[248]^0x10
        print(f"mov {ar[248]^0x10} to ar[{ar[240]}]")
    elif ar[240]==6:    #xor
        ar[248]=ar[240]
        ar[236]=ar[244]
        init()
        ar[236]=ar[240]
        init()
        a=ar[240]
        ar[240]=ar[ar[240]]^0x10
        ar[244]=ar[240]
        ar[240]=ar[236]
        b=ar[240]
        ar[240]=ar[ar[240]]^0x10
        ar[240]=ar[244]^ar[240]
        print(f"xor ar[{a}] and ar[{b}] : {ar[240]}",end='')
        ar[248]=ar[240]
        ar[240]=ar[236]
        ar[ar[240]]=ar[248]^0x10
        print(f", ar[{ar[240]}]")
    elif ar[240]==7:    #cmp
        init()
        a=ar[240]
        ar[240]=ar[ar[240]]^0x10
        ar[248]=ar[240]
        init()
        b=ar[240]
        ar[240]=ar[ar[240]]^0x10
        ar[244]=ar[240]
        if ar[248]==ar[244]:
            ar[248]=1
        else:
            ar[248]=0
        ar[ar[240]]=ar[248]^0x10
        print(f"cmp ar[{a}] and ar[{b}] res in ar[{ar[240]}]={ar[ar[240]]^0x10}")
    elif ar[240]==9:    #jz
        ar[248]=ar[240]
        init()
        ar[248]=ar[240]
        ar[240]=8
        ar[240]=ar[ar[240]]^0x10
        print(f"if ar[8] ({ar[8]^0x10} is 0, ar[240]={ar[248]},ar[0xa]={ar[0xa]^0x10}+ar[240]^0x10")
        if ar[240]==0:
            ar[240]=ar[248]
            ar[9]=(ar[0xa]^0x10)+ar[240]^0x10
 
    elif ar[240]==10:    #jump
        init()
        ar[9]=(ar[0xa]^0x10)+ar[240]^0x10
        print(f"plus {ar[0xa]^0x10} and {ar[240]} = ar[9] ({ar[9]})")
    elif ar[240]==11:
        ar[240]=0
        ar[240]=ar[ar[240]]^0x10
        ar[248]=ar[240]            
        ar[240]=1
        ar[240]=ar[ar[240]]^0x10
        ar[244]=ar[240]
        ar[240]=ar[248]
        ar[248]=ar[244]
        print(f"case 11, {ar[240]}, {ar[248]}")
    else:
        break
cs

대충 이런식으로 분석했다.

switch 문 별로 해당하는 어셈코드와 같은 동작을 했다.

ar[0]~ar[6] 은 입력값으로, 비교당하고

ar[8] 은 ZF 으로, 분기여부를 결정하고

ar[9] 는 IP 로, 분기할때 사용되고,

ar[236] ~ ar[248] 은 레지스터로 사용됐다.

 

중간중간 0x10 과 xor 하는 이유는, 0x10 이 암호화방식이기 때문이다 ㅋㅋ

더 어려운 문제는 코드를 더 어렵게 암호화할 것같았다.

 

아마 값을 하나씩 가져와 제시하는 값과 입력값이 같지 않으면, 이상한곳으로 JUMP 시키는 것같다.

저 코드를 실행시켰는데, 값이 같지 않으니 실행되다가 중간에 멈춰버렸다.

 

그래서 어셈코드로 만들었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
ar=[4, 4, 4, 4, 4, 4, 4, 4, 4, 69, 69, 196, 196, 4, 4, 101, 68, 228, 8, 196, 4, 228, 68, 228, 37, 228, 4, 228, 37, 68, 68, 228, 200, 196, 36, 228, 68, 228, 68, 228, 36, 228, 37, 68, 68, 228, 166, 196, 68, 228, 68, 228, 192, 228, 68, 228, 37, 68, 68, 228, 228, 196, 100, 228, 68, 228, 161, 228, 100, 228, 37, 68, 68, 228, 141, 196, 132, 228, 68, 228, 64, 228, 132, 228, 37, 68, 68, 228, 104, 196, 164, 228, 68, 228, 228, 228, 164, 228, 37, 68, 68, 228, 11, 196, 196, 228, 68, 228, 6, 228, 196, 228, 37, 68, 68, 4, 36, 101, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
def ROL3(d):
    return ((d<<3)|(d>>5))&0xff
 
input=b'i1234567p'
 
for i in range(200):
    ar[i]=ROL3(ar[i])^0x20
for i in range(9):
    ar[i]=input[i]
 
for i in range(200):
    ar[i]^=0x10
ar+=[0]*50
 
code=ar[10:]
i=0
d=[]
flag=""
while i<len(code):
    if code[i]^0x10==2:
        print(f"mov ar[{code[i+1]^0x10}], {code[i+2]^0x10}")
        d.append(code[i+2]^0x10)
        i+=3
 
    elif code[i]^0x10==6:
        print(f"xor ar[{code[i+1]^0x10}], ar[{code[i+2]^0x10}]")
        i+=3
 
    elif code[i]^0x10==7:
        print(f"cmp ar[{code[i+1]^0x10}], ar[{code[i+2]^0x10}]")
        i+=3
 
    elif code[i]^0x10==9:
        print(f"if zero, jump ar[8]^0x10 +ar[{code[i+1]^0x10}]")
        i+=2
        
    elif code[i]^0x10==10:
        print(f"jmp {code[0]^0x10}+ar[{code[i+1]^0x10}]")
        i+=2

    elif code[i]^0x10==11:
        print('CORRECT!\nEND')
        i+=1

    else:
        print('END')
        break
print('-'*0x15+'FLAG'+'-'*0x15)
for i in range(0,len(d)-1,2):
    print(chr(d[i]^d[i+1]),end='')
print('\n'+'-'*0x30)
cs

 

이런식으로 사용하는 인자수+1 만큼 i 값을 증가시키면서 디스어셈블 했다.

 

대충 보니 mov 로 옮기는 두 수를 가져와 xor 하면 알맞은 값이 나올 것같다.

cmp 로 비교하는데 jz로 이상한값으로 점프하는건 실제 어셈코드와 다르다.

이 문제에서는 값이 같으면 ZF 를 1로, 다르면 0으로 표시한다. (실제 어셈코드와 다르게)

 

어쨋든, mov 으로 이동하는 수들을 모아, 2개씩 짝지어서 xor 하면 알맞은 값이 나온다.

야호