Debug - AliCTF - 2016 - Reverse

题目内容

下载地址

第一部分

首先用OD载入程序,跳过没有用的代码,来到此处。

00401460   $  68 84704000   push    00407084                         ; /MutexName = "ALICTF:Bigtang"
00401465   .  6A 00         push    0                                ; |InitialOwner = FALSE
00401467   .  6A 00         push    0                                ; |pSecurity = NULL
00401469   .  FF15 04604000 call    dword ptr [<&KERNEL32.CreateMute>; \CreateMutexA
0040146F   .  85C0          test    eax, eax
00401471   .  75 15         jnz     short 00401488
00401473   .  FF15 00604000 call    dword ptr [<&KERNEL32.GetLastErr>; [GetLastError
00401479   .  50            push    eax
0040147A   .  68 68704000   push    00407068                         ;  ASCII "CreateMutex() failed! [%d]",LF
0040147F   .  E8 04040000   call    00401888
00401484   .  83C4 08       add     esp, 8
00401487   .  C3            retn
00401488   >  FF15 00604000 call    dword ptr [<&KERNEL32.GetLastErr>; [GetLastError
0040148E   .  3D B7000000   cmp     eax, 0B7
00401493   .  74 05         je      short 0040149A
00401495   .  E9 36000000   jmp     004014D0
0040149A   >  E9 01000000   jmp     004014A0

查了下CreateMutexA,又是创建新进程。那么程序到底如何判断自己是不是新进程呢?幸运的是,这次我搜到了满意的答案(尽管提问者似乎不满意哈哈):

如何一个软件多次打开

先暂且不管,看看父进程干了些什么事。在经过了一些看起来就没什么卵用的代码之后,我们来到这里:

0040157F  push    edx                              ; /pProcessInfo
00401580  push    eax                              ; |pStartupInfo
00401581  push    esi                              ; |CurrentDir => NULL
00401582  push    esi                              ; |pEnvironment => NULL
00401583  push    3                                ; |CreationFlags = DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS
00401585  push    esi                              ; |InheritHandles => FALSE
00401586  push    esi                              ; |pThreadSecurity => NULL
00401587  lea     ecx, dword ptr [esp+100]         ; |
0040158E  push    esi                              ; |pProcessSecurity => NULL
0040158F  push    ecx                              ; |CommandLine
00401590  push    esi                              ; |ModuleFileName => NULL
00401591  call    dword ptr [<&KERNEL32.CreateProc>; \CreateProcessA
00401597  test    eax, eax
00401599  jnz     short 004015BA
0040159B  call    dword ptr [<&KERNEL32.GetLastErr>; [GetLastError
004015A1  push    eax
004015A2  push    004070B8                         ;  ASCII "CreateProcess() failed! [%d]",LF
004015A7  call    00401888
004015AC  add     esp, 8
004015AF  pop     edi
004015B0  pop     esi
004015B1  pop     ebp
004015B2  pop     ebx
004015B3  add     esp, 4A4
004015B9  retn

原来这里才算真正创建了进程,而且是以调试的方式创建。再跟踪,看看程序搞什么鬼~ (注意程序路径不能有中文,不然创建进程可能会失败)

004015BA mov     esi, dword ptr [<&KERNEL32.Write>;  kernel32.WriteProcessMemory
004015C0 mov     ebp, dword ptr [<&KERNEL32.GetTh>;  kernel32.Wow64GetThreadContext
004015C6 mov     ebx, dword ptr [<&KERNEL32.SetTh>;  kernel32.Wow64SetThreadContext
004015CC mov     ecx, 18
004015D1 xor     eax, eax
004015D3 lea     edi, dword ptr [esp+40]
004015D7 lea     edx, dword ptr [esp+40]
004015DB push    -1                               ; /Timeout = INFINITE
004015DD push    edx                              ; |pDebugEvent
004015DE rep     stos dword ptr es:[edi]          ; |
004015E0 call    dword ptr [<&KERNEL32.WaitForDeb>; \WaitForDebugEvent
004015E6 test    eax, eax
004015E8 je      00401704
004015EE mov     eax, dword ptr [esp+40]
004015F2 cmp     eax, 1
004015F5 jnz     004016E5
......
004016E5 cmp     eax, 5
004016E8 je      short 00401718
004016EA mov     edx, dword ptr [esp+48]
004016EE mov     eax, dword ptr [esp+44]
004016F2 push    10002                            ; /ContinueStatus = DBG_CONTINUE
004016F7 push    edx                              ; |ThreadId
004016F8 push    eax                              ; |ProcessId
004016F9 call    dword ptr [<&KERNEL32.ContinueDe>; \ContinueDebugEvent
004016FF jmp     004015CC

可以看到程序将要写进程内存,此外还有WaitForDebugEvent ContinueDebugEvent等等,当时也没去查啥意思,因为这段代码确实看起来没有干实事,只是做些准备,所以就没研究太多。但可以确定是父进程在调试新进程。在经过一些单步之后,来到了下面的代码处:

00401614 mov     ecx, dword ptr [esp+10]
00401618 push    0                                ; /pBytesRead = NULL
0040161A lea     eax, dword ptr [esp+24]          ; |
0040161E push    4                                ; |BytesToRead = 4
00401620 push    eax                              ; |Buffer
00401621 push    004014A8                         ; |pBaseAddress = 4014A8
00401626 push    ecx                              ; |hProcess
00401627 call    dword ptr [<&KERNEL32.ReadProces>; \ReadProcessMemory
0040162D xor     eax, eax
0040162F mov     cl, byte ptr [esp+eax+20]
00401633 xor     cl, 7F
00401636 mov     byte ptr [esp+eax+20], cl
0040163A inc     eax
0040163B cmp     eax, 4
0040163E jb      short 0040162F
00401640 mov     eax, dword ptr [esp+10]
00401644 push    0
00401646 lea     edx, dword ptr [esp+24]
0040164A push    4
0040164C push    edx
0040164D push    004014A8
00401652 push    eax
00401653 call    esi                              ;  kernel32.WriteProcessMemory

这是在读新进程的数据(虽然没记进程ID但错不了),读的是0x4014A8位置的4个字节,然后循环4次将它们与0x7F异或再放回到buffer里面,最后写回进程原位置。这时候可以将程序复制一份了,父进程的操作都模拟一下,把复制后的程序改掉,将来可以直接调试的。

用16进制编辑器打开复制后的程序,转到0x14A8位置,检查是不是ReadProcessMemory的值(是),然后将其值修改为WriteProcessMemory要修改的值。

patch1

然后继续跟踪,下面有重要操作:

00401660 push    ecx
00401661 push    edx
00401662 mov     dword ptr [esp+1F0], 10007
0040166D call    ebp                              ;  kernel32.Wow64GetThreadContext
0040166F mov     edx, dword ptr [esp+2A0]
00401676 mov     ecx, dword ptr [esp+14]
0040167A lea     eax, dword ptr [esp+1E8]
00401681 add     edx, 2                           ;  eip+=2
00401684 push    eax
00401685 push    ecx
00401686 mov     dword ptr [esp+2A8], edx
0040168D call    ebx
0040168F jmp     short 004016EA

百度了下,Wow64GetThreadContext可以拿到寄存器的值,一整个结构体,我不知道edx拿到的是哪个值。本来应该对照结构体去看,机智的我打开了IDA,原来这段代码是这样的:

.text:00401655  mov     edx, [esp+hThread]
.text:00401659  lea     ecx, [esp+Context]
.text:00401660  push    ecx             ; lpContext
.text:00401661  push    edx             ; hThread
.text:00401662  mov     [esp+8+Context.ContextFlags], 10007h
.text:0040166D  call    ebp ; GetThreadContext
.text:0040166F  mov     edx, [esp+Context._Eip]
.text:00401676  mov     ecx, [esp+hThread]
.text:0040167A  lea     eax, [esp+Context]
.text:00401681  add     edx, 2
.text:00401684  push    eax             ; lpContext
.text:00401685  push    ecx             ; hThread
.text:00401686  mov     [esp+8+Context._Eip], edx
.text:0040168D  call    ebx ; SetThreadContext
.text:0040168F  jmp     short loc_4016EA

拿的是eip,程序把他+2之后就放回去了。感觉这题目有点意思了。这里记一下拿回来的eip的值,是0x4014A6,于是跳回去看看是啥内容:

4014A6

哈哈哈原来如此,我们把0x4014A6处的两个字节改成0x90,让OD重新分析一下,发现代码还是看不懂。忽然想起前边有对0x4014A8进行异或来着,怎么给忘了。异或之后得到

004014A8   je      short 004014AF
004014AA   jnz     short 004014AF
004014AC   cdq
004014AD   cwde
004014AE   xchg    eax, edi
004014AF   je      short 004014B9
004014B1   jnz     short 004014B9

这就是跳到0x4014B9呗。但是这个位置的代码目前还是非法的,估计后面还有操作,先继续跟踪下去。

00401698 xor     eax, eax
0040169A mov     cl, byte ptr [eax+407040]
004016A0 xor     cl, 31
004016A3 mov     byte ptr [eax+407040], cl
004016A9 inc     eax
004016AA cmp     eax, 10
004016AD jl      short 0040169A
004016AF mov     edx, dword ptr [esp+10]
004016B3 push    0
004016B5 push    10
004016B7 push    00407040
004016BC push    00407040
004016C1 push    edx
004016C2 call    esi                 ; kernel32.WriteProcessMemory

程序把自己进程里面的0x407040处16个字节与0x31异或一下,然后写到新进程。可以跳过去看异或之后的值,然后在复制后的文件里面改一改。再继续调试:

004016C4  mov     ecx, dword ptr [esp+10]
004016C8  push    0
004016CA  lea     eax, dword ptr [esp+24]
004016CE  push    2
004016D0  push    eax
004016D1  push    004014B9
004016D6  push    ecx
004016D7  mov     byte ptr [esp+34], 0E8
004016DC  mov     byte ptr [esp+35], 0B2
004016E1  call    esi                         ; kernel32.WriteProcessMemory
004016E3  jmp     short 004016EA

向新进程的0x4014B9处写入0xE8和0xB2。注意一下,eax指向esp+24,经过4次push之后就是esp+34。所以我们跳到004014B9去改下代码(注意把004014B8改成90,不然要被干扰),发现现在代码可以读了,然后去新文件里面改一改。

再继续调试,新进程以及运行起来等待输入了,输入完后继续调试,接下来就是终止进程等,没有其他内容了。

接下来我们将文件按父进程的操作将新文件再修改一下,以便迅速调试新进程的内容(注释的语句代表被修改过,可以利用程序用不到的代码空间)。

00401460   $ /EB 3E         jmp     short 004014A0         ;跳过创建进程的部分
......(略)
004014A0  /> \55            push    ebp
004014A1  |.  8BEC          mov     ebp, esp
004014A3  |.  53            push    ebx
004014A4  |>  56            push    esi
004014A5  |.  57            push    edi
004014A6  |.  EB 11         jmp     short 004014B9          ;直接跳到下一条有效指令
......
004014B9  |> \E8 B2FEFFFF   call    00401370
004014BE  |.  5F            pop     edi
004014BF  |.  5E            pop     esi
004014C0  |.  5B            pop     ebx
004014C1  |.  5D            pop     ebp
004014C2  \.  C3            retn

好了,接下来可以愉快的调试复制后的新文件啦!

第二部分

前面已经知道0x401370处是新进程的主要内容,所以先用IDA看看:

int sub_401370()
{
  signed int v0; // eax@1
  signed int v1; // esi@3
  signed int v2; // ecx@7
  char v3; // al@8
  char v5[4]; // [sp+0h] [bp-10h]@1
  int v6; // [sp+4h] [bp-Ch]@1
  int v7; // [sp+8h] [bp-8h]@1
  int v8; // [sp+Ch] [bp-4h]@1

  strcpy(v5, "\x14R1");
  v6 = dword_40705C;
  v8 = dword_407064;
  v7 = dword_407060;
  v0 = 0;
  do
  {
    v5[v0] ^= 0x31u;
    ++v0;
  }
  while ( v0 < 3 );
  v1 = 0;
  do
    printf(v5, *((_BYTE *)&v6 + v1++) ^ 0x31);
  while ( v1 < 11 );
  gets(byte_407960);
  if ( strlen(byte_407960) != 32 )
    exit(0);
  v2 = 0;
  do
  {
    v3 = byte_407960[v2];
    if ( v3 < 48 || v3 > 122 || v3 > 57 && v3 < 97 )
      exit(0);
    ++v2;
  }
  while ( v2 < 32 );
  sub_401290();
  sub_4010C0();
  sub_401100();
  sub_401000((int)&dword_407990, (int)&dword_4079A0);
  sub_401000((int)&dword_407998, (int)&dword_4079A0);
  sub_4011E0();
  return sub_401210();
}

从中可知,输入字符串长度为32,各个字符为数字或小写。然后戳进0x401290看看,代码乱七八糟的不好理解,于是用OD动态跟踪一下看看它干了些什么。根据个人习惯,flag我输入0123456789abcdef0123456789abcdef进行测试。

跟踪后发现,这个程序将0x407040的16个字节变换后存入0x4070A0,因为不涉及输入,所以不必了解变换的算法细节。然后我们看下一个函数:

int sub_4010C0()
{
  signed int v0; // esi@1
  char v1; // bl@2
  char v2; // bl@2
  __int64 v3; // rax@2
  int result; // eax@2

  v0 = 0;
  do
  {
    v1 = 16 * sub_401090(byte_407960[v0]);
    v2 = sub_401090(byte_407961[v0]) + v1;
    v3 = v0;
    v0 += 2;
    result = ((signed int)v3 - HIDWORD(v3)) >> 1;
    *((_BYTE *)&dword_407980 + result) = v2;
  }
  while ( v0 < 32 );
  return result;
}

记住,0x407960存放是我们输入字符串的地方。下面我们再戳进sub_401090:

int __cdecl sub_401090(char a1)
{
  int result; // eax@2

  if ( isdigit(a1) )
  {
    result = a1 - 48;
  }
  else if ( isalpha(a1) )
  {
    result = a1 - 87;
  }
  else
  {
    result = 0;
  }
  return result;
}

代码很简单,但代码想做什么却不直观。动态跟踪时,我看着我的输入本来是”0123456789abcdef0123456789abcdef”,经过了0x4010C0后变成了16个字节01 23 45 67 89 ab cd ef 01 23 45 67 89 ab cd ef存入了0x407980,于是大致明白了这个函数做了些什么。再看下一个函数0x401100,也是乱七八糟的,用动态跟踪。

函数将0x407980的16字节变换后存入0x407990。结束后内存如图,是不是看出来什么?

memory

第一行是用户输入;第二行是用户输入的copy,第三行是第一次变换,第四行是第二次变换,第五行是0x407040变换后的值(原来的值是00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff)。由于后面的变换都基于第四行,我们猜一下变换的目的:

>>> s='0123456789abcdef0123456789abcdef'
>>> l=[]
>>> for i in range(0,len(s),8):
    n=int(s[i:i+8],16)
    l.append(hex(n))

>>> l
['0x1234567', '0x89abcdef', '0x1234567', '0x89abcdef']

像l中的4个整数(为了直观我按hex输出的),用little-endian方式存储,就是第四行的内容。

然后来看0x401000,这是最重要的变换:

unsigned int __cdecl sub_401000(int a1, int a2)
{
  int v3; // edx@1
  unsigned int c1; // eax@1
  unsigned int c2; // ecx@1
  signed int v6; // edi@1
  int key0; // [sp+10h] [bp-Ch]@1
  int key1; // [sp+24h] [bp+8h]@1

  v3 = 0;
  c1 = *(_DWORD *)a1;                         // c1=01234567
  c2 = *(_DWORD *)(a1 + 4);                   // c2=89ABCDEF
  key0 = *(_DWORD *)a2;                       // key0=00112233
  key1 = *(_DWORD *)(a2 + 4);                 // key1=44556677

  v6 = 128;
  do
  {
    v3 -= 1640531527;
    c1 += (v3 + c2) ^ (key0 + 16 * c2) ^ (key1 + (c2 >> 5));
    c2 += (v3 + c1) ^ (*(_DWORD *)(a2 + 8) + 16 * c1) ^ (*(_DWORD *)(a2 + 12) + (c1 >> 5));
    --v6;
  }
  while ( v6 );
  *(_DWORD *)a1 = c1;
  *(_DWORD *)(a1 + 4) = c2;
  return c1;
}

这段代码相当于传进我们输入的字符串变换后的前两个整数,经过128轮变换存回。我一开始没仔细看这个变换,感觉这么复杂应该是不可逆,必须爆破。实际爆破的时候发现根本不可能,所以我们还得仔细分析一下这个算法:

  • 会改变的值共3个,一个是v3,另外两个是我们的整数。
  • 变换只是加或减的操作
  • 加上或减去的值与自身无关,此外v3的变化值与我们的整数无关

所以解密算法其实简单到爆,就是把原算法倒置一下就好了:

v3=465362048;
int v6 = 128;
do
{
    c2 -= (v3 + c1) ^ (key[2] + 16 * c1) ^ (key[3] + (c1 >> 5));
    c1 -= (v3 + c2) ^ (key[0] + 16 * c2) ^ (key[1] + (c2 >> 5));
    v3 += 1640531527;
    --v6;
}
while ( v6 );

其中v3的初始值可以由0减去1640531527共128次得到。

下面0x401000还要调用一次,只是变换另外两个整数,而且这两组整数互不影响,根本没什么好担心的。再继续看下一个函数:

signed int sub_4011E0()
{
  signed int i; // eax@1
  char v1; // dl@2
  i = 0;
  do
  {
    v1 = *((_BYTE *)&dword_407998 + i) ^ 0x31;
    *((_BYTE *)&dword_407980 + i) = *((_BYTE *)&dword_407990 + i) ^ 0x31;
    *((_BYTE *)&dword_407988 + i++) = v1;
  }
  while ( i < 8 );
  return i;
}

很简单,就是把第四行的各个字节异或一下,存回第三行。现在只剩下最后一个函数了!

int sub_401210()
{
  signed int v0; // eax@1
  signed int v1; // eax@3
  signed int v2; // esi@6
  int result; // eax@7
  char v4[4]; // [sp+0h] [bp-Ch]@1
  int v5; // [sp+4h] [bp-8h]@6
  __int16 v6; // [sp+8h] [bp-4h]@6

  strcpy(v4, "\x14R1");
  v0 = 0;
  do
  {
    v4[v0] ^= 0x31u;
    ++v0;
  }
  while ( v0 < 3 );
  v1 = 0;
  do
  {
    if ( *((_BYTE *)&dword_407980 + v1) != byte_407030[v1] )
      exit(0);
    ++v1;
  }
  while ( v1 < 16 );
  v5 = dword_407050;
  v6 = word_407054;
  v2 = 0;
  do
    result = printf(v4, *((_BYTE *)&v5 + v2++) ^ 0x31);
  while ( v2 < 5 );
  return result;
}

判断第四行的值是不是和0x407030的值都相同。现在可以解密了!

#include<stdio.h>

unsigned int key[]={0x112233,0x44556677,0x8899aabb,0xccddeeff};

void change(unsigned int*p)
{
    unsigned int v3=465362048,c1=*p,c2=*(p+1);
    int v6 = 128;
    do
    {
    c2 -= (v3 + c1) ^ (key[2] + 16 * c1) ^ (key[3] + (c1 >> 5));
    c1 -= (v3 + c2) ^ (key[0] + 16 * c2) ^ (key[1] + (c2 >> 5));

    v3 += 1640531527;
    --v6;
    }
    while ( v6 );
    printf("%x",c1);
    printf("%x",c2);
}

int main()
{
    unsigned int c[2];
    c[0]=0xed17ff5d;
    c[1]=0xe987f714;
    change(c);
    c[0]=0xdca14228;
    c[1]=0x32f7970a;
    change(c);
}

答案是c6bf3d7cdad82ea712cea62cccbafddf

这道题是有bug的,要不是我机智的输入了 0~f 的字符,比如我输入的都是ghijkl之类的,肯定是看不出规律,但是这些值不会被程序判定为不合法。比如前面的c6,我改成bm也是可以的(原理请自己想):

E:\ctf\AliCTF\reverse>debug
Input Flag:bmbf3d7cdad82ea712cea62cccbafddf
Good!

我虽然没有闲心去试,但估计提交这个flag不会被通过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值