HGAME 2021 PWN Writeup

真是好久没打ctf了,趁这个机会学习一下

WEEK 1

whitegive

名字叫白给- -签到题。

emmm,说是白给,我是真花了点时间的,一开始直接输入字符串很明显是不行的,然后我把密码转为long long int再输入就如下图:

想到整形到内存里肯定转hex的,但是没有办法整字符串。看到RAX灵机一动,把rdx和rax整成一样那必然可以过cmp。

hgame{W3lCOme_t0_Hg4m3_2222Z222zO2l}

letter

init()里面是seccomp,先看这个main()吧,首先让你输入length,然后根据length读,看样子读的时候是无符号整数,溢出。

妙啊,现在就是想想把shellcode布置在哪里,orw shellcode。

一开始考虑用read把orw shellcode读到bss然后做个栈迁移(问题是buf被覆盖掉了- -)。

现在有个思路,因为buf我们是可控的,相当于一次任意地址写。后面想了很多怎么去利用,还是找不到解决方案。

说一下最后的思路:无符号数溢出造成栈溢出,ROP将read_shellcode以及call rsi读到bss+0x100的位置然后ret执行bss上的read_shellcode。执行这个read_shellcode,将orw_shellcode读到bss+0x200的位置(那么read shellcode的rsi应该是bss+0x200),然后call rsi就去执行orw_shellcode了。

比较巧妙的就是虽然只有一次溢出,但是通过ret,call rsi以及shellcode构造利用链。至于说为什么不第一次就把orw_shellcode读到bss并执行,是因为没有控制rdx的gadget,t调试发现rop只能写0x24大小的数据,所以只能构造比较短的个read_shellcode再次写。

hgame{400a48b3d1b03dc8b9947174a3255bbc2783494c97c90a2ad76c7ed22158048f}

once

没开canary,看一下代码:

是一个栈溢出+格式化字符串,开了PIE,很明显一次绝对无法getshell。去看了下栈的情况

这就很简单了,可以看到rbp+8位置的ret和main()只有最低一个字节有差异,而低三位是一般是固定的,所以思路清晰了:首先格式化字符串leak出栈上的libc_start_main,并且栈溢出覆盖ret低一个字节让它返回的vuln(),我测试的返回main会出问题。之后一次栈溢出写one_gadget即可。

hgame{b73c0d87f1c49e4e4e0962dcddd8f38c95fc835a2b4b66243444505229119328}

SteinsGate2

ubuntu 20.04

说实话,代码量太大了…IDA给出来的结果不太友好,不过好在有源代码。

event_ibn5100()这里有一个溢出。但是开了canary,所以要想办法绕过。同时如果想触发栈溢出需要满足choice==1并且save.days==1的条件。并且因为是else if,所以不能让程序进入前面两个分支。看到第一个if,是只要把save.know_truth搞成1就行了。

跟进调试可以发现真正的值应该是0.898834229。

触发了栈溢出,接下来就是想办法去leak出canary进行ROP了。

发现两个输出变量的地方,分别是sence.h的SRCIPT_PRINT以及world.h的PUTDMAIL。

再大致看了下源代码,发现PUTDMAIL只有一处调用,SRCIPT_PRINT有几处调用,并输出了变量。下面给出调用SCRIPT_PRINT并输出可控变量的位置。

首选绝对是envent_hacking(),因为name虽也是可控,但是只能输入19个字节,可能不足以写到canary前面。触发如下:

自己调试看一下偏移即可,其实站上有libc地址,可以通过它leak出来,但是这个event_hacking只能调用一次。因为开了PIE,所以如果想通过ROP leak libc再getshell也是有一些困难。

来看一下event_dmail()这个函数:

调用了world.h中的STARTMAIL():

也就是说,调用了longjmp就会回到调用setjmp时的状态,看一下什么时候调用了SETDMAIL(),刚好时程序开始,只要我们调用了longjmp()就可以再次leak

问题时如何调用event_dmail()呢?

通过envent_banana()即可调用event_dmail(),看banana知道只要把scene->count整成2就能调用dmail。但是只有再save.tube存在的时候scene->count才会加1.这里逻辑我没太弄懂(后面来填坑),但是经测试9次就可以调用DMAIL。

这里注意要回到第一天,因为我们时第一天调用的event_hacking。回去按照往常的套路leak 出来libc就可以了,然后就ROP,gadget可以去libc里面找。

