티스토리 뷰

들어가기 전에..

바쁜 현대인들을 위한 3줄요약

1. 일이 영 손에 안잡혔음.
2. VMP 샘플인데 호출하는 Windows API 가 그지같이 되어있어서 스크립트짬.
3. 동적으로 찾아주면 편한걸 굳이 정적으로 추적해서 코드를 패치해주는 스크립트를 만듬 <- 병신 

VWAAD (VMProtect Windows API Address Decoder) : https://github.com/saweol/vwaad

개요

최근 이슈 추적 중 사이버안전국을 사칭해 악성코드 설치를 유도하는 스피어 피싱 이메일을 발견했다.

피싱메일에는 "사이버안전국.egg" 라는 egg포맷의 압축파일이 하는데 메일 본문에서 이를 "랜섬웨어 공격을 방지하기 위한 컴퓨터검사프로그램" 으로 소개하고있다. 

eml : A470BC9E090097A2BB9EA14979F8C8BD
사이버안전국.egg :  4FB6711BEB27CC5936DC3AF41D3386E7
사이버안전국.exe : 096A99A0B465EB843F5720EB1C8D199B

당연한 이야기지만 압축을 풀고 실행하면 악성코드가 설치된다.

 

 

악성코드 기본 정보

압축을 해제하면 "사이버안전국.exe"라는 파일이 나온다. 이 파일은 리소스 영역에 존재하는 압축파일을 %temp%\st[randomNumber] 폴더에 res2.tmp라는 이름으로 드롭하고 rundll32.exe를 통해 Export 함수인 Run을 실행시켜주는 역할을 한다. 

res2.tmp 파일은 VMProtect로 패킹되어 있지만 별다른 안티 디버깅이 존재하지 않는지 실행하면 .text영역에 패킹이 해제된 코드가 나온다. (악성코드 분석은 더 이상 진행하지 않는다. 분석중에 엄한데 꽂혀버려서...)

 

VMProtect Api Address Redir 분석 

아래 그림에서 .text섹션에 있는 call문이 Windows API의 주소가 아닌 .vmp0 영역의 주소를 가리키고 있다.
.vmp0 섹션에서 더미코드들을 지나고 나면 PUSH/RETN을 통해 CreateThread 함수를 호출한다.
즉 .text:0x10001F74 : call 0x10073801  함수는 결국 call CreateThread 임을 의미한다. 저런 더러운 CALL문이 한둘도 아니고 수작업으로 다 찾아서 바꿔주기 귀찮아 찾아주는 스크립트를 짜보자는 생각이 들었다.

더러운 .vmp0 섹션을 분석해보니 retn시 함수로 점프하는 주소를 PUSH하고 RETN으로 함수를 호출된다는걸 확인했다. PUSH하기 조금 전으로 돌아가면 0x1007409C위치에서 ESI의 값과 [ESP+58]을 xchg로 바꾼다. 이는 함수시작에서 RETN까지의 과정에서 ESI 값을 추적하면 실제로 호출하는 API의 주소를 가져올 수 있을것으로 추정했다. ESI 추적 고고.

1007409C                        877424 58       XCHG DWORD PTR SS:[ESP+58],ESI   //ESI에 있는 값을 ESP+58 이랑 바꿈       
100740A0                        885C24 04       MOV BYTE PTR SS:[ESP+4],BL
100740A4                        C74424 08 14AC7>MOV DWORD PTR SS:[ESP+8],C07EAC14
100740AC                        FF7424 58       PUSH DWORD PTR SS:[ESP+58] //ESP+58 위치에 값을 스택에 PUSH
100740B0                        C2 5C00         RETN 5C  //RETN으로 함수 호출

CALL 시작부터 RETN까지의 모든 실행코드를 추적했다.

10073801                        9C              PUSHFD
10073802                        66:0FB6F1       MOVZX SI,CL
10073806                        90              NOP
10073807                        60              PUSHAD
10073808                        60              PUSHAD
10073809                        8B7424 44       MOV ESI,DWORD PTR SS:[ESP+44]
1007380D                        881C24          MOV BYTE PTR SS:[ESP],BL
10073810                        884C24 04       MOV BYTE PTR SS:[ESP+4],CL
10073814                        8D6424 48       LEA ESP,DWORD PTR SS:[ESP+48]
10073818                        0F87 9B050000   JA 3.10073DB9

