这几次比赛下来,真的是深刻的让我感受到了自己逆向水平的不足,这次的0ctf也更是好不容易在第二天下午才发现这题怎么做,才总算没有在逆向题上无功而返。
这道题说实在的,确实是挺简单的,不过很麻烦倒是真的,唯独的一个问题就是不能用Hex-rays来看,反编译得到的代码是有问题的。
signed __int64 __usercall handle@<rax>(__int64 a1@<rbp>) { __int64 *src; // rsi@1 __int64 *dst; // rdi@1 signed __int64 i; // rcx@1 *(_DWORD *)(a1 - 4) = accept(3, 0LL, 0LL); recv(*(_DWORD *)(a1 - 4), save_flag, 0x1000uLL, 0); close(*(_DWORD *)(a1 - 4)); src = qword_E0B00A0; dst = qword_E0AF0A0; for ( i = 0x200LL; i; --i ) { *dst = *src; ++src; ++dst; } return 0xE0AF8A0LL; }
处理部分反编译代码如上,我们会发现非常奇怪的是,这里做了一个字符串固定字符串的复制之后接返回了,什么事都没做啊,遇到这种情况当然得看看汇编代码,但不知道自己眼睛怎么长的,最开始一直没有看到问题……
然后,我还尝试通过strings往回找flag相关部分,发现确实是有输出flag的地方,可完全不知道从哪里跳过来的,然后发现了一段乱七八糟的代码片段,每个片段都很短,如下:
.text:000000000DEAD1E4 jmp short loc_DEAD1E8 .text:000000000DEAD1E6 ; --------------------------------------------------------------------------- .text:000000000DEAD1E6 jp short loc_DEAD181 .text:000000000DEAD1E8 .text:000000000DEAD1E8 loc_DEAD1E8: ; CODE XREF: .text:000000000DEAD1E4 .text:000000000DEAD1E8 pop rax .text:000000000DEAD1E9 retn
看起来似乎000000000DEAD1E6那里有2个无意义的字节,问题肯定就在这些代码片段里了,然后还发现,程序复制的那个字符串里中间有些部分看起来是一些指向这些代码片段的指针,但还是没有什么解题的想法。
最后,某次突然在这段汇编代码的最后部分发现了问题:
.text:000000000DEAD40E mov eax, offset save_flag .text:000000000DEAD413 mov rdi, rax .text:000000000DEAD416 mov eax, offset unk_E0B20C0 .text:000000000DEAD41B mov rsi, rax .text:000000000DEAD41E mov eax, (offset qword_E0AF0A0+800h) .text:000000000DEAD423 mov rsp, rax .text:000000000DEAD426 retn .text:000000000DEAD426 handle end
仔细一看,这里000000000DEAD423这一行竟然把rsp给改了,我勒个去,这是直接换栈了,那这下就啥都说的通了,它复制的那个字符串里面都是他提前设定好的地址,然后那些代码片段最后就通过ret来返回到特定的地方,一句话说就是以ROP的方式在运行程序,只能说感觉我也是醉了。
知道方法后,做起来也是麻烦,没有什么好的办法,只能一点点的手动把代码扒出来。
首先,先是把那个字符串中看起来是需要的部分拎出来:
arr = [233492980, 8, 233493105, 1384820248253759637, 233492771, 233492717, 233492996, 233493095, 233492728, 233492739, 233492717, 233493114, 233493006, 233492728, 233492972, 51966, 233492801, 233492717, 233492996, 233493124, 233492728, 233492717, 233493114, 233493006, 233492728, 233492972, 48879, 233492781, 233492717, 233492996, 233493124, 233492728, 233493192, 1, 233493134, 13337, 233492964, 0, 233492972, 0, 233492988, 472, 233492891, 233492717, 233493143, 233493182, 233492728, 233492717, 233493172, 233493006, 233492728, 233492972, 1, 233492851, 233492717, 233492996, 233493182, 233492728, 233492717, 233493172, 233493006, 233492728, 233492972, 1, 233492988, 104, 233492906, 233492717, 233493201, 233493006, 233492728, 233492717, 233493085, 233493026, 233492728, 233492801, 233492717, 233492996, 233493211, 233492728, 233492717, 233493085, 233493006, 233492728, 233492717, 233493085, 233493026, 233492728, 233492801, 233492717, 233492996, 233493095, 233492728, 233492717, 233493143, 233493006, 233492728, 233492881, 233492717, 233492996, 233493153, 233492728, 233492717, 233493143, 233493006, 233492728, 233492972, 0, 233492988, 18446744073709551072L, 233492906, 233492717, 233493201, 233493006, 233492728, 233492717, 233493114, 233493026, 233492728, 233492988, 32, 233492906, 233492988, 18446744073709550648L, 233492951, 233493308] #, 233493423, 14950972352827054927L, 13643839583502281931L]
然后一点点的得到地址与代码的对应关系表:
d = {} d[0x000000000DEAD1F4] = 'pop rcx' d[0x000000000DEAD271] = 'pop r9' d[0x000000000DEAD123] = 'mov rax, [rdi]' d[0x000000000DEAD0ED] = 'add rsi, 8' d[0x000000000DEAD204] = 'mov [rsi], rax' d[0x000000000DEAD267] = 'mov r8, [rsi]' d[0x000000000DEAD0F8] = 'sub rsi, 8' d[0x000000000DEAD103] = 'add rdi, 8' d[0xdead27a] = 'mov [rsi], r9' d[0xdead20e] = 'mov rax, [rsi]' d[0xdead1ec] = 'pop rbx' d[0xdead141] = 'imul rax, rbx' d[0xdead284] = 'mov r9, [rsi]' d[0xdead12d] = 'add rax, rbx' d[0xdead2c8] = 'pop r12' d[0xdead28e] = 'pop r10' d[0xdead1e4] = 'pop rax' d[0xdead1fc] = 'pop rdx' d[0xdead19b] = 'cmp rax, rbxnjnz out1nadd rsp, rdxnout1:' d[0xdead297] = 'mov [rsi], r10' d[0xdead2be] = 'mov r11, [rsi]' d[0xdead2b4] = 'mov [rsi], r11' d[0xdead173] = 'and rax, rbx' d[0xdead1aa] = 'cmp rax, rbxnjz out2nadd rsp, rdxnout2:' d[0xdead2d1] = 'mov [rsi], r12' d[0xdead25d] = 'mov [rsi], r8' d[0xdead222] = 'mov rbx, [rsi]' d[0xdead2db] = 'mov r12, [rsi]' d[0xdead191] = 'shr rax, 1' d[0xdead2a1] = 'mov r10, [rsi]' d[0xdead1d7] = 'loop success, out3nsuccess:nadd rsp, rdxnout3:' d[0xdead33c] = 'get flag' [d[one] for one in arr if one > 0xdead000 and one < 0xdeaf000]
然后按这个表替换之后得到代码如下:
pop rcx pop r9 mov rax, [rdi] add rsi, 8 mov [rsi], rax mov r8, [rsi] sub rsi, 8 add rdi, 8 ... mov [rsi], r9 mov rbx, [rsi] sub rsi, 8 pop rdx cmp rax, rbx jz next3 add rsp, rdx next3: pop rdx loop success1, fail2 add rsp, rdx get flag
我们发现里面从来没有做过任何修改栈上值的操作,以及唯一对rsp的修改就是add rsp,也就是相当于把代码跳转回去,就是实现循环的作用了。
于是乎,为了看的更清楚,我们给每个语句编上地址,使我们可以知道每次add rsp是跳转到哪里,以及每次pop出来的值是多少:
[8, 1384820248253759637, 51966, 48879, 1, 13337, 0, 0, 472, 1, 1, 104, 0, 18446744073709551072L, 32, 18446744073709550648L] i = 0 for one in s: if one.find('pop') != -1: print one.replace('pop ', 'mov_s') + ', ' + hex(t[i]) i += 1 else: print one
我们这里会将pop替换成mov_s指令,当做mov指令看就好,加个_s是为了区分下而已:
dst = 0xE0AF0A0 src = 0xE0B00A0 buffer = 0xE0B20C0 save_flag = input rdi = save_flag rsi = buffer 0x008: mov_s rcx, 0x8 0x018: mov_s r9, 0x1337deadbeef0095 0x028: mov rax, [rdi] 0x030: add rsi, 8 0x038: mov [rsi], rax 0x040: mov r8, [rsi] 0x048: sub rsi, 8 0x050: add rdi, 8 0x058: add rsi, 8 0x060: mov [rsi], r9 0x068: mov rax, [rsi] 0x070: sub rsi, 8 // 0x078: mov_s rbx, 0xcafe 0x088: imul rax, rbx 0x090: add rsi, 8 0x098: mov [rsi], rax 0x0a0: mov r9, [rsi] 0x0a8: sub rsi, 8 0x0b0: add rsi, 8 0x0b8: mov [rsi], r9 0x0c0: mov rax, [rsi] 0x0c8: sub rsi, 8 0x0d0: mov_s rbx, 0xbeef 0x0e0: add rax, rbx 0x0e8: add rsi, 8 0x0f0: mov [rsi], rax 0x0f8: mov r9, [rsi] 0x100: sub rsi, 8 0x108: mov_s r12, 0x1 0x118: mov_s r10, 0x3419 0x128: mov_s rax, 0x0 0x138: mov_s rbx, 0x0 0x148: mov_s rdx, 0x1d8 0x158: cmp rax, rbx jnz out1 add rsp, rdx out1: 0x160: add rsi, 8 0x168: mov [rsi], r10 0x170: mov r11, [rsi] 0x178: sub rsi, 8 0x180: add rsi, 8 0x188: mov [rsi], r11 0x190: mov rax, [rsi] 0x198: sub rsi, 8 0x1a0: mov_s rbx, 0x1 0x1b0: and rax, rbx 0x1b8: add rsi, 8 0x1c0: mov [rsi], rax 0x1c8: mov r11, [rsi] 0x1d0: sub rsi, 8 0x1d8: add rsi, 8 0x1e0: mov [rsi], r11 0x1e8: mov rax, [rsi] 0x1f0: sub rsi, 8 0x1f8: mov_s rbx, 0x1 0x208: mov_s rdx, 0x68 0x218: cmp rax, rbx jz out2 add rsp, rdx out2: 0x220: add rsi, 8 0x228: mov [rsi], r12 0x230: mov rax, [rsi] 0x238: sub rsi, 8 0x240: add rsi, 8 0x248: mov [rsi], r8 0x250: mov rbx, [rsi] 0x258: sub rsi, 8 0x260: imul rax, rbx 0x268: add rsi, 8 0x270: mov [rsi], rax 0x278: mov r12, [rsi] 0x280: sub rsi, 8 0x288: add rsi, 8 0x290: mov [rsi], r8 0x298: mov rax, [rsi] 0x2a0: sub rsi, 8 0x2a8: add rsi, 8 0x2b0: mov [rsi], r8 0x2b8: mov rbx, [rsi] 0x2c0: sub rsi, 8 0x2c8: imul rax, rbx 0x2d0: add rsi, 8 0x2d8: mov [rsi], rax 0x2e0: mov r8, [rsi] 0x2e8: sub rsi, 8 0x2f0: add rsi, 8 0x2f8: mov [rsi], r10 0x300: mov rax, [rsi] 0x308: sub rsi, 8 0x310: shr rax, 1 0x318: add rsi, 8 0x320: mov [rsi], rax 0x328: mov r10, [rsi] 0x330: sub rsi, 8 0x338: add rsi, 8 0x340: mov [rsi], r10 0x348: mov rax, [rsi] 0x350: sub rsi, 8 0x358: mov_s rbx, 0x0 0x368: mov_s rdx, 0xfffffffffffffde0L 0x378: cmp rax, rbx jz out2 add rsp, rdx out2: 0x380: add rsi, 8 0x388: mov [rsi], r12 0x390: mov rax, [rsi] 0x398: sub rsi, 8 0x3a0: add rsi, 8 0x3a8: mov [rsi], r9 0x3b0: mov rbx, [rsi] 0x3b8: sub rsi, 8 0x3c0: mov_s rdx, 0x20 0x3d0: cmp rax, rbx jz out2 add rsp, rdx out2: 0x3d8: mov_s rdx, 0xfffffffffffffc38L 0x3e8: loop success, out3 success: add rsp, rdx out3: 0x3f0: get flag
然后这里看跳转的规则是,用那句add esp前面的标号,加上这句指令给rsp增加的值,得到的地址,就是下一条指令了。
于是乎,慢慢看下这段程序,会发现就是一个快速幂(PS:这里最后几句代码可能对应的有点问题,但不太影响整体逻辑的理解),就是求输入8个字节(作为一个长整数看)的0x3419次方,与一个目标值比较,然后会这样比较9轮(好吧,我承认这一部分是动态调出来了,看出快速幂后就没管这段程序了),而目标值的初始值是0x1337DEADBEEF0095(初始值不算一轮),下一轮的值为前一轮的值乘以0xcafe后加0xbeef。
果断计算出每轮目标值,然后用WolframAlpha算出应该的输入(比如第一轮的输入),最后得flag的程序如下:
import zio # aim = 0x1337DEADBEEF0095 # for i in xrange(9): # aim = aim * 0xcafe + 0xbeef # print aim % (2 ** 64) # # WolframAlpha ans = [15397851891508532645, 5882479468299745861, 6203462900357968133, 11756062250229433221, 6010465035347615365, 10697597399494305925, 14617956564689458309, 9036388475871214725, 17260895165766855813] payload = '' for i in ans: payload += zio.l64(i) io = zio.zio(('127.0.0.1', 0x3419)) io.write(payload)
这题150分搞成这样,也是有点无语了,知道方法后,最后的一点苦力工作当时都不想干了,都是交给队友看的了~~~