写exp写到最后发现,一开始找到的溢出要求save.days==1,所以想到改变一下流程,day1先去溢出,day2再去leak,这样day1和day2都可以返回,但是实际测试发现如果这么做,DMAIL会无法使用。

经过测试,day1调用IBN5100,day2调用banana,day3调用hacking,然后day3 leak完后调用DMAIL回到day1再调用IBN5100,day2调用banana,day3调用hacking leak完后回到day1进行ROP。(并且调用banana的次数发生变化,去掉前面调用的一次还要调用七次,这里有点没太搞懂逻辑),并且就算按照day1,day2,day3这样调用完后,第一次直接回到day3继续leak,然后再回到day1也是不行的。

pop_rdi那个gadget拿不到shell,可能是因为需要栈对齐的原因。

hgame{d2dce5833e1f6c569e978cecb867a168676fb909344dcdbbbe710f0175f3bf7b}

来填一下上面的坑,主要是day调用顺序的问题。

只有day2和day5把tube设为1,所以我们必须day2和day5都要调用banana。


WEEK 2

patriot’s note

libc 2.27,保护全开。

看一下功能吧,增删改查都有。

take(),大小限制的比较宽,但是数量只能4个。

delete(),UAF,并且delete之后并没有减少noteid的数量,因此只能申请四次。

无符号数这是,但是size限制了,不能溢出。show()就是普通输出。

本来想着size限制不大直接创建unsorted bin chunk 来leak libc,但是碍于申请次数的限制,不太可行。发现index只做了大于noteid的限制,并没有限制负数,数组越界直接读stderr然后常规double free然后劫持hook就行了。

我可能是nt了,因为free并没有清空lisit,所以我们完全不用double free去浪费一次add的机会,直接unsorted bin leak用掉一次add,然后 UAF申请用掉三次(一次正常add然后free,一次申请出正常chunk,一次申请出来hook)。

其实第一种思路是可行的,但是由于用了write作为输出,所以\x00并不截断,所以一下输出太多,导致远程EOF。

hgame{0ce14ff5603d093c6bdbc55e63f0a573ed6a040300166e96972f05de99d54288}

rop_primary

check后就是vuln(),栈溢出。

看看check()

就是两个随机矩阵做乘法运算。建议获取数据后好好看看- -我就是因为一个数字整了半天,不知道哪里出的问题,后面发现B矩阵第一位缺失了,后面就是普通ROP。

注意栈对齐。

hgame{10578e800f8a0e1695ca5f6970e0228fec1e15b06a7622360dffa1f4aa09cdd6}

killerqueen

主要看一下choice1和choice2:

choice2()是有格式化字符串的,因为先printf才read,再配合killGuy,我们在循环体内有一次格式化字符串的机会,break以后还有一次。即一次leak libc+栈地址,一次修改ret。

这里新学了一种劫持姿势,就是劫持exit_hook,可以自己本地调试一下,说一下流程

程序调用exit后,首先会调用__run_exit_handlers:

继续跟进可以看到调用了_dl_fini:

 1 #ifdef SHARED
 2   int do_audit = 0;
 3  again:
 4 #endif
 5   for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
 6     {
 7       /* Protect against concurrent loads and unloads.  */
 8       __rtld_lock_lock_recursive (GL(dl_load_lock));
 9 
10       unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
11       /* No need to do anything for empty namespaces or those used for
12      auditing DSOs.  */
13       if (nloaded == 0
14 #ifdef SHARED
15       || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
16 #endif
17       )
18     __rtld_lock_unlock_recursive (GL(dl_load_lock));

看到8行和18行分别调用了__rtld_lock_lock_recursivy以及__rtld_lock_unlock_recursive

这两个函数地址都存放在_rtld_global结构体中,只要劫持其中一个即可。

pwndbg> distance 0x00007ffpwndbg> distance 0x7ffff7ffdf48 0x7ffff7a0d000
0x7ffff7ffdf48->0x7ffff7a0d000 is -0x5f0f48 bytes (-0xbe1e9 words)

pwndbg> distance 0x7ffff7ffdf50 0x7ffff7a0d000
0x7ffff7ffdf50->0x7ffff7a0d000 is -0x5f0f50 bytes (-0xbe1ea words)

上面的偏移就是距离libc的偏移,实验环境2.23,测试了一下远程环境应该是2.27

###libc-2.23
exit_hook = libc_base+0x5f0040+3848
exit_hook = libc_base+0x5f0040+3856

###libc-2.27
exit_hook = libc_base+0x619060+3840
exit_hook = libc_base+0x619060+3848

可以看到是ok的。构造payload如下:

如下:

但是远程libc我没有- -估计是libc_base计算的不对,拿不到shell。不过没关系,我是来学习的- –

the_shop_of_cosmos

总结来说前两个没什么用,后面两个都是读文件的:

但是可以看到只能用一次0day_1任意读,0day_2可以看到是可以写的。

提示给了proc系统。

/proc目录
  Linux系统内核提供了一种通过/proc文件系统,在程序运行时访问内核数据,改变内核设置的机制。/proc是一种伪文件结构,也就是说是仅存在于内存中,不存在于外存中的。/proc中一般比较重要的目录是sys,net和scsi,sys目录是可写的,可以通过它来访问和修改内核的参数。

  /proc中还有一些以PID命名(进程号)的进程目录,可以读取对应进程的信息。另外还有一个/self目录,用于记录本进程的信息。

/proc/self目录
  由上面的可知,我们可以通过/proc/$PID/目录来获得该进程的信息,但是这个方法需要知道进程的PID是多少,在fork、daemon等情况下,PID可能还会发生变化。所以Linux提供了self目录,来解决这个问题,这个目录比较独特,不同的进程来访问获得的信息是不同的,内容等价于/proc/本进程PID/目录下的内容。所以可以通过self目录直接获得自身的信息,不需要知道PID。

/proc/self/maps
  这个文件用于记录当前进程的内存映射关系,类似于gdb下的vmmap指令,通过读取该文件可以获得内存代码段基地址。

/proc/self/mem
  该文件记录的是进程的内存信息,通过修改该文件相当于直接修改进程的内存。这个文件是可读可写的,但是并不能直接进行读,需要结合maps的映射信息来确定读的偏移值,无法读取未被映射的区域,只有读取的偏移值是被映射的区域才能正确读取出内容。

可以直接通过/proc/self/maps来获取libc地址和程序基址,这样不需要pid也能获取。

那么这里其实不止一种利用方式,可以获得基址通过/proc/self/mem去修改.text段为shellcode,或者劫持__free_hook。并且通过mem修改不需要考虑权限,也可以修改检测的flag字符串。

看到余额的计算是这样的,并没有检测负数,所以可以修改金额。

远程libc-2.31的环境。

我没拿到shell…改了flag。

hgame{8ec3076d22b69fbee5a965057539dd270349daebe55a37f5382fa0b0a4839429}

WEEK3

blackgive

明显的stack pivot,也没canary也没PIE的。但是leak完后ret回main的时候有点问题,就是因为rsp还保持bss的地址,所以buf的地址就会出错(后面发现原因是不可写)

后面跳到start,然后payload写到bss+0xa0的地方,跟一下看到eof:

因为这段地址没有写权限,我们加到0xa8的地方,看到可以过去了,但是还是有点问题,断在了libc_start_main,但是直接回到溢出的地方是ok的。

考虑跳到main试一下:

断在了setbuf里面,问了其他师傅说应该是进行一些初始化操作的时候把stdout、stdin这些在bss的数据给改掉了。(最后ret到了write)

但是两种情况都是一样的,跳到start是因为某些操作写数据想写到不可写的地方,调高我们的fake rbp即可(即把payload写高一点的地址),同样跳回到main也是一样的,需要写到高一点的地方避免一些操作写到原有的重要数据上。所以即使此刻的栈还是在bss上,只要可写,也是没关系的。

进行第二次溢出的时候又出现了点问题:

这样可能因为栈的原因,read的ret回到了我们payload-0x10的地方,尝试提前0x10的话system也执行不成功,只能尝试一下one_gadget。

这个要总结一下,比较曲折的栈迁移:首先正常迁移发现leak完后直接EOF,尝试返回了start以及main还有漏洞点均失败,后面发现是因为一些初始化操作想写不可写区域(payload前面),后面提高payload的位置(写的下payload的情况下尽量高吧),然后第二次迁移发现又是EOF,调试看到是read完以后ret的时候ret到了第二次payload-0x10的位置,即把payload提前了0x10个字节,然而system执行不成功,遂改成one然后成功getshell。

hgame{cd510a967125a3983713b9b5080dc901b361396586b0e6fea5c7ecfb4b0ac314}

todolist

保护全开,看主要漏洞点:

依旧是UAF,并且主要功能比较齐全,不过只可以申请4个chunk:

没啥说的main_arena leak然后UAF劫持free_hook即可。

hgame{e68f962592ba8c5f46bbec4cf03c04c770485b418be6733ceafea99c8190ba07}

todolist2

和todolist相比做了以下调整:

申请次数数量从4上调到了9 ,并且修复了UAF.

所以又仔细看了遍程序,发现应该有一个溢出。

确实可以溢出。

思路如下:直接让一个chunk进unsorted bin然后再申请回来即可leak(注意gap to top),然后利用溢出,从那个gap chunk溢出到tcache里面的chunk劫持free_hook,因为tcache的指针是直接指向chunk的user mem区域,所以在gap chunk的开始写上sh字符串即可。

system不知道为啥就是没执行,还是用one吧。

system不成功的原因,把sh给清了。

hgame{f7c35b7c8e29e2623cc2e16b1a5df7a6f6b8a964af2c78e1df03e3d4c0d810ae}

Library management System

先是大体看了下主要功能,没有什么问题,并且限制size<-0x70,但是read_cnt()这个函数有问题:

off-by-one,这种也比较常见了,通常就是修改size并构造chunk overlapping使两个指针指向同一个chunk。

确实存在off-by-one。

那么leak思路如下:通过off-by-one修改size进行overlapping使得chunk进入unsorted bin然后再申请回来其中的第一部分leak libc。

leak完后发现unsorted bin中还存在着一个0x60的chunk,就是我们overlapping的第二个chunk,给他申请出来就行了,那这时候同时存在两个指针指向了chunk2.

分别是index2和index4,这样直接fastbin attack劫持malloc_hook即可。

注意fastbin是有double free检测机制的。

发现one都不满足约束,并且常用realloc调试的偏移也无法getshell,不过题目里给了libc以及ld,加载到本地调一下:

payload为malloc_hook-8为one,malloc_hook为realloc+

可以看到,rsp+0x30为约束,-0x30是我们能调整的范围。直接就满足了条件。

再看上图realloc,既然满足了条件证明我们少push 6次即可。

hgame{e900090d44f6dff1a7bdafe31fa0c530e91a9a729d1520f5df7fd73b49db4e70}

without_leak

这题我不看就知道是爆破stdout – -比较烦这种…

草率了,不是这么回事儿。没办法leaklibc,可以选择ret2dl_resolve,但是我对这个了解真的比较菜…所以参考了另一位师傅一道题的思路。

ret2dl_resolve,菜鸡对这方面了解有点少,回头补了写。


WEEK 4

rop_senior

这种情况可以比较容易想到SROP。

首先我们需要正常执行程序来将syscall_read的payload写到bss上,目的是后面写入execve

这样程序首先返回到正常的vuln,然后我们输入15个字符,此时read将rax设为15。执行完毕后避免xor将rax置0,回到了no_xor即跳过了xor,然后rax为15即执行了sigreturn。

因为我们第一个sigframe是调用read读到bss处,所以后面的execve_sigframe内容会写到bss。rdi不用多说是sh字符串设置位于bss+0x120的位置。同样的第一个payload先调用正常的vuln来设置rax,然后返回到跳过xor的地方执行syscall,rax为15是sigreturn。

SROP的核心思想就是控制rax寄存器通过sys call达成我们的目的。

hgame{e900090d44f6dff1a7bdafe31fa0c530e91a9a729d1520f5df7fd73b49db4e70}

house_of_cosmos

主要功能比较齐全,但是show功能是没有的。看下漏洞点:

可以溢出,size为0即可,在没有show、没PIE、没RELO情况下优先考虑unlink。

堆块布局如上,下图为chunk伪造。

一般过程:此时free chunk2,根据chunk2 prev_size走到fake chunk size处不做校验,再根据fake chunk size往下走,正常应该是刚才的chunk2 prev_size,此时做校验。因此我们只需要根据fake chunk size往下走的位置处伪造prev_size即可。

当然这个题因为直接的溢出,不需要伪造也可以,只需要把fake chunk size设置成0x90,校验时就直接走到真正的prev_size处通过验证。

因为fake chunk在chunk1中,而chunk1在list的位置是list+0x10,fake_FD对应。

注意修改list的时候写了两个atoi_got,第一个用来leak,因为delete()后就清空的该位置,第二个用来修改system。

hgame{6f239ec3b9a20680072dfd553a891fe4d1c8c1d1656b116341ded1c6849e4c7d}

Leave a Reply

Your email address will not be published. Required fields are marked *

five + 3 =