10073DB9                        51              PUSH ECX
10073DBA                        60              PUSHAD
10073DBB                        60              PUSHAD
10073DBC                        877424 44       XCHG DWORD PTR SS:[ESP+44],ESI
10073DC0                        9C              PUSHFD
10073DC1                        9C              PUSHFD
10073DC2                        E8 06420100     CALL 3.10087FCD

10087FCD                        897424 4C       MOV DWORD PTR SS:[ESP+4C],ESI
10087FD1                        66:0FCE         BSWAP SI
10087FD4                        66:895424 18    MOV WORD PTR SS:[ESP+18],DX
10087FD9                      ^ E9 4C12FFFF     JMP 3.1007922A

1007922A                        BE DB270010     MOV ESI,3.100027DB
1007922F                      ^ E9 B0D9FFFF     JMP 3.10076BE4

10076BE4                        54              PUSH ESP
10076BE5                        66:894C24 04    MOV WORD PTR SS:[ESP+4],CX
10076BEA                        E8 95CE0000     CALL 3.10083A84

10083A84                        8BB6 C3250800   MOV ESI,DWORD PTR DS:[ESI+825C3]
10083A8A                      ^ E9 FD37FEFF     JMP 3.1006728C

1006728C                       /E9 3CDB0100     JMP 3.10084DCD

10084DCD                        8DB6 ED8670D4   LEA ESI,DWORD PTR DS:[ESI+D47086ED]
10084DD3                      ^ E9 F5F0FEFF     JMP 3.10073ECD

10073ECD                        E8 CA010000     CALL 3.1007409C

1007409C                        877424 58       XCHG DWORD PTR SS:[ESP+58],ESI
100740A0                        885C24 04       MOV BYTE PTR SS:[ESP+4],BL
100740A4                        C74424 08 14AC7>MOV DWORD PTR SS:[ESP+8],C07EAC14
100740AC                        FF7424 58       PUSH DWORD PTR SS:[ESP+58]
100740B0                        C2 5C00         RETN 5C

볼드 처리된 값들이 ESI 레지스터를 사용하는 코드들과 API호출 코드들이다. 더러운 더미코드들은 집어치우고 볼드처리된 중요한 값들만 가져와보자

10073802                        66:0FB6F1       MOVZX SI,CL
10073809                        8B7424 44       MOV ESI,DWORD PTR SS:[ESP+44]
10073DBC                        877424 44       XCHG DWORD PTR SS:[ESP+44],ESI
10087FCD                        897424 4C       MOV DWORD PTR SS:[ESP+4C],ESI
10087FD1                        66:0FCE         BSWAP SI
1007922A                        BE DB270010     MOV ESI,3.100027DB
10083A84                        8BB6 C3250800   MOV ESI,DWORD PTR DS:[ESI+825C3]
10084DCD                        8DB6 ED8670D4   LEA ESI,DWORD PTR DS:[ESI+D47086ED]
1007409C                        877424 58       XCHG DWORD PTR SS:[ESP+58],ESI
100740AC                        FF7424 58       PUSH DWORD PTR SS:[ESP+58]
100740B0                        C2 5C00         RETN 5C

ESI 레지스터를 사용하는 코드들 중 볼드 처리된 0x1007922A 위치에서 MOV ESI,3.100027DB 명령으로 ESI에 상수값을 넣어준다. 즉 저 위치 이전에 ESI를 사용하는코드들은 모두 더미코드이다. 더미코드를 또다시 집어치우고 중요한 코드만 보자.

1007922A                        BE DB270010     MOV ESI,3.100027DB
10083A84                        8BB6 C3250800   MOV ESI,DWORD PTR DS:[ESI+825C3]
10084DCD                        8DB6 ED8670D4   LEA ESI,DWORD PTR DS:[ESI+D47086ED]
1007409C                        877424 58       XCHG DWORD PTR SS:[ESP+58],ESI
100740AC                        FF7424 58       PUSH DWORD PTR SS:[ESP+58]
100740B0                        C2 5C00         RETN 5C

코드가 심플하게 추려졌다. 볼드처리된 0x1007409C 위치에서 XCHG를 수행할때 이미 ESI에는 API의 주소값이 들어있다. 즉 실제 API주소를 가져오는 코드는 XCHG 명령위에 3개의 코드이다.  말로 풀어보자면...

1. MOV ESI,3.100027DB : 레지스터에 상수값을 넣음
2. MOV ESI,DWORD PTR DS:[ESI+825C3]레지스터에 특정값을 더해 주소값을 구해 그 위치의 데이터를 레지스터에 로드
3. LEA ESI,DWORD PTR DS:[ESI+D47086ED] : 로드한 값에 상수값을 더해 최종 API 주소를 구함

과 같이 진행된다. 위 세가지 형태를 편의를 위해 가젯이라 부르자. 자동화를 위해 가젯을 정의하면

gadget 1 :  MOV R32, CONST
gadget 2 : MOV R32, [R32 + CONST]
gadget 3 : LEA R32, [R32+CONST]

위와같다. 이대로 뽑아내면 주소를 모두 찾아낼 수 있겠지..라는 안일한 생각으로 다른함수를 보는데 jnz, je, jno같은 보건분기문이 나왔다.

 CALL이나 JMP같은 분기문은 그냥 따라가면되지만 조건분기문은 특정 조건에따라 이동하는 위치가 달라지므로 잘못이동하면 X될수도 있다는 생각이 들어 다른함수들을 추적해보았다.

10001F8A                   .  E8 DE200700   CALL 3.1007406D -> START

1007406D                    0FB6D8          MOVZX EBX,AL
10074070                    9C              PUSHFD
10074071                    66:0FBEDA       MOVSX BX,DL
10074075                  ^ E9 CBE9FFFF     JMP 3.10072A45

10072A45                    90              NOP
10072A46                  ^ E9 561BFFFF     JMP 3.100645A1

100645A1                    8D1C4D 17C82EFA LEA EBX,DWORD PTR DS:[ECX*2+FA2EC817]
100645A8                    F7D3            NOT EBX
100645AA                    8B5C24 04       MOV EBX,DWORD PTR SS:[ESP+4]
100645AE                    60              PUSHAD
100645AF                    54              PUSH ESP
100645B0                    E9 57030200     JMP 3.1008490C

1008490C                    875C24 2C       XCHG DWORD PTR SS:[ESP+2C],EBX
10084910                    68 F737CE3F     PUSH 3FCE37F7
10084915                    8D6424 30       LEA ESP,DWORD PTR SS:[ESP+30]
10084919                  ^ 0F80 AD08FFFF   JO 3.100751CC                        -> 여기서 부터. 점프를 할경우

//JMP할 경우

100751CC          60              PUSHAD
100751CD          895C24 1C       MOV SS:[ESP+1C],EBX
100751D1          66:0FBEDA       MOVSX BX,DL
100751D5          5B              POP EBX
100751D6          0FB6D9          MOVZX EBX,CL
100751D9          66:0FCB         BSWAP BX
100751DC          BB 70E90010     MOV EBX,3.1000E970
100751E1           E8 3CAEFEFF     CALL 3.10060022

10060022           8B9B 94890700   MOV EBX, DS:[EBX+78994]
10060028           E9 2B7F0100     JMP 3.10077F58

10077F58           8D9B 47BB6CDA   LEA EBX,DS:[EBX+DA6CBB47]
10077F5E           881C24          MOV BYTE PTR SS:[ESP],BL
10077F61           885C24 04       MOV BYTE PTR SS:[ESP+4],BL

10077F65           FF3424          PUSH SS:[ESP]
10077F68           E8 76CA0000     CALL 3.100849E3

100849E3           875C24 24       XCHG SS:[ESP+24],EBX
100849E7           9C              PUSHFD
100849E8           FF7424 28       PUSH DWORD PTR SS:[ESP+28]
100849EC           C2 2C00         RETN 2C

//JMP하지 않을 경우

1008491F         53   PUSH EBX
10084920         8D1C95 03747C63 LEA EBX,DS:[EDX*4+637C7403]
10084927         9C   PUSHFD
10084928         BB 70E90010     MOV EBX,3.1000E970
1008492D         60   PUSHAD
1008492E         9C   PUSHFD
1008492F         E8 C91AFEFF     CALL 3.100663FD

100663FD         68 5A909A7C     PUSH 7C9A905A
10066402         8B9B 94890700   MOV EBX,DS:[EBX+78994]
10066408         E9 B8370100     JMP 3.10079BC5

10079BC5         E8 E0820000     CALL 3.10081EAA

10081EAA         8D9B 47BB6CDA   LEA EBX,DS:[EBX+DA6CBB47]
10081EB0         E8 DB58FFFF     CALL 3.10077790

10077790         875C24 38       XCHG SS:[ESP+38],EBX
10077794         885424 08       MOV BYTE PTR SS:[ESP+8],DL
10077798         9C   PUSHFD
10077799         FF7424 3C       PUSH SS:[ESP+3C]
1007779D         C2 4000         RETN 40

감사하게도 조건분기 여부와 관계없이 동일한 가젯이 사용되므로 아무 문제가 없다. 

API주소를 가져오는 방식에 대해 분석을 마쳤으니 스크립트가 어떻게 동작할지를 정의하자.

1.  에뮬레이터를 사용하지 않고 스태틱하게 뽑아낼 예정
2. 메모리 덤프 기반이므로 IDAScript로 작성 
3. .text 섹션에서 .vmp0로 CALL 하는 모든 위치 정보를 가져옴
4. 3에서 가져온 위치정보를 기반으로 CALL문 내부 코드를 추적해 RETN까지 진행하며 가젯을 찾음.
5. 찾은 가젯을 기반으로 원본 주소를 복구.

VMProtect Api Address Decoder 

.text에서 .vmp0로 CALL하는 모든 위치정보를 가져오자. getVmpCallList 함수에 섹션이름들을 넣어 섹션 시작, 끝, 사이즈를 가져온다. .text섹션의 어셈블리코드를 라인별로 디스어셈블해 CALL .vmp0인 것들을 리스트로 반환해주도록 하자.

'''
Collect All calls thet .vmp0 from .text
arg : targetSeg Address, vmpSeg Address
return : True
'''
def getVmpCallList(self, targetSeg, vmpSeg):
    idx = 0
    addr = 0
    dis_ea, dis_end, dis_size = self.getSegName(targetSeg)   
    vmp_ea, vmp_end, vmp_size = self.getSegName(vmpSeg)

    
    while dis_ea < dis_end:
        if idc.GetDisasm(dis_ea)[:4] == "call":
            addr = get_operand_value(dis_ea,0)
            if self.checkAddrInSegment(addr, vmp_ea, vmp_end) is True:
                idx = idx + 1
                self.vmp_list.append(addr)
                self.call_list.append(dis_ea)

        dis_ea = idc.NextHead(dis_ea)
    
    return True

 

기능점검

정상적으로 잘 가져온다.

CALL 리스트를 찾았으니 .vmp0의 함수별 트레이싱 코드를 작성해야한다. 레지스터/스택 값의 변화를 추적할 필요가 없고 조건분기를 신경쓰지 않아도되니 조건 분기를 신경 쓰지 않아도 되니 스태틱한 방식으로 트레이싱을 진행한다. 함수내부에서 EIP를 증가시키면서 어셈블리코드를 뽑아와 가젯여부를 확인한다. CALL/JMP의 경우 EIP값을 변경해주고 지정된 gadget과 동일한 형태일 경우 리스트로 저장했다.

'''
Function Tracing without conditional branching
arg : function address, debug falg
return : gadget list / False
'''
def functionTracer(self, addr, debug = False):
    idx = 0
    depth = self.depth
    result = []
    ep = addr
    
    while idx < depth:
        idx = idx + 1
        if debug is True:
            print( '{0} {1} {2} {3}'.format(idx, hex(addr), idc.GetDisasm(addr), hex(get_operand_value(addr,0)) ))
        
        if idc.GetDisasm(addr)[:4] == "retn":
            break
        
        elif idc.GetDisasm(addr)[:3] == "jmp" or idc.GetDisasm(addr)[:4] == "call":
            #print get_operand_value(addr,0)
            addr = get_operand_value(addr,0)
        
        elif self.findFirstGadget(addr) == True:
            if len(result) > 0:
                result = []
            result.append(self.getGadget(addr, ep, 1))
            addr = idc.NextHead(addr)
        
        elif len(result) == 1 and self.findSecondGadget(addr, result[0]['reg']) == True:
            #input gadget
            result.append(self.getGadget(addr,ep, 2))
            addr = idc.NextHead(addr)

        elif len(result) == 2 and self.findThirdGadget(addr, result[0]['reg']) == True:
            result.append(self.getGadget(addr,ep, 3))
            addr = idc.NextHead(addr)
        
        else:
            addr = idc.NextHead(addr)

    if len(result) == 3:
        return result
    else:
        return False

 

테스트 실행결과가 만족스럽다.

추적에 실패한 함수들도 출력해줬다.

실패한 친구들을 위해 Debug옵션을 넣어 어셈블리 라인으로 추적가능하도록 작성했다.

CALL문도 다 뽑았고. 가젯리스트도 다 뽑았다. 이제 가젯들의 최종 결과들을 계산해주면된다. 별로 설명할게 없다.....

'''
decode address from gadget
arg : None
return : None
'''
def decodeAddress(self):
    idx = 0
    data = 0

    for gadget in self.gadget_list:
        for y in gadget:
            if y['flow'] == 2:
                data = next(GetDataList(data + y['const'],1, itemsize=4))
            else:
                data = data + y['const']

        data = data & 0xFFFFFFFF
        self.decoded_api.append(dict( \
            {'original_addr' : gadget[0]['entrypoint'], \
            'original_assembly' : idc.GetDisasm(gadget[0]['entrypoint']), \
            'decoded_address' : data}))
        data = 0

일련의 미친짓들을 거치고나면 아래처럼 .vmp0 섹션으로 CALL하는 모든 주소가 디코드 되어 나온다.

기능은 모두 완성했다. 하지만 저 주소들을 Ollydbg에서 한땀한땀 입력해주기 귀찮으니까.. OllyScript로 패치해주는 함수를 만들어주자.

'''
patch script for ollydbg
arg : None
return : None
'''
def patchOllyScript(self):
    for gadget in self.decoded_api:
        print ('asm {0}, \"call {1}\"'.format(hex(gadget['original_addr']).rstrip("L").lstrip("0x"), 
            hex(gadget['decoded_address']).rstrip("L")))


 

기능은 정상적으로 동작한다.

다 만들었으니 테스트해보자.

함수 복구 전
함수 복구 후

잘 동작한다. 이쁜건 IDA로도 봐줘야 한다. IDA에서 다시 보자.

함수 복구 전
함수 복구 후

 

후기 

개발은 잘못하는데 필요성은 점점 느끼고만 있다가 뜬금없이 페이스북에 올라온 PPT를 보고 자극받아 만들어보았다.
(일하기 싫음 80%/ 자극받음 20%)
(ref : https://www.slideshare.net/dkswognsdi/ss-142458326?fbclid=IwAR3AN8PLXrJpRy-rZf7E7NkyAGlVMkyGqqFqH9-8yLsI2nSHH9Dh8qoKzLA)

그럴 리는 없겠지만 이런 변태 짓에 관심이 있는 분들이 계실까 봐 일단 github에 올려두었다. 
(https://github.com/saweol/vwaad)

그럼이만 술마시러 뿅!

PS 1. 코드가 매우 그지같으니 주의하기 바람. 
PS 2. 코드 고쳐주셔도 됩니다. 보고 저도 많이배우겠습니다.
PS 3. 시험기간이 다가오니 별게 다 재미있어지는 학생의 기분이다. 이제 정신차리고 밀린일들부터 처리하자 ㅠㅠ

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/11   »
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
글 보